Skip to content

Commit ef3fdd7

Browse files
os-zhuangclaude
andauthored
feat(showcase): Revenue Pulse — interactive dashboard-level filters demo over two objects (#2501) (#3038)
New showcase_revenue_pulse dashboard: one dateRange + one region filter driving invoice AND account charts, each widget mapping the filters to ITS OWN fields via filterBindings (issued_on/region on invoices vs signed_on/sales_region on accounts), plus an all-time KPI opted out of both — the acceptance scenario of #2501 on a real backend. - objects: showcase_account gains sales_region (select) + signed_on (date); showcase_invoice gains region (select). - seed: accounts carry regions + signed_on spread over ~2.5 years (prospects deliberately unsigned); invoices grow 4 → 12 spread across ~6 months and all three regions so filter changes move the charts visibly. - datasets: showcase_invoice_metrics + showcase_account_metrics (count/sum measures, month-granular date dimensions). - registered in objectstack.config.ts + Analytics nav ('Revenue Pulse (filtered)'); coverage.ts notes the filters demo. Verified against a live server (objectstack dev --fresh): dashboard metadata round-trips name/filterBindings through spec validation, and dataset queries with runtimeFilter produce correct scoped SQL and rows (12 invoices all-time → 7 in last 90d → 4 with region=amer; account-side signed_on/sales_region re-mapping and month/industry groupings correct). 55/55 showcase tests pass. Claude-Session: https://claude.ai/code/session_01HG5LQnPbQbjAAyofghzz3Z Co-authored-by: Claude <noreply@anthropic.com>
1 parent d918c9f commit ef3fdd7

10 files changed

Lines changed: 186 additions & 23 deletions

File tree

examples/app-showcase/objectstack.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import { registerRecalcEndpoint } from './src/system/server/recalc-endpoint.js';
2020
import { registerShowcasePositionBindings } from './src/security/bind-position-sets.js';
2121
import { TaskViews, ProjectViews, InquiryViews, BusinessUnitViews } from './src/ui/views/index.js';
2222
import { ShowcaseApp } from './src/ui/apps/index.js';
23-
import { ChartGalleryDashboard, OpsDashboard } from './src/ui/dashboards/index.js';
24-
import { ShowcaseTaskDataset, ShowcaseProjectDataset } from './src/ui/datasets/index.js';
23+
import { ChartGalleryDashboard, OpsDashboard, RevenuePulseDashboard } from './src/ui/dashboards/index.js';
24+
import { ShowcaseTaskDataset, ShowcaseProjectDataset, ShowcaseInvoiceDataset, ShowcaseAccountDataset } from './src/ui/datasets/index.js';
2525
import { allReports } from './src/ui/reports/index.js';
2626
import { allActions } from './src/ui/actions/index.js';
2727
import { CapabilityMapPage, StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage } from './src/ui/pages/index.js';
@@ -183,9 +183,9 @@ export default defineStack({
183183
portals: allPortals,
184184
views: [TaskViews, ProjectViews, InquiryViews, BusinessUnitViews],
185185
pages: [CapabilityMapPage, StartHerePage, ComponentGalleryPage, ProjectWorkspacePage, ProjectDetailPage, TaskWorkbenchPage, TaskTriagePage, TaskBoardPage, TaskCalendarPage, TaskGalleryPage, TaskSchedulePage, TaskTimelinePage, TaskMapPage, TaskAllViewsPage, ActiveProjectsPage, TaskDetailPage, ReviewQueuePage, NewProjectWizardPage, MyWorkPage, SettingsPage, StylingGalleryPage, CommandCenterPage, CommandCenterJsxPage, CrmWorkbenchPage, TaskDeskPage, PageVariablesPage, ContactFormPage, RenewalsPipelinePage],
186-
dashboards: [ChartGalleryDashboard, OpsDashboard],
186+
dashboards: [ChartGalleryDashboard, OpsDashboard, RevenuePulseDashboard],
187187
books: allBooks,
188-
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset],
188+
datasets: [ShowcaseTaskDataset, ShowcaseProjectDataset, ShowcaseInvoiceDataset, ShowcaseAccountDataset],
189189
reports: allReports,
190190
actions: allActions,
191191
themes: allThemes,

examples/app-showcase/src/coverage.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,12 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
8484
// ── ui ──
8585
view: { status: 'demonstrated', files: ['src/ui/views/task.view.ts', 'src/ui/views/project.view.ts'] },
8686
page: { status: 'demonstrated', files: ['src/ui/pages/index.ts'] },
87-
dashboard: { status: 'demonstrated', files: ['src/ui/dashboards/chart-gallery.dashboard.ts'] },
87+
dashboard: {
88+
status: 'demonstrated',
89+
files: ['src/ui/dashboards/chart-gallery.dashboard.ts', 'src/ui/dashboards/revenue-pulse.dashboard.ts'],
90+
notes:
91+
'revenue-pulse also demonstrates dashboard-level filters (framework#2501): dateRange + globalFilters driving two objects via per-widget filterBindings.',
92+
},
8893
app: { status: 'demonstrated', files: ['src/ui/apps/index.ts'] },
8994
action: { status: 'demonstrated', files: ['src/ui/actions/index.ts'] },
9095
report: { status: 'demonstrated', files: ['src/ui/reports/index.ts'] },

examples/app-showcase/src/data/objects/account.object.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ export const Account = ObjectSchema.create({
7474
{ label: 'Churned', value: 'churned', color: '#EF4444' },
7575
],
7676
}),
77+
// Region + signed date power the Revenue Pulse dashboard's dashboard-level
78+
// filters (framework#2501): the shared "region" filter maps to THIS
79+
// object's `sales_region` (invoices carry their own `region`), and the
80+
// dashboard date range maps to `signed_on` (invoices use `issued_on`) —
81+
// the cross-object per-widget `filterBindings` demo.
82+
sales_region: Field.select({
83+
label: 'Sales Region',
84+
options: [
85+
{ label: 'AMER', value: 'amer', default: true },
86+
{ label: 'EMEA', value: 'emea' },
87+
{ label: 'APAC', value: 'apac' },
88+
],
89+
}),
90+
signed_on: Field.date({ label: 'Customer Since' }),
7791
// EIN-style tax id — the `format` rule below enforces the NN-NNNNNNN shape
7892
// with a regex (a deliberately non-trivial pattern, unlike the built-in
7993
// email/url field validators).

examples/app-showcase/src/data/objects/invoice.object.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,17 @@ export const Invoice = ObjectSchema.create({
8787
// invoices; because `showcase_invoice_line` is `controlled_by_parent`, the
8888
// lines follow automatically (ADR-0055). Mirror of `project.owner`.
8989
owner: Field.text({ label: 'Owner', maxLength: 200 }),
90+
// Region the sale was booked in — the invoice-side target of the Revenue
91+
// Pulse dashboard's shared "region" filter (framework#2501): accounts map
92+
// the same filter to their own `sales_region` via `filterBindings`.
93+
region: Field.select({
94+
label: 'Region',
95+
options: [
96+
{ label: 'AMER', value: 'amer', default: true },
97+
{ label: 'EMEA', value: 'emea' },
98+
{ label: 'APAC', value: 'apac' },
99+
],
100+
}),
90101
status: Field.select({
91102
label: 'Status',
92103
required: true,

examples/app-showcase/src/data/seed/index.ts

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,30 @@ const accounts = defineSeed(Account, {
3939
// `status` is required and no default is applied at seed-insert time, so it
4040
// must be set explicitly or the row is rejected (this is why Accounts was
4141
// empty). `hq` also exercises the location field.
42-
{ name: 'Northwind', industry: 'retail', annual_revenue: 8_000_000, website: 'https://northwind.example', status: 'active', hq: { lat: 47.6062, lng: -122.3321 }, tax_id: '91-1144442', billing_email: 'ap@northwind.example' },
43-
{ name: 'Contoso', industry: 'technology', annual_revenue: 25_000_000, website: 'https://contoso.example', status: 'active', hq: { lat: 37.7749, lng: -122.4194 }, tax_id: '20-3399881', billing_email: 'billing@contoso.example' },
44-
{ name: 'Fabrikam', industry: 'healthcare', annual_revenue: 12_000_000, website: 'https://fabrikam.example', status: 'prospect', hq: { lat: 40.7128, lng: -74.0060 }, tax_id: '46-7782013', billing_email: 'accounts@fabrikam.example' },
42+
// `sales_region` + `signed_on` feed the Revenue Pulse filtered dashboard
43+
// (framework#2501): its region filter maps here via filterBindings
44+
// (`region → sales_region`) and its date range via `dateRange → signed_on`.
45+
// Prospects have no `signed_on` yet — date-scoped account charts exclude
46+
// them by design.
47+
{ name: 'Northwind', industry: 'retail', annual_revenue: 8_000_000, website: 'https://northwind.example', status: 'active', sales_region: 'amer', signed_on: cel`daysAgo(400)`, hq: { lat: 47.6062, lng: -122.3321 }, tax_id: '91-1144442', billing_email: 'ap@northwind.example' },
48+
{ name: 'Contoso', industry: 'technology', annual_revenue: 25_000_000, website: 'https://contoso.example', status: 'active', sales_region: 'amer', signed_on: cel`daysAgo(700)`, hq: { lat: 37.7749, lng: -122.4194 }, tax_id: '20-3399881', billing_email: 'billing@contoso.example' },
49+
{ name: 'Fabrikam', industry: 'healthcare', annual_revenue: 12_000_000, website: 'https://fabrikam.example', status: 'prospect', sales_region: 'emea', hq: { lat: 40.7128, lng: -74.0060 }, tax_id: '46-7782013', billing_email: 'accounts@fabrikam.example' },
4550
// Extra accounts so the record picker has enough volume to exercise search,
4651
// sorting, pagination and the (non-churned) scoping filter on invoices.
47-
{ name: 'Initech', industry: 'finance', annual_revenue: 5_400_000, website: 'https://initech.example', status: 'active', hq: { lat: 30.2672, lng: -97.7431 }, tax_id: '74-2233110', billing_email: 'ap@initech.example' },
48-
{ name: 'Globex', industry: 'technology', annual_revenue: 42_000_000, website: 'https://globex.example', status: 'active', hq: { lat: 34.0522, lng: -118.2437 }, tax_id: '95-8841200', billing_email: 'billing@globex.example' },
49-
{ name: 'Stark Industries', industry: 'technology', annual_revenue: 180_000_000, website: 'https://stark.example', status: 'active', hq: { lat: 40.7580, lng: -73.9855 }, tax_id: '13-5567421', billing_email: 'ap@stark.example' },
52+
{ name: 'Initech', industry: 'finance', annual_revenue: 5_400_000, website: 'https://initech.example', status: 'active', sales_region: 'amer', signed_on: cel`daysAgo(300)`, hq: { lat: 30.2672, lng: -97.7431 }, tax_id: '74-2233110', billing_email: 'ap@initech.example' },
53+
{ name: 'Globex', industry: 'technology', annual_revenue: 42_000_000, website: 'https://globex.example', status: 'active', sales_region: 'amer', signed_on: cel`daysAgo(500)`, hq: { lat: 34.0522, lng: -118.2437 }, tax_id: '95-8841200', billing_email: 'billing@globex.example' },
54+
{ name: 'Stark Industries', industry: 'technology', annual_revenue: 180_000_000, website: 'https://stark.example', status: 'active', sales_region: 'amer', signed_on: cel`daysAgo(800)`, hq: { lat: 40.7580, lng: -73.9855 }, tax_id: '13-5567421', billing_email: 'ap@stark.example' },
5055
// CJK-named account so pinyin search recall (#2486) is demonstrable out of
5156
// the box: with the zh-CN locale configured, `$search=huaning` / `hnkj`
5257
// must find 华宁科技 in the record picker and list quick-search.
53-
{ name: '华宁科技', industry: 'technology', annual_revenue: 36_000_000, website: 'https://huaning.example', status: 'active', hq: { lat: 31.2304, lng: 121.4737 }, tax_id: '91-3100001', billing_email: 'billing@huaning.example' },
54-
{ name: 'Wayne Enterprises', industry: 'finance', annual_revenue: 210_000_000, website: 'https://wayne.example', status: 'active', hq: { lat: 40.7128, lng: -74.0060 }, tax_id: '22-9087733', billing_email: 'billing@wayne.example' },
55-
{ name: 'Acme Retail', industry: 'retail', annual_revenue: 3_200_000, website: 'https://acme.example', status: 'prospect', hq: { lat: 41.8781, lng: -87.6298 }, tax_id: '36-4471209', billing_email: 'accounts@acme.example' },
56-
{ name: 'Soylent Foods', industry: 'healthcare', annual_revenue: 9_900_000, website: 'https://soylent.example', status: 'prospect', hq: { lat: 37.3382, lng: -121.8863 }, tax_id: '77-1029384', billing_email: 'ap@soylent.example' },
57-
{ name: 'Hooli', industry: 'technology', annual_revenue: 96_000_000, website: 'https://hooli.example', status: 'active', hq: { lat: 37.3861, lng: -122.0839 }, tax_id: '45-7781230', billing_email: 'billing@hooli.example' },
58-
{ name: 'Vandelay Industries', industry: 'finance', annual_revenue: 6_700_000, website: 'https://vandelay.example', status: 'active', hq: { lat: 40.6782, lng: -73.9442 }, tax_id: '11-3344556', billing_email: 'ap@vandelay.example' },
59-
{ name: 'Umbrella Health', industry: 'healthcare', annual_revenue: 33_000_000, website: 'https://umbrella.example', status: 'churned', hq: { lat: 39.9526, lng: -75.1652 }, tax_id: '88-2200117', churn_reason: 'Switched to in-house platform', billing_email: 'accounts@umbrella.example' },
60-
{ name: 'Wonka Brands', industry: 'retail', annual_revenue: 14_500_000, website: 'https://wonka.example', status: 'churned', hq: { lat: 41.4993, lng: -81.6944 }, tax_id: '52-7741093', churn_reason: 'Budget cuts', billing_email: 'ap@wonka.example' },
58+
{ name: '华宁科技', industry: 'technology', annual_revenue: 36_000_000, website: 'https://huaning.example', status: 'active', sales_region: 'apac', signed_on: cel`daysAgo(200)`, hq: { lat: 31.2304, lng: 121.4737 }, tax_id: '91-3100001', billing_email: 'billing@huaning.example' },
59+
{ name: 'Wayne Enterprises', industry: 'finance', annual_revenue: 210_000_000, website: 'https://wayne.example', status: 'active', sales_region: 'amer', signed_on: cel`daysAgo(900)`, hq: { lat: 40.7128, lng: -74.0060 }, tax_id: '22-9087733', billing_email: 'billing@wayne.example' },
60+
{ name: 'Acme Retail', industry: 'retail', annual_revenue: 3_200_000, website: 'https://acme.example', status: 'prospect', sales_region: 'amer', hq: { lat: 41.8781, lng: -87.6298 }, tax_id: '36-4471209', billing_email: 'accounts@acme.example' },
61+
{ name: 'Soylent Foods', industry: 'healthcare', annual_revenue: 9_900_000, website: 'https://soylent.example', status: 'prospect', sales_region: 'emea', hq: { lat: 37.3382, lng: -121.8863 }, tax_id: '77-1029384', billing_email: 'ap@soylent.example' },
62+
{ name: 'Hooli', industry: 'technology', annual_revenue: 96_000_000, website: 'https://hooli.example', status: 'active', sales_region: 'amer', signed_on: cel`daysAgo(150)`, hq: { lat: 37.3861, lng: -122.0839 }, tax_id: '45-7781230', billing_email: 'billing@hooli.example' },
63+
{ name: 'Vandelay Industries', industry: 'finance', annual_revenue: 6_700_000, website: 'https://vandelay.example', status: 'active', sales_region: 'emea', signed_on: cel`daysAgo(450)`, hq: { lat: 40.6782, lng: -73.9442 }, tax_id: '11-3344556', billing_email: 'ap@vandelay.example' },
64+
{ name: 'Umbrella Health', industry: 'healthcare', annual_revenue: 33_000_000, website: 'https://umbrella.example', status: 'churned', sales_region: 'amer', signed_on: cel`daysAgo(600)`, hq: { lat: 39.9526, lng: -75.1652 }, tax_id: '88-2200117', churn_reason: 'Switched to in-house platform', billing_email: 'accounts@umbrella.example' },
65+
{ name: 'Wonka Brands', industry: 'retail', annual_revenue: 14_500_000, website: 'https://wonka.example', status: 'churned', sales_region: 'emea', signed_on: cel`daysAgo(350)`, hq: { lat: 41.4993, lng: -81.6944 }, tax_id: '52-7741093', churn_reason: 'Budget cuts', billing_email: 'ap@wonka.example' },
6166
],
6267
});
6368

@@ -261,10 +266,22 @@ const invoices = defineSeed(Invoice, {
261266
mode: 'upsert',
262267
externalId: 'name',
263268
records: [
264-
{ name: 'INV-1001', account: 'Northwind', owner: 'ada@example.com', status: 'sent', issued_on: cel`daysAgo(10)`, tax_rate: 8 },
265-
{ name: 'INV-1002', account: 'Contoso', owner: 'ada@example.com', status: 'draft', tax_rate: 0 },
266-
{ name: 'INV-1003', account: 'Contoso', owner: 'linus@example.com', status: 'paid', issued_on: cel`daysAgo(30)`, paid_on: cel`daysAgo(2)`, tax_rate: 8 },
267-
{ name: 'INV-1004', account: 'Fabrikam', owner: 'grace@example.com', status: 'draft', tax_rate: 0 },
269+
{ name: 'INV-1001', account: 'Northwind', owner: 'ada@example.com', status: 'sent', issued_on: cel`daysAgo(10)`, tax_rate: 8, region: 'amer' },
270+
{ name: 'INV-1002', account: 'Contoso', owner: 'ada@example.com', status: 'draft', tax_rate: 0, region: 'amer' },
271+
{ name: 'INV-1003', account: 'Contoso', owner: 'linus@example.com', status: 'paid', issued_on: cel`daysAgo(30)`, paid_on: cel`daysAgo(2)`, tax_rate: 8, region: 'amer' },
272+
{ name: 'INV-1004', account: 'Fabrikam', owner: 'grace@example.com', status: 'draft', tax_rate: 0, region: 'emea' },
273+
// Volume spread across months + regions so the Revenue Pulse filtered
274+
// dashboard (framework#2501) shows visible movement when the date range
275+
// or region filter changes — not just one bar. `issued_on` spans ~6
276+
// months; regions cover AMER / EMEA / APAC.
277+
{ name: 'INV-1005', account: 'Contoso', owner: 'ada@example.com', status: 'paid', issued_on: cel`daysAgo(45)`, paid_on: cel`daysAgo(40)`, tax_rate: 8, region: 'amer' },
278+
{ name: 'INV-1006', account: 'Globex', owner: 'linus@example.com', status: 'sent', issued_on: cel`daysAgo(15)`, tax_rate: 8, region: 'amer' },
279+
{ name: 'INV-1007', account: '华宁科技', owner: 'grace@example.com', status: 'paid', issued_on: cel`daysAgo(60)`, paid_on: cel`daysAgo(50)`, tax_rate: 6, region: 'apac' },
280+
{ name: 'INV-1008', account: '华宁科技', owner: 'ada@example.com', status: 'sent', issued_on: cel`daysAgo(5)`, tax_rate: 6, region: 'apac' },
281+
{ name: 'INV-1009', account: 'Vandelay Industries', owner: 'linus@example.com', status: 'paid', issued_on: cel`daysAgo(100)`, paid_on: cel`daysAgo(90)`, tax_rate: 20, region: 'emea' },
282+
{ name: 'INV-1010', account: 'Fabrikam', owner: 'grace@example.com', status: 'sent', issued_on: cel`daysAgo(75)`, tax_rate: 20, region: 'emea' },
283+
{ name: 'INV-1011', account: 'Stark Industries', owner: 'ada@example.com', status: 'paid', issued_on: cel`daysAgo(130)`, paid_on: cel`daysAgo(120)`, tax_rate: 8, region: 'amer' },
284+
{ name: 'INV-1012', account: 'Hooli', owner: 'linus@example.com', status: 'sent', issued_on: cel`daysAgo(170)`, tax_rate: 8, region: 'amer' },
268285
],
269286
});
270287

examples/app-showcase/src/ui/apps/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const ShowcaseApp = App.create({
8888
children: [
8989
{ id: 'nav_command_center', type: 'page', pageName: 'showcase_command_center', label: 'Command Center (大屏)', icon: 'monitor-dot' },
9090
{ id: 'nav_ops', type: 'dashboard', dashboardName: 'showcase_ops_dashboard', label: 'Delivery Operations', icon: 'gauge' },
91+
{ id: 'nav_revenue_pulse', type: 'dashboard', dashboardName: 'showcase_revenue_pulse', label: 'Revenue Pulse (filtered)', icon: 'sliders-horizontal' },
9192
{ id: 'nav_charts', type: 'dashboard', dashboardName: 'showcase_chart_gallery', label: 'Chart Gallery', icon: 'layout-dashboard' },
9293
{ id: 'nav_report_tabular', type: 'object', objectName: 'showcase_task', viewName: 'tabular', label: 'Task List', icon: 'table' },
9394
{ id: 'nav_report_summary', type: 'report', reportName: 'showcase_hours_by_status', label: 'Hours by Status', icon: 'sigma' },

examples/app-showcase/src/ui/dashboards/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
export { ChartGalleryDashboard } from './chart-gallery.dashboard.js';
44
export { OpsDashboard } from './ops-dashboard.dashboard.js';
5+
export { RevenuePulseDashboard } from './revenue-pulse.dashboard.js';

0 commit comments

Comments
 (0)