Skip to content

[HDX-4003] [HDX-4004] feat(cli): dashboard list/export/create/import and saved-searches commands - #2914

Merged
kodiakhq[bot] merged 5 commits into
mainfrom
warren/cli-post-dashboard
Oct 22, 2026
Merged

kodiakhq[bot] merged 5 commits into
mainfrom
warren/cli-post-dashboard

Conversation

@wrn14897

@wrn14897 wrn14897 commented Aug 14, 2026 •

Copy link
Copy Markdown
Member

Why

The hdx CLI could list dashboards and render tiles, but had no way to create, export, or import dashboards or saved searches — authoring them required the web UI or raw API calls. This implements the dashboard read/write command surface from HDX-4003 and HDX-4004, making the CLI a complete authoring surface for scripted/agent-driven workflows (author a dashboard, create it, then immediately validate every tile with hdx chart -d).

What

Dashboards (hdx dashboards, alias hdx dashboard)

  • list (default subcommand; bare hdx dashboards unchanged) — adds --query <substr> name filtering and --format table|json|csv (--json kept as back-compat alias). CSV emits id,name,tags,createdAt,updatedAt,tileCount. (HDX-4003)
  • export --id <id-or-name> — prints the full dashboard definition (charts, layout, filters) as JSON with server-managed fields stripped, re-validated against DashboardWithoutIdSchema so export | import is guaranteed round-trip compatible. Legacy dashboards that no longer validate are still exported with a stderr warning. (HDX-4003)
  • create — two modes: (HDX-4004)
    • --file <json> (or - for stdin): full DashboardWithoutId definition, validated locally with readable per-issue zod errors; missing tile ids auto-generated with the web's makeId() pattern
    • --name <name> + repeatable --chart '<json>' / --chart-file <path>: each chart is a full tile ({x,y,w,h,config}) or a bare chart config — charts without layout auto-flow onto the 24-column grid (12×4, two per row)
    • --if-not-exists skips creation when the name already exists (prints the existing id/URL, exit 0)
  • import --file <json> — recreates an exported dashboard, with --if-not-exists and --name-override. (HDX-4004)

Saved searches (hdx saved-searches)

  • list (default) — human-readable or --json
  • create --name --source <name-or-id> --where [--where-language lucene|sql] [--select] [--order-by] [--tags] — resolves sources by name or id

Bug fix

ApiClient.getSavedSearches() requested /saved-searches, but the API mounts the router at the singular /saved-search (packages/api/src/api-app.ts). The call always 404'd and the failure was silently swallowed in the TUI. Now fixed — saved searches load in the TUI source picker again.

Notes

  • Tickets sketched hdx dashboard (singular) and -s <url>; this follows the CLI's established conventions (hdx dashboards, -a/--app-url — -s already means --source on chart) and adds a dashboard alias for the group.
  • Updated the command table in packages/cli/AGENTS.md; changeset included (@hyperdx/cli minor).

Validation

Verified end-to-end against a live deployment with OTel Demo data:

  • Created 4 dashboards (24 builder tiles: line/stacked_bar/number/table, trace + log + histogram-metric sources, dashboard filter) via hdx dashboards create --file — all accepted by the server-side schema; every tile rendered status: ok via hdx chart -d <dashboard> --json.
  • list --query flow --format csv / --format json / bare --json back-compat all verified.
  • Round trip: export --id "Shop Fleet Overview" → import --name-override "… (copy)" → all 9 tiles of the copy rendered ok → re-running import --if-not-exists skipped with the existing id.
  • Inline-chart mode: create --name "CLI Smoke Test" --chart '…' --chart '…' auto-laid-out both tiles and both rendered ok; --if-not-exists on the same name skipped. (Test dashboards deleted afterwards.)
  • Created 5 saved searches via hdx saved-searches create (lucene + sql filters, tags) and confirmed with hdx saved-searches list.
  • yarn tsc --noEmit, yarn ci:lint, and prettier pass.

- hdx dashboards create --file <json>: validates against
  DashboardWithoutIdSchema locally, auto-generates missing tile ids,
  and POSTs to /dashboards
