Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/app-showcase/src/views/project.view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ export const ProjectViews = defineView({
type: 'chart',
data,
columns: ['account', 'budget'],
chart: { chartType: 'bar', xAxisField: 'account', yAxisFields: ['budget', 'spent'], aggregation: 'sum' },
chart: {
chartType: 'bar', xAxisField: 'account', yAxisFields: ['budget', 'spent'], aggregation: 'sum',
// ADR-0021 dual-form — bind to the project dataset.
dataset: 'showcase_project_metrics', dimensions: ['account'], values: ['budget_sum', 'spent_sum'],
},
},
},
formViews: {
Expand Down
4 changes: 4 additions & 0 deletions examples/app-showcase/src/views/task.view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ export const TaskViews = defineView({
yAxisFields: ['estimate_hours'],
aggregation: 'sum',
groupByField: 'priority',
// ADR-0021 dual-form — bind to the task dataset.
dataset: 'showcase_task_metrics',
dimensions: ['status', 'priority'],
values: ['est_hours'],
},
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/platform-objects/src/apps/dashboards/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
*/

export { SystemOverviewDashboard } from './system_overview.dashboard.js';
export { SystemOverviewDatasets } from './system.datasets.js';
63 changes: 63 additions & 0 deletions packages/platform-objects/src/apps/dashboards/system.datasets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { defineDataset } from '@objectstack/spec/ui';

/**
* Datasets backing the System Overview dashboard (ADR-0021). Each is a single-
* object count/breakdown over a platform `sys_*` object. The audit objects live
* in `@objectstack/plugin-audit` and `sys_package_installation` is cloud-only —
* datasets reference objects BY NAME, so co-locating them with the dashboard
* (registered by plugin-auth) is fine; the widgets that target absent objects
* are runtime-gated via `requiresObject`.
*/

export const SysUserDataset = defineDataset({
name: 'sys_user_metrics',
label: 'User Metrics',
object: 'sys_user',
dimensions: [],
measures: [{ name: 'user_count', label: 'Users', aggregate: 'count' }],
});

export const SysOrganizationDataset = defineDataset({
name: 'sys_organization_metrics',
label: 'Organization Metrics',
object: 'sys_organization',
dimensions: [],
measures: [{ name: 'org_count', label: 'Organizations', aggregate: 'count' }],
});

export const SysSessionDataset = defineDataset({
name: 'sys_session_metrics',
label: 'Session Metrics',
object: 'sys_session',
dimensions: [],
measures: [{ name: 'session_count', label: 'Sessions', aggregate: 'count' }],
});

export const SysPackageInstallationDataset = defineDataset({
name: 'sys_package_installation_metrics',
label: 'Package Installation Metrics',
object: 'sys_package_installation',
dimensions: [],
measures: [{ name: 'package_count', label: 'Installations', aggregate: 'count' }],
});

export const SysAuditLogDataset = defineDataset({
name: 'sys_audit_log_metrics',
label: 'Audit Log Metrics',
object: 'sys_audit_log',
dimensions: [
{ name: 'action', label: 'Action', field: 'action', type: 'string' },
{ name: 'user_id', label: 'User', field: 'user_id', type: 'lookup' },
],
measures: [{ name: 'event_count', label: 'Events', aggregate: 'count' }],
});

export const SystemOverviewDatasets = [
SysUserDataset,
SysOrganizationDataset,
SysSessionDataset,
SysPackageInstallationDataset,
SysAuditLogDataset,
];
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const SystemOverviewDashboard = Dashboard.create({
// ── Row 1: Platform KPIs ────────────────────────────────────────
{
id: 'widget_total_users',
dataset: 'sys_user_metrics', values: ['user_count'],
title: 'Total Users',
type: 'metric',
object: 'sys_user',
Expand All @@ -40,6 +41,7 @@ export const SystemOverviewDashboard = Dashboard.create({
},
{
id: 'widget_organizations',
dataset: 'sys_organization_metrics', values: ['org_count'],
title: 'Organizations',
type: 'metric',
object: 'sys_organization',
Expand All @@ -50,6 +52,7 @@ export const SystemOverviewDashboard = Dashboard.create({
},
{
id: 'widget_active_sessions',
dataset: 'sys_session_metrics', values: ['session_count'],
title: 'Active Sessions',
type: 'metric',
object: 'sys_session',
Expand All @@ -60,6 +63,7 @@ export const SystemOverviewDashboard = Dashboard.create({
},
{
id: 'widget_packages_installed',
dataset: 'sys_package_installation_metrics', values: ['package_count'],
title: 'Packages Installed',
type: 'metric',
object: 'sys_package_installation',
Expand All @@ -80,6 +84,7 @@ export const SystemOverviewDashboard = Dashboard.create({
// need a richer enum or a separate detail field first.
{
id: 'widget_login_events',
dataset: 'sys_audit_log_metrics', values: ['event_count'],
title: 'Login Events',
type: 'metric',
object: 'sys_audit_log',
Expand All @@ -91,6 +96,7 @@ export const SystemOverviewDashboard = Dashboard.create({
},
{
id: 'widget_permission_changes',
dataset: 'sys_audit_log_metrics', values: ['event_count'],
title: 'Permission Changes',
type: 'metric',
object: 'sys_audit_log',
Expand All @@ -102,6 +108,7 @@ export const SystemOverviewDashboard = Dashboard.create({
},
{
id: 'widget_config_changes',
dataset: 'sys_audit_log_metrics', values: ['event_count'],
title: 'Config Changes',
type: 'metric',
object: 'sys_audit_log',
Expand All @@ -120,6 +127,7 @@ export const SystemOverviewDashboard = Dashboard.create({
// to scope these widgets.
{
id: 'widget_events_by_type',
dataset: 'sys_audit_log_metrics', dimensions: ['action'], values: ['event_count'],
title: 'Audit Events by Action',
description: 'Distribution of audit events by action type',
type: 'pie',
Expand All @@ -130,6 +138,7 @@ export const SystemOverviewDashboard = Dashboard.create({
},
{
id: 'widget_events_by_user',
dataset: 'sys_audit_log_metrics', dimensions: ['user_id'], values: ['event_count'],
title: 'Events by User',
description: 'Activity distribution across users',
type: 'bar',
Expand Down
3 changes: 3 additions & 0 deletions packages/plugins/plugin-auth/src/auth-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
STUDIO_APP,
ACCOUNT_APP,
SystemOverviewDashboard,
SystemOverviewDatasets,
} from '@objectstack/platform-objects/apps';
import { SysOrganizationDetailPage, SysUserDetailPage } from '@objectstack/platform-objects/pages';
import { AuthManager, type AuthManagerOptions } from './auth-manager.js';
Expand Down Expand Up @@ -203,6 +204,8 @@ export class AuthPlugin implements Plugin {
// not exist on sys_user). Schema-embedded listViews is the single
// source of truth.
dashboards: [SystemOverviewDashboard],
// ADR-0021 — datasets backing the System Overview dashboard's widgets.
datasets: SystemOverviewDatasets,
});

ctx.logger.info('Auth Plugin initialized successfully');
Expand Down
33 changes: 29 additions & 4 deletions packages/spec/src/ui/view.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,10 +303,35 @@ export const KanbanConfigSchema = lazySchema(() => z.object({
*/
export const ListChartConfigSchema = lazySchema(() => z.object({
chartType: z.enum(['bar', 'line', 'pie', 'area', 'scatter']).default('bar').describe('Chart visualisation type'),
xAxisField: z.string().describe('Field used as the X axis / category dimension'),
yAxisFields: z.array(z.string()).min(1).describe('Field(s) used as the Y axis / measures'),
aggregation: z.enum(['sum', 'avg', 'count', 'min', 'max']).optional().describe('Aggregation function applied to Y axis fields'),
groupByField: z.string().optional().describe('Optional field used to split / stack the chart'),
// Inline shape — optional since ADR-0021: a charted list view may instead bind
// to a semantic-layer `dataset` (see `dataset`/`dimensions`/`values` below).
xAxisField: z.string().optional().describe('Field used as the X axis / category dimension (inline)'),
yAxisFields: z.array(z.string()).min(1).optional().describe('Field(s) used as the Y axis / measures (inline)'),
aggregation: z.enum(['sum', 'avg', 'count', 'min', 'max']).optional().describe('Aggregation function applied to Y axis fields (inline)'),
groupByField: z.string().optional().describe('Optional field used to split / stack the chart (inline)'),

/**
* ADR-0021 — bind this chart to a semantic-layer `dataset` (the governed
* alternative to the inline `xAxisField` + `yAxisFields` + `aggregation`
* query), mirroring dashboard widgets. Selects dimensions/measures BY NAME so
* the chart's numbers stay consistent with every other surface using the same
* dataset. Additive / dual-form: inline charts are unchanged.
*/
dataset: SnakeCaseIdentifierSchema.optional().describe('Dataset name to bind (ADR-0021)'),
/** Dimension names (from the dataset) for X / group / split. Dataset-bound only. */
dimensions: z.array(z.string()).optional().describe('Dimension names — X/group/split (dataset-bound)'),
/** Measure names (from the dataset) for the value axis. Dataset-bound only. */
values: z.array(z.string()).optional().describe('Measure names — Y (dataset-bound)'),
}).superRefine((c, ctx) => {
// ADR-0021 dual-form: a chart is EITHER dataset-bound (needs `values`) OR an
// inline query (needs `xAxisField` + `yAxisFields`).
if (c.dataset) {
if (!c.values || c.values.length === 0) {
ctx.addIssue({ code: 'custom', message: 'a dataset-bound chart needs `values` (measure names).', path: ['values'] });
}
} else if (!c.xAxisField || !c.yAxisFields || c.yAxisFields.length === 0) {
ctx.addIssue({ code: 'custom', message: 'an inline chart needs `xAxisField` + `yAxisFields` (or bind a `dataset`).', path: ['xAxisField'] });
}
}).describe('List chart view configuration'));

/**
Expand Down
Loading