Skip to content

feat(analytics): add useMetricView client experience - #488

Merged
atilafassina merged 75 commits into
mainfrom
mv-hook
Aug 12, 2026
Merged

feat(analytics): add useMetricView client experience#488
atilafassina merged 75 commits into
mainfrom
mv-hook

Conversation

@atilafassina

@atilafassinaatilafassina commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Delivers the complete Metric Views client experience on top of #512: applications can query typed Metric Views, format results from catalog metadata, and build interactive cross-filtering charts without duplicating query or presentation logic.

Delivered

  • Add useMetricView with inferred selected-row types, metadata, loading/error state, cancellation, HMR refresh, and shared warehouse-readiness reporting.
  • Consolidate the Analytics SSE lifecycle used by useMetricView and useAnalyticsQuery.
  • Add framework-agnostic formatValue, formatLabel, toD3Format, and toMetricFilter utilities.
  • Add chart click and controlled-selection APIs (onDataClick and selected) for cross-filter interactions.
  • Add an end-to-end Metric Views playground covering metadata-driven labels, formatting, filters, charts, and table output.
  • Document hook usage, formatting, filters, and Plotly/ECharts integration.
  • Cover hook typing, SSE behavior, format/filter utilities, chart interactions, and the playground integration with focused tests.

Stacked on #512.

@github-actions

Copy link
Copy Markdown
Contributor

🔬 Run evals on this PR · Go to Evals Monitor →

@github-actions

