Przejdź do głównej zawartości

Document components for code dashboards

Ta treść nie jest jeszcze dostępna w Twoim języku.

Document components present an answer with a clear reading order, such as an analysis, a proposal, or an estimate, in a React dashboard edited through the workspace pull, edit, and push workflow. They can contain embedded results, live data, or interactive scenarios. They do not freeze data or provide an export format. For the steps to write one, see Write a document dashboard in code.

Most documents need no code: the Document template renders a document written as data, with the same components. See Document specification. Code is for what the specification cannot express, and such a dashboard can still render a document with DocumentView.

@overvis/hmi-workspace-ui exports the document components below, DocumentChart, and DocumentChartDataType. @overvis/hmi-workspace-ui/ui exports the UI primitives they compose with, including:

  • TableCaption, TableHeader, TableBody, TableFooter, TableRow, TableHead, and TableCell, composed inside DocumentTable.
  • Alert, AlertTitle, and AlertDescription. Alert variant is default, info, warning, or destructive.
  • Button, with variant default, outline, secondary, ghost, destructive, or link.
  • Field, FieldTitle, FieldDescription, FieldGroup, and Slider for scenario controls.
  • Badge. Icons come from lucide-react.

DashboardRootProps comes from @overvis/hmi-shared/dashboard. DocumentChart needs no additional chart dependency.

The empty dashboard template creates four source files. index.tsx re-exports the others, dashboard.tsx holds the default component, config.ts holds dashboardConfigType, and manifest.ts declares supportedLocales.

ComponentContent and behavior
DashboardDocumentAn article with a maximum reading width, vertical section spacing, and a named container for responsive columns, inside the normal dashboard host.
DocumentLeadRequired title, optional context, and prose children. The host already displays the dashboard catalog name.
DocumentSectionRequired title and arbitrary children, with an associated h2 and consistent section spacing. DocumentProse styles h3 sub-headings.
DocumentColumnscolumns={2} by default or columns={3}. Equal columns stack when the document is narrow, even on a wide browser with the assistant panel open.
DocumentProseParagraphs, lists, emphasis, and links as React children. Provides line length, spacing, and wrapping. Does not parse Markdown or raw HTML.
DocumentMetricRequired label and value, optional detail. Units or currency are part of the supplied content.
DocumentFigureArbitrary figure children and required caption. Supplies containment and an associated caption; the figure content owns its height.
DocumentTableComposes the existing TableHeader, TableBody, TableRow, TableCell, and related components. Wraps descriptions in the first column; other columns retain compact values. Wide tables scroll within their container.

Layout components accept the underlying HTML element’s props and className. Text outside these components and DocumentProse gets the document’s body size but no spacing.

A scenario control is a Field with a FieldTitle that has an id, the control labeled by that id, and an optional FieldDescription. Option Buttons sit in the same Field. FieldGroup spaces several fields.

Slider onValueChange receives number | readonly number[]. typeof value === "number" narrows it; Array.isArray does not narrow a readonly array, so a helper built on it fails the dashboard type check.

const rateLabel = useId();
const [rate, setRate] = useState(60);
<Field>
<FieldTitle id={rateLabel}>Labor rate: €{rate}/hour</FieldTitle>
<Slider
aria-labelledby={rateLabel}
min={30}
max={120}
step={5}
value={[rate]}
onValueChange={(value) => setRate(typeof value === "number" ? value : (value[0] ?? 30))}
/>
<FieldDescription>An allowance, not a quoted rate.</FieldDescription>
</Field>;

DocumentChart draws kind="bar" for category comparisons and kind="line" for ordered trends. It has a stable height, theme colors, labeled series, accessible chart navigation, and tooltips. It displays all supplied rows.

Required props:

  • data: { rows: [{ label, values }], series: [{ key, label }] }. Each row’s values maps series keys to numbers or null. Series order determines legend order and color. Colors cycle after five series.
  • kind: "bar" or "line".
  • label: accessible name describing the chart.
  • formatValue: (value: number) => string, used for the value axis and tooltip.
  • empty: localized React content displayed when no selected series has a value.

DocumentChartDataType is the TypeBox schema and DocumentChartData its derived type. Missing keys and null represent gaps. Zero remains a real value. Lines do not connect across missing values. Every series shares one value axis. Long category labels are shortened on the axis and retained in tooltips. Category positions are equally spaced; the chart is not timestamp-aware. The history report widgets draw irregular stored readings.

<DocumentChart
kind="bar"
label="Weekly consumption"
formatValue={(value) => `${value.toLocaleString(locale)} kWh`}
empty={<p>No measured consumption in this period.</p>}
data={{
series: [{ key: "energy", label: "Consumption" }],
rows: [
{ label: "Week 1", values: { energy: 120 } },
{ label: "Week 2", values: { energy: null } },
{ label: "Week 3", values: { energy: 0 } },
],
}}
/>

A code calculator reads the price list once with billing.prices and prices its inputs with estimateCost from @overvis/hmi-shared/billing, the estimator behind billing.estimate. It updates on every input change without a request, and it never holds prices of its own. A document written as data prices its inputs with a billing.estimate source instead; see API procedures.

import { estimateCost } from "@overvis/hmi-shared/billing";
import { trpc } from "@overvis/hmi-workspace-ui";
const priceList = trpc.billing.prices.useQuery().data?.current ?? null;
const [meters, setMeters] = useState(10);
const estimate = useMemo(() => {
if (priceList == null) return null;
const group = { devices: meters, parametersPerDevice: 5, pollSeconds: 60, endpoints: 1 };
return estimateCost({ deviceGroups: [{ ...group, retentionDays: 365 }] }, priceList, 24);
}, [priceList, meters]);

The profile and the result are the input and answer of billing.estimate; see Estimate through the API. Amounts are micro-euros with VAT. While the price list is null, show that prices are unavailable.