Skip to content

feat: Add static value list filters to schemas and APIs - #3017

Open
pulpdrew wants to merge 2 commits into
mainfrom
drew/accept-static-value-filters
Open

feat: Add static value list filters to schemas and APIs#3017
pulpdrew wants to merge 2 commits into
mainfrom
drew/accept-static-value-filters

Conversation

@pulpdrew

@pulpdrew pulpdrew commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR updates schemas, types, and API contracts to support Static List type dashboard filters, which show a static list of author-provided options instead of values queried from ClickHouse.

These new filters have no associated expression and therefore support only variable mode (they cannot be converted to a SQL condition for broadcast mode).

The updated DashboardFilter type / schema is a discriminated union on type, where STATIC_LIST is a new type.

The internal and external API have been updated to accept, transform, and validate the new type. Dashboard import has been updated to support the new type. The UI has been updated only to avoid type errors, and will not display the new filter type.

Followup work includes:

  1. Displaying static value list filters on the UI
  2. Allowing the user to create and edit static value list filters on the UI
  3. Support static value list filters in MCP (dependent on variable support in MCP feat: Support dashboard variables in MCP server #2951)

Screenshots or video

Create via API

Screenshot 2026-08-28 at 2 44 26 PM

Get via API

Screenshot 2026-08-28 at 2 45 04 PM

Included in export

Screenshot 2026-08-28 at 2 50 17 PM

Not shown in import (no source to remap)

Screenshot 2026-08-28 at 2 49 23 PM

MCP can still fetch a dashboard's filters

Screenshot 2026-08-28 at 2 54 21 PM

How to test locally

  • Create a dashboard with a static filter using import or the API
  • Try importing, exporting, make sure the existing filters still work
  • Note that it's expected that the filter is not show as a dropdown in the UI yet
Importable Dashboard
{
  "version": "0.1.0",
  "name": "Test Static Filters",
  "tiles": [
    {
      "id": "bd61ewiiu2fs52pkhwe9u",
      "x": 0,
      "y": 10,
      "w": 8,
      "h": 10,
      "config": {
        "name": "",
        "source": "Logs",
        "displayType": "line",
        "granularity": "auto",
        "alignDateRangeToGranularity": true,
        "select": [
          {
            "aggFn": "count",
            "aggCondition": "",
            "aggConditionLanguage": "lucene",
            "valueExpression": ""
          }
        ],
        "where": "",
        "whereLanguage": "lucene"
      }
    },
    {
      "id": "hmlgrh1me14q19ewy9pci",
      "x": 8,
      "y": 10,
      "w": 8,
      "h": 10,
      "config": {
        "name": "",
        "source": "Demo Logs",
        "displayType": "line",
        "granularity": "auto",
        "alignDateRangeToGranularity": true,
        "select": [
          {
            "aggFn": "count",
            "aggCondition": "",
            "aggConditionLanguage": "lucene",
            "valueExpression": ""
          }
        ],
        "where": "",
        "whereLanguage": "lucene"
      }
    },
    {
      "id": "7jwjomsz0pdmigm0kyorgp",
      "x": 0,
      "y": 0,
      "w": 9,
      "h": 10,
      "config": {
        "displayType": "table",
        "granularity": "auto",
        "alignDateRangeToGranularity": true,
        "configType": "sql",
        "sqlTemplate": "SELECT $env",
        "connection": "Local ClickHouse",
        "source": "Logs",
        "name": "Selected $env values"
      }
    }
  ],
  "filters": [
    {
      "id": "6a91d80f1e4e008c07f1c411",
      "name": "ServiceName (Traces)",
      "type": "QUERY_EXPRESSION",
      "expression": "ServiceName",
      "source": "JSON Traces"
    },
    {
      "id": "6a91d7081e4e008c07f1c265",
      "name": "Environment",
      "variableName": "env",
      "type": "STATIC_LIST",
      "options": [
        "dev",
        "staging",
        "prod"
      ],
      "isBroadcastEnabled": false,
      "isVariableEnabled": true
    },
    {
      "id": "6a91d7af1e4e008c07f1c35d",
      "name": "Status (Traces)",
      "type": "QUERY_EXPRESSION",
      "expression": "StatusCode",
      "source": "JSON Traces"
    },
    {
      "id": "7ca33140-cdfc-4434-bd29-62782c7aa3b0",
      "name": "Severity",
      "variableName": "Severity",
      "type": "QUERY_EXPRESSION",
      "expression": "SeverityText",
      "source": "Demo Logs",
      "appliesToSourceIds": [
        "Demo Logs",
        "Demo Traces"
      ],
      "isBroadcastEnabled": true,
      "isVariableEnabled": true
    }
  ],
  "containers": []
}

References

  • Linear Issue: Related to HDX-5059
  • Related PRs:

@changeset-bot

changeset-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7fc1722

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/common-utils Patch
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 28, 2026 6:11pm
hyperdx-storybook Ready Ready Preview Aug 28, 2026 6:11pm

Request Review

@greptile-apps

greptile-apps Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR extends dashboard filter schemas and API contracts with a variable-only STATIC_LIST variant.

  • Adds discriminated internal and external filter schemas, validation, translation, OpenAPI definitions, and provisioning/import support.
  • Updates shared filter utilities and dashboard hooks to distinguish queried filters from authored static lists.
  • Adds integration and unit coverage for persistence, validation, conversion, and mixed filter types.

Confidence Score: 4/5

The PR is not yet safe to merge because static-list URL selections can still inject values outside the authored option set into dashboard tile variables.

Variable-keyed filter values are accepted unchanged by resolveFilterSelection and then propagated directly into chart variables, so the previously reported invalid-selection behavior remains unresolved.

Files Needing Attention: packages/common-utils/src/dashboardFilterValues.ts; packages/app/src/hooks/useDashboardFilters.tsx

Important Files Changed

Filename Overview
packages/common-utils/src/types.ts Defines the discriminated dashboard-filter union and the variable-only static-list schema.
packages/common-utils/src/dashboardFilterValues.ts Adds variable-keyed static-list selection resolution while the previously reported option-domain issue remains outstanding.
packages/common-utils/src/dashboardValidation.ts Adds shared validation for static-list option uniqueness and filter-type field constraints.
packages/api/src/utils/zod.ts Exposes strict external API schemas for query-expression and static-list filter variants.
packages/api/src/routers/external-api/v2/utils/dashboards.ts Integrates static-list validation and avoids source lookup for sourceless filter variants.
packages/app/src/DBDashboardImportPage.tsx Preserves static filters during dashboard import while remapping sources only for queried filters.
packages/app/src/DashboardFilters.tsx Restricts ClickHouse-backed value queries to query-expression filters as part of the intentionally deferred static-filter UI work.
packages/app/src/components/DashboardFiltersModal/index.tsx Keeps the existing editor limited to query-expression filters pending the explicitly deferred static-list editing UI.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Author[API or dashboard import] --> Validate[Discriminated filter validation]
  Validate --> Persist[(Dashboard configuration)]
  Persist --> Load[Dashboard filters]
  Load --> Query[QUERY_EXPRESSION values from ClickHouse]
  Load --> Static[STATIC_LIST authored options]
  Query --> Selection[Dashboard selection state]
  Static --> Selection
  Selection --> Variables[Tile variables]
  Selection --> Broadcast[Query-expression broadcast filters]
Loading

Reviews (5): Last reviewed commit: "fix: Fix tests and lint" | Re-trigger Greptile

Comment thread packages/app/src/DashboardFilters.tsx
Comment thread packages/common-utils/src/dashboardFilterValues.ts
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 323 passed • 1 skipped • 1405s

Status Count
✅ Passed 323
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

@pulpdrew
pulpdrew force-pushed the drew/accept-static-value-filters branch from 92804e3 to 6b98b88 Compare August 28, 2026 13:59
Comment thread packages/app/src/components/DashboardFiltersModal/index.tsx
@pulpdrew
pulpdrew force-pushed the drew/accept-static-value-filters branch from 72ab9d2 to 7fc1722 Compare August 28, 2026 18:07
@pulpdrew
pulpdrew marked this pull request as ready for review August 28, 2026 18:55
@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Critical-path files (2) — tenancy, public API, or shipped database config:
    • packages/api/src/routers/external-api/v2/dashboards.ts
    • packages/api/src/routers/external-api/v2/utils/dashboards.ts
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)