github-actionsBot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 834 KB (-2.4 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

distrawgzip
JS (runtime)863 KB (-1.0 KB)301 KB (-552 B)
Type declarations314 KB109 KB
Source maps1.7 MB (-8.4 KB)562 KB (-2.6 KB)
Other11 KB3.7 KB
Total2.8 MB (-9.4 KB)975 KB (-3.2 KB)
Per-entry composition (own code — deps external (as shipped))
EntryInitial (gz)Lazy (gz)Total (gz)node_modules (min)Own code (min)
.88 KB2.5 KB91 KBexternal288 KB
./beta48 KB459 B48 KBexternal141 KB
./type-generator21 KB0 B21 KBexternal60 KB

Chunks:

EntryChunkLoadSize (gz)
.index.jsinitial84 KB
.utils.jsinitial4.0 KB
.remote-tunnel-manager.jslazy2.5 KB
./betabeta.jsinitial32 KB
./betastream-manager.jsinitial5.8 KB
./betawide-event-emitter.jsinitial3.2 KB
./betadatabricks.jsinitial3.0 KB
./betaconfiguration.jsinitial2.1 KB
./betaservice-context.jsinitial1.3 KB
./betaclient.jsinitial431 B
./betaclient-options.jsinitial220 B
./betasupervisor-api.jslazy194 B
./betadatabricks.jslazy142 B
./betaindex.jslazy123 B
./type-generatorindex.jsinitial21 KB

@databricks/appkit-ui

npm tarball (packed): 342 KB (+1003 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

distrawgzip
JS (runtime)390 KB (+752 B)130 KB (+192 B)
Type declarations228 KB (+281 B)83 KB (+61 B)
Source maps753 KB (+2.1 KB)248 KB (+754 B)
CSS16 KB3.3 KB
Total1.4 MB (+3.1 KB)464 KB (+1007 B)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
EntryInitial (gz)Lazy (gz)Total (gz)node_modules (min)Own code (min)
./js5.3 KB49 KB55 KB208 KB14 KB
./js/beta20 B0 B20 B0 B0 B
./react432 KB (+99 B)49 KB480 KB (+99 B)1.3 MB175 KB (+265 B)
./react/beta1.0 KB0 B1.0 KB0 B1.9 KB

Chunks:

EntryChunkLoadSize (gz)
./jsindex.jsinitial5.2 KB
./jschunkinitial120 B
./jsapache-arrowlazy49 KB
./js/betabeta.jsinitial20 B
./reactindex.jsinitial430 KB
./reacttslibinitial2.1 KB
./reactapache-arrowlazy49 KB
./react/betabeta.jsinitial1.0 KB

@atilafassinaatilafassina changed the title feat(analytics): metric-view runtime hook + format utilities + payload metadata (PR5)feat(analytics): useMetricViewsJul 23, 2026
@atilafassina
atilafassinaforce-pushed the mv-hook branch 2 times, most recently from 7456890 to 613d5cbCompareJuly 29, 2026 08:24
@github-actions

github-actionsBot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 31596951960 -R databricks/appkit -n appkit-template-0.58.0-pr.3330d85-mv-hook-488 -D appkit-pr-488 \
&& unzip -o "appkit-pr-488/appkit-template-0.58.0-pr.3330d85-mv-hook-488.zip" -d "appkit-pr-488" \
&& databricks apps init --template "appkit-pr-488"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

Freeze the three shared seams the metric-view hook/server/generator phases
compile against:
- S1: MetricColumnMeta + MetricViewsMetadata value types in packages/shared
- S2: optional per-column metadata on the SSE result message + makeResultMessage
- S3: base MetricRegistry, MetricKey, Infer* helpers, MetricFilter mirrors, and
UseMetricViewOptions/UseMetricViewResult in appkit-ui hook types
Types only (plus a makeResultMessage passthrough); existing /query callers are
unchanged since metadata is optional.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
… metadata (PR5 1-4)
Implement the four parallel PR5 slices against the frozen phase-0 contracts:
- Generator (type-generator): emit metric-views.ts (was .d.ts) carrying both
the erasable declare-module MetricRegistry augmentation and a runtime
`export const metricViewsMetadata = {...} as const`. Header is a type-only
import (no runtime side-effect import on the Node server). Rename propagated
through METRIC_TYPES_FILE, mvOutFile, vite-plugin, CLI announce, and tests;
generated .ts added to Biome ignore.
- Server (analytics plugin): accept an injected `metricViewsMetadata` config
and stamp the responding metric's per-column slice (scoped to the requested
measures/dimensions) into the SSE result payload. Metadata is response
decoration — it never enters composeMetricCacheKey and never alters SQL.
- Hook (appkit-ui): `useMetricView(key, opts)` mirroring useAnalyticsQuery
(SSE, abort-on-arg-change, autoStart), returning
{ data, loading, error, errorCode, metadata }.
- Formatters (appkit-ui js): pure, React-free, tree-shakeable formatValue /
formatLabel / toD3Format taking the format spec / column metadata as args.
Also fix a pre-existing latent port collision: analytics.integration.test.ts
and server.integration.test.ts both hardcoded port 9879; under the added
metric-test weight they could bind concurrently in the shared vitest worker
pool, so an analytics request hit the server-plugin app and 404'd. Switch the
analytics integration test to an OS-assigned ephemeral port (port: 0), matching
the files plugin integration test.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
…se 5)
Integrate the metric-view runtime in dev-playground and document it:
- Regenerate the generated artifact as shared/appkit-types/metric-views.ts
(delete the legacy .d.ts). Verified byte-for-byte identical to a live
`generate-types` DESCRIBE against a real UC Metric View (warehouse
dd43ee29fedd958d, dogfood): display_name/format/description genuinely flow
from the UC YAML through typegen into the runtime metricViewsMetadata const.
- Inject the const server-side: analytics({ metricViewsMetadata }).
- Add a /metric-views demo route calling useMetricView("revenue", …) with
timeGrain/timeDimension, rendering a chart + table whose labels and value
formats come from the payload metadata (never hand-typed), degrading
gracefully when metadata is absent.
- Docs: extend plugins/analytics.md with the useMetricView + format-utility +
metricViewsMetadata injection story (Plotly + ECharts examples), and fix the
stale metric-views.d.ts references in development/type-generation.md.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
… builder to /js
Move the twelve-operator MetricFilter grammar out of react/hooks/types.ts into a
canonical, framework-agnostic js/metric-filter/ module and add a toMetricFilter
builder that compiles a { dimension -> value(s) } shorthand into a MetricFilter
(scalar -> equals, array -> in, omit undefined/empty). react/hooks/types.ts now
re-exports the types so the /react public surface and UseMetricViewOptions.filter
are unchanged. Wire the dev-playground metric-views route's buildFilter onto
toMetricFilter, keeping only the app-specific cross-filter facet-exclusion local.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Adds two public chart props, inherited by every chart type via the factory:
- onDataClick?(datum: ChartClickDatum): fire-and-forget click callback. base.tsx
builds a memoized internal echarts onEvents={{ click }} only when the handler is
set (no idle listener), mapping raw params via the pure mapToDatum. Pointer-only
(canvas) — documented to require a keyboard-accessible equivalent.
- selected?: string | string[]: controlled, name-based visual emphasis. base.tsx
runs the pure applySelectionEmphasis transform over the built option so matching
bar/pie-donut categories stay prominent and the rest dim; no-op when unset.
ChartClickDatum is the only new public (barrel) symbol; mapToDatum,
applySelectionEmphasis and SelectionEmphasisOptions are internal. echarts types
stay out of the public API (datum.raw is unknown). Phases 1 and 2 are committed
together so the producer helpers have their consumer (satisfies knip).
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
…egment charts
Wire the new appkit-ui chart props into the metric-views demo: the region BarChart
and segment DonutChart get onDataClick={(d) => setDimension(dim, d.name)} — reusing
the same setDimension the table row-click uses — and selected={selection[dim]} so the
clicked category is emphasized. LineChart, Table, and the existing (keyboard-accessible)
table row-click are unchanged.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
… review cleanup
Address adversarial-review findings on the useMetricView / metric-route branch:
- Type soundness: infer rows from the selected measure/dimension tuples
(PickMetricRow) and correlate timeDimension/timeGrain to temporal dims only.
- Formatting: preserve every currency symbol the generator emits end-to-end and
keep bigint precision (no Number() rounding).
- Cache correctness: stamp fresh per-column metadata AFTER the cached execute()
so a cache hit never serves stale labels/formats after a redeploy.
- Charts: guard selected="" as a no-op, split [x,y] click tuples into x/y, and
memoize onEvents on handler presence (no listener thrash per SSE tick).
- Typegen: sweep a stale sibling metric-views.d.ts on upgrade and reject a
.d.ts mvOutFile.
- Drop the unused public notify export and the fake "Write back" demo; remove
the dead autoStart option; align AnalyticsStreamMessage; tighten the biome
ignore; add tests + comment cleanup.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Drop comments that restated adjacent JSDoc/functions:
- base.tsx: the inline-handler re-subscribe rationale lived in both the
`interactive` and `onEvents` comments; keep it once at `onEvents` (where
the subscription happens) and point the `interactive` note at it.
- use-metric-view.ts: result-branch comment re-explained metadata narrowing
already documented on `asMetricMetadata`; defer to that doc.
- js/format/index.ts: call-site comment restated `currencyPrefix`'s own JSDoc.
- render-types.ts: convert `generateMetricTypeDeclarations`'s // block to
/** */ so the exported function's rationale surfaces on IDE hover.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
useMetricView never implemented autoStart — the row was copied from
useAnalyticsQuery's options table. The hook's effect calls start()
unconditionally and UseMetricViewOptions has no such field.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
- Centralize analytics SSE parsing across React hooks
- Share metric filter types and runtime operator vocabulary
- Reuse metadata and label formatters; refresh size baseline
Signed-off-by: Atila Fassina <atila@fassina.eu>
- analytics.md: make the Plotly and ECharts chart-library references
links to plotly.com/javascript and echarts.apache.org; minor wording
tidy (hardcode).
- charts/types.ts: trim the ChartClickDatum doc comment.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Phase 1 of typegen-ci-resilient-describe. In blocking (`--wait`) mode the
type generator now suppresses the `.d.ts` write on ANY degraded result
(query `result: unknown` or degraded metric), leaving the committed types
untouched as the CI fallback of record, then throws as before. The prior
path wrote degraded (`unknown`) declarations first and threw after, which
clobbered good committed types on a fresh CI checkout — including via the
auth/timeout/bad-id/DELETED fatal-degrade path.
Non-blocking mode is unchanged (still writes degraded types for the
detached worker to refresh). Tests inverted to assert no-write-on-degrade
while preserving throw + behavioral assertions; adds coverage for the
query-side fatal-degrade clobber-prevention case.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Phase 2 of typegen-ci-resilient-describe. Adds a pure
classifyBlockingFailure(error) → "deterministic" | "environmental" to
type-generator/errors.ts, building on the existing getErrorStatus and
isConnectivityError helpers.
Deterministic (build must crash regardless of committed types): HTTP 404
(bad warehouse id) and 400 (malformed request), checked first and walked
through cause/AggregateError chains. Environmental (has-types gate applies
later): 401/403 auth, connectivity, DELETED/DELETING, wait-timeout, and any
unrecognized failure (the default). The auth status set is a one-line
change point for the auth-owning team. No behavior change to
isConnectivityError. Adds tests/errors.test.ts.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
…wait
Phase 3 of typegen-ci-resilient-describe (join point). Wires the
classifyBlockingFailure taxonomy into the reordered blocking write path so
`--wait` degrades gracefully on environmental failures instead of always
crashing:
- Deterministic failures (SQL syntax, HTTP 404/400) still crash the build.
- Environmental failures (401/403 auth, connectivity, DELETED/DELETING,
wait-timeout, unrecognized) now flow through a has-types gate: if committed
analytics/metric-views .d.ts exist, skip the (already-suppressed) write,
emit one loud greppable stderr warning naming the coarse cause
(auth blocked / warehouse unreachable / warehouse unavailable) + warehouse
id, and exit 0 using the committed types as the fallback of record. If no
committed types exist, crash with a generic 'run generate-types --wait
locally' remedy.
Serving types are excluded from the gate (gitignored, degrade independently).
Non-blocking mode is unchanged. Threads deterministic-vs-environmental and a
coarse cause label out of the query + metric preflights. Adds gate-matrix
coverage: environmental+present (per cause) → warning+exit0, environmental+
absent → crash, deterministic (404/400/syntax) → crash regardless of types,
partial presence, serving-exclusion, and CI-safe (ANSI-free) warning output.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Phase 4 of typegen-ci-resilient-describe. Updates the type-generation docs
to describe the committed-types fallback and two-bucket failure taxonomy for
blocking (`--wait`) builds: committed .d.ts as the fallback of record,
--wait never overwriting good types with degraded ones, deterministic
failures (SQL syntax / 404 / 400) crashing vs. environmental failures
(auth / connectivity / deleted / timeout) gating on committed-type presence,
the loud stderr warning, and the run-locally remedy for a first build with no
committed types. Notes the metric-views-only edge case (empty analytics.d.ts
satisfies the gate). Refreshes the metric-view section to reference the same
taxonomy instead of the old always-fail framing.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
…anges
Wash of the typegen-ci-resilient-describe branch: strips loop-process
"Phase N" labels from comments, test names, and describe titles (keeping
the semantic text), removes an unnecessary comment / empty else-block /
useless default parameter, rewrites two stale+duplicated write-suppression
comments to match the actual behavior, and converts errors.test.ts's
`(error as any)` casts to the sibling `Object.assign(new Error(...), { ... })`
idiom. Comments, names, and test-setup style only — no logic or assertion
changes (537 tests still pass).
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Brings in main's revert of PR #502 (#509), which relocated the typegen
cache back to node_modules/.databricks. This branch was cut from the
#502 merge commit and inherited that surface without ever depending on
it, so the sync is a clean subtraction: cache-paths.ts, the committed
apps/dev-playground/.appkit/ fixtures, the cache-serialization tests,
and the warehouse-less-deploy docs all drop out, while the metric-view
runtime (useMetricView, formatters, metric-views.ts codegen) is
untouched.
bundle-size-baseline.json conflicted because both sides regenerated it;
resolved by taking main's, to be regenerated against the merged tree.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
The merge took main's baseline to resolve the conflict; this remeasures
against the post-merge build so the numbers reflect the actual tree.
`size:compare` now reports no change.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
The exact-formatting path only recognised integer-shaped strings, so a
fractional value off the JSON_ARRAY wire fell through to `Number()` and lost
precision past ±2^53 — a cents-denominated total or a fixed-scale DECIMAL
rendered as a neighbouring value.
Parse plain decimal strings into a fixed-point `{coefficient, negative, scale}`
and format from that, so the value never passes through a JS number. Rounding
is half-away-from-zero, matching `Intl.NumberFormat`'s default. The percent
path now scales the coefficient by 100 exactly instead of refusing the input.
Exponent-form strings still take the float path, having no exact reading.
Signed-off-by: Atila Fassina <atila@fassina.eu>
…tricView
`MetricOrderBy` was exported so callers could name a hoisted `orderBy`
constant, but its `field` was `string` while `UseMetricViewOptions` required the
selected-field literal union — so the exported type could not be used for the
thing it was exported for. Hoisting is the pattern the metric-views route
follows, since the hook's payload memo compares by value.
Make `MetricOrderBy<Field extends string = string>` generic and instantiate it
in `UseMetricViewOptions` at the selected measures/dimensions, replacing the
inline structural duplicate that let the two definitions drift.
The default instantiation stays `field: string`, so the server's wire type
(`IAnalyticsMetricRequest`) is unchanged. A bare `MetricOrderBy[]` is still
rejected by the hook, deliberately: `MetricOrderBy<string>` cannot prove its
fields were selected, and accepting it would forfeit the compile-time guarantee
that you cannot order by an unselected column. Hook callers parameterize with
their selected fields; the JSDoc now says so and the type probe pins both the
accepted and rejected forms.
Signed-off-by: Atila Fassina <atila@fassina.eu>
The `Rule A:` / `Rule B:` labels in the metric request `superRefine` restated
their own `addIssue` messages, and the sibling rules in that same function carry
no such labels. The `renderOrderByClause` lead comment narrated the step rather
than the intent; the ordering-is-caller-significant point it carried is stated
where it is load-bearing, in the cache key.
Comments only — no behaviour change.
Signed-off-by: Atila Fassina <atila@fassina.eu>
`2226fde4` established that an empty-string dimension value is a real value
filtering on `equals ''`, distinct from a NULL group key's `IS NULL`, and gave
the filter dropdown an `EMPTY` sentinel to say so. The chart-click path still
collapsed the two: `fromChartName("")` returned `null`, so clicking an
empty-string category filtered for SQL NULL, and `toSelectionSet` drops `""`, so
that category could never render as selected.
The ambiguity starts upstream of the click — `ChartClickDatum.name` is a string,
and `mapToDatum` derives it from ECharts' own `p.name`, so a NULL group key and
a genuine `""` are already indistinguishable by the time a handler sees them.
Project the dimension into the sentinel space the dropdown already uses before
handing rows to the chart, and decode it back on click. `toSelectionSet`'s
documented "empty selection = no emphasis" guard is untouched: sentinels are
non-empty, so they pass through it.
A dimension with no filter at all stays `undefined` rather than being mapped to
the NONE sentinel, which would have emphasized the NULL bar whenever nothing
was selected.
Signed-off-by: Atila Fassina <atila@fassina.eu>
`formatValue` re-derived `isPercent`, `grouping`, `decimals` and `prefix` from
the format string on every call — two of those run a regex — and callers invoke
it per table cell, so a render redid the same parse rows x columns times.
Memoize the parse keyed by the spec string. The realistic key space is
metric-view catalog metadata, so it is tiny, but the function is public and
takes an arbitrary string: cap the map and stop inserting once full rather than
letting a caller that generates specs in a loop grow it without bound. A miss
past the cap still parses and returns the right value, so correctness never
depends on cache state.
`toD3Format` parsed the same spec separately and now shares the cache.
Per-value work (`parseExactDecimal`, thousands grouping) is unchanged — it
cannot be cached, since it depends on the value rather than the spec.
Signed-off-by: Atila Fassina <atila@fassina.eu>

@pkosiecpkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review: useMetricView client experience

Solid PR — the hook, SSE consolidation, and format/filter utilities are well-built. I verified the feature live in the dev-playground against the dogfood warehouse: labels, currency formatting, all filter paths (dropdowns, table-row, chart-click), multi-dimension filters, chip removal, and Clear all all work.

One thing is broken: the time-series line chart scrambles into spaghetti after any filter (details inline). Everything else is UX polish or API consistency.

Findings

  • 🔴 P1 — trend line chart renders unordered → scrambled (reproduced)
  • 🟠 P2 — every filter blanks all visuals to skeletons (flash)
  • 🟠 P2useMetricView doesn't return warehouseStatus (no cold-start UI)
  • nit × 6 — charts ignore metadata format (playground), empty-key throw, autoStart parity, selected scope, leftover console.log, dense comments

Third-party (not this PR, FYI): filtering spams the console with TypeError: Cannot read properties of undefined (reading 'disconnect') (×10 in one session). Traced to size-sensor (resizeObserver.jsdestroy()), a transitive dep of echarts-for-react — not our code. The rapid chart remount-on-filter (see the skeleton-flash finding) is what surfaces it. Fixing that flash likely quiets it; otherwise a separate ticket / dep note.

Non-blocking (COMMENT). Nothing needs to gate merge except your own call on the P1.

