Skip to content

Latest commit

 

History

History
363 lines (304 loc) · 13.2 KB

File metadata and controls

363 lines (304 loc) · 13.2 KB
title Plugin Dashboard

import { InteractiveDemo } from '@/app/components/InteractiveDemo'; import { PluginLoader } from '@/app/components/PluginLoader';

Dashboard layouts and metric widgets for creating beautiful dashboards with KPIs, charts, and statistics.

Installation

npm install @object-ui/plugin-dashboard

<PluginLoader plugins={['dashboard']}>

Interactive Examples

Features

  • Dashboard Layouts - Responsive grid-based layouts
  • Metric Cards - Display KPIs with trends and icons
  • Widget System - Modular widget architecture
  • Customizable - Full Tailwind CSS styling support

Schema API

Dashboard

{
  type: 'dashboard',
  widgets: Widget[],
  label?: string | LocaleMap,       // Header title — spec-canonical spelling; a string or { en, "zh-CN", ... }
  description?: string | LocaleMap, // Header description, under the title
  header?: {                        // Header block — strict: exactly these keys
    showTitle?: boolean,
    showDescription?: boolean,
    actions?: { label, actionUrl?, actionType?, icon? }[]
  },
  globalFilters?: GlobalFilter[],   // Dashboard-level filter bar — see "Dashboard-level filters"
  dateRange?: {                     // Built-in date-range filter — see "Dashboard-level filters"
    field?: string,
    defaultRange?: string,          // a spec date preset, or 'custom'
    allowCustomRange?: boolean
  },
  refreshInterval?: number,         // Auto-refresh period in seconds; runs only when the host wires onRefresh
  columns?: number,                 // Grid columns (default: 3)
  gap?: number,                     // Gap between widgets
  className?: string
}

The header renders only when header is declared, and costs zero pixels when everything it would show is suppressed. The legacy title spelling of label is still read (documents in the wild carry it) but is not authoring surface — the spec rejects it by name, so new documents author label. The retired aria key is neither read nor authorable.

Metric Card

{
  type: 'metric-card',
  title: string,
  value: string | number,
  icon?: string,                  // Lucide icon name
  trend?: 'up' | 'down' | 'neutral',
  trendValue?: string,
  description?: string,
  className?: string
}

Usage

Auto-registration (Side-effect Import)

import '@object-ui/plugin-dashboard';

What the side-effect import registers

That single import is the whole of registration — there is no components map to iterate over. Importing the entry runs the eight ComponentRegistry.register(...) calls in packages/plugin-dashboard/src/index.tsx, which claim exactly these schema types. The keys below are read off those calls.

Namespaced key Bare-name fallback Renderer behind it
view:dashboard dashboard DashboardRenderer — the widget container
plugin-dashboard:metric metric MetricWidget — one KPI value
plugin-dashboard:metric-card metric-card MetricCard — KPI with trend and icon
plugin-dashboard:object-metric object-metric internal wrapper around ObjectMetricWidget — a metric aggregated over an object
plugin-dashboard:pivot pivot PivotTable — pivot over rows you pass in
plugin-dashboard:object-pivot object-pivot internal wrapper around ObjectPivotTable — pivot queried from an object
plugin-dashboard:dashboard-grid dashboard-grid DashboardGridLayout — the drag/resize editable grid
plugin-dashboard:object-data-table object-data-table ObjectDataTable — table queried from an object

ComponentRegistry.register publishes namespace:type, and — unless the call passes skipFallback: true — the bare type as a back-compat fallback (packages/core/src/registry/Registry.ts:194, fallback branch at :226). No call in this package passes skipFallback, so each type above resolves under both spellings; the schema snippets on this page use the bare one.

The two object-* rows name a wrapper rather than an export on purpose. ObjectMetricBlock and ObjectPivotBlock are internal to src/index.tsx: each first resolves the element's dataSource binding through ElementDataSourceGate (from @object-ui/react) and only then renders the exported ObjectMetricWidget / ObjectPivotTable.

Registering a component under your own key

The import above needs no follow-up call. What a manual registration is for here is serving one of this package's exported components under a key of your own — you register the component, not a map:

import { ComponentRegistry } from '@object-ui/core';
import { MetricCard } from '@object-ui/plugin-dashboard';

ComponentRegistry.register('my-metric', MetricCard, {
  namespace: 'my-app',
  label: 'My Metric',
  category: 'Dashboard',
});

The package also exports a dashboardComponents object: the manual-integration map, keyed by the same eight schema types as the table above (objectui#5064 re-keyed it from component class names). Each key maps to the exact component the import registers for that type — for the two object-* types that is the internal data-source-gate wrapper, not the exported widget. Iterating it with ComponentRegistry.register(type, component) therefore re-registers the eight types the import has already claimed, which is still not the registration above: each such call passes no meta, so it trips the no-namespace deprecation warning in register (packages/core/src/registry/Registry.ts:198) and rewrites each bare-name registry entry without its label/category metadata (the namespace:type entries are untouched).

Examples

Dashboard with Multiple Metrics

{
  "type": "dashboard",
  "columns": 4,
  "gap": 6,
  "widgets": [
    {
      "type": "metric-card",
      "title": "Total Sales",
      "value": "$123,456",
      "icon": "shopping-cart",
      "trend": "up",
      "trendValue": "+15%"
    },
    {
      "type": "metric-card",
      "title": "New Customers",
      "value": "856",
      "icon": "user-plus",
      "trend": "up",
      "trendValue": "+22%"
    },
    {
      "type": "metric-card",
      "title": "Bounce Rate",
      "value": "2.4%",
      "icon": "trending-down",
      "trend": "down",
      "trendValue": "-5%"
    },
    {
      "type": "metric-card",
      "title": "Avg. Order Value",
      "value": "$144.20",
      "icon": "dollar-sign",
      "trend": "up",
      "trendValue": "+8%"
    }
  ]
}

Dashboard with Charts

{
  "type": "dashboard",
  "widgets": [
    {
      "type": "metric-card",
      "title": "Total Revenue",
      "value": "$123,456"
    },
    {
      "type": "card",
      "title": "Sales Trend",
      "body": {
        "type": "line-chart",
        "data": [],
        "height": 300
      }
    }
  ]
}

Dashboard-level filters

A dashboard can declare top-level filters — a date range plus any number of select / text filters — whose values drive every bound widget at once. Filter values live as dashboard-level variables; each widget declares which of its own fields a filter binds to via filterBindings, and the dashboard merges the active values into each bound widget's inline query (AND-combined with the widget's own filter).