Additional context: touches background tasks or the delivery pipeline lightly (8 lines, under the 30-line bar for Tier 4)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 24
  • Production lines changed: 735 (+ 1170 in test files, excluded from tier calculation)
  • Critical-path lines changed: 104
  • Branch: drew/accept-static-value-filters
  • Author: pulpdrew

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

Copy link
Copy Markdown
Contributor

Deep Review

No critical issues found. The STATIC_LIST discriminated-union refactor is carefully constructed: the type guards narrow soundly, the option-uniqueness validator and z.literal(false)/z.literal(true) mode-locks are wired into the internal API, external v2, and provisioning write paths, and QUERY_EXPRESSION field parity is preserved through the schema split. Adversarial and security passes found no new injection sink — STATIC_LIST values flow through the same escaped substitution as existing variable filters. The findings below are non-blocking.

🟡 P2 -- recommended

  • packages/app/src/DBDashboardImportPage.tsx:466 -- The Mapping component gained load-bearing STATIC_LIST control flow (skips source/applies-to mapping, omits the source key on submit so a static filter is never stamped with an empty-string source that re-import would reject) but DBDashboardImportPage.test.tsx was not updated and has no static-filter case; the invariant is only exercised at the convertToDashboardTemplate unit level, not through the component that does the remap and submit.
    • Fix: Add a Mapping test with a mixed static + queried template asserting no Data Source row renders for the static filter and the submitted filter carries no source key.
    • testing
