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
24 changes: 23 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,29 @@ auto-generated per-PR notes; this file is the curated, human-readable history.
name, the chart now draws on the **tile's own background** (instead of the
darker results-table background), and the value-axis **gridlines are hidden**
on tiles (they read as noisy light lines on a dark panel). Drag-to-reorder and
1/2-column tile spans arrive in the next phase (#149 D3).
1/2-column tile spans arrive in a later phase (#149 D4).
- **Dashboard (phase 3): global filter bar** (#149, #152). A **filter bar** in
the dashboard toolbar renders one text field per `{name:Type}` parameter
detected across every favorited tile's SQL (`dashboardParams`, unique by
name, first-appearance order) — absent entirely when no favorite has one.
Fields share the same persisted `state.varValues` the SQL Browser workbench
already uses (#134): a value typed on the dashboard shows up in the
workbench's variable strip for the same name, and vice versa. Typing
debounces (~500 ms idle) before re-running only the tiles that reference the
changed name — not the whole grid; Enter or blur commits immediately,
bypassing the debounce. A tile whose SQL still has an empty/absent parameter
never runs its query — it shows a distinct "Enter a value for: …" placeholder
(excluded from the "N not shown" count, since one filter value away it
becomes chartable). Tiles now live in **stable per-favorite slots** built up
front and updated in place (loading/unfilled/error/chart) rather than
inserted/removed, so a filter-driven tile flipping states repeatedly never
reorders the grid or orphans its identity; each slot's fetch carries a
monotonically increasing generation counter so a superseded in-flight
response can never overwrite a newer edit's result. `ch-client.js`'s
`queryJson`/`queryDashboardTile` gained an optional `params` argument
(backward compatible) to forward `param_<name>` args to ClickHouse. Per-tile
Type/X/Y overrides, KPI tiles, and dropdown/cascading filters arrive in later
phases (#149 D5–D7).
- **Schema-aware, FROM-driven autocompletion** (#84) — column completion now
fires *while you type*, driven by the statement's `FROM`/`JOIN` clause, so you
no longer have to expand a table in the sidebar first. A new pure module
Expand Down
24 changes: 24 additions & 0 deletions src/core/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { autoChart, chartCfgValid, cloneChartCfg, normalizeChartCfg } from './chart-data.js';
import { withTrailingFormat } from './format.js';
import { readStatementParams } from './query-params.js';

/**
* True on the standalone dashboard route (a path ending in `/dashboard`,
Expand Down Expand Up @@ -84,6 +85,29 @@ export function parseJsonResult(json) {
};
}

/**
* The union of every `{name:Type}` parameter referenced by any favorite's
* row-returning SQL (#149 D3): unique by name, first-appearance order
* (favorite order, then in-SQL order — `readStatementParams`' own order per
* favorite). Drives which fields the dashboard's global filter bar renders;
* a favorite with no row-returning statement contributes nothing. Pure.
* @param {{sql: string}[]} favorites
* @returns {{name: string, type: string}[]}
*/
export function dashboardParams(favorites) {
const out = [];
const seen = new Set();
for (const fav of favorites || []) {
for (const p of readStatementParams(fav.sql)) {
if (!seen.has(p.name)) {
seen.add(p.name);
out.push(p);
}
}
}
return out;
}

/**
* Classify a favorite's result into a dashboard tile. In D1:
* - 0 rows → skip (empty)
Expand Down
16 changes: 10 additions & 6 deletions src/net/ch-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ export async function authedFetch(ctx, url, sql, signal) {
/**
* Run a query and return parsed JSON (FORMAT JSON). Throws on CH error. `signal`
* (optional) aborts the request. `extra` (optional) adds HTTP query-string
* settings (e.g. `{ readonly: 2 }` for a read-only tile).
* settings (e.g. `{ readonly: 2 }` for a read-only tile). `params` (optional)
* adds `param_<name>` query-string args for native ClickHouse query parameters
* (#134) — omitted for every existing call site, so this is backward compatible.
*/
export async function queryJson(ctx, sql, signal, extra) {
const resp = await authedFetch(ctx, chUrl(ctx.origin, { format: 'JSON', extra }), sql, signal);
export async function queryJson(ctx, sql, signal, extra, params) {
const resp = await authedFetch(ctx, chUrl(ctx.origin, { format: 'JSON', extra, params }), sql, signal);
if (!resp.ok) throw new Error(parseExceptionText(await resp.text()));
return resp.json();
}
Expand All @@ -93,10 +95,12 @@ export async function queryJson(ctx, sql, signal, extra) {
* the `readonly=2` HTTP setting, so a favorite that happens to contain a write
* (INSERT / ALTER / DROP / …) is rejected server-side rather than executed when
* the dashboard opens or refreshes — level 2 still permits SELECT and
* query-level `SETTINGS`. Returns parsed JSON; throws CH's reason on error.
* query-level `SETTINGS`. `params` (optional, #149 D3) forwards `param_<name>`
* args for the dashboard's global filter bar. Returns parsed JSON; throws CH's
* reason on error.
*/
export function queryDashboardTile(ctx, sql, signal) {
return queryJson(ctx, sql, signal, { readonly: 2 });
export function queryDashboardTile(ctx, sql, signal, params) {
return queryJson(ctx, sql, signal, { readonly: 2 }, params);
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,11 @@ table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }
.dash-cols-wrap { display: inline-flex; align-items: center; gap: 8px; }
.dash-seg-cols .dash-seg-btn { padding: 4px 10px; font-family: var(--mono); }
.dash-seg-label { font-size: 11px; color: var(--fg-faint); }
/* Global filter bar (#149 D3) — one field per detected {name:Type}, reusing the
workbench's var-field/var-name/var-input classes. Sits in the toolbar's
existing flex-wrap row (no new sticky/scroll container needed); absent
entirely (display:none, no gap) when no favorite has a param. */
.dash-filters { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.dash-icobtn {
width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center;
background: var(--bg-chip); color: var(--fg-mute); border: 1px solid var(--border);
Expand Down Expand Up @@ -1845,6 +1850,11 @@ table.res-table tbody tr:hover td.idx { background: var(--bg-hover); }
border: 1px solid var(--error-bd); border-radius: 8px; font-size: 12px;
font-family: var(--mono); max-width: 90%;
}
/* A tile blocked on an empty {name:Type} filter value (#149 D3) — one filter
value away from chartable, so it reads as a prompt, not an error. */
.dash-tile-unfilled {
margin: auto; padding: 12px; color: var(--fg-mute); font-size: 12px; text-align: center;
}
.dash-tile-foot {
display: flex; align-items: center; gap: 12px; padding: 7px 12px;
border-top: 1px solid var(--border-faint); font-family: var(--mono);
Expand Down
12 changes: 8 additions & 4 deletions src/ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1647,9 +1647,12 @@ export function createApp(env = {}) {

// Run one favorite's SQL for a dashboard tile: read-only (writes rejected
// server-side by queryDashboardTile), FORMAT JSON, transformed to the
// array-row shape renderChart wants. Returns { columns, rows, meta } on
// success or { error } on failure. The token is resolved up front by
// ensureFreshToken (above), so this does not itself drive sign-out.
// array-row shape renderChart wants. Substitutes the shared `state.varValues`
// as `param_<name>` args (#149 D3), mirroring the workbench's run() — a
// no-op when the tile's SQL has no `{name:Type}` placeholder. Returns
// { columns, rows, meta } on success or { error } on failure. The token is
// resolved up front by ensureFreshToken (above), so this does not itself
// drive sign-out.
async function runTile(sql) {
try {
// ensureConfig + getToken are inside the try: getToken→refresh can THROW on
Expand All @@ -1659,7 +1662,8 @@ export function createApp(env = {}) {
// cheap and keeps runTile usable on its own.
await ensureConfig();
if (!(await getToken())) return { error: 'Not signed in' };
const json = await ch.queryDashboardTile(chCtx, dashboardTileSql(sql));
const json = await ch.queryDashboardTile(chCtx, dashboardTileSql(sql), undefined,
paramArgs(sql, app.state.varValues));
return parseJsonResult(json);
} catch (e) {
return { error: String((e && e.message) || e) };
Expand Down
Loading