Skip to content

Commit 948307b

Browse files
committed
feat(playground): wire metric-view runtime end-to-end + docs (PR5 phase 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>
1 parent 9102d12 commit 948307b

7 files changed

Lines changed: 394 additions & 5 deletions

File tree

apps/dev-playground/client/src/lib/nav.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
SearchIcon,
1414
ServerIcon,
1515
ShieldIcon,
16+
SigmaIcon,
1617
Wand2Icon,
1718
ZapIcon,
1819
} from "lucide-react";
@@ -64,6 +65,13 @@ export const NAV_GROUPS: ReadonlyArray<NavGroup> = [
6465
"Same dashboard — served over Apache Arrow streaming for zero-copy speed.",
6566
icon: ZapIcon,
6667
},
68+
{
69+
to: "/metric-views",
70+
label: "Metric Views",
71+
description:
72+
"Measure a governed UC metric view with useMetricView — labels and formats from injected metadata.",
73+
icon: SigmaIcon,
74+
},
6775
{
6876
to: "/lakebase",
6977
label: "Lakebase",

apps/dev-playground/client/src/routeTree.gen.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Route as SmartDashboardRouteRouteImport } from './routes/smart-dashboar
1818
import { Route as ServingRouteRouteImport } from './routes/serving.route'
1919
import { Route as ReconnectRouteRouteImport } from './routes/reconnect.route'
2020
import { Route as PolicyMatrixRouteRouteImport } from './routes/policy-matrix.route'
21+
import { Route as MetricViewsRouteRouteImport } from './routes/metric-views.route'
2122
import { Route as LakebaseRouteRouteImport } from './routes/lakebase.route'
2223
import { Route as JobsRouteRouteImport } from './routes/jobs.route'
2324
import { Route as GenieRouteRouteImport } from './routes/genie.route'
@@ -74,6 +75,11 @@ const PolicyMatrixRouteRoute = PolicyMatrixRouteRouteImport.update({
7475
path: '/policy-matrix',
7576
getParentRoute: () => rootRouteImport,
7677
} as any)
78+
const MetricViewsRouteRoute = MetricViewsRouteRouteImport.update({
79+
id: '/metric-views',
80+
path: '/metric-views',
81+
getParentRoute: () => rootRouteImport,
82+
} as any)
7783
const LakebaseRouteRoute = LakebaseRouteRouteImport.update({
7884
id: '/lakebase',
7985
path: '/lakebase',
@@ -136,6 +142,7 @@ export interface FileRoutesByFullPath {
136142
'/genie': typeof GenieRouteRoute
137143
'/jobs': typeof JobsRouteRoute
138144
'/lakebase': typeof LakebaseRouteRoute
145+
'/metric-views': typeof MetricViewsRouteRoute
139146
'/policy-matrix': typeof PolicyMatrixRouteRoute
140147
'/reconnect': typeof ReconnectRouteRoute
141148
'/serving': typeof ServingRouteRoute
@@ -157,6 +164,7 @@ export interface FileRoutesByTo {
157164
'/genie': typeof GenieRouteRoute
158165
'/jobs': typeof JobsRouteRoute
159166
'/lakebase': typeof LakebaseRouteRoute
167+
'/metric-views': typeof MetricViewsRouteRoute
160168
'/policy-matrix': typeof PolicyMatrixRouteRoute
161169
'/reconnect': typeof ReconnectRouteRoute
162170
'/serving': typeof ServingRouteRoute
@@ -179,6 +187,7 @@ export interface FileRoutesById {
179187
'/genie': typeof GenieRouteRoute
180188
'/jobs': typeof JobsRouteRoute
181189
'/lakebase': typeof LakebaseRouteRoute
190+
'/metric-views': typeof MetricViewsRouteRoute
182191
'/policy-matrix': typeof PolicyMatrixRouteRoute
183192
'/reconnect': typeof ReconnectRouteRoute
184193
'/serving': typeof ServingRouteRoute
@@ -202,6 +211,7 @@ export interface FileRouteTypes {
202211
| '/genie'
203212
| '/jobs'
204213
| '/lakebase'
214+
| '/metric-views'
205215
| '/policy-matrix'
206216
| '/reconnect'
207217
| '/serving'
@@ -223,6 +233,7 @@ export interface FileRouteTypes {
223233
| '/genie'
224234
| '/jobs'
225235
| '/lakebase'
236+
| '/metric-views'
226237
| '/policy-matrix'
227238
| '/reconnect'
228239
| '/serving'
@@ -244,6 +255,7 @@ export interface FileRouteTypes {
244255
| '/genie'
245256
| '/jobs'
246257
| '/lakebase'
258+
| '/metric-views'
247259
| '/policy-matrix'
248260
| '/reconnect'
249261
| '/serving'
@@ -266,6 +278,7 @@ export interface RootRouteChildren {
266278
GenieRouteRoute: typeof GenieRouteRoute
267279
JobsRouteRoute: typeof JobsRouteRoute
268280
LakebaseRouteRoute: typeof LakebaseRouteRoute
281+
MetricViewsRouteRoute: typeof MetricViewsRouteRoute
269282
PolicyMatrixRouteRoute: typeof PolicyMatrixRouteRoute
270283
ReconnectRouteRoute: typeof ReconnectRouteRoute
271284
ServingRouteRoute: typeof ServingRouteRoute
@@ -342,6 +355,13 @@ declare module '@tanstack/react-router' {
342355
preLoaderRoute: typeof PolicyMatrixRouteRouteImport
343356
parentRoute: typeof rootRouteImport
344357
}
358+
'/metric-views': {
359+
id: '/metric-views'
360+
path: '/metric-views'
361+
fullPath: '/metric-views'
362+
preLoaderRoute: typeof MetricViewsRouteRouteImport
363+
parentRoute: typeof rootRouteImport
364+
}
345365
'/lakebase': {
346366
id: '/lakebase'
347367
path: '/lakebase'
@@ -426,6 +446,7 @@ const rootRouteChildren: RootRouteChildren = {
426446
GenieRouteRoute: GenieRouteRoute,
427447
JobsRouteRoute: JobsRouteRoute,
428448
LakebaseRouteRoute: LakebaseRouteRoute,
449+
MetricViewsRouteRoute: MetricViewsRouteRoute,
429450
PolicyMatrixRouteRoute: PolicyMatrixRouteRoute,
430451
ReconnectRouteRoute: ReconnectRouteRoute,
431452
ServingRouteRoute: ServingRouteRoute,
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { formatLabel, formatValue } from "@databricks/appkit-ui/js";
2+
import {
3+
Card,
4+
CardContent,
5+
CardDescription,
6+
CardHeader,
7+
CardTitle,
8+
LineChart,
9+
Skeleton,
10+
Table,
11+
TableBody,
12+
TableCell,
13+
TableHead,
14+
TableHeader,
15+
TableRow,
16+
useMetricView,
17+
} from "@databricks/appkit-ui/react";
18+
import { createFileRoute } from "@tanstack/react-router";
19+
import { Header } from "@/components/layout/header";
20+
21+
export const Route = createFileRoute("/metric-views")({
22+
component: MetricViewsRoute,
23+
});
24+
25+
// Columns we ask the metric view for. Declared at module scope so their
26+
// array identities stay stable across renders — `useMetricView` serializes
27+
// the request body, so this also keeps the SSE subscription from re-firing.
28+
const MEASURES = ["arr", "mrr"] as const;
29+
const DIMENSIONS = ["created_at"] as const;
30+
31+
function MetricViewsRoute() {
32+
// Measure the `revenue` metric view: annual + monthly recurring revenue,
33+
// bucketed by month over the `created_at` time dimension. Measure /
34+
// dimension names, the time grain, and the row shape are all inferred from
35+
// the generated `MetricRegistry` augmentation (shared/appkit-types/metric-views.ts).
36+
const { data, loading, error, metadata } = useMetricView("revenue", {
37+
measures: MEASURES,
38+
dimensions: DIMENSIONS,
39+
timeGrain: "month",
40+
timeDimension: "created_at",
41+
});
42+
43+
// The columns we rendered, in display order. `metadata` is the
44+
// payload-carried, client-agnostic per-column display metadata the server
45+
// stamped onto the SSE result from `analytics({ metricViewsMetadata })`.
46+
const columns = ["created_at", ...MEASURES] as const;
47+
48+
return (
49+
<div className="min-h-screen bg-background">
50+
<div className="max-w-[1100px] mx-auto px-6 py-12">
51+
<Header
52+
title="Metric Views"
53+
description="Measure a governed Unity Catalog metric view with useMetricView — no SQL, and every label + number format sourced from server-injected metadata."
54+
tooltip="POST /api/analytics/metric/revenue streams typed rows plus per-column metadata (display_name / format). The client never hard-codes a format string."
55+
/>
56+
57+
<Card>
58+
<CardHeader>
59+
<CardTitle>Recurring revenue by month</CardTitle>
60+
<CardDescription>
61+
revenue · measures {MEASURES.join(", ")} · grouped by month
62+
</CardDescription>
63+
</CardHeader>
64+
<CardContent className="flex flex-col gap-8">
65+
{loading && <Skeleton className="h-64 w-full" />}
66+
67+
{error && (
68+
<div className="text-destructive text-sm" role="alert">
69+
{error}
70+
</div>
71+
)}
72+
73+
{!loading && !error && (!data || data.length === 0) && (
74+
<div className="text-muted-foreground text-sm">
75+
No results for this metric view.
76+
</div>
77+
)}
78+
79+
{!loading && !error && data && data.length > 0 && (
80+
<>
81+
<LineChart
82+
data={data}
83+
xKey="created_at"
84+
yKey={[...MEASURES]}
85+
height={320}
86+
showLegend
87+
title="ARR vs MRR over time"
88+
/>
89+
90+
<div className="overflow-x-auto">
91+
<Table>
92+
<TableHeader>
93+
<TableRow>
94+
{columns.map((col) => (
95+
<TableHead key={col}>
96+
{/* Human label from metadata.display_name,
97+
else a humanized fallback. */}
98+
{formatLabel(col, metadata?.[col])}
99+
</TableHead>
100+
))}
101+
</TableRow>
102+
</TableHeader>
103+
<TableBody>
104+
{data.map((row, i) => (
105+
<TableRow key={`${String(row.created_at)}-${i}`}>
106+
{columns.map((col) => (
107+
<TableCell key={col}>
108+
{/* Format string comes from metadata, never
109+
hand-typed. When `metadata` is undefined
110+
(server injected none / unknown key),
111+
`metadata?.[col]?.format` is undefined and
112+
formatValue degrades to a sensible default. */}
113+
{formatValue(row[col], metadata?.[col]?.format)}
114+
</TableCell>
115+
))}
116+
</TableRow>
117+
))}
118+
</TableBody>
119+
</Table>
120+
</div>
121+
</>
122+
)}
123+
</CardContent>
124+
</Card>
125+
</div>
126+
</div>
127+
);
128+
}

apps/dev-playground/server/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import {
2020
} from "@databricks/appkit/beta";
2121
import { WorkspaceClient } from "@databricks/sdk-experimental";
2222
import { z } from "zod";
23+
// Build-generated per-metric column metadata (display_name / format / type /
24+
// description), emitted by the metric-views type generator alongside the
25+
// MetricRegistry augmentation. Injecting it into `analytics({ metricViewsMetadata })`
26+
// lets the metric route stamp per-column display metadata into the SSE result
27+
// so the client can label/format columns without hard-coding format strings.
28+
import { metricViewsMetadata } from "../shared/appkit-types/metric-views";
2329
import { lakebaseExamples } from "./lakebase-examples-plugin";
2430
import { reconnect } from "./reconnect-plugin";
2531
import { telemetryExamples } from "./telemetry-example-plugin";
@@ -377,7 +383,7 @@ createApp({
377383
server(),
378384
reconnect(),
379385
telemetryExamples(),
380-
analytics({}),
386+
analytics({ metricViewsMetadata }),
381387
genie({
382388
spaces: { demo: process.env.DATABRICKS_GENIE_SPACE_ID ?? "placeholder" },
383389
}),

apps/dev-playground/shared/appkit-types/metric-views.d.ts renamed to apps/dev-playground/shared/appkit-types/metric-views.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Auto-generated by AppKit - DO NOT EDIT
22
// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build
3-
import "@databricks/appkit-ui/react";
3+
import type {} from "@databricks/appkit-ui/react";
44
declare module "@databricks/appkit-ui/react" {
55
interface MetricRegistry {
66
"customers": {
@@ -127,3 +127,31 @@ declare module "@databricks/appkit-ui/react" {
127127
};
128128
}
129129
}
130+
131+
export const metricViewsMetadata = {
132+
"customers": {
133+
measures: {
134+
"active_accounts": { type: "bigint", display_name: "Active Accounts", format: "#,##0" },
135+
"churn_rate": { type: "decimal", display_name: "Churn Rate" },
136+
"avg_ltv": { type: "double", display_name: "Average LTV", format: "$#,##0.00" },
137+
},
138+
dimensions: {
139+
"segment": { type: "string", display_name: "Customer Segment" },
140+
"region": { type: "string", display_name: "Region" },
141+
"csm_email": { type: "string", display_name: "CSM Email" },
142+
},
143+
},
144+
"revenue": {
145+
measures: {
146+
"mrr": { type: "double", display_name: "Monthly Recurring Revenue", format: "$#,##0.00" },
147+
"arr": { type: "double", display_name: "Annual Recurring Revenue", format: "$#,##0.00", description: "Annualized contract value across all active subscriptions" },
148+
"new_arr": { type: "double", display_name: "New ARR", format: "$#,##0.00" },
149+
"churned_arr": { type: "double", display_name: "Churned ARR", format: "$#,##0.00" },
150+
},
151+
dimensions: {
152+
"region": { type: "string", display_name: "Region" },
153+
"segment": { type: "string", display_name: "Customer Segment" },
154+
"created_at": { type: "timestamp_ltz", display_name: "Subscription Start" },
155+
},
156+
},
157+
} as const;

docs/docs/development/type-generation.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ AppKit can automatically generate TypeScript types for your SQL queries, providi
1010

1111
Generate type-safe TypeScript declarations for query keys, parameters, and result rows.
1212

13-
All generated files live in `shared/appkit-types/`, one per concern: `analytics.d.ts` (SQL query types), `serving.d.ts` (model-serving endpoint types), and `metric-views.d.ts`. A single command (and the Vite plugin) produces them all in one pass; see [Metric-view types](#metric-view-types). The `.d.ts` files use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
13+
All generated files live in `shared/appkit-types/`, one per concern: `analytics.d.ts` (SQL query types), `serving.d.ts` (model-serving endpoint types), and `metric-views.ts`. A single command (and the Vite plugin) produces them all in one pass; see [Metric-view types](#metric-view-types). The declaration files use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. (`metric-views.ts` is a real source file rather than a `.d.ts` because it *also* carries a runtime `metricViewsMetadata` constant alongside the augmentation — see [Metric-view types](#metric-view-types).) TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
1414

1515
## Vite plugin: `appKitTypesPlugin`
1616

@@ -106,9 +106,9 @@ This imposes a rollout ordering: **produce and commit `.appkit/` while the wareh
106106

107107
## Metric-view types
108108

109-
`generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/metric-views/definitions.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](../plugins/analytics.md) and writes `metric-views.d.ts` into `shared/appkit-types/`:
109+
`generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/metric-views/definitions.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](../plugins/analytics.md) and writes `metric-views.ts` into `shared/appkit-types/`:
110110

111-
- `metric-views.d.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level.
111+
- `metric-views.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level. The same file also exports a runtime `metricViewsMetadata` constant (the same metadata as a value, not just types) — inject it via `analytics({ metricViewsMetadata })` so the metric route can carry per-column display metadata in its response payload. The type augmentation erases at build; the constant is a normal named export and is tree-shaken away when unused. See [the analytics plugin's metric-view docs](../plugins/analytics.md) for the hook + format-utility wiring.
112112

113113
If `config/metric-views/definitions.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` that same situation fails the build so CI never ships incomplete metric types. A malformed `definitions.json` (invalid JSON, or a source that isn't a three-part UC FQN) fails fast in every mode.
114114

0 commit comments

Comments
 (0)