mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: support saved query/filter values in external api (#1814)
In https://github.com/hyperdxio/hyperdx/pull/1584 we added saved default query/filter values support to dashboards. This PR extends that support to the external API. Fixes HDX-3519
This commit is contained in:
parent
f5828d1bfa
commit
daab2cace1
9 changed files with 453 additions and 20 deletions
5
.changeset/angry-wasps-boil.md
Normal file
5
.changeset/angry-wasps-boil.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/api": patch
|
||||
---
|
||||
|
||||
support saved query/filter values in external api
|
||||
|
|
@ -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": [
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
@ -57,6 +60,9 @@ describe('External API v2 Dashboards - old format', () => {
|
|||
],
|
||||
tags: TEST_TAGS,
|
||||
filters: [],
|
||||
savedQuery: null,
|
||||
savedQueryLanguage: null,
|
||||
savedFilterValues: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
@ -280,6 +286,9 @@ describe('External API v2 Dashboards - old format', () => {
|
|||
],
|
||||
tags: ['format-test'],
|
||||
filters: [],
|
||||
savedQuery: null,
|
||||
savedQueryLanguage: null,
|
||||
savedFilterValues: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -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',
|
||||
|
|
@ -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();
|
||||
|
|
@ -1814,6 +1947,9 @@ describe('External API v2 Dashboards - new format', () => {
|
|||
],
|
||||
tags: ['format-test'],
|
||||
filters: [],
|
||||
savedQuery: null,
|
||||
savedQueryLanguage: null,
|
||||
savedFilterValues: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -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',
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { SearchConditionLanguageSchema as whereLanguageSchema } from '@hyperdx/common-utils/dist/types';
|
||||
import express from 'express';
|
||||
import { uniq } from 'lodash';
|
||||
import { ObjectId } from 'mongodb';
|
||||
|
|
@ -17,6 +18,7 @@ import {
|
|||
externalDashboardFilterSchema,
|
||||
externalDashboardFilterSchemaWithId,
|
||||
ExternalDashboardFilterWithId,
|
||||
externalDashboardSavedFilterValueSchema,
|
||||
externalDashboardTileListSchema,
|
||||
ExternalDashboardTileWithId,
|
||||
objectIdSchema,
|
||||
|
|
@ -67,6 +69,62 @@ async function getMissingSources(
|
|||
return [...sourceIds].filter(sourceId => !existingSourceIds.has(sourceId));
|
||||
}
|
||||
|
||||
type SavedQueryLanguage = z.infer<typeof whereLanguageSchema>;
|
||||
|
||||
function resolveSavedQueryLanguage(params: {
|
||||
savedQuery: string | null | undefined;
|
||||
savedQueryLanguage: SavedQueryLanguage | null | undefined;
|
||||
}): SavedQueryLanguage | null | undefined {
|
||||
const { savedQuery, savedQueryLanguage } = params;
|
||||
if (savedQueryLanguage !== undefined) return savedQueryLanguage;
|
||||
if (savedQuery === null) return null;
|
||||
if (savedQuery) return 'lucene';
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dashboardBodyBaseShape = {
|
||||
name: z.string().max(1024),
|
||||
tiles: externalDashboardTileListSchema,
|
||||
tags: tagsSchema,
|
||||
savedQuery: z.string().nullable().optional(),
|
||||
savedQueryLanguage: whereLanguageSchema.nullable().optional(),
|
||||
savedFilterValues: z
|
||||
.array(externalDashboardSavedFilterValueSchema)
|
||||
.optional(),
|
||||
};
|
||||
|
||||
function buildDashboardBodySchema(filterSchema: z.ZodTypeAny): z.ZodEffects<
|
||||
z.ZodObject<
|
||||
typeof dashboardBodyBaseShape & {
|
||||
filters: z.ZodOptional<z.ZodArray<z.ZodTypeAny>>;
|
||||
}
|
||||
>
|
||||
> {
|
||||
return z
|
||||
.object({
|
||||
...dashboardBodyBaseShape,
|
||||
filters: z.array(filterSchema).optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.savedQuery != null && data.savedQueryLanguage === null) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message:
|
||||
'savedQueryLanguage cannot be null when savedQuery is provided',
|
||||
path: ['savedQueryLanguage'],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const createDashboardBodySchema = buildDashboardBodySchema(
|
||||
externalDashboardFilterSchema,
|
||||
);
|
||||
const updateDashboardBodySchema = buildDashboardBodySchema(
|
||||
externalDashboardFilterSchemaWithId,
|
||||
);
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* components:
|
||||
|
|
@ -83,6 +141,18 @@ async function getMissingSources(
|
|||
* type: string
|
||||
* enum: [sql, lucene]
|
||||
* 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: [sum, gauge, histogram, summary, exponential histogram]
|
||||
|
|
@ -838,6 +908,22 @@ async function getMissingSources(
|
|||
* description: Dashboard filter keys added to the dashboard and applied to all tiles
|
||||
* 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'
|
||||
*
|
||||
* CreateDashboardRequest:
|
||||
* type: object
|
||||
|
|
@ -865,6 +951,22 @@ async function getMissingSources(
|
|||
* description: Dashboard filter keys to add to the dashboard and apply across all tiles
|
||||
* 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'
|
||||
*
|
||||
* UpdateDashboardRequest:
|
||||
* type: object
|
||||
|
|
@ -893,6 +995,22 @@ async function getMissingSources(
|
|||
* description: Dashboard filter keys on the dashboard, applied across all tiles
|
||||
* 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'
|
||||
*
|
||||
* DashboardResponse:
|
||||
* allOf:
|
||||
|
|
@ -994,7 +1112,16 @@ router.get('/', async (req, res, next) => {
|
|||
|
||||
const dashboards = await Dashboard.find(
|
||||
{ team: teamId },
|
||||
{ _id: 1, name: 1, tiles: 1, tags: 1, filters: 1 },
|
||||
{
|
||||
_id: 1,
|
||||
name: 1,
|
||||
tiles: 1,
|
||||
tags: 1,
|
||||
filters: 1,
|
||||
savedQuery: 1,
|
||||
savedQueryLanguage: 1,
|
||||
savedFilterValues: 1,
|
||||
},
|
||||
).sort({ name: -1 });
|
||||
|
||||
res.json({
|
||||
|
|
@ -1102,7 +1229,16 @@ router.get(
|
|||
|
||||
const dashboard = await Dashboard.findOne(
|
||||
{ team: teamId, _id: req.params.id },
|
||||
{ _id: 1, name: 1, tiles: 1, tags: 1, filters: 1 },
|
||||
{
|
||||
_id: 1,
|
||||
name: 1,
|
||||
tiles: 1,
|
||||
tags: 1,
|
||||
filters: 1,
|
||||
savedQuery: 1,
|
||||
savedQueryLanguage: 1,
|
||||
savedFilterValues: 1,
|
||||
},
|
||||
);
|
||||
|
||||
if (dashboard == null) {
|
||||
|
|
@ -1252,12 +1388,7 @@ router.get(
|
|||
router.post(
|
||||
'/',
|
||||
validateRequest({
|
||||
body: z.object({
|
||||
name: z.string().max(1024),
|
||||
tiles: externalDashboardTileListSchema,
|
||||
tags: tagsSchema,
|
||||
filters: z.array(externalDashboardFilterSchema).optional(),
|
||||
}),
|
||||
body: createDashboardBodySchema,
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
|
|
@ -1266,7 +1397,15 @@ router.post(
|
|||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
const { name, tiles, tags, filters } = req.body;
|
||||
const {
|
||||
name,
|
||||
tiles,
|
||||
tags,
|
||||
filters,
|
||||
savedQuery,
|
||||
savedQueryLanguage,
|
||||
savedFilterValues,
|
||||
} = req.body;
|
||||
|
||||
const missingSources = await getMissingSources(teamId, tiles, filters);
|
||||
if (missingSources.length > 0) {
|
||||
|
|
@ -1299,11 +1438,19 @@ router.post(
|
|||
}),
|
||||
);
|
||||
|
||||
const normalizedSavedQueryLanguage = resolveSavedQueryLanguage({
|
||||
savedQuery,
|
||||
savedQueryLanguage,
|
||||
});
|
||||
|
||||
const newDashboard = await new Dashboard({
|
||||
name,
|
||||
tiles: internalTiles,
|
||||
tags: tags && uniq(tags),
|
||||
filters: filtersWithIds,
|
||||
savedQuery,
|
||||
savedQueryLanguage: normalizedSavedQueryLanguage,
|
||||
savedFilterValues,
|
||||
team: teamId,
|
||||
}).save();
|
||||
|
||||
|
|
@ -1460,12 +1607,7 @@ router.put(
|
|||
params: z.object({
|
||||
id: objectIdSchema,
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().max(1024),
|
||||
tiles: externalDashboardTileListSchema,
|
||||
tags: tagsSchema,
|
||||
filters: z.array(externalDashboardFilterSchemaWithId).optional(),
|
||||
}),
|
||||
body: updateDashboardBodySchema,
|
||||
}),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
|
|
@ -1478,7 +1620,15 @@ router.put(
|
|||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
const { name, tiles, tags, filters } = req.body ?? {};
|
||||
const {
|
||||
name,
|
||||
tiles,
|
||||
tags,
|
||||
filters,
|
||||
savedQuery,
|
||||
savedQueryLanguage,
|
||||
savedFilterValues,
|
||||
} = req.body ?? {};
|
||||
|
||||
const missingSources = await getMissingSources(teamId, tiles, filters);
|
||||
if (missingSources.length > 0) {
|
||||
|
|
@ -1528,6 +1678,19 @@ router.put(
|
|||
},
|
||||
);
|
||||
}
|
||||
if (savedQuery !== undefined) {
|
||||
setPayload.savedQuery = savedQuery;
|
||||
}
|
||||
const normalizedSavedQueryLanguage = resolveSavedQueryLanguage({
|
||||
savedQuery,
|
||||
savedQueryLanguage,
|
||||
});
|
||||
if (normalizedSavedQueryLanguage !== undefined) {
|
||||
setPayload.savedQueryLanguage = normalizedSavedQueryLanguage;
|
||||
}
|
||||
if (savedFilterValues !== undefined) {
|
||||
setPayload.savedFilterValues = savedFilterValues;
|
||||
}
|
||||
|
||||
const updatedDashboard = await Dashboard.findOneAndUpdate(
|
||||
{ _id: dashboardId, team: teamId },
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ export type ExternalDashboard = {
|
|||
tiles: ExternalDashboardTileWithId[];
|
||||
tags?: string[];
|
||||
filters?: ExternalDashboardFilterWithId[];
|
||||
savedQuery?: string | null;
|
||||
savedQueryLanguage?: string | null;
|
||||
savedFilterValues?: DashboardDocument['savedFilterValues'];
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
|
|
@ -196,6 +199,9 @@ export function convertToExternalDashboard(
|
|||
tiles: dashboard.tiles.map(convertTileToExternalChart),
|
||||
tags: dashboard.tags || [],
|
||||
filters: dashboard.filters?.map(translateFilterToExternalFilter) || [],
|
||||
savedQuery: dashboard.savedQuery ?? null,
|
||||
savedQueryLanguage: dashboard.savedQueryLanguage ?? null,
|
||||
savedFilterValues: dashboard.savedFilterValues ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -143,6 +143,15 @@ export type ExternalDashboardFilter = z.infer<
|
|||
typeof externalDashboardFilterSchema
|
||||
>;
|
||||
|
||||
export const externalDashboardSavedFilterValueSchema = z.object({
|
||||
type: z.literal('sql').optional().default('sql'),
|
||||
condition: z.string().max(10000),
|
||||
});
|
||||
|
||||
export type ExternalDashboardSavedFilterValue = z.infer<
|
||||
typeof externalDashboardSavedFilterValueSchema
|
||||
>;
|
||||
|
||||
// ================================
|
||||
// Dashboards (new format)
|
||||
// ================================
|
||||
|
|
|
|||
|
|
@ -913,7 +913,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
: null;
|
||||
const currentFilterValues = rawFilterQueries?.length
|
||||
? rawFilterQueries
|
||||
: null;
|
||||
: [];
|
||||
|
||||
setDashboard(
|
||||
produce(dashboard, draft => {
|
||||
|
|
@ -946,7 +946,7 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
|
|||
produce(dashboard, draft => {
|
||||
draft.savedQuery = null;
|
||||
draft.savedQueryLanguage = null;
|
||||
draft.savedFilterValues = null;
|
||||
draft.savedFilterValues = [];
|
||||
}),
|
||||
() => {
|
||||
notifications.show({
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export type Dashboard = {
|
|||
filters?: DashboardFilter[];
|
||||
savedQuery?: string | null;
|
||||
savedQueryLanguage?: SearchConditionLanguage | null;
|
||||
savedFilterValues?: Filter[] | null;
|
||||
savedFilterValues?: Filter[];
|
||||
};
|
||||
|
||||
export function useUpdateDashboard() {
|
||||
|
|
|
|||
|
|
@ -572,7 +572,7 @@ export const DashboardSchema = z.object({
|
|||
filters: z.array(DashboardFilterSchema).optional(),
|
||||
savedQuery: z.string().nullable().optional(),
|
||||
savedQueryLanguage: SearchConditionLanguageSchema.nullable().optional(),
|
||||
savedFilterValues: z.array(FilterSchema).nullable().optional(),
|
||||
savedFilterValues: z.array(FilterSchema).optional(),
|
||||
});
|
||||
export const DashboardWithoutIdSchema = DashboardSchema.omit({ id: true });
|
||||
export type DashboardWithoutId = z.infer<typeof DashboardWithoutIdSchema>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue