Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angry-wasps-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/api": patch
---

support saved query/filter values in external api
80 changes: 80 additions & 0 deletions packages/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,26 @@
],
"description": "Query language for the where clause."
},
"SavedFilterValue": {
"type": "object",
"required": [
"condition"
],
"properties": {
"type": {
"type": "string",
"enum": [
"sql"
],
"default": "sql"
},
"condition": {
"type": "string",
"description": "SQL filter condition. For example use expressions in the form \"column IN ('value')\".",
"example": "ServiceName IN ('hdx-oss-dev-api')"
}
}
},
"MetricDataType": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -1525,6 +1545,26 @@
"items": {
"$ref": "#/components/schemas/Filter"
}
},
"savedQuery": {
"type": "string",
"nullable": true,
"description": "Optional default dashboard query restored when loading the dashboard.",
"example": "service.name = 'api'"
},
"savedQueryLanguage": {
"$ref": "#/components/schemas/QueryLanguage",
"nullable": true,
"description": "Query language used by savedQuery.",
"default": "lucene",
"example": "sql"
},
"savedFilterValues": {
"type": "array",
"description": "Optional default dashboard filter values restored when loading the dashboard.",
"items": {
"$ref": "#/components/schemas/SavedFilterValue"
}
}
}
},
Expand Down Expand Up @@ -1563,6 +1603,26 @@
"items": {
"$ref": "#/components/schemas/FilterInput"
}
},
"savedQuery": {
"type": "string",
"nullable": true,
"description": "Optional default dashboard query to persist on the dashboard.",
"example": "service.name = 'api'"
},
"savedQueryLanguage": {
"$ref": "#/components/schemas/QueryLanguage",
"nullable": true,
"description": "Query language used by savedQuery.",
"default": "lucene",
"example": "sql"
},
"savedFilterValues": {
"type": "array",
"description": "Optional default dashboard filter values to persist on the dashboard.",
"items": {
"$ref": "#/components/schemas/SavedFilterValue"
}
}
}
},
Expand Down Expand Up @@ -1603,6 +1663,26 @@
"items": {
"$ref": "#/components/schemas/Filter"
}
},
"savedQuery": {
"type": "string",
"nullable": true,
"description": "Optional default dashboard query to persist on the dashboard.",
"example": "service.name = 'api'"
},
"savedQueryLanguage": {
"$ref": "#/components/schemas/QueryLanguage",
"nullable": true,
"description": "Query language used by savedQuery.",
"default": "lucene",
"example": "sql"
},
"savedFilterValues": {
"type": "array",
"description": "Optional default dashboard filter values to persist on the dashboard.",
"items": {
"$ref": "#/components/schemas/SavedFilterValue"
}
}
}
},
Expand Down
170 changes: 170 additions & 0 deletions packages/api/src/routers/external-api/__tests__/dashboards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ describe('External API v2 Dashboards - old format', () => {
name: 'Test External Dashboard',
tiles: [makeExternalChart({ sourceId }), makeExternalChart({ sourceId })],
tags: TEST_TAGS,
savedQuery: null,
savedQueryLanguage: null,
savedFilterValues: [],
...overrides,
});

Expand All @@ -57,6 +60,9 @@ describe('External API v2 Dashboards - old format', () => {
],
tags: TEST_TAGS,
filters: [],
savedQuery: null,
savedQueryLanguage: null,
savedFilterValues: [],
...overrides,
});

Expand Down Expand Up @@ -280,6 +286,9 @@ describe('External API v2 Dashboards - old format', () => {
],
tags: ['format-test'],
filters: [],
savedQuery: null,
savedQueryLanguage: null,
savedFilterValues: [],
},
});

Expand Down Expand Up @@ -362,6 +371,80 @@ describe('External API v2 Dashboards - old format', () => {
expect(dashboards[0].tiles).toHaveLength(2);
});

it('should create a dashboard with saved query defaults', async () => {
const mockDashboard = createMockDashboard(traceSource._id.toString(), {
savedQuery: "service.name = 'api'",
savedQueryLanguage: 'sql',
savedFilterValues: [
{
type: 'sql',
condition: "ServiceName IN ('hdx-oss-dev-api')",
},
],
});

const response = await authRequest('post', BASE_URL)
.send(mockDashboard)
.expect(200);

expect(response.body.data.savedQuery).toBe("service.name = 'api'");
expect(response.body.data.savedQueryLanguage).toBe('sql');
expect(response.body.data.savedFilterValues).toEqual([
{
type: 'sql',
condition: "ServiceName IN ('hdx-oss-dev-api')",
},
]);

const dashboardInDb = await Dashboard.findById(
response.body.data.id,
).lean();
expect(dashboardInDb?.savedQuery).toBe("service.name = 'api'");
expect(dashboardInDb?.savedQueryLanguage).toBe('sql');
expect(dashboardInDb?.savedFilterValues).toEqual([
{
type: 'sql',
condition: "ServiceName IN ('hdx-oss-dev-api')",
},
]);
});