Comment threadapps/dev-playground/client/src/routes/metric-views.route.tsx
Comment threadpackages/appkit-ui/src/react/hooks/use-metric-view.ts Outdated
Comment threadpackages/appkit-ui/src/react/hooks/use-metric-view.ts Outdated
Comment threadpackages/appkit-ui/src/react/hooks/use-metric-view.ts
Comment threadapps/dev-playground/client/src/routes/metric-views.route.tsx Outdated
Comment threadpackages/appkit-ui/src/react/hooks/types.ts
Comment threadpackages/appkit-ui/src/react/charts/base.tsx
Comment threadapps/dev-playground/client/src/routes/metric-views.route.tsx Outdated
Comment threadpackages/appkit/src/plugins/analytics/mv/cache.ts Outdated

@pkosiecpkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review: useMetricView client experience

Solid PR — the hook, SSE consolidation, and format/filter utilities are well-built. I verified the feature live in the dev-playground against the dogfood warehouse: labels, currency formatting, all filter paths (dropdowns, table-row, chart-click), multi-dimension filters, chip removal, and Clear all all work.

One thing is broken: the time-series line chart scrambles into spaghetti after any filter (details inline). Everything else is UX polish or API consistency.

Findings

  • 🔴 P1 — trend line chart renders unordered → scrambled (reproduced)
  • 🟠 P2 — every filter blanks all visuals to skeletons (flash)
  • 🟠 P2useMetricView doesn't return warehouseStatus (no cold-start UI)
  • nit × 6 — charts ignore metadata format (playground), empty-key throw, autoStart parity, selected scope, leftover console.log, dense comments