{
  "type": "dashboard",
  "dateRange": { "field": "created_at", "defaultRange": "last_30_days", "allowCustomRange": true },
  "globalFilters": [
    {
      "name": "region", "field": "region", "label": "Region", "type": "select",
      "options": [
        { "value": "EMEA", "label": "EMEA" },
        { "value": "APAC", "label": "APAC" },
        { "value": "AMER", "label": "AMER" }
      ]
    }
  ],
  "widgets": [
    { "id": "w1", "type": "bar", "dataset": "invoices", "dimensions": ["status"], "values": ["count"] },
    {
      "id": "w2", "type": "line", "dataset": "accounts", "dimensions": ["signed_month"], "values": ["count"],
      "filterBindings": { "dateRange": "signed_at", "region": "sales_region" }
    },
    {
      "id": "w3", "type": "metric", "dataset": "invoices", "values": ["count"],
      "filterBindings": { "region": false }
    }
  ]
}

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

Binding rules, in precedence order:

  1. filterBindings[name] as a string — apply the filter to that field.
  2. filterBindings[name]: false — opt this widget out.
  3. Legacy targetWidgets on the filter — when set, only listed widget ids get the default binding (an explicit filterBindings entry still wins).
  4. Otherwise the filter applies to its own field (the built-in date range defaults to dateRange.field ?? 'created_at').

Date presets stay symbolic (date-macro tokens such as {30_days_ago}) until query time. Dataset-bound widgets receive the merged filter through the dataset query's runtimeFilter. Static-data widgets (inline data arrays) have no query to scope and are not filtered. Filter values are also readable in widget expressions as page.<name>.

For a step-by-step tutorial — filter types, optionsFrom dynamic options, page.* expression usage, and known limitations with workarounds — see the Dashboard-Level Filters guide. The schema catalog ships runnable variants under plugin-dashboard/filtered-dashboard* (dynamic options, filter types, dataset widgets, targetWidgets, date presets).

Widget options — what is actually read

@objectstack/spec's DashboardWidgetOptionsSchema declares five keys and then rides .passthrough() ("declared query keys + open renderer extras"), so any other key parses and validates cleanly — whether or not anything reads it. On a dataset-bound widget (the only spec-legal form: dataset is required), the renderers read exactly:

Key Effect
dateGranularity groups a date dimension by day / week / month / …
sortBy / sortOrder orders by a projected dimension or measure
limit caps the row count
stageOrder explicit stage order for funnel / pyramid
description metric-card sub-caption; the widgets.{id}.subCaption translation channel writes this key

Every other options key on a dataset-bound widget reaches no renderer. The authoring validator (validateTree in @object-ui/sdui-parser) reports each one as a unconsumed-widget-option warning naming the set above — the document still parses, saves and renders; open extras stay open, they just stop being silent. A widget with a genuine out-of-band consumer can opt out in metadata with the spec's own escape hatch: "suppressWarnings": ["unconsumed-widget-option"].

Gauge / solid-gauge / kpi / bullet render as a metric card

There is no dial/arc/target renderer: the single-value families are routed by construction to the metric-card path (objectui#4295) and render as one formatted number. The number's format — including percent handling and currency — comes from the dataset measure's own metadata (the measure's format / currency / percentScale returned by the dataset query), not from widget options. In particular, on a dataset-bound gauge:

  • options.format is not read — declare the format on the dataset measure instead;
  • options.thresholds is not read — the census header in packages/sdui-parser/src/dashboard-widget-options.ts is the canonical statement of that closure claim, and a gate re-derives it on every test run;
  • options.invert is not read — if a measure needs to be displayed as its complement (e.g. a compliance rate stored as a violation rate), add a derived measure to the dataset (derived: { op: 'ratio', … }) and bind the widget to it.

TypeScript Support

import type { DashboardComponentSchema, DashboardWidgetSchema } from '@object-ui/types';

const widget: DashboardWidgetSchema = {
  id: 'revenue',
  title: 'Revenue',
  type: 'metric-card',
  layout: { x: 0, y: 0, w: 1, h: 1 },
};

const dashboard: DashboardComponentSchema = {
  type: 'dashboard',
  columns: 3,
  widgets: [widget],
};

Type-aware list/table widget cells

type: 'table' widgets bound to an objectName infer the renderer for each cell from the object's field type — Badges for select, expanded display name for lookup/user/owner, formatted currency/percent/date for numeric fields, and so on. Lookup columns are auto-expanded server-side via $expand, so you don't see raw FK ids. See @object-ui/plugin-dashboard README for the full mapping table and an example.

License

MIT