- hdx saved-searches list/create: list saved searches and create new
  ones (--name/--source/--where/--where-language/--select/--order-by/--tags),
  resolving sources by name or id
- fix getSavedSearches() to hit /saved-search (the API mounts the
  singular path; the old /saved-searches always 404'd and was silently
  swallowed)
@changeset-bot

changeset-bot Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: eb13849

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

This PR includes changesets to release 1 package
Name Type
@hyperdx/cli Minor

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 14, 2026 •

Copy link
Copy Markdown

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

2 Skipped Deployments
Project Deployment Actions Updated
hyperdx-oss Ignored Ignored Preview Oct 22, 2026 10:17pm UTC
hyperdx-storybook Ignored Ignored Preview Oct 22, 2026 10:17pm UTC

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Aug 14, 2026
@github-actions

github-actions Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 811 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 2
  • Production lines changed: 811
  • Branch: warren/cli-post-dashboard
  • Author: wrn14897

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

@greptile-apps

greptile-apps Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 3/5

[Medium impact] The changes affect dashboard and saved-search workflows in the CLI and TUI.

The PR needs fixes before merging because dashboard exports lose saved time ranges and child commands ignore parent flags.

Fix All in Claude CodeFindings

  1. P1 Exports lose saved time ranges ▶
  2. P1 Dashboard commands ignore parent flags ▶
  3. P2 CSV records can split ▶
  4. P2 Entry point exceeds file limit ▶

Summary

Adds dashboard authoring and saved-search commands:

  • Dashboard lists can be filtered by name and printed in several formats.
  • Dashboard exports produce definitions that can be imported again.
  • The CLI creates dashboards from JSON or charts and imports exported definitions.
  • Saved searches can be listed and created from the CLI, and the TUI can load them.

Reviews (5) · Last reviewed commit: "Merge branch 'main' into warren/cli-post..." · Reviewed by Greptile

Comment thread packages/cli/src/cli.tsx Outdated
@github-actions

github-actions Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found.

This PR adds hdx dashboards list/export/create/import and hdx saved-searches list/create commands plus the matching API-client methods. The saved-search route fix (/saved-searches → /saved-search) was verified correct against packages/api/src/api-app.ts:118, and the createSavedSearch payload (Omit<SavedSearch,'id'>) satisfies the server's SavedSearchSchema.omit({id}).extend({name}) POST validator. Input is schema-validated locally before any write. Findings are limited to test coverage and minor polish.

🟡 P2 — recommended

  • packages/cli/src/api/client.ts:1 -- Substantial new pure logic (csvField CSV quoting, makeTileId, chart-mode auto-layout, validateDashboardOrExit id-fill, --format/--where-language validation) ships with no unit tests, and the cli package appears to have no test for these paths.
    • Fix: Add unit tests covering csvField quoting edge cases, auto-layout tile assignment, tile-id backfill, and the format/where-language validation branches.
🔵 P3 nitpicks (4)
  • packages/cli/src/cli.tsx:1283 -- In dashboards create chart mode, auto-placement derives x/y from the chart's array index, so mixing layout-bearing --chart entries with layout-less ones can position auto-placed tiles on top of explicitly placed ones.
    • Fix: Compute the next free grid slot from already-placed tiles, or require all-or-nothing layout when any chart specifies a position.
  • packages/cli/src/cli.tsx:966 -- csvField quotes only for ", ,, and newline, leaving CSV output open to spreadsheet formula injection when a dashboard name or tag begins with =, +, -, or @.
    • Fix: Prefix leading formula characters with a safe quote, or document that the CSV output is not spreadsheet-safe.
  • packages/cli/src/cli.tsx:1196 -- The parent dashboards command and its list subcommand duplicate the full --app-url/--json/--format/--query option set, and the tag-splitting and source-name-resolution blocks are duplicated across dashboards create and saved-searches create.
    • Fix: Extract the shared option definitions and helpers so the two command paths cannot drift.
  • packages/cli/src/api/client.ts:566 -- The DashboardTile doc comment claims an "index signature escape hatch on top of the typed shape," but the alias is just type DashboardTile = Tile with no index signature.
    • Fix: Update the comment to match the actual alias.

Reviewers (11): correctness, testing, maintainability, project-standards, api-contract, reliability, kieran-typescript, adversarial, previous-comments, agent-native, learnings-researcher.

Testing gaps:

  • No automated coverage for the new CSV/JSON formatting, chart auto-layout, tile-id generation, or source/dashboard name resolution.
  • The prior Greptile help-text comment was verified addressed: create/import help now references the DashboardWithoutId schema and the export output shape, not the --json list output.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

@github-actions

github-actions Bot commented Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 457 passed • 1 skipped • 1576s

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

Tests ran across 4 shards in parallel.

View full report →

@vercel
vercel Bot temporarily deployed to Preview – hyperdx-storybook August 14, 2026 21:23 Inactive
…3, HDX-4004)

- hdx dashboards list (default subcommand): --query name filter and
  --format table|json|csv; --json kept as a back-compat alias
- hdx dashboards export --id <id-or-name>: emits a round-trip-safe
  definition (server-managed fields stripped, re-validated against
  DashboardWithoutIdSchema so export | import always round-trips)
- hdx dashboards create: new inline-chart mode (--name + repeatable
  --chart / --chart-file with auto grid layout) alongside --file, plus
  --if-not-exists for idempotent creation
- hdx dashboards import --file: recreates exported dashboards with
  --if-not-exists and --name-override
- add 'dashboard' alias for the command group
@wrn14897 wrn14897 changed the title feat(cli): add dashboards create and saved-searches commands [HDX-4003] [HDX-4004] feat(cli): dashboard list/export/create/import and saved-searches commands Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Duplicate code analysis

No new duplicate code introduced by this PR.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 116 in total (1.43%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

Comment thread packages/cli/src/cli.tsx
const doc: Record<string, unknown> = {
name: dashboard.name,
tags: dashboard.tags ?? [],
tiles: dashboard.tiles,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 major — dashboards export keeps each tile's config.alert, so export | import creates a second copy of every tile alert

getDashboards (packages/api/src/controllers/dashboard.ts:160-171) attaches the live alert to every tile.config.alert, and the TileSchema re-parse keeps it. On import, POST /dashboards → syncDashboardAlerts → createOrUpdateDashboardAlerts (controllers/alerts.ts:417) upserts a new alert for the new dashboard id. Every alert on the original then fires twice, and a cross-team import points at webhooks the importing team doesn't own. Remove config.alert from tiles before output, the way the web export does with TileTemplateSchema / *SavedChartConfigWithoutAlertSchema (packages/common-utils/src/types.ts:2208). You could also reuse convertToDashboardTemplate.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Comment thread packages/cli/src/cli.tsx
'savedQuery',
'savedQueryLanguage',
'savedFilterValues',
'savedSourceFilterValues',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Hand-written export key list drops savedDateRange and includes savedSourceFilterValues, a field that doesn't exist

DashboardSchema (common-utils/src/types.ts:2504) has savedDateRange, but the allowlist leaves it out, so the export → import round trip the PR promises loses the dashboard's saved time range. savedSourceFilterValues isn't defined anywhere in the repo. Drop the manual list: run DashboardWithoutIdSchema.safeParse(dashboard) directly. Zod already strips id, _id, createdAt and other unknown keys.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Comment thread packages/cli/src/cli.tsx
const tile: Record<string, unknown> = isTile
? { ...(chart as Record<string, unknown>) }
: { config: chart };
tile.x = tile.x ?? (i % 2) * 12;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Auto-layout places tiles by their index among all charts, so bare configs can overlap tiles that set their own position

x/y come from i, which also counts tiles that have explicit layout. Example: --chart '{"x":0,"y":0,"w":24,"h":4,"config":…}' --chart '<bare config>' puts the second tile at x=12, y=0, on top of the first. Lay out only the tiles that have no position, and start below the lowest y + h of the explicit tiles.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Comment thread packages/cli/src/cli.tsx
tags:
typeof opts.tags === 'string' && opts.tags.length > 0
? opts.tags
.split(',')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Comma-separated tag parsing is written out twice when splitAndTrimCSV already does it

The split/trim/filter pipeline appears in both dashboards create and saved-searches create (line 1695). Use splitAndTrimCSV from @hyperdx/common-utils/dist/core/utils, as the REQUIRED 'Before you add a type, schema, or helper' convention says.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Comment thread packages/cli/src/cli.tsx
* Random id for dashboard tiles, matching the web frontend's `makeId()`
* (packages/app/src/utils/tilePositioning.ts).
*/
const makeTileId = () =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — New dashboard authoring logic (tile id fill, auto-layout, export field stripping, round trip) has no tests

Move validateDashboardOrExit's id filling, the chart-to-tile layout code and the export document builder into a module under packages/cli/src/ that doesn't depend on Commander. Add tests in src/__tests__/ alongside tileConfig.test.ts. They should check that an exported dashboard re-parses with DashboardWithoutIdSchema with no alert fields and with savedDateRange kept, and that mixed explicit/bare charts don't overlap.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

@github-actions

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

5 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 4 minor

5 posted as inline comment(s) on the changed lines.


Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

Comment thread packages/cli/src/cli.tsx
Comment on lines +1305 to +1312
for (const key of [
'filters',
'savedQuery',
'savedQueryLanguage',
'savedFilterValues',
'savedSourceFilterValues',
'containers',
] as const) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Exports lose saved time ranges

export drops savedDateRange, even though the shared schema accepts it and the web page uses it as the dashboard’s default time range. Exporting and importing a dashboard with a saved range silently changes which period its charts show. Include savedDateRange in the exported fields.

Suggested change
for (const key of [
'filters',
'savedQuery',
'savedQueryLanguage',
'savedFilterValues',
'savedSourceFilterValues',
'containers',
] as const) {
for (const key of [
'filters',
'savedQuery',
'savedQueryLanguage',
'savedFilterValues',
'savedDateRange',
'savedSourceFilterValues',
'containers',
] as const) {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Comment thread packages/cli/src/cli.tsx

const dashboard = validateDashboardOrExit(parsedJson);

const client = await ensureSession(opts.appUrl);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dashboard commands ignore parent flags

Options before a dashboard subcommand are stored on dashboardsCmd, but the child actions read only their own opts. For example, hdx dashboards --app-url <other-server> create --file dashboard.json ignores the requested server and falls back to the saved session’s server. This can create the dashboard on the wrong server.

The same placement loses --json, --format, and --query. Merge the parent options into each child’s options while preserving explicit child values.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Comment thread packages/cli/src/cli.tsx

/** Quote a value for CSV output (RFC 4180 style). */
function csvField(value: string): string {
return /[",\n]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 CSV records can split

csvField does not quote values containing a carriage return (\r). Dashboard names and tags accept that character, so a value such as team\rwest is emitted as an unquoted field and can split a CSV record. Include \r in the characters that trigger quoting.

Suggested change
return /[",\n]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;
return /[",\r\n]/.test(value) ? `"${value.replaceAll('"', '""')}"` : value;

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Comment thread packages/cli/src/cli.tsx
Comment on lines +1195 to +1202
const dashboardsCmd = program
.command('dashboards')
.description('List dashboards with tile summaries')
.alias('dashboard')
.description('List, export, create, or import dashboards')
.option('-a, --app-url <url>', 'HyperDX app URL')
.option('--json', 'Output as JSON (for programmatic consumption)')
.option('--json', 'Output as JSON (shorthand for --format json)')
.option('--format <fmt>', 'Output format: table, json, or csv', 'table')
.option('--query <substr>', 'Filter dashboards by name (substring match)')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Entry point exceeds file limit

The new dashboard and saved-search commands add hundreds of lines to cli.tsx, which now has 2,416 lines. This violates the repository’s directive, “File Size: Max 300 lines - refactor when approaching limit.” Move the new command groups and helpers into focused modules. This repository requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

@kodiakhq
kodiakhq Bot merged commit 0b1e078 into main Oct 22, 2026
29 checks passed
@kodiakhq
kodiakhq Bot deleted the warren/cli-post-dashboard branch October 22, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants