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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ auto-generated per-PR notes; this file is the curated, human-readable history.

## [Unreleased]

### Fixed
- **Dashboard tiles now respect an explicit saved `view: "table"`** (#368).
The compatibility form resolves to a Table base presentation before runtime
panel detection, while an explicit `panel` and its variants/overrides retain
their existing precedence.

### Added
- **Searchable multiselect for query-backed Dashboard filters** (#189). A
source-backed filter whose executable consumers agree on one `Array(T)`
Expand Down
16 changes: 15 additions & 1 deletion src/dashboard/model/presentation-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,21 @@ export function resolvePresentation(input: ResolvePresentationInput): ResolvePre
({ ok: false, diagnostics: sortDiagnostics(diagnostics) });

const spec = isObject(query) ? query.spec : undefined;
const basePanel = isObject(spec) && isObject(spec.panel) ? cloneJson(spec.panel) : {};
// `view: 'table'` predates the first-class panel form but is still an
// explicit saved presentation choice. Preserve it as a Table base before
// deriving any runtime panel: otherwise resolvePanel() sees no renderer and
// correctly auto-detects a chart, KPI, or Logs panel instead. A panel object
// without its own `cfg` is metadata, not an explicit renderer, so retain
// that metadata while supplying the compatibility Table cfg. An existing
// cfg (including a malformed/null one) remains authoritative and is left to
// normal validation rather than silently repaired.
const persistedPanel = isObject(spec) && isObject(spec.panel)
? cloneJson(spec.panel)
: {};
const hasExplicitCfg = isObject(spec) && isObject(spec.panel) && Object.hasOwn(spec.panel, 'cfg');
const basePanel = isObject(spec) && spec.view === 'table' && !hasExplicitCfg
? { ...persistedPanel, cfg: { type: 'table' } }
: persistedPanel;
const baseType = cfgType(basePanel);
const dashboard = isObject(spec) && isObject(spec.dashboard) ? spec.dashboard : undefined;
const variants = dashboard && isObject(dashboard.variants) ? dashboard.variants : undefined;
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/dashboard-viewer-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,28 @@ describe('createDashboardViewerSession', () => {
expect(session.state.value.tiles[0].isKpi).toBe(false);
});

it('keeps an explicit legacy Table view on a chartable, log-shaped, and scalar Dashboard tile', async () => {
const { exec } = makeExec((sql) => {
if (sql.includes('chart')) return { columns: [{ name: 'time', type: 'DateTime' }, { name: 'value', type: 'UInt64' }], rows: [['2026-01-01', 1], ['2026-01-02', 2]] };
if (sql.includes('logs')) return { columns: [{ name: 'event_time', type: 'DateTime' }, { name: 'message', type: 'String' }], rows: [['2026-01-01', 'hello']] };
return { columns: [{ name: 'value', type: 'UInt64' }], rows: [[1]] };
});
const document = doc({ tiles: [tile('chart', 'chart'), tile('logs', 'logs'), tile('scalar', 'scalar')] });
const session = createDashboardViewerSession(makeDeps({
document, exec,
queries: [
query('chart', 'SELECT chart', { view: 'table' }),
query('logs', 'SELECT logs', { view: 'table' }),
query('scalar', 'SELECT scalar', { view: 'table' }),
],
}));
await session.start();
expect(session.state.value.tiles.map((entry) => entry.panel)).toEqual([
{ cfg: { type: 'table' } }, { cfg: { type: 'table' } }, { cfg: { type: 'table' } },
]);
expect(session.state.value.tiles.every((entry) => !entry.isKpi)).toBe(true);
});

it('shows an unfilled tile when a required param has no value, issuing no request', async () => {
const { exec, calls } = makeExec();
const document = doc({ tiles: [tile('t1', 'q1')] });
Expand Down
52 changes: 52 additions & 0 deletions tests/unit/presentation-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,58 @@ describe('resolvePresentation', () => {
if (result.ok) expect(result.panel).toEqual(basePanel());
});

it('treats the legacy explicit Table view as a Table base presentation', () => {
const query = { id: 'q', sql: 'SELECT a,b', specVersion: 1, spec: { name: 'q', view: 'table' } };
const result = resolvePresentation({ query, tile: tileFor() });
expect(result).toEqual({ ok: true, panel: { cfg: { type: 'table' } } });
});

it('adds a Table cfg while preserving panel metadata when cfg is absent', () => {
const emptyPanel = {
id: 'q', sql: 'SELECT a', specVersion: 1,
spec: { name: 'q', view: 'table', panel: {} },
};
const fieldConfigPanel = {
id: 'q', sql: 'SELECT a', specVersion: 1,
spec: { name: 'q', view: 'table', panel: { fieldConfig: { defaults: { decimals: 2 } } } },
};
expect(resolvePresentation({ query: emptyPanel, tile: tileFor() })).toEqual({
ok: true, panel: { cfg: { type: 'table' } },
});
expect(resolvePresentation({ query: fieldConfigPanel, tile: tileFor() })).toEqual({
ok: true, panel: { cfg: { type: 'table' }, fieldConfig: { defaults: { decimals: 2 } } },
});
});

it('leaves a query without an explicit presentation unconfigured for runtime auto-detection', () => {
const query = { id: 'q', sql: 'SELECT a,b', specVersion: 1, spec: { name: 'q' } };
const result = resolvePresentation({ query, tile: tileFor() });
expect(result).toEqual({ ok: true, panel: {} });
});

it('keeps an explicit panel authoritative over the legacy Table view', () => {
const query = {
id: 'q', sql: 'SELECT a', specVersion: 1,
spec: { name: 'q', view: 'table', panel: { cfg: { type: 'kpi' } } },
};
const result = resolvePresentation({ query, tile: tileFor() });
expect(result).toEqual({ ok: true, panel: { cfg: { type: 'kpi' } } });
});

it('applies named variants and tile overrides over the legacy Table base', () => {
const query = {
id: 'q', sql: 'SELECT a', specVersion: 1,
spec: { name: 'q', view: 'table', dashboard: { variants: { compact: { fieldConfig: { defaults: { decimals: 1 } } } } } },
};
const result = resolvePresentation({
query, tile: tileFor({ variant: 'compact', override: { fieldConfig: { columns: { a: { unit: 'ms' } } } } }),
});
expect(result).toEqual({ ok: true, panel: {
cfg: { type: 'table' },
fieldConfig: { defaults: { decimals: 1 }, columns: { a: { unit: 'ms' } } },
} });
});

it('applies a valid named variant patch', () => {
const query = makeQuery({ variants: { alt: { fieldConfig: { defaults: { unit: 'ms' } } } } });
const result = resolvePresentation({ query, tile: tileFor({ variant: 'alt' }) });
Expand Down
Loading