🔵 P3 nitpicks (8)
  • packages/app/src/components/DashboardFiltersModal/index.tsx:93 -- DashboardFiltersList renders an edit action for every filter, but the modal only opens the edit form when isQueryExpressionFilter(selectedFilter) is true, so clicking edit on a persisted STATIC_LIST filter (creatable via API) silently re-renders the list — a reachable dead control.
    • Fix: Hide or disable the edit action for STATIC_LIST filters in DashboardFiltersList (with a tooltip noting editing is not yet supported) until the deferred editor lands.
    • correctness, adversarial, kieran-typescript, maintainability, previous-comments
  • packages/common-utils/src/dashboardFilterValues.ts:147 -- resolveFilterSelection returns a URL/savedFilterValues-supplied selection for a STATIC_LIST filter without checking membership in filter.options, so a value outside the authored list reaches the $variableName substitution; the value is escaped and this mirrors existing QUERY_EXPRESSION variable behavior, so it is not an injection vector, but it defeats the constrained-list expectation.
    • Fix: Intersect the resolved byVariable values with filter.options for static filters and drop out-of-domain values, or document that the option list is UI-only.
    • adversarial, previous-comments, security
  • packages/api/src/tasks/provisionDashboards/index.ts:32 -- provisionedDashboardSchema runs only validateDashboardFilterOptionUniqueness, while the API create/update and external v2 paths also run mode and variable-name-uniqueness checks, so a provisioned dashboard with duplicate variable names or a no-mode-enabled filter is accepted at provision time but would be rejected by the API.
    • Fix: Extract a single shared validateDashboardFilters helper and register it at every create/update/import/provision superRefine so validation cannot diverge silently.
    • maintainability, correctness
  • packages/common-utils/src/dashboardFilterValues.ts:156 -- new Map(Object.entries(parsed.byExpression)).get(expression) allocates a whole Map on every call, and resolveFilterSelection runs twice per filter inside rebuildEntries; the only behavioral gain over the prior bracket access is dodging Object.prototype keys.
    • Fix: Guard with Object.prototype.hasOwnProperty.call(...) and index directly, or build byExpression as a Map once at the parse boundary.
    • kieran-typescript, maintainability
  • packages/api/src/mcp/tools/dashboards/schemas.ts:1155 -- MCP getDashboard returns STATIC_LIST filters verbatim, but the MCP save/create input schema restricts type to QUERY_EXPRESSION, so an agent copying a returned static filter into a save call hits a strict-validation rejection.
    • Fix: Note in the MCP output (or the existing TODO) that STATIC_LIST filters are read-only via MCP, or preserve unknown-type filters through the save path.
    • api-contract, project-standards
  • packages/api/src/utils/zod.ts:156 -- The external v2 dashboard response filter union now includes STATIC_LIST shapes lacking expression/sourceId, an additive-but-technically-breaking response change for clients that strictly deserialize filters.
    • Fix: Confirm the v2 compatibility policy treats new response-union members as non-breaking and call it out in the changeset/release notes.
    • api-contract
  • scripts/ci/ratchet-baseline.json:5 -- The added file-wide eslint-disable security/detect-non-literal-fs-filename in the provision test bumped the api eslint-disable baseline 31→32, against the ratchet's documented "may only go down" policy; the base test file already had many fs calls passing under the warning budget, so the disable is optional cleanup.
    • Fix: Remove the disable to keep the baseline at 31, or scope it narrowly and justify the bump.
    • project-standards
  • packages/app/src/DashboardFilters.tsx:221 -- queriedFilters excludes STATIC_LIST from the only live filter renderer, so authored static filters cannot be selected in the UI and their variable-dependent tiles cannot be driven interactively; this matches the PR's explicit deferral of static-filter display.
    • Fix: Track the display/editing UI as the stated follow-up; no change required in this PR.
    • adversarial, correctness, previous-comments

Reviewers (9): correctness, adversarial, api-contract, kieran-typescript, testing, maintainability, security, previous-comments, project-standards.

Testing gaps:

  • No test asserts a URL-supplied STATIC_LIST value containing SQL metacharacters is escaped when substituted into a tile WHERE clause.
  • The provision duplicate-options reject path is not tested at its call site (validator is unit-tested; only the wiring is uncovered).
  • useDashboardFilters and usePresetDashboardFilters have no STATIC_LIST case (variable-exposed / no-query behavior and the client-side save no-op are covered only transitively).
  • Dropped to coverage: unbounded aggregate payload from many max-size options sets (pre-existing, filters array has no .max()); regenerated packages/api/openapi.json (~100 lines) was not re-verified against a fresh docgen run.

@pulpdrew pulpdrew changed the title feat: Add static filters to schemas and APIs feat: Add static value list filters to schemas and APIs Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant