Skip to content
Closed
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
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,20 @@ auto-generated per-PR notes; this file is the curated, human-readable history.

## [Unreleased]

### Fixed
- Keep the shared connection host visible after the server-version probe,
restore Dashboard filters to their initial default-derived active state, and
retain Dashboard time-range announcements when no ordinary filters exist.

### Added
- **Unified SQL Browser and Dashboard chrome.** Both surfaces now share the
same brand, surface, workspace, connection, examples, shortcuts, theme, and
user zones. Dashboard layout, tile count/search, time range, refresh, and
View/Edit controls live in a sticky primary tool row; ordinary parameters
live in a separate sticky filter row. Tile search matches effective titles
and descriptions without rerunning queries or changing saved layout/order,
and ordinary-filter Clear all restores defaults in one execution wave while
preserving every time-range pair.
- **Unified `/sql` routing for Workbench and Dashboard surfaces** (#407).
Workspace identity, surface, and presentation mode now live in canonical
`ws`, `surface`, and `mode` query parameters. Workbench/Dashboard switches
Expand All @@ -19,8 +32,7 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
falls back, and an empty workspace offers Create only in edit mode. The old
surface becomes an inert loading route before cross-workspace navigation,
while same-workspace Back/Forward switches immediately. Dashboard uses the
shared header row for its surface, tile count, File, active-style menu, name,
View/Edit, update, Refresh, and theme controls. Global Workbench shortcuts
shared application header and route controls. Global Workbench shortcuts
fail closed outside a ready matching route, and renderer generations let in-flight
durable writes finish without obsolete Dashboard or Workbench callbacks
repainting the selected surface. Direct Dashboard startup now shares the
Expand Down
79 changes: 62 additions & 17 deletions src/dashboard/application/dashboard-viewer-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export interface ViewerTileState {
tileId: string;
queryId: string;
title: string;
/** Dashboard-local description override, then the saved-query description. */
description: string;
status: ViewerTileStatus;
isKpi: boolean;
/** The resolved effective panel (base + variant + override), or null when the
Expand Down Expand Up @@ -171,7 +173,13 @@ export type DashboardLayoutView =
| { engine: 'grafana-grid'; grid: GrafanaGridLayoutModel; renderMode: GridRenderMode };

export interface DashboardViewState {
/** Search-matched tiles only, in their unchanged saved order. */
tiles: ViewerTileState[];
totalTileCount: number;
visibleTileCount: number;
tileSearch: string;
/** Filter ids whose current value/activation differs from its declared default. */
resettableFilterIds: string[];
filters: ViewerFilterState[];
layout: DashboardLayoutView;
/** Count of ACTIVE filter DEFINITIONS (not non-empty stored values, #188). */
Expand Down Expand Up @@ -301,9 +309,13 @@ export interface DashboardViewerSession {
/** Deactivate one filter WITHOUT discarding its value (reactivation restores
* it); one affected-panel wave (#188 clear-one). */
clearFilter(filterId: string): Promise<void>;
/** Reset every filter to its `defaultActive`/`defaultValue`, coalesced into
/** Restore every filter to its initial default-derived state, coalesced into
* ONE affected-panel wave (#188 clear-all). */
clearAllFilters(): Promise<void>;
/** Reset only the named filter definitions to their declared defaults. */
resetFilters(filterIds: readonly string[]): Promise<void>;
/** Set the transient presentation-only tile search. */
setTileSearch(query: string): void;
/** Abort one tile's in-flight request. */
cancelTile(tileId: string): void;
/** Adopt a layout/order-edited document (reorder, span/height, preset) WITHOUT
Expand Down Expand Up @@ -421,6 +433,14 @@ const toParamValue = (value: unknown): unknown =>
* state must never alias an array a caller (the document, the persisted
* seed, `setFilter`/`applyFilter`'s own caller) still holds a reference to. */
const copyValue = (value: unknown): unknown => (Array.isArray(value) ? value.slice() : value);
const filterInitialActive = (def: DashboardFilterDefinitionV1): boolean =>
def.defaultActive ?? (Array.isArray(def.defaultValue)
? def.defaultValue.length > 0
: def.defaultValue != null && def.defaultValue !== '');
const filterDefaultValue = (def: DashboardFilterDefinitionV1): string | string[] =>
(Array.isArray(def.defaultValue)
? def.defaultValue.map(toValueString)
: toValueString(def.defaultValue));

/** Local copy of `effectiveFilterActive` (state.ts is off-limits to this
* layer): a param with an explicit activation entry uses it; otherwise a
Expand Down Expand Up @@ -469,6 +489,7 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
// outside `documentRef` — never read/written by any command, never
// persisted. A fresh session always starts at 'tiles'.
let gridRenderMode: GridRenderMode = 'tiles';
let tileSearch = '';
// #335: the single wall-clock snapshot the LATEST execution wave resolved its
// relative tokens against (published as `state.waveWallNowMs`). `null` until
// the first wave; every wave entry point (`refresh`/`runAffectedWave`/the
Expand Down Expand Up @@ -505,8 +526,10 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
const isText = type === 'text';
const explicit: Panel | null = isObject(panel) && isObject(panel.cfg) ? (panel as unknown as Panel) : null;
const title = (typeof tile.title === 'string' && tile.title) || (query ? queryName(query) : tile.queryId) || tile.id;
const description = (typeof tile.description === 'string' && tile.description)
|| (typeof query?.spec?.description === 'string' ? query.spec.description : '');
const state: ViewerTileState = {
tileId: tile.id, queryId: tile.queryId, title, isKpi, panel,
tileId: tile.id, queryId: tile.queryId, title, description, isKpi, panel,
status: presentationError ? 'error' : 'idle',
columns: null, rows: null, meta: null,
error: presentationError ? presentationError.message : null,
Expand All @@ -529,14 +552,8 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa

// Filter runtime records, in filter order.
const filters: FilterRuntime[] = (Array.isArray(documentRef.filters) ? documentRef.filters : []).map((def) => {
const defaultValue = copyValue(def.defaultValue ?? '');
// Merge-gate review (Finding B): array-aware — an omitted `defaultActive`
// infers INACTIVE from an empty array default (`[]`, matching `''`), and
// ACTIVE from a non-empty one, the same length-based rule `setFilter`'s
// own value-implies-active already applies to a committed array.
const defaultActive = def.defaultActive ?? (Array.isArray(def.defaultValue)
? def.defaultValue.length > 0
: (def.defaultValue != null && def.defaultValue !== ''));
const defaultValue = filterDefaultValue(def);
const defaultActive = filterInitialActive(def);
// #303: a persisted seed for this filter's id overrides the pure-default
// init above (untouched when `initialFilters` is absent/empty, or has no
// entry for `def.id`). #189: `copyValue` defends against aliasing the
Expand Down Expand Up @@ -872,7 +889,15 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa

function buildState(running: boolean, updatedAt: number | null): DashboardViewState {
const mobile = !!deps.isMobile?.();
const visible = tiles.map((runtime) => ({ id: runtime.tile.id, isKpi: runtime.isKpi }));
const normalizeSearch = (value: string): string =>
value.trim().replace(/\s+/g, ' ').toLocaleLowerCase();
const normalizedSearch = normalizeSearch(tileSearch);
const visibleRuntimes = normalizedSearch
? tiles.filter((runtime) =>
normalizeSearch(runtime.state.title).includes(normalizedSearch)
|| normalizeSearch(runtime.state.description).includes(normalizedSearch))
: tiles;
const visible = visibleRuntimes.map((runtime) => ({ id: runtime.tile.id, isKpi: runtime.isKpi }));
// #291: route to whichever engine the CURRENT document's layout resolves
// to (`resolveLayoutPluginSync` — the same sync helper the application
// layer's other non-awaitable call sites use, since this runs on every
Expand All @@ -891,7 +916,15 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
}
: { engine: 'flow', ...computeFlowLayout({ tiles: visible, layout: documentRef.layout, mobile }) };
return {
tiles: tiles.map((runtime) => ({ ...runtime.state })),
tiles: visibleRuntimes.map((runtime) => ({ ...runtime.state })),
totalTileCount: tiles.length,
visibleTileCount: visibleRuntimes.length,
tileSearch,
resettableFilterIds: filters.filter((filter) => {
const defaultActive = filterInitialActive(filter.def);
const defaultValue = filterDefaultValue(filter.def);
return filter.state.active !== defaultActive || !sameSelection(filter.state.value, defaultValue);
}).map((filter) => filter.def.id),
filters: filters.map((filter) => ({ ...filter.state })),
layout,
activeFilterCount: filters.filter((filter) => filter.state.active).length,
Expand Down Expand Up @@ -1662,27 +1695,39 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
}

async function clearAllFilters(): Promise<void> {
await resetFilters(filters.map((filter) => filter.def.id));
}

async function resetFilters(filterIds: readonly string[]): Promise<void> {
if (destroyed) return;
const ids = new Set(filterIds);
const changed: string[] = [];
for (const filter of filters) {
const nextActive = filter.def.defaultActive ?? false;
if (!ids.has(filter.def.id)) continue;
const nextActive = filterInitialActive(filter.def);
// #189: `copyValue` defends the default against aliasing (a
// `defaultValue` array literal on the document); `sameSelection`
// (filter-selection.ts) compares STRUCTURALLY so an array value/default
// never falls through the old `!==` reference check into a spurious
// "changed" on every reset.
const nextValue = copyValue(filter.def.defaultValue ?? '');
const nextValue = filterDefaultValue(filter.def);
if (filter.state.active !== nextActive || !sameSelection(filter.state.value, nextValue)) {
changed.push(filter.def.parameter);
}
filter.state.active = nextActive;
filter.state.value = nextValue;
}
publish();
if (changed.length) publish();
// Coalesce every reset into ONE affected-panel wave (#188 clear-all).
if (changed.length) await commitAndRerun(changed);
}

function setTileSearch(query: string): void {
if (destroyed || tileSearch === query) return;
tileSearch = query;
publish();
}

function cancelTile(tileId: string): void {
const runtime = tiles.find((entry) => entry.tile.id === tileId);
if (!runtime) return;
Expand Down Expand Up @@ -1735,7 +1780,7 @@ export function createDashboardViewerSession(deps: DashboardViewerDeps): Dashboa
return {
state: stateSignal as ReadonlySignal<DashboardViewState>,
controls, timeRangeGroups, getFilterField,
start, refresh, refreshTile, setFilter, applyFilter, applyFilters, clearFilter, clearAllFilters,
cancelTile, syncDocument, setGridRenderMode, destroy,
start, refresh, refreshTile, setFilter, applyFilter, applyFilters, clearFilter, clearAllFilters, resetFilters,
setTileSearch, cancelTile, syncDocument, setGridRenderMode, destroy,
};
}
Loading
Loading