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
7 changes: 7 additions & 0 deletions .changeset/static-list-dashboard-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@hyperdx/common-utils': patch
'@hyperdx/api': patch
'@hyperdx/app': patch
---

feat: Add static filters to schemas and APIs
100 changes: 98 additions & 2 deletions packages/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3040,8 +3040,26 @@
]
},
"FilterInput": {
"description": "A drop-down filter on a dashboard. Depending on type, filters can either broadcast\nselected value(s) to every tile (isBroadcastEnabled), expose selected\nvalues as a variable (isVariableEnabled) or both. Typically a filter should\ndo just one of these things.\n",
"oneOf": [
{
"$ref": "#/components/schemas/QueryExpressionFilterInput"
},
{
"$ref": "#/components/schemas/StaticListFilterInput"
}
],
"discriminator": {
"propertyName": "type",
"mapping": {
"QUERY_EXPRESSION": "#/components/schemas/QueryExpressionFilterInput",
"STATIC_LIST": "#/components/schemas/StaticListFilterInput"
}
}
},
"QueryExpressionFilterInput": {
"type": "object",
"description": "A drop-down filter on a dashboard. Every filter can either broadcast its\nselected value(s) to every tile (isBroadcastEnabled), expose its selected\nvalue as a variable (isVariableEnabled) or both. Typically a filter should\ndo just one of these things.\n",
"description": "A filter whose dropdown values are queried from ClickHouse: \"expression\"\nnames the column and \"sourceId\" the source to read them from. Its\nselection can be broadcast into matching tiles' WHERE clauses.\n",
"required": [
"type",
"name",
Expand All @@ -3054,7 +3072,7 @@
"enum": [
"QUERY_EXPRESSION"
],
"description": "Filter type. Must be \"QUERY_EXPRESSION\".",
"description": "Filter type discriminator. Must be \"QUERY_EXPRESSION\".",
"example": "QUERY_EXPRESSION"
},
"name": {
Expand Down Expand Up @@ -3132,6 +3150,72 @@
}
}
},
"StaticListFilterInput": {
"type": "object",
"description": "A filter whose dropdown offers a static custom \"options\" list. It carries no\nexpression, so it cannot be broadcast, and supports only variable mode.\n",
"required": [
"type",
"name",
"options"
],
"properties": {
"type": {
"type": "string",
"enum": [
"STATIC_LIST"
],
"description": "Discriminator. Must be \"STATIC_LIST\".",
"example": "STATIC_LIST"
},
"name": {
"type": "string",
"minLength": 1,
"description": "Display name for the dashboard filter key",
"example": "Environment"
},
"options": {
"type": "array",
"minItems": 1,
"maxItems": 1000,
"items": {
"type": "string",
"minLength": 1,
"maxLength": 10000
},
"description": "The values this filter's dropdown offers. Must be non-empty and\nfree of duplicates.\n",
"example": [
"prod",
"staging",
"dev"
]
},
"isBroadcastEnabled": {
"type": "boolean",
"enum": [
false
],
"default": false,
"description": "Must be false — there is no expression to broadcast. Omit it and it\nis false.\n",
"example": false
},
"isVariableEnabled": {
"type": "boolean",
"enum": [
true
],
"default": true,
"description": "Must be true if provided, and is true when omitted. Tiles reference\nthe selection as `$variableName`, or with the `$__filter(<expression>, $<variableName>)`\nand `$__conditionalAll(<condition>, $<variableName>)` macros. The\none-argument `$__filter($<variableName>)` form renders the filter's\nown expression, which this kind of filter does not have, so it\nreports that the expression must be passed explicitly.\n",
"example": true
},
"variableName": {
"type": "string",
"maxLength": 64,
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Token tiles reference this filter's selected value by, as\n`$variableName`. Must start with a letter and may contain only\nletters, numbers, and underscores. Defaults to the display name with\nwhitespace replaced by underscores and remaining illegal characters\nremoved, so a variable-enabled filter whose name derives nothing\nusable must send this field explicitly. Variable names must be unique\nacross a dashboard's variable-enabled filters. Names the variable\nonly, so the field is rejected when isVariableEnabled is not true, and\nis omitted from responses for such a filter.\n",
"example": "environment"
}
}
},
"Filter": {
"allOf": [
{
Expand Down Expand Up @@ -6147,6 +6231,18 @@
"name": "Service",
"expression": "service_name",
"sourceId": "65f5e4a3b9e77c001a111111"
},
{
"type": "STATIC_LIST",
"name": "Environment",
"options": [
"prod",
"staging",
"dev"
],
"isBroadcastEnabled": false,
"isVariableEnabled": true,
"variableName": "env"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
getTileVariableWarnings,
} from '@/mcp/tools/dashboards/validation';
import type {
ExternalDashboardFilterWithId,
ExternalDashboardTileWithId,
ExternalQueryExpressionFilterWithId,
} from '@/utils/zod';

const connectionId = new mongoose.Types.ObjectId().toString();
Expand Down Expand Up @@ -448,8 +448,8 @@ describe('getRawSqlTileMacroHints', () => {
// ─── Variable-enabled filters ────────────────────────────────────────────────

function makeFilter(
overrides: Partial<ExternalDashboardFilterWithId> = {},
): ExternalDashboardFilterWithId {
overrides: Partial<ExternalQueryExpressionFilterWithId> = {},
): ExternalQueryExpressionFilterWithId {
return {
id: new mongoose.Types.ObjectId().toString(),
type: 'QUERY_EXPRESSION',
Expand Down Expand Up @@ -803,7 +803,7 @@ describe('getFilterVariableWarnings', () => {
});

const endpointFilter = (
overrides: Partial<ExternalDashboardFilterWithId> = {},
overrides: Partial<ExternalQueryExpressionFilterWithId> = {},
) =>
makeFilter({
name: 'Endpoint',
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/mcp/tools/dashboards/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,8 @@ const mcpDashboardFilterSchema = z
'one. On CREATE (no top-level `id` on the dashboard call), filter `id` may ' +
'be omitted and one will be generated server-side.',
),
type: DashboardFilterType.describe(
// TODO: Add static value filter type when variables are supported in MCP
type: DashboardFilterType.extract(['QUERY_EXPRESSION']).describe(
'Filter type. Currently only "QUERY_EXPRESSION" is supported.',
),
name: z
Expand Down
9 changes: 6 additions & 3 deletions packages/api/src/mcp/tools/dashboards/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
TIME_RANGE_MACROS,
} from '@hyperdx/common-utils/dist/macros';
import {
DashboardFilter,
DisplayType,
QueryExpressionDashboardFilter,
SavedChartConfig,
SearchConditionLanguage,
} from '@hyperdx/common-utils/dist/types';
Expand Down Expand Up @@ -151,9 +151,12 @@ export function getRawSqlTileMacroWarnings(
return hints;
}

/** Minimal projection needed to check a filter's dropdown-values query. */
/**
* Minimal projection needed to check a filter's dropdown-values query. Static
* list filters have no dropdown query, so `where` / `whereLanguage` are optional.
*/
export type FilterForVariableValidation = FilterForVariableDeclaration &
Partial<Pick<DashboardFilter, 'where' | 'whereLanguage'>>;
Partial<Pick<QueryExpressionDashboardFilter, 'where' | 'whereLanguage'>>;

/**
* Non-blocking checks on the dashboard variables references in a filter's `where`.
Expand Down
Loading
Loading