Third-party (not this PR, FYI): filtering spams the console with TypeError: Cannot read properties of undefined (reading 'disconnect') (×10 in one session). Traced to size-sensor (resizeObserver.jsdestroy()), a transitive dep of echarts-for-react — not our code. The rapid chart remount-on-filter (see the skeleton-flash finding) is what surfaces it. Fixing that flash likely quiets it; otherwise a separate ticket / dep note.

Non-blocking (COMMENT). Nothing needs to gate merge except your own call on the P1.

Comment threadapps/dev-playground/client/src/routes/metric-views.route.tsx
Comment threadpackages/appkit-ui/src/react/hooks/use-metric-view.ts Outdated
Comment threadpackages/appkit-ui/src/react/hooks/use-metric-view.ts Outdated
Comment threadpackages/appkit-ui/src/react/hooks/use-metric-view.ts
Comment threadapps/dev-playground/client/src/routes/metric-views.route.tsx Outdated
Comment threadpackages/appkit-ui/src/react/hooks/types.ts
Comment threadpackages/appkit-ui/src/react/charts/base.tsx
Comment threadapps/dev-playground/client/src/routes/metric-views.route.tsx Outdated
Comment threadpackages/appkit/src/plugins/analytics/mv/cache.ts Outdated
Wash pass over the useMetricView branch:
- drop the orderBy-is-not-sorted rationale duplicated in analytics.ts;
it stays whole at its canonical site in mv/cache.ts
- collapse the verbatim-duplicated `selected` JSDoc in BaseChartProps to
a link to ChartBaseProps.selected
- remove a dangling review-finding reference from a test name
- bring back warehouseStartupTimeoutMs's description and
autoStartWarehouse's cost-control rationale, which an earlier
comment-trim pass had removed from the IDE hover
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Signed-off-by: Atila Fassina <atila@fassina.eu>
Signed-off-by: Atila Fassina <atila@fassina.eu>
Comment threadapps/dev-playground/server/index.ts Outdated
Comment threadapps/dev-playground/shared/appkit-types/metric-views.d.ts Outdated
Comment threadpackages/appkit/src/plugins/analytics/mv/constants.ts Outdated
The handler only logged the clicked datum to the console. That log was
residue from b9be4f1, which removed the `notify` export and the fake
"Write back" demo that `dbef8f84` had added around this handler.
Keep the handler as an intentional placeholder for the write-back flow and
drop the console write. The empty body is deliberate: `BaseChart` derives
`interactive` from handler presence, so removing the prop would switch off
line-stroke click emission and the seam with it.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
`analytics({ metricViewsMetadata })` made every app import a generated const
and hand it to the plugin. The plugin already discovers metric views from
`config/metric-views/definitions.json` with no wiring at all, so display
metadata — the same data, from the same generator pass — took a second,
redundant channel into the same plugin.
The reason was the artifact's format, not a design choice: the metadata was
emitted as a TypeScript `as const`, and a value can only reach the server via
an import. That format also forced the type artifact to be a real `.ts`
(an object-literal const in an ambient declaration is TS1254), which is why
typegen had to reject a `.d.ts` `mvOutFile` and sweep a stale sibling.
The generator now writes the runtime half to
`config/metric-views/metadata.generated.json`, beside the hand-authored
`definitions.json` (separate file: a generator writing into a user-authored
one would clobber hand edits). The route reads it through
`AppManager.readMetricViewsConfig`, inheriting the dev tunnel and the
traversal guard, and caches the parse keyed on raw contents so a regenerated
bundle is picked up without a restart.
Unlike `definitions.json`, every failure mode here degrades instead of
throwing: metadata is pure response decoration, so an absent, malformed, or
future-versioned bundle yields unlabeled columns rather than failing a query
that would otherwise have succeeded. Malformed and version-mismatched cases
warn; a registered view with no metadata entry warns too, since that was
previously silent.
`metricViewsMetadata` stays as an explicit override that wins over the
discovered bundle, so this is not a breaking change and apps building
metadata another way keep working.
With the const gone the type artifact reverts to `metric-views.d.ts`, which
drops the `mvOutFile` `.d.ts` rejection, inverts the stale-sibling sweep, and
removes the special case in the `--wait` committed-types gate. Both artifacts
are written under one anti-clobber gate, so a degraded blocking pass never
pairs committed types with emptied-out metadata.
Verified against the built bundle, not just source: unbundle mode preserves
`METRIC_TYPES_FILE = "metric-views.d.ts"`, the `METRIC_METADATA_FILE` write
in the generator, and `loadMetricMetadata` on the mv barrel.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
The warning named one remedy — "regenerate types to refresh
metadata.generated.json" — regardless of where the metadata came from. On the
`analytics({ metricViewsMetadata })` override path that advice is wrong twice
over: regenerating cannot change an injected value, and the bundle is never
read there at all.
Hold the injected value in a variable so the source survives the `??`, then
warn per branch. Two separate calls rather than a conditional format string, so
neither carries a dangling argument.
Tests cover both branches. The discovered-bundle case needs a bundle on disk
that omits the queried key — an absent bundle resolves to `undefined`, which is
dormancy and skips the block entirely, so a fixture without one asserts
nothing.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Review feedback: several comment blocks read as design docs rather than code
comments. Keep the rationale, drop the worked-through reasoning.
`mv/cache.ts` 7 comment lines to 2, `use-metric-view.ts` 8 to 4. Every
mechanism a maintainer needs to evaluate the code survives: order-by sequence
matters under LIMIT, commas are valid in identifiers so the key must be
JSON-encoded, and shape changes clear rows while filter revalidations keep
them.
`js/metric-filter/index.ts` was also cited but left alone — its density is
public-API JSDoc with `@example` blocks, which the same review exempted.
Comments only; no code lines changed.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
`metric-views.d.ts` was the only generated artifact using
`import type {} from "@databricks/appkit-ui/react"`; `analytics.d.ts` and
`serving.d.ts` both use a bare side-effect import. Use the same form.
The type-only variant was correct while the artifact was a real `metric-views.ts`
carrying the runtime metadata const, where a side-effect import would have
executed the client package entry on the Node server. It became a `.d.ts` when
the metadata moved to a JSON bundle, and a `.d.ts` is never emitted to JS, so
the runtime concern the comment cited no longer exists. Verified: both forms
typecheck identically.
The import itself stays, and the comment now says why. It marks the file a
module, which is what makes `declare module` an augmentation that merges into
the real one. Without it the block is an ambient declaration that SHADOWS
`@databricks/appkit-ui/react` and hides every genuine export — confirmed by
removing it: `Module has no exported member 'useMetricView'`.
The tests asserted the previous form, so they inverted with it. They now pin the
invariant that matters (import present + augmentation present) rather than which
import spelling was used.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
# Conflicts:
#	bundle-size-baseline.json

@pkosiecpkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, just one comment from an agentic review - PTAL before merge. Thanks!

Comment threadpackages/appkit-ui/src/react/hooks/index.ts

@MarioCadenasMarioCadenas left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🚀

useMetricView returns metadata as Record<string, MetricViewColumnDisplay>
and the public formatLabel() takes it as a parameter, but the type only
lived in the workspace-private shared package. Consumers had no importable
name for it, so wrapping formatLabel or typing a column renderer was
impossible without reaching into a private dist path.
Re-export it from js/format alongside the sibling Metric* types, and
surface it through the react hooks barrel.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
React Strict Mode runs setup/cleanup/setup on mount, which sent a POST,
aborted it, and sent an identical second one. Track the active request key
and defer teardown by a microtask so the second setup can claim the still
open stream. A genuine unmount has no new lease, so the abort still fires.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
A sub-percent category rendered thinner than a CSS pixel, so it could not
be clicked or hovered for cross-filtering. Set minAngle: 3 — ECharts still
reports labels and tooltips from the true values.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Add a Playwright spec over the metric-views route with a mocked metric
endpoint, plus the ariaLabel/testId hooks the charts need to be
addressable from it.
Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
@atilafassina
atilafassina merged commit bd1cbd5 into mainAug 12, 2026
10 checks passed
@atilafassina
atilafassina deleted the mv-hook branch August 12, 2026 12:49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@atilafassina@pkosiec@MarioCadenas