Skip to content

Commit 207e59a

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 6b7aa86 commit 207e59a

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
ZapIcon,
1718
} from "lucide-react";
1819

@@ -63,6 +64,13 @@ export const NAV_GROUPS: ReadonlyArray<NavGroup> = [
6364
"Same dashboard — served over Apache Arrow streaming for zero-copy speed.",
6465
icon: ZapIcon,
6566
},
67+
{
68+
to: "/metric-views",
69+
label: "Metric Views",
70+
description:
71+
"Measure a governed UC metric view with useMetricView — labels and formats from injected metadata.",
72+
icon: SigmaIcon,
73+
},
6674
{
6775
to: "/lakebase",
6876
label: "Lakebase",

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Route as SmartDashboardRouteRouteImport } from './routes/smart-dashboar
1717
import { Route as ServingRouteRouteImport } from './routes/serving.route'
1818
import { Route as ReconnectRouteRouteImport } from './routes/reconnect.route'
1919
import { Route as PolicyMatrixRouteRouteImport } from './routes/policy-matrix.route'
20+
import { Route as MetricViewsRouteRouteImport } from './routes/metric-views.route'
2021
import { Route as LakebaseRouteRouteImport } from './routes/lakebase.route'
2122
import { Route as JobsRouteRouteImport } from './routes/jobs.route'
2223
import { Route as GenieRouteRouteImport } from './routes/genie.route'
@@ -68,6 +69,11 @@ const PolicyMatrixRouteRoute = PolicyMatrixRouteRouteImport.update({
6869
path: '/policy-matrix',
6970
getParentRoute: () => rootRouteImport,
7071
} as any)
72+
const MetricViewsRouteRoute = MetricViewsRouteRouteImport.update({
73+
id: '/metric-views',
74+
path: '/metric-views',
75+
getParentRoute: () => rootRouteImport,
76+
} as any)
7177
const LakebaseRouteRoute = LakebaseRouteRouteImport.update({
7278
id: '/lakebase',
7379
path: '/lakebase',
@@ -130,6 +136,7 @@ export interface FileRoutesByFullPath {
130136
'/genie': typeof GenieRouteRoute
131137
'/jobs': typeof JobsRouteRoute
132138
'/lakebase': typeof LakebaseRouteRoute
139+
'/metric-views': typeof MetricViewsRouteRoute
133140
'/policy-matrix': typeof PolicyMatrixRouteRoute
134141
'/reconnect': typeof ReconnectRouteRoute
135142
'/serving': typeof ServingRouteRoute
@@ -150,6 +157,7 @@ export interface FileRoutesByTo {
150157
'/genie': typeof GenieRouteRoute
151158
'/jobs': typeof JobsRouteRoute
152159
'/lakebase': typeof LakebaseRouteRoute
160+
'/metric-views': typeof MetricViewsRouteRoute
153161
'/policy-matrix': typeof PolicyMatrixRouteRoute
154162
'/reconnect': typeof ReconnectRouteRoute
155163
'/serving': typeof ServingRouteRoute
@@ -171,6 +179,7 @@ export interface FileRoutesById {
171179
'/genie': typeof GenieRouteRoute
172180
'/jobs': typeof JobsRouteRoute
173181
'/lakebase': typeof LakebaseRouteRoute
182+
'/metric-views': typeof MetricViewsRouteRoute
174183
'/policy-matrix': typeof PolicyMatrixRouteRoute
175184
'/reconnect': typeof ReconnectRouteRoute
176185
'/serving': typeof ServingRouteRoute
@@ -193,6 +202,7 @@ export interface FileRouteTypes {
193202
| '/genie'
194203
| '/jobs'
195204
| '/lakebase'
205+
| '/metric-views'
196206
| '/policy-matrix'
197207
| '/reconnect'
198208
| '/serving'
@@ -213,6 +223,7 @@ export interface FileRouteTypes {
213223
| '/genie'
214224
| '/jobs'
215225
| '/lakebase'
226+
| '/metric-views'
216227
| '/policy-matrix'
217228
| '/reconnect'
218229
| '/serving'
@@ -233,6 +244,7 @@ export interface FileRouteTypes {
233244
| '/genie'
234245
| '/jobs'
235246
| '/lakebase'
247+
| '/metric-views'
236248
| '/policy-matrix'
237249
| '/reconnect'
238250
| '/serving'
@@ -254,6 +266,7 @@ export interface RootRouteChildren {
254266
GenieRouteRoute: typeof GenieRouteRoute
255267
JobsRouteRoute: typeof JobsRouteRoute
256268
LakebaseRouteRoute: typeof LakebaseRouteRoute
269+
MetricViewsRouteRoute: typeof MetricViewsRouteRoute
257270
PolicyMatrixRouteRoute: typeof PolicyMatrixRouteRoute
258271
ReconnectRouteRoute: typeof ReconnectRouteRoute
259272
ServingRouteRoute: typeof ServingRouteRoute
@@ -322,6 +335,13 @@ declare module '@tanstack/react-router' {
322335
preLoaderRoute: typeof PolicyMatrixRouteRouteImport
323336
parentRoute: typeof rootRouteImport
324337
}
338+
'/metric-views': {
339+
id: '/metric-views'
340+
path: '/metric-views'
341+
fullPath: '/metric-views'
342+
preLoaderRoute: typeof MetricViewsRouteRouteImport
343+
parentRoute: typeof rootRouteImport
344+
}
325345
'/lakebase': {
326346
id: '/lakebase'
327347
path: '/lakebase'
@@ -406,6 +426,7 @@ const rootRouteChildren: RootRouteChildren = {
406426
GenieRouteRoute: GenieRouteRoute,
407427
JobsRouteRoute: JobsRouteRoute,
408428
LakebaseRouteRoute: LakebaseRouteRoute,
429+
MetricViewsRouteRoute: MetricViewsRouteRoute,
409430
PolicyMatrixRouteRoute: PolicyMatrixRouteRoute,
410431
ReconnectRouteRoute: ReconnectRouteRoute,
411432
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
@@ -14,6 +14,12 @@ import {
1414
import { agents, createAgent, tool } from "@databricks/appkit/beta";
1515
import { WorkspaceClient } from "@databricks/sdk-experimental";
1616
import { z } from "zod";
17+
// Build-generated per-metric column metadata (display_name / format / type /
18+
// description), emitted by the metric-views type generator alongside the
19+
// MetricRegistry augmentation. Injecting it into `analytics({ metricViewsMetadata })`
20+
// lets the metric route stamp per-column display metadata into the SSE result
21+
// so the client can label/format columns without hard-coding format strings.
22+
import { metricViewsMetadata } from "../shared/appkit-types/metric-views";
1723
import { lakebaseExamples } from "./lakebase-examples-plugin";
1824
import { reconnect } from "./reconnect-plugin";
1925
import { telemetryExamples } from "./telemetry-example-plugin";
@@ -342,7 +348,7 @@ createApp({
342348
server(),
343349
reconnect(),
344350
telemetryExamples(),
345-
analytics({}),
351+
analytics({ metricViewsMetadata }),
346352
genie({
347353
spaces: { demo: process.env.DATABRICKS_GENIE_SPACE_ID ?? "placeholder" },
348354
}),

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

@@ -86,9 +86,9 @@ In blocking mode the generator starts a stopped warehouse, waits (bounded) for i
8686

8787
## Metric-view types
8888

89-
`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/`:
89+
`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/`:
9090

91-
- `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.
91+
- `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.
9292

9393
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.
9494

0 commit comments

Comments
 (0)