Workbenches and Embeds
/ui gives a host four ready surfaces, split along two axes: changing how one observes or showing what someone already decided, and data views or dashboards.
| Data views (records and analyses) | Dashboards | |
|---|---|---|
| Workbench: list, edit, save | DataWorkbench | DashboardWorkbench |
| Embed: read-only, inside a business page | EmbeddedView | EmbeddedDashboard |
A workbench lets a user change how they observe; an embed lets a page show what someone already decided, so an embed has no view list, no condition editor and no save — nothing it does is written anywhere. They are split by resource for the same reason: a host that embeds a board says so, and a record view handed to the board's entry is refused as a view it cannot show.
All four may leave out engine, messages and locale: they take the ViewHost's above.
<DataWorkbench definitionId="orders" />Guides: Fitting the View Engine into a Host (embeds that show a decided view inside a business page); Getting Started with the View Engine (step 8 puts DataWorkbench on a page); Theming the View Engine (how the four surfaces look).
DataWorkbench
The workbench of one data definition: its record views and analysis views in one list, which the user switches between as between any two views.
| Prop | Role |
|---|---|
definitionId | The definition to draw |
instanceId, onInstanceChange | Which view is open, as value is on an input: leaving it out lets the workbench own it from the effective default on, and passing it — a string, or null for that default — puts a host's route in charge, every later change opening what it names. It goes through the leave guard, so a pushed view never takes an unsaved draft away without asking. onInstanceChange reports in the same vocabulary, so a host puts it straight into a route |
viewKinds | Which kinds it lists and draws, in the order the "new view" menu offers them. Both by default; naming one is a real narrowing — the other kind is neither listed nor openable there |
record | What the host says about the record views (RecordViewProps): business actions, how a cell is read, whether rows can be picked, what an empty result says. The binding (bind's actions, slots, reading) is the default of each; one given here wins |
features | Which of the workbench's own controls are on screen: export, layouts, columns, sort, search, visualization, manage. All on by default; one turned off is absent, not disabled |
templates | What a new view of each kind starts from: defaultRecordConfig and defaultAnalysisConfig when left out. The view still opens unsaved, and the first save asks for its name and audience |
handOver | A view a dashboard or an embed handed the host's route, opened here as it came: the saved view with what the reader set on the board among its conditions ("Modified", each removable, "Restore" takes them off), or a view nobody saved. Each new object opens once, through the leave guard |
onNavigate | The way back to the board a view was handed from: with it, the workbench draws "Back to ⟨dashboard⟩" under the title bar. The ViewHost's route when left out |
expandable | Whether it offers to fill the screen; on by default |
landmark | The landmark the work column is: main by default; region where the page already has its own <main> |
defaultSidebarOpen, onSidebarOpenChange | Where the sidebar starts: view state, never saved and never asked about by the leave guard. Left out, a column narrower than md opens folded |
density, preset, theme, tokens | The density, a pinned preset, the mode, and the host's own --fve-* for this surface and its popups |
onRenderFailure | Told of a render failure one of its boundaries caught; the part shows a recoverable error state in place regardless |
export interface DataWorkbenchProps {
defaultSidebarOpen?: boolean;
definitionId: string;
density?: ViewDensity;
engine?: ViewEngine;
expandable?: boolean;
features?: WorkbenchFeatures;
handOver?: ViewHandOver | null;
instanceId?: string | null;
landmark?: WorkbenchLandmark;
locale?: string;
messages?: ViewMessages;
onInstanceChange?(id: string | null): void;
onNavigate?(to: ViewNavigation): void;
onRenderFailure?: RenderFailureHandler;
onSidebarOpenChange?(open: boolean): void;
optionsFor?(remote: string): FieldOption[] | undefined;
preset?: ViewPreset;
record?: RecordViewProps;
templates?: { record?: RecordViewConfig; analysis?: AnalysisViewConfig };
theme?: ViewTheme;
tokens?: ViewSurfaceProps['tokens'];
viewKinds?: readonly DataViewKind[];
}
export declare function DataWorkbench(props: DataWorkbenchProps): import('react').JSX.Element;RecordViewProps
What a host says about the record views a workbench draws. They are one object because none of them means anything to an analysis view (an analysis view has no business actions).
| Prop | Role |
|---|---|
actions, slots | Declared actions and the host's own markup (see declared actions) |
detail | The record detail: which record is open, for a host that keeps it in its address, and the host's own sections. Left out, the detail is the workbench's own — a row opens it, its close closes it — holding the definition's field groups alone |
renderCell | Renders one cell of the table, everything else kept. Fall back to cellValue for the cells it has nothing special to say about, and enum labels, the zone and number formats keep working |
selectable | Whether rows can be picked; on by default. A host with nothing to do with a selection turns it off rather than show a column of checkboxes that lead nowhere |
emptyTitle, emptyDescription, emptyAction | The empty result in the host's own words; emptyAction is what its one button does, null no button |
onExported | Told whenever an export has been handed to the browser — the file's name, its contents and how many rows of which scope. A host that audits what leaves the application reads it |
export interface RecordViewProps {
actions?: RecordActions;
detail?: RecordDetailOptions;
emptyAction?: (() => void) | null;
emptyDescription?: string;
emptyTitle?: string;
onExported?(file: ExportedFile): void;
renderCell?(cell: RecordCell): import('react').ReactNode;
selectable?: boolean;
slots?: RecordActionSlots;
}DashboardWorkbench
The workbench of one dashboard definition: browse, edit and save boards. A board's filter values are the reader's and never the board's config — a host that wants them in its address listens to onFiltersChange and onTabChange; the package never touches the address itself.
| Prop | Role |
|---|---|
instanceId, onInstanceChange | As on DataWorkbench |
initialTab, onTabChange | The tab the board opens on, as a host's route has it; left out, or a tab the board does not have, the board opens where its reader last read it |
initialFilters, onFiltersChange | What the filters hold as the board opens, as a host's address has them; left out, every filter starts at its default, and what the board does not take is left out |
onNavigate | Every way off the board goes through it — "Open in workbench" on a panel, the follow-up menu on a group, a panel's custom destination. Without it none of these exist: a press on a group does nothing unless the panel cross-filters |
features | manage (the view manager) and export (a record panel's "Export data…") only; a board whose data must not leave the page turns export off |
template | What a new board starts from; an empty one when left out |
The other props (definitionId, expandable, landmark, the sidebar, the look, onRenderFailure) are those of DataWorkbench.
export interface DashboardWorkbenchProps {
defaultSidebarOpen?: boolean;
definitionId: string;
density?: ViewDensity;
engine?: ViewEngine;
expandable?: boolean;
features?: Pick<WorkbenchFeatures, 'manage' | 'export'>;
initialFilters?: DashboardFilters | null;
initialTab?: string | null;
instanceId?: string | null;
landmark?: WorkbenchLandmark;
locale?: string;
messages?: ViewMessages;
onFiltersChange?(filters: DashboardFilters): void;
onInstanceChange?(id: string | null): void;
onNavigate?(to: ViewNavigation): void;
onRenderFailure?: RenderFailureHandler;
onSidebarOpenChange?(open: boolean): void;
onTabChange?(tabId: string | null): void;
optionsFor?(remote: string): FieldOption[] | undefined;
preset?: ViewPreset;
template?: DashboardViewConfig;
theme?: ViewTheme;
tokens?: ViewSurfaceProps['tokens'];
}
export declare function DashboardWorkbench(props: DashboardWorkbenchProps): import('react').JSX.Element;What both embeds take
Everything an embed drops is chrome; admission, paging, auto-refresh and the request budget are the runtime's, identical to the workbench's.
| Prop | Role |
|---|---|
instanceId | The saved view or board to show; a code-declared system one works too |
interaction | How far the reader may go (EmbedInteraction); static by default |
withTitle, headingLevel | Whether its title is drawn as a heading at headingLevel (off by default: a page usually names what it embeds in its own words); the level is 2 by default, since only the host knows its outline |
autoRefresh | Whether it refreshes itself on the interval its author saved; on by default. Off, the timer never runs and nothing on screen offers it back |
expandable | "Fill the screen" in the first row, in the interactive tier; off by default, and never in the static tier |
openInWorkbench | Whether "Open in workbench" is offered in the interactive tier, with a route to go by; on by default, and never in the static tier |
onNavigate | The host's route: "Open in workbench", the follow-up menu on a group and a panel's destination go through it. Without it — here, or the ViewHost's router or navigate — none of them exist |
size | How tall it is: content (the default) or fill |
ref | The surface it draws on; a host that wants the fill-the-screen control in its own chrome points useViewExpansion at it |
export interface EmbedBaseProps {
autoRefresh?: boolean;
className?: string;
density?: ViewDensity;
engine?: ViewEngine;
expandable?: boolean;
headingLevel?: PanelHeadingLevel;
instanceId: string;
locale?: string;
messages?: ViewMessages;
onNavigate?(to: ViewNavigation): void;
onRenderFailure?: RenderFailureHandler;
openInWorkbench?: boolean;
preset?: ViewPreset;
ref?: import('react').Ref<HTMLDivElement>;
size?: EmbedSize;
theme?: ViewTheme;
tokens?: ViewSurfaceProps['tokens'];
withTitle?: boolean;
}Interaction tiers
Neither tier writes anything: an embed never saves a view, a board or a preference.
static— what the page shows, as its author saved it: the rows, the chart or the board and what they were fetched under; nothing on it filters, sorts, pages, redraws or leads anywhere. The default, since a business page shows what someone else set up.interactive— the reader may look closer, for this viewing only: change a board's filters, sort by a header, page, switch an analysis between table and chart, press a group (the follow-up menu, a board's cross-filter), fill the screen where the host offers it, and open the view in the workbench. None of it is saved; the ways off the page go through the host's route.
export type EmbedInteraction = 'static' | 'interactive';EmbeddedView
One saved record or analysis view inside a business page: the result, and what the host switched on around it.
| Prop | Role |
|---|---|
scopeFilter | An outer condition ANDed onto the view's own, in the view's field names: the page's narrowing, locked — the reader sees it in the applied band and cannot take it off. It is admitted like a user's own filter, so a host cannot widen a view past what its definition allows, and it never reaches the saved config |
withSearch | The view's search box at the end of the applied band, where the definition declares a search field (off by default). Record views and the interactive tier only |
withExport | The export button and window in the first row (off by default). Record views and the interactive tier only; with it, rows can be picked, since the window offers to take the picked ones |
detail | A record's detail, opened from its row (off by default): true opens it as the host reads the definition's records (bind's reading), its open record this embed's own, written to no address; RecordDetailOptions lets the host hold which record is open and add sections. Record views and the interactive tier only; read-only |
rowActions | What the host offers on one row of a record view; there is no view to command here, so the slot takes the row alone |
Neither scopeFilter nor a locked board filter is a security boundary: they decide what the page shows, not what the reader is allowed to read — authorization is the server's.
export interface EmbeddedViewProps extends EmbedBaseProps {
detail?: boolean | RecordDetailOptions;
interaction?: EmbedInteraction;
rowActions?(row: RecordRow): import('react').ReactNode;
scopeFilter?: FilterTree | null;
withExport?: boolean;
withSearch?: boolean;
}
export declare function EmbeddedView(given: EmbeddedViewProps): import('react').JSX.Element;EmbeddedDashboard
One saved dashboard inside a business page: the board, its filter bar with each filter in the mode the page gives it, and what the host switched on. It reads the board and never writes: no "Edit", no save, no "Save as", no preference — what the reader changes lives in this viewing alone. Building a board is DashboardWorkbench's.
| Prop | Role |
|---|---|
filterModes | How each of the board's filters is offered, by name: adjustable (the default, on the bar, the reader's), locked (on the bar as what it holds, fixed), hidden (not on the bar, still narrowing what it is wired to). A locked or hidden filter holds what pageValues gives it, or its default |
groupingMode | The time grouping's mode, likewise; adjustable when left out |
pageValues | What the page holds: the value of each locked or hidden filter and, with groupingMode locked or hidden, the time grouping's unit. In force from the first query and followed as it changes — a customer page moving to the next customer. It is the page's own, never the address's (a reader can edit an address) |
initialFilters, onFiltersChange | What the reader's filters open at, from the host's address, and told as they change — the adjustable filters only, since a locked or hidden value is the page's |
initialTab, onTabChange | The tab it opens on, and told as it changes |
withPanelTitles | Whether the panels' titles are drawn; on by default |
withRefresh | "Updated 10:32" in the first row and, in the interactive tier, the refresh button beside it (off by default); it offers no interval, which stays the author's and autoRefresh's |
withExport | "Export data…" in a panel's "⋯" (off by default); no effect in the static tier |
caption | What the board's numbers are read as, in the host's words, drawn under the title while the board fills the screen; on the page the host draws it itself beside its own title |
export interface EmbeddedDashboardProps extends EmbedBaseProps {
caption?: import('react').ReactNode;
filterModes?: Readonly<Record<string, DashboardFilterMode>>;
groupingMode?: DashboardFilterMode;
initialFilters?: DashboardFilters | null;
initialTab?: string | null;
interaction?: EmbedInteraction;
onFiltersChange?(filters: DashboardFilters): void;
onTabChange?(tabId: string | null): void;
pageValues?: DashboardFilters | null;
withExport?: boolean;
withPanelTitles?: boolean;
withRefresh?: boolean;
}
export declare function EmbeddedDashboard(given: EmbeddedDashboardProps): import('react').JSX.Element;The full working version
- Storybook: record workbench, dashboard, EmbeddedView, EmbeddedDashboard.
- Source files:
DataWorkbench.tsx,DashboardWorkbench.tsx,EmbeddedView.tsx,EmbeddedDashboard.tsx,embed/options.ts.