it('should default savedQueryLanguage to lucene when savedQuery is provided without a language', async () => {
const mockDashboard = omit(
createMockDashboard(traceSource._id.toString(), {
savedQuery: "service.name = 'api'",
}),
'savedQueryLanguage',
);

const response = await authRequest('post', BASE_URL)
.send(mockDashboard)
.expect(200);

expect(response.body.data.savedQuery).toBe("service.name = 'api'");
expect(response.body.data.savedQueryLanguage).toBe('lucene');

const dashboardInDb = await Dashboard.findById(
response.body.data.id,
).lean();
expect(dashboardInDb?.savedQueryLanguage).toBe('lucene');
});

it('should return 400 when savedQueryLanguage is null and savedQuery is provided', async () => {
const mockDashboard = createMockDashboard(traceSource._id.toString(), {
savedQuery: "service.name = 'api'",
savedQueryLanguage: null,
});

const response = await authRequest('post', BASE_URL)
.send(mockDashboard)
.expect(400);

expect(response.body.message).toContain(
'savedQueryLanguage cannot be null when savedQuery is provided',
);
});

it('can create all chart types', async () => {
const dashboardWithAllCharts = {
name: 'Test Dashboard with All Chart Types',
Expand Down Expand Up @@ -881,6 +964,56 @@ describe('External API v2 Dashboards - old format', () => {
expect(updatedDashboardInDb?.tiles).toHaveLength(2);
});

it('should update and clear saved query defaults', async () => {
const dashboard = await createTestDashboard({
savedQuery: 'service:api',
savedQueryLanguage: 'lucene',
savedFilterValues: [
{
type: 'lucene',
condition: 'env:prod',
},
],
});
const updatedDashboard = createMockDashboardWithIds(
traceSource._id.toString(),
);

const response = await authRequest('put', `${BASE_URL}/${dashboard._id}`)
.send(updatedDashboard)
.expect(200);

expect(response.body.data.savedQuery).toBeNull();
expect(response.body.data.savedQueryLanguage).toBeNull();
expect(response.body.data.savedFilterValues).toEqual([]);

const updatedDashboardInDb = await Dashboard.findById(
dashboard._id,
).lean();
expect(updatedDashboardInDb?.savedQuery).toBeNull();
expect(updatedDashboardInDb?.savedQueryLanguage).toBeNull();
expect(updatedDashboardInDb?.savedFilterValues).toEqual([]);
});

it('should return 400 when savedQueryLanguage is null and savedQuery is provided on update', async () => {
const dashboard = await createTestDashboard();
const updatedDashboard = createMockDashboardWithIds(
traceSource._id.toString(),
{
savedQuery: "service.name = 'api'",
savedQueryLanguage: null,
},
);

const response = await authRequest('put', `${BASE_URL}/${dashboard._id}`)
.send(updatedDashboard)
.expect(400);

expect(response.body.message).toContain(
'savedQueryLanguage cannot be null when savedQuery is provided',
);
});

it('should update dashboard filters when provided', async () => {
const dashboard = await createTestDashboard();
const filterId1 = new ObjectId().toString();
Expand Down Expand Up @@ -1814,6 +1947,9 @@ describe('External API v2 Dashboards - new format', () => {
],
tags: ['format-test'],
filters: [],
savedQuery: null,
savedQueryLanguage: null,
savedFilterValues: [],
},
});

Expand Down Expand Up @@ -1924,6 +2060,21 @@ describe('External API v2 Dashboards - new format', () => {
expect(dashboards[0].tiles).toHaveLength(2);
});

it('should return 400 when savedQueryLanguage is null and savedQuery is provided', async () => {
const mockDashboard = createMockDashboard(traceSource._id.toString(), {
savedQuery: "service.name = 'api'",
savedQueryLanguage: null,
});

const response = await authRequest('post', BASE_URL)
.send(mockDashboard)
.expect(400);

expect(response.body.message).toContain(
'savedQueryLanguage cannot be null when savedQuery is provided',
);
});

it('can create all chart types', async () => {
const dashboardWithAllCharts = {
name: 'Test Dashboard with All Chart Types',
Expand Down Expand Up @@ -2306,6 +2457,25 @@ describe('External API v2 Dashboards - new format', () => {
expect(updatedDashboardInDb?.tiles).toHaveLength(2);
});

it('should return 400 when savedQueryLanguage is null and savedQuery is provided on update', async () => {
const dashboard = await createTestDashboard();
const updatedDashboard = createMockDashboardWithIds(
traceSource._id.toString(),
{
savedQuery: "service.name = 'api'",
savedQueryLanguage: null,
},
);

const response = await authRequest('put', `${BASE_URL}/${dashboard._id}`)
.send(updatedDashboard)
.expect(400);

expect(response.body.message).toContain(
'savedQueryLanguage cannot be null when savedQuery is provided',
);
});

it('should update dashboard filters when provided', async () => {
const dashboard = await createTestDashboard();
const filterId1 = new ObjectId().toString();
Expand Down
Loading
Loading