Skip to content

Commit e028dfc

Browse files
yinlianghuiclaude
andauthored
fix(examples,docs): the dashboard gallery renders working charts again — filtered-* entries leave the retired shape (#4600) (#4615)
* fix(examples,docs): the dashboard gallery renders working charts again — filtered-* entries leave the retired shape (#4600) Implements (d1) of #4600's ruling. The 6 `filtered-*` catalog entries taught and rendered the pre-ADR-0021 inline analytics shape, which the renderer retired in framework#3320: 15 of the category's 28 widgets drew the "retired data format" placeholder and 2 more drew DatasetWidget's unsupported-source error. - The 6 entries now bind live surfaces: inline `options.data` (static series / `options.value` metrics) for the five inline examples, and the dataset variant keeps its two `dataset`-bound widgets, which now render through a gallery stub. Every `globalFilters` / `filterBindings` / `targetWidgets` / `dateRange` declaration is unchanged — the filter pedagogy is the point of these entries — and the #4356 pair-form options stay pair-form. - `apps/site` gains `galleryDataSource` (canned `queryDataset` rows) and `registerCatalogBlocks` (the dashboard + chart registrations the gallery page never loaded). Measured: before this change every dashboard tile in the gallery was the registry's "Unknown component type: dashboard" panel. - The 3 teaching locations move off the retired shape and the false prose is corrected ("Working examples", "mix freely", the metadata-check claim). - New render pin: every entry is rendered through the real SchemaRenderer and may show none of the three diagnostics. Red before this change (retired=15 across 5 entries), green after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 * fix(site): SchemaThumbnail names the layout registrar directly again (#4600) The #3904 guard (scripts/__tests__/site-playground-layout-registration-3904. test.ts) discovers every apps/site SchemaRenderer host and reads its imports for `registerLayoutBlocks` by name. Routing that import through the new `registerCatalogBlocks` module satisfied it in the module graph but not in the guard's source-text view, so the host read as one that registers nothing — CI red, correctly. Each host now names both registrars, one per responsibility: layout blocks (#3787) and the gallery's dashboard/chart blocks (#4600). registerCatalogBlocks no longer re-exports the layout one, so the guard's discovery property stays literally true for any host added later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b8bda9a commit e028dfc

17 files changed

Lines changed: 584 additions & 92 deletions

apps/site/app/components/SchemaThumbnail.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,22 @@ import { SchemaRenderer, SchemaRendererContext, toRenderableSchema } from '@obje
2020
import { SidebarProvider } from '@object-ui/components';
2121
import type { SchemaNode } from '@object-ui/core';
2222
// Registers `page-header` & friends — see the module header (objectui#3787).
23+
// Named directly, not reached through the module below: `scripts/__tests__/
24+
// site-playground-layout-registration-3904.test.ts` discovers every
25+
// `SchemaRenderer` host and requires it to import THIS module, and a host that
26+
// pulled it in transitively would read to that guard as a host that registers
27+
// nothing (measured — it went red on exactly that).
2328
import './registerLayoutBlocks';
29+
// Registers the dashboard + chart blocks the gallery draws (objectui#4600).
30+
import './registerCatalogBlocks';
31+
import { galleryDataSource } from './galleryDataSource';
2432

25-
const defaultCtx = { dataSource: {} };
33+
// The gallery's data source. It is handed to `SchemaRenderer` BOTH ways on
34+
// purpose: the context is what nested blocks read, while `DashboardRenderer`
35+
// takes `dataSource` as a React prop — a context-only value never reaches it,
36+
// which is why its dataset-bound widgets rendered "This data source does not
37+
// support dataset queries." while the context already held one (objectui#4600).
38+
const defaultCtx = { dataSource: galleryDataSource };
2639

2740
/**
2841
* Tiny class-based error boundary so a single bad schema doesn't take down
@@ -135,7 +148,10 @@ export function SchemaThumbnail({
135148
<SchemaRendererContext.Provider value={ctx}>
136149
<SidebarProvider className="min-h-0 w-full" defaultOpen={false}>
137150
<div className="w-full p-4">
138-
<SchemaRenderer schema={toRenderableSchema(schema)} />
151+
<SchemaRenderer
152+
schema={toRenderableSchema(schema)}
153+
dataSource={galleryDataSource}
154+
/>
139155
</div>
140156
</SidebarProvider>
141157
</SchemaRendererContext.Provider>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*/
5+
6+
/**
7+
* The docs gallery's stand-in data source (objectui#4600).
8+
*
9+
* The catalog is a **presentation corpus**: it ships JSON, not a backend, and
10+
* the gallery renders it with no application behind it. Most entries need
11+
* nothing — their data is inline. Dataset-bound dashboard widgets are the
12+
* exception: `DatasetWidget` routes through `dataSource.queryDataset`, and with
13+
* no such function it renders "This data source does not support dataset
14+
* queries." — which is what `plugin-dashboard/filtered-dashboard-dataset-
15+
* widgets` showed in the gallery until this file existed.
16+
*
17+
* So this is the smallest thing that lets a dataset-bound widget draw: canned
18+
* rows shaped from the query's own `dimensions` / `measures`, so any widget
19+
* gets a two-bucket series (or a single value when it selects no dimension)
20+
* regardless of which dataset it names. It is a DEMO fixture — it does not
21+
* filter, aggregate or honour `runtimeFilter`, and it must never be mistaken
22+
* for a data-source implementation. Real ones live in `@object-ui/data-*`.
23+
*
24+
* It is gallery-only on purpose: `apps/site` is `private`, so nothing here is
25+
* a published package surface.
26+
*/
27+
28+
/** The subset of a dataset query this fixture reads. */
29+
interface GalleryDatasetQuery {
30+
dimensions?: string[];
31+
measures?: string[];
32+
}
33+
34+
/** One canned row: dimension values plus measure values. */
35+
type GalleryRow = Record<string, unknown>;
36+
37+
export const galleryDataSource = {
38+
async queryDataset(dataset: string, query: GalleryDatasetQuery) {
39+
const dimensions = query?.dimensions ?? [];
40+
const measures = query?.measures ?? [];
41+
const measure = measures[0] ?? 'value';
42+
const rows: GalleryRow[] = dimensions.length
43+
? [
44+
{ [dimensions[0]]: 'Alpha', [measure]: 42 },
45+
{ [dimensions[0]]: 'Beta', [measure]: 27 },
46+
]
47+
: [{ [measure]: 69 }];
48+
// `object` is what makes a chart drillable; a demo fixture has nothing to
49+
// drill INTO, so it is deliberately omitted along with `dimensionFields`.
50+
return { rows, fields: [] };
51+
},
52+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*/
5+
6+
/**
7+
* Registers the plugin blocks the **catalog gallery** renders. A companion to
8+
* `./registerLayoutBlocks`, which every example host imports directly and which
9+
* this module deliberately does NOT re-export: `scripts/__tests__/site-
10+
* playground-layout-registration-3904.test.ts` discovers each `SchemaRenderer`
11+
* host and reads its imports, so a host reaching the layout registrar through
12+
* here would read as a host that registers nothing. Two imports, two
13+
* responsibilities, one visible in each host.
14+
*
15+
* Why this exists (objectui#4600): `ComponentRegistry` only knows a type once
16+
* the package owning it has been loaded, and `/docs/guide/schema-catalog`
17+
* loads no plugin at all — `SchemaCatalogIndex` -> `SchemaThumbnail` imported
18+
* `@object-ui/react`, `@object-ui/components` and the layout blocks, and
19+
* nothing else. `PluginLoader` covers the individual docs pages that embed a
20+
* demo, but the gallery embeds EVERY example and never wraps them in one.
21+
*
22+
* Measured on `origin/main` before this file: all 9 `plugin-dashboard` entries
23+
* rendered the registry's red "Unknown component type: dashboard (OBJUI-001)"
24+
* panel in the gallery — not the dashboard, and not the retired-widget
25+
* placeholder that the entries produce once the plugin IS loaded. This is the
26+
* objectui#3787 defect one layer up: there it was `page-header` resolving to
27+
* nothing, here it is the whole `dashboard` block.
28+
*
29+
* `@object-ui/plugin-charts` comes with it because it owns `chart` — what a
30+
* dashboard's widgets (static, provider-backed and dataset-bound alike) draw
31+
* with, and the root type of the `plugin-charts/*` catalog entries.
32+
*
33+
* EAGER, at module scope, for the same reason `./registerLayoutBlocks` is
34+
* eager: registration has then already happened for the server render, so the
35+
* thumbnails stay in the prerendered HTML instead of appearing on hydration.
36+
* Neither package declares `sideEffects: false`, so a bare side-effect import
37+
* survives bundling (checked); the layout package is the one that needs an
38+
* explicit call, which its own module does.
39+
*
40+
* Deliberately NOT imported by `InteractiveDemo` / `LiveSplitDemo`: those host
41+
* demos on ordinary docs pages, which opt into their plugins through
42+
* `PluginLoader`, and making them eager would pull the chart + dashboard graphs
43+
* into every page carrying any demo. The gallery page's own modal preview is
44+
* unaffected — it renders after `SchemaThumbnail` has already registered these
45+
* on that page.
46+
*
47+
* Guarded by `examples/schema-catalog/test/plugin-dashboard-gallery-render.
48+
* test.tsx`, which mirrors this registration set and fails if this file stops
49+
* loading either package.
50+
*/
51+
import '@object-ui/plugin-dashboard';
52+
import '@object-ui/plugin-charts';

content/docs/guide/dashboard-filters.md

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@ that drives **several charts at once**. ObjectUI models this as a
1616
Charts stay inline and self-contained; one place owns the filter; each chart
1717
edit stays local.
1818

19-
> Working examples: the schema catalog ships a `plugin-dashboard/filtered-dashboard`
20-
> example plus variants for dynamic options, text/number/lookup filter types,
21-
> dataset widgets, the `targetWidgets` allow-list, and date presets with a
22-
> custom range.
19+
> **Working examples**: the schema catalog ships a
20+
> `plugin-dashboard/filtered-dashboard` example plus variants for dynamic
21+
> options, text/number/lookup filter types, dataset widgets, the
22+
> `targetWidgets` allow-list, and date presets with a custom range. They are
23+
> **presentation** examples — the filter declarations are what they teach, so
24+
> their widgets carry inline demo data (the dataset variant additionally binds
25+
> two widgets to a `dataset`), which is what lets the docs gallery draw them
26+
> with no application behind it. Inline static data is never filtered; see
27+
> Known limitations at the end of this page.
2328
2429
## Tutorial: from zero to a filtered dashboard
2530

2631
### Step 1 — a plain dashboard
2732

28-
Start from two charts over **different** objects. Without filters they always
33+
Start from two charts over **different** datasets. Without filters they always
2934
show everything:
3035

3136
```json
@@ -37,23 +42,44 @@ show everything:
3742
"id": "invoices_by_status",
3843
"title": "Invoices by Status",
3944
"type": "bar",
40-
"object": "invoices",
41-
"categoryField": "status",
42-
"aggregate": "count"
45+
"dataset": "invoices",
46+
"dimensions": ["status"],
47+
"values": ["count"]
4348
},
4449
{
4550
"id": "accounts_signed",
4651
"title": "Accounts Signed",
4752
"type": "line",
48-
"object": "accounts",
49-
"categoryField": "signed_at",
50-
"categoryGranularity": "month",
51-
"aggregate": "count"
53+
"dataset": "accounts",
54+
"dimensions": ["signed_month"],
55+
"values": ["count"]
5256
}
5357
]
5458
}
5559
```
5660

61+
#### Where a widget's data comes from
62+
63+
Filters scope a widget's **query**, so which data surface a widget uses decides
64+
whether it can respond at all:
65+
66+
| Surface | Shape | Filtered? |
67+
| --- | --- | --- |
68+
| Semantic-layer dataset (ADR-0021) | `"dataset": "invoices"` + `dimensions` + `values` | yes — merged into the dataset query as `runtimeFilter` |
69+
| Inline object query | `"options": { "data": { "provider": "object", "object": "invoices", "aggregate": { "function": "count", "groupBy": "status" } } }` | yes — `AND`-merged into that query |
70+
| Inline static data | `"options": { "data": [ … ], "xField": "status", "yField": "count" }` | no — there is no query to scope |
71+
72+
> **Retired: the top-level inline analytics shape.** `object` +
73+
> `categoryField` / `valueField` / `aggregate` on the widget itself (and the
74+
> pivot `rowField` / `columnField` pair) was **removed** — the renderer no
75+
> longer reads those keys, and a stored widget still carrying them renders a
76+
> visible *"This widget uses a retired data format. Edit it to bind a dataset."*
77+
> prompt instead of a chart. Rebind such a widget to a `dataset` (select its
78+
> `dimensions` and `values` by name), or — for a renderer-internal query with
79+
> no semantic layer behind it — move the query under
80+
> `options.data` with `"provider": "object"`. `@objectstack/spec` refuses the
81+
> retired shape at publish, so this is not a soft deprecation.
82+
5783
### Step 2 — add the built-in date range
5884

5985
Declare `dateRange` at the dashboard level. A preset/custom date-range control
@@ -179,25 +205,24 @@ widget stores the concept under a different field — or should ignore a filter
179205
{
180206
"id": "invoices_by_status",
181207
"type": "bar",
182-
"object": "invoices",
183-
"categoryField": "status",
184-
"aggregate": "count"
208+
"dataset": "invoices",
209+
"dimensions": ["status"],
210+
"values": ["count"]
185211
},
186212
{
187213
"id": "accounts_signed",
188214
"type": "line",
189-
"object": "accounts",
190-
"categoryField": "signed_at",
191-
"categoryGranularity": "month",
192-
"aggregate": "count",
215+
"dataset": "accounts",
216+
"dimensions": ["signed_month"],
217+
"values": ["count"],
193218
"filterBindings": { "dateRange": "signed_at", "region": "sales_region" }
194219
},
195220
{
196221
"id": "total_invoices",
197222
"title": "Total Invoices (all regions)",
198223
"type": "metric",
199-
"object": "invoices",
200-
"aggregate": "count",
224+
"dataset": "invoices",
225+
"values": ["count"],
201226
"filterBindings": { "region": false }
202227
}
203228
]
@@ -265,9 +290,13 @@ is `{ "preset": "last_30_days" }`, a custom range is
265290

266291
Widgets bound to a semantic-layer `dataset` participate the same way: the
267292
dashboard merges the scoped filter into the widget's `filter`, which the
268-
dataset widget forwards to the dataset query as `runtimeFilter`. Inline
269-
(`object`-based) and dataset-bound widgets can mix freely on one filtered
270-
dashboard.
293+
dataset widget forwards to the dataset query as `runtimeFilter`. Dataset-bound
294+
and inline widgets mix freely on one filtered dashboard — the
295+
`plugin-dashboard/filtered-dashboard-dataset-widgets` catalog entry is exactly
296+
that, two dataset-bound widgets beside an inline one. What differs is only what
297+
each surface can answer: an inline **object query**
298+
(`options.data` with `"provider": "object"`) is scoped like a dataset widget,
299+
while an inline **static array** carries no query and is left untouched.
271300

272301
## Nested variable scopes
273302

@@ -281,18 +310,18 @@ stay in sync.
281310

282311
## Known limitations
283312

284-
- **Static-data widgets are not filtered** — a widget with an inline `data`
285-
array has no query to scope, so dashboard filters do not apply to it. Bind
286-
the widget to an `object` (or a `dataset`) if it should respond to filters.
287-
- **Default bindings are metadata-checked for `object` widgets only** — when
288-
a filter's default `field` does not exist on an inline widget's object, the
289-
binding is skipped with a console warning instead of issuing a query that
290-
matches nothing. Dataset-bound widgets can't be checked this way (the
291-
dashboard doesn't know the dataset's base-object fields), so map their
292-
filters explicitly with `filterBindings: { "<name>": "<field>" }` or opt
293-
out with `false`. Explicit string bindings are always honoured as written —
294-
a typo shows up as a visibly empty widget rather than a silently dropped
295-
filter.
313+
- **Static-data widgets are not filtered** — a widget whose `options.data` is
314+
an inline array has no query to scope, so dashboard filters do not apply to
315+
it. Bind the widget to a `dataset` (or give it an `options.data` object
316+
query) if it should respond to filters.
317+
- **A binding is applied as written** — the dashboard does not know a
318+
dataset's fields, so it cannot check a binding target for you. A default
319+
binding whose field the widget's data does not have produces an empty
320+
widget rather than a silent no-op, which is the visible, fixable failure:
321+
map the filter explicitly with `filterBindings: { "<name>": "<field>" }`, or
322+
opt out with `false`. (`buildWidgetScopedFilter` can skip an unknown default
323+
field with a console warning when a host passes it the widget's known field
324+
names; the dashboard renderer does not.)
296325

297326
## i18n
298327

content/docs/plugins/plugin-dashboard.mdx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,28 @@ the widget's own `filter`).
172172
}
173173
],
174174
"widgets": [
175-
{ "id": "w1", "type": "bar", "object": "invoices", "aggregate": "count" },
175+
{ "id": "w1", "type": "bar", "dataset": "invoices", "dimensions": ["status"], "values": ["count"] },
176176
{
177-
"id": "w2", "type": "line", "object": "accounts", "aggregate": "count",
177+
"id": "w2", "type": "line", "dataset": "accounts", "dimensions": ["signed_month"], "values": ["count"],
178178
"filterBindings": { "dateRange": "signed_at", "region": "sales_region" }
179179
},
180180
{
181-
"id": "w3", "type": "metric", "object": "invoices", "aggregate": "count",
181+
"id": "w3", "type": "metric", "dataset": "invoices", "values": ["count"],
182182
"filterBindings": { "region": false }
183183
}
184184
]
185185
}
186186
```
187187

188+
The widgets above bind a **dataset** (ADR-0021). The pre-ADR-0021 top-level
189+
`object` + `categoryField` / `valueField` / `aggregate` shape was removed: the
190+
renderer no longer reads those keys and shows a *"This widget uses a retired
191+
data format. Edit it to bind a dataset."* prompt instead of a chart. A widget
192+
that needs a renderer-internal query rather than a semantic-layer one puts it
193+
under `options.data` as `{ "provider": "object", "object": "invoices",
194+
"aggregate": { "function": "count", "groupBy": "status" } }`; an
195+
`options.data` **array** is fixed demo data and is not filtered.
196+
188197
Binding rules, in precedence order:
189198

190199
1. `filterBindings[name]` as a string — apply the filter to that field.

examples/schema-catalog/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"@object-ui/core": "workspace:*",
3434
"@object-ui/fields": "workspace:*",
3535
"@object-ui/layout": "workspace:*",
36+
"@object-ui/plugin-charts": "workspace:*",
37+
"@object-ui/plugin-dashboard": "workspace:*",
3638
"@object-ui/react": "workspace:*",
3739
"@object-ui/sdui-parser": "workspace:*",
3840
"typescript": "^6.0.3"

examples/schema-catalog/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3947,7 +3947,7 @@ const REGISTRY: Record<string, Example> = {
39473947
id: 'plugin-dashboard/filtered-dashboard-dataset-widgets',
39483948
meta: {
39493949
title: "Filtered Dashboard — Dataset + Inline Widgets",
3950-
description: "Dashboard filters scoping dataset-bound widgets (via the dataset query's runtimeFilter) alongside inline object widgets",
3950+
description: "Dashboard filters scoping dataset-bound widgets (via the dataset query's runtimeFilter) alongside an inline widget",
39513951
category: 'plugin-dashboard',
39523952
},
39533953
schema: plugin_dashboard_filtered_dashboard_dataset_widgets,

examples/schema-catalog/src/schemas/plugin-dashboard/filtered-dashboard-dataset-widgets.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,16 @@
4141
"id": "invoices_by_status",
4242
"title": "Invoices by Status",
4343
"type": "bar",
44-
"object": "invoices",
45-
"categoryField": "status",
46-
"aggregate": "count"
44+
"options": {
45+
"xField": "status",
46+
"yField": "count",
47+
"data": [
48+
{ "status": "Draft", "count": 18 },
49+
{ "status": "Sent", "count": 31 },
50+
{ "status": "Paid", "count": 47 },
51+
{ "status": "Void", "count": 4 }
52+
]
53+
}
4754
}
4855
]
4956
}

0 commit comments

Comments
 (0)