@e-llm-studio/federated-dashboard-components
v2.2.7-test
Published
Reusable React dashboard components for charts, metrics, filters, tables, and dashboard cards.
Maintainers
Readme
@e-llm-studio/federated-dashboard-components
A React component library for building config-driven executive dashboards. The
flagship export, ExecutiveDashboardWrapper, renders a complete dashboard —
date filters, a multi-tier line-chart, KPI metric cards, a filter sidebar, and
data tables — from a single configId. The wrapper fetches its own layout and
data from the backend, so the host application only has to mount one component
and (optionally) describe how its tables should render.
The package also ships the underlying building blocks (Graph, MetricCard,
FTable, TableCard, DashboardCard, DashboardDateFilter, FiltersSidebar,
chart primitives, and KPI formatting helpers) for teams that want to compose a
dashboard by hand.
Table of contents
- Installation
- Quick start
- How it works (architecture)
ExecutiveDashboardWrapperprops- The
tablesprop in depth - Backend API contract
- Exported building blocks
- KPI formatting utilities
- TypeScript types reference
- Troubleshooting
1. Installation
npm install @e-llm-studio/federated-dashboard-componentsPeer dependencies
These must be installed in the host application (they are intentionally not bundled, so a single copy is shared):
npm install react@^18.3.1 react-dom@^18.3.1 \
primereact@^10.9.7 primeicons@^7.0.0 \
lucide-react@^0.476.0 recharts@^2.13.0 \
react-router-dom@^7.6.2| Peer dependency | Version | Used for |
| ------------------ | ---------- | ----------------------------------------- |
| react / react-dom | ^18.3.1 | Core runtime |
| primereact | ^10.9.7 | Underlying DataTable, sidebar, dropdowns |
| primeicons | ^7.0.0 | Icon font used by PrimeReact widgets |
| lucide-react | ^0.476.0 | Iconography (chart/KPI/table icons) |
| recharts | ^2.13.0 | Line-chart rendering |
| react-router-dom | ^7.6.2 | Router context expected by the demo shell |
Import the stylesheet once
The library ships a single extracted stylesheet. Import it once at your app root (it is side-effecting and required for correct layout):
import "@e-llm-studio/federated-dashboard-components/dist/styles.css";You will typically also want PrimeReact's own base CSS in your app shell:
import "primereact/resources/themes/lara-light-indigo/theme.css"; // or your theme
import "primereact/resources/primereact.min.css";
import "primeicons/primeicons.css";2. Quick start
The wrapper is fully self-contained. Give it a configId and a baseUrl; it
does the rest.
import "@e-llm-studio/federated-dashboard-components/dist/styles.css";
import { ExecutiveDashboardWrapper } from "@e-llm-studio/federated-dashboard-components";
export function ClaimsDashboard() {
return (
<ExecutiveDashboardWrapper
configId="6a579569dc07b01447bd9797"
baseUrl="https://dev.appmod.ai"
/>
);
}That single mount will:
- Call the UI config API once to learn the dashboard's structure (graph tiers, plot options, KPI cards, sidebar filters, date presets, table layout).
- Seed the date range from the config's default preset.
- Call the data API to load graph series + KPI values, and re-call it every time the date range or applied filters change.
- Render a skeleton while loading and an inline error + Retry button on failure.
Exports:
ExecutiveDashboardWrapperis exported from the package root (src/index.ts), alongside the presentationalExecutiveDashboard, the built-in API fetchers (fetchUIConfig,fetchDashboardData,DashboardApiError), and the full set of container / config / data types.
With custom table rendering
To control how the dashboard's tables look (links, badges, currency, impact
chips), pass a tables config keyed by each table's key from the UI config.
See §5 for the full walkthrough.
import type { DashboardTablesConfig } from "@e-llm-studio/federated-dashboard-components";
const TABLES: DashboardTablesConfig = {
top_plant_locations_by_claim_cost: {
tableProps: { showGridlines: true },
card: {
title: { fontSize: 16, color: "#111827" },
icon: { size: 18, color: "#059669" },
},
columns: [
{
key: "plant_location",
header: "Plant Location",
body: (row) => <a href="#">{row.plant_location}</a>,
},
{
key: "claim_count",
header: "Claim Count",
body: (row) => <span>{row.claim_count}</span>,
},
],
},
};
<ExecutiveDashboardWrapper
configId="6a579569dc07b01447bd9797"
baseUrl="https://dev.appmod.ai"
tables={TABLES}
/>;A full worked example (money/impact/status cell helpers, a custom app header) lives in src/App.tsx.
3. How it works (architecture)
┌──────────────────────────────┐
configId, baseUrl → │ ExecutiveDashboardWrapper │
│ (state + data orchestration) │
└──────────────┬───────────────┘
│
┌───────────────────────────┼───────────────────────────┐
│ 1. UI config API (once) │ 2. Data API (on range/ │
│ /federated/filters/ui │ filter change) │
│ + .../distinct-value │ /federated/filters/ │
│ │ get-data │
▼ ▼ │
structure & labels numeric series + KPI values │
└───────────────┬───────────────────────────────────────┘
▼
┌────────────────────┐
│ ExecutiveDashboard │ (presentational)
│ • DashboardDateFilter
│ • Graph (tiers + plot selectors)
│ • MetricCard × N
│ • FiltersSidebar
│ • TableCard × N
└────────────────────┘- State lives in the wrapper (ExecutiveDashboardWrapper.tsx):
selected plot-by option per tier, filter values (draft vs. applied), and the
active date range. All fetches are
AbortController-guarded, so rapid filter changes cancel in-flight requests. - Two logical fetch phases. The UI config load also fetches the distinct
values for each sidebar filter and merges them in as
options. The data load is re-run wheneverdateRangeorappliedFilterschange. - The inner
ExecutiveDashboardis presentational — it receives everything as props and reports interactions back up via callbacks.
4. ExecutiveDashboardWrapper props
Defined by the DashboardContainerProps interface in
src/types/index.ts. All props below are wired and
consumed by the wrapper.
| Prop | Type | Required | Description |
| ------------------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| configId | string | ✅ Yes | Identifies which dashboard config to load. Sent as config_id in the body of every API call. |
| baseUrl | string | No | API origin. Defaults to "https://dev.appmod.ai". Trailing slashes are trimmed. Override for other environments (staging, prod, localhost). |
| graphEmptyState | React.ReactNode | No | Custom node shown inside the graph area when no series have data. Falls back to a built-in empty state when omitted. |
| tables | DashboardTablesConfig | No | Per-table render config keyed by the table key from the UI config's tableBuilder. Tables without an entry auto-render as plain text. See §5. |
| tableLayout | DashboardTableLayoutConfig | No | Grid layout for the tables section — how many tables sit per row, which options the toolbar offers, and how narrow a card may get before the grid reflows. See §5.1. |
| sectionOrder | DashboardSectionKey[] | No | Vertical order of the dashboard sections, by UI config key — e.g. ["tableBuilder", "kpiCards", "graphBuilder"]. Partial lists are allowed. See §4.1. |
| hiddenSections | DashboardSectionKey[] | No | Parts to leave out entirely, e.g. ["graphBuilder", "sidebarFilters"]. Applied after sectionOrder, and unlike it, also accepts the pinned dateFilters / sidebarFilters. See §4.1. |
| layout | DashboardLayoutConfig | No | Parent-controlled grid: column count plus per-component colSpan / rowSpan / order / hidden, keyed by the get-data response keys. Off unless enabled: true. See §4.2. |
| fetchUIConfig | FetchUIConfig | No | Override the built-in UI-config fetcher — (config, signal?) => Promise<DashboardUIConfig>. Useful for mocks/tests or a custom transport. Defaults to the built-in /federated/filters/ui fetcher. |
| fetchDashboardData | FetchDashboardData | No | Override the built-in data fetcher — (config, params, signal?) => Promise<DashboardDataResponse>. Defaults to the built-in /federated/filters/get-data fetcher. |
| initialStartDate | Date | No | Force the initial range start instead of deriving it from the config's default preset. Must be paired with initialEndDate. |
| initialEndDate | Date | No | Force the initial range end. Must be paired with initialStartDate. |
| onDateChange | (payload: any) => void | No | Fires whenever the resolved date range changes (payload carries { startDate, endDate }). |
| onFiltersApplied | (filters: Record<string, string \| string[]>) => void | No | Fires after the sidebar's Apply is pressed, with the applied filter values. |
| renderLoading | () => React.ReactNode | No | Replace the default skeleton loader. |
| renderError | (error: Error, retry: () => void) => React.ReactNode | No | Replace the default inline error + retry UI. retry re-runs the UI-config load. |
Notes
initialStartDate/initialEndDateonly seed the first render. If only one is provided, the wrapper falls back to the config's default preset.- Fetcher overrides receive an
AbortControllersignal and are expected to honour it — the wrapper cancels in-flight requests when inputs change.
<ExecutiveDashboardWrapper
configId="6a579569dc07b01447bd9797"
initialStartDate={new Date("2026-01-01")}
initialEndDate={new Date("2026-04-18")}
onDateChange={(p) => console.log("range:", p)}
onFiltersApplied={(f) => console.log("filters:", f)}
renderLoading={() => <MySpinner />}
renderError={(err, retry) => <MyError message={err.message} onRetry={retry} />}
/>4.1 Section order (sectionOrder / hiddenSections)
Everything in the dashboard body can be re-ordered by passing the same keys the
API sends — today graphBuilder, kpiCards and tableBuilder:
// Tables on top, then the KPI cards, then the chart.
<ExecutiveDashboardWrapper
configId="6a579569dc07b01447bd9797"
sectionOrder={["tableBuilder", "kpiCards", "graphBuilder"]}
/>Two parts have a fixed position: the date filter bar stays pinned at the
top and the filters sidebar is an overlay, so its position never affects the
layout. sectionOrder therefore ignores dateFilters / sidebarFilters — handy
when you're just reordering the config's key list wholesale:
// Same result as above.
sectionOrder={["dateFilters", "sidebarFilters", "tableBuilder", "kpiCards", "graphBuilder"]}Fixed position doesn't mean permanent, though — hiddenSections can remove
them:
hiddenSections={["dateFilters", "sidebarFilters"]} // both goneNew sections work automatically. When the backend grows another section, pass
its key the same way — sectionOrder={["kpiCards", "pieChartBuilder", "graphBuilder"]}.
Nothing about ordering needs to change: the order is resolved against whatever
sections the dashboard currently renders, and the prop type accepts any string, so
a new key never trips the type checker.
Details:
- Partial lists are fine. Anything omitted keeps its default position, so
sectionOrder={["tableBuilder"]}alone means "tables first, then the rest as usual". hiddenSectionsdrops a part — separate fromsectionOrder, so nothing disappears just for being left out of the order. It's applied last, so hiding always wins, and it accepts the pinned parts as well as the body sections:
HidingsectionOrder={["kpiCards", "graphBuilder"]} hiddenSections={["tableBuilder", "sidebarFilters"]}sidebarFiltersalso removes the Filter button (and its count) from the date bar, since there'd be no panel left for it to open. HidingdateFiltersonly removes the bar — the dashboard still loads data for the config's default date range.- Key matching is forgiving — case-insensitive, and spaces/underscores/dashes
are ignored, so
"Table Builder","table_builder"and"tableBuilder"are the same key. - Unknown keys are ignored, not thrown — they'd usually come from config. In development an ignored key logs a console warning naming it and listing the available sections, so a typo is easy to spot. Duplicates collapse.
Driving the order from the backend config
The UI config payload may also carry the order as data.sectionOrder, which is
useful when the layout should travel with the dashboard config instead of being
hard-coded in the host app:
{
"data": {
"sectionOrder": ["tableBuilder", "kpiCards", "graphBuilder"],
"dateFilters": { "...": "..." },
"graphBuilder": { "...": "..." },
"kpiCards": ["..."],
"tableBuilder": ["..."]
}
}The sectionOrder prop wins over data.sectionOrder, so a host can always
override what the config asks for. Both go through the same resolution, so the
same keys work in either place.
The resolver is exported if a host needs the same behaviour in its own layout UI:
import {
DASHBOARD_SECTION_KEYS, // ["graphBuilder", "kpiCards", "tableBuilder"]
PINNED_SECTION_KEYS, // ["dateFilters", "sidebarFilters"]
resolveSectionOrder, // (order?, hidden?, configOrder?, available?) → string[]
isSectionHidden, // (hidden, "dateFilters") → boolean
} from "@e-llm-studio/federated-dashboard-components";Ordering within a section still comes from the config: KPI cards honour
displayOrder, graph tiers/series honour theirs, and table placement inside the grid is controlled bytableLayout.spans.
4.2 Grid layout (layout)
The grid layout puts every dashboard component into a single CSS grid. It sends the number of columns plus, for any component to size, a grid config keyed by that component's key in the get-data response.
It comes from the backend. The UI config API (POST /federated/filters/ui)
returns it as data.dashboardLayout, so hosts don't need to pass anything:
{
"data": {
"dashboardLayout": {
"enabled": true,
"columns": 4,
"items": {
"primary": { "colSpan": 4, "rowSpan": 3, "order": 1 },
"Total Incurred Cost": { "colSpan": 1, "order": 2 },
"test kpi card": { "colSpan": 4, "order": 6, "endRow": true }
}
}
}
}When the backend config version changes while live polling is running, the dashboard refetches the UI config quietly and the new layout applies without a reload. The user's filters and plot selections are kept.
The UI config's components flags are respected too. gridDesign: false
turns the backend grid off (the regular layout renders), and a component type
set to false (kpiCards, graphBuilder, tableBuilder) is left off the
grid. A missing flag counts as on.
A host can still override the backend layout by passing the layout prop,
which takes the same shape:
<ExecutiveDashboardWrapper
configId="..."
layout={{
enabled: true,
columns: 4,
items: {
"Total Incurred Cost": { colSpan: 1, order: 1 },
"Total litigated claim": { colSpan: 1, order: 2 },
"test kpi card": { hidden: true },
graph: { colSpan: 3, rowSpan: 3, order: 3 },
claim_cost_by_incident_type: { colSpan: 2, rowSpan: 3 },
highest_value_litigation_claims: { colSpan: 4, rowSpan: 3 },
},
}}
/>The grid is read-only: there is no editing UI and nothing is saved. To
change the layout, pass a different layout object and the grid re-renders
immediately. When layout is missing or enabled isn't true, the dashboard
renders exactly as before (sectionOrder, tableLayout and so on). While the
grid is on, items[].order replaces sectionOrder. The date filter bar and
the filters sidebar stay outside the grid, where they always are, and
hiddenSections still hides them.
Keys: taken from the get-data response
Given a response like:
{
"graph": { "tier": [ { "key": "primary" }, { "key": "tier2" } ] },
"kpiCards": { "Total Incurred Cost": { "currentValue": 0, "baselineValue": 50537473 }, "Total Claims Count": {} },
"barChart": { "cost_by_region": [] },
"pieChart": { "claims_by_status": [] },
"table": { "claim_cost_by_incident_type": [], "highest_value_litigation_claims": [] }
}| Component | Key accepted in items | Widget id |
| ---------------- | -------------------------------------------------------------------- | ------------------------------ |
| KPI card | its key in data.kpiCards (= UI config title) or UI config key | kpi:<card.key ?? card.title> |
| Graph card | "graph", "graphBuilder" or any data.graph.tier[].key | chart:graphBuilder |
| Bar chart | its key in data.barChart or its UI config title | bar:<key> |
| Pie chart | its key in data.pieChart or its UI config title | pie:<key> |
| Table | its key in data.table | table:<key> |
| Host widget | extraWidgets[].key | as given |
Matching rules:
- Keys are compared canonically: trimmed, lower-cased, with spaces,
_and-removed."Total Incurred Cost"and"total_incurred_cost"are the same key. - A widget answers to its full id, the part after the colon, and its aliases. Full ids always win.
- A short name claimed by two different widgets matches neither (e.g. a
table and a KPI card both keyed
costs). Use the full id ("table:costs"). - Unknown keys are ignored. Outside production they log one console warning per key, listing every available widget id.
- Two keys that resolve to the same widget merge, with later fields winning.
- Components not listed in
itemsstill render, at their default size and position. Usehidden: trueto remove one.
Bar and pie charts: one card each
Unlike the graph, every bar and pie chart is its own card, the way tables
are — they already sit side by side outside the grid. So one items entry sizes
one chart: cost_by_region: { colSpan: 2, rowSpan: 3 }. They default to
colSpan: 1, rowSpan: 3 with a 380 px minimum width, and re-flow onto their own
row when the grid is too narrow to hold them at that width. A chart in a
fixed-height cell sizes its plot to the cell; rowSpan: "fit" keeps the chart's
own default height instead. Charts are ordered by their config displayOrder,
bar charts before pie charts, unless order says otherwise.
The graph: one card for all tiers
The graph is always one card with every tier stacked inside it, exactly as
when the grid is off. Tiers are never split into separate grid cells. One
items entry sizes the whole card, e.g. graph: { colSpan: 3, rowSpan: 3, order: 5 },
and its height is shared across the tiers (the first a little taller). A tier key
works as that entry's key too, so primary: {...} sizes the same card.
Per-component settings (items[key])
| Setting | Type | Meaning |
| ---------- | -------------------- | ---------------------------------------------------------------------------------------- |
| colSpan | number | Width in columns. Rounded and clamped to 1..columns. An invalid value uses the default. |
| rowSpan | number \| "auto" \| "fit" | Height in row units (max 12). A number spans that many grid rows and is n × rowHeight + (n − 1) × gap tall; its content never makes it taller (it scrolls inside), and it only grows when a card beside it makes its rows taller, so bottom edges line up. "auto" sizes to the content (at least one row unit), stretched to the tallest card in its row. "fit" is the same as defaultHeight: true. |
| defaultHeight | boolean | The component is as tall as its content (height: fit-content): no fixed height and no inner scroll. With matchRowHeights (the default) it then stretches to the tallest card in its row; with matchRowHeights: false it is not stretched. Takes priority over rowSpan. Charts use their regular-layout heights. |
| order | number | Components with an order come first, ascending. The rest keep the default order. |
| hidden | boolean | Leave the component off the grid. |
| minWidth | number (px) | The narrowest this component may get. On a screen where the configured layout would squeeze it below this, its row re-flows instead (see Responsiveness). Defaults below. |
| newRow | boolean | Start a new section: column 1, below everything placed so far (including tall cards). Later components never move up past it. |
| endRow | boolean | Nothing else goes on this component's row. The rest of the row stays empty and the next component starts a new section. |
| colStart | number | 1-based column to start at. The component moves down until that spot is free. Clamped so the whole colSpan fits. |
Numeric strings ("3") are accepted. Booleans and empty strings count as missing.
Defaults per component:
| Component | colSpan × rowSpan | minWidth |
| ---------- | --------------------- | ---------- |
| KPI card | 1 × auto | 266 |
| Graph card | 2 × 3 | 360 |
| Table | 1 × 3 | 340 |
| Host widget| its own defaults | its own, else 200 |
The default order is KPI cards (by displayOrder), then the graph card, tables, and finally host widgets.
Row placement
The grid computes an explicit position for every component (it does not rely on the browser's auto-placement). Components fill rows left to right, and cards spanning several rows are respected: smaller cards flow into the columns beside them.
No holes. With the default packing: "dense", a card that doesn't fit the
space left in a row doesn't leave that space empty: the next card that fits
moves up into it. For example, with a 6-row graph in columns 1–4 and KPI cards
beside it, a colSpan: 2 card that meets a single free column drops a row, and
the following 1-column card fills the gap. Set packing: "ordered" to keep
strict reading order instead (holes are then left as they are).
newRow, endRow and colStart control placement, and they work the same for
every component type:
layout={{
enabled: true,
columns: 4,
items: {
// Cards 1–4 fill the first row. Card 5 sits alone in column 1 of the
// second row, and columns 2–4 of that row stay empty.
"Average Cost per Claim": { colSpan: 1, order: 5, endRow: true },
// The graph starts a fresh row regardless of what came before it.
graph: { colSpan: 3, rowSpan: 3, newRow: true },
// A table placed in column 2 of its row, with column 1 left empty.
claim_cost_by_incident_type: { colSpan: 2, rowSpan: 3, colStart: 2 },
},
}}endRowon one component has the same effect asnewRowon the next visible one.- A section started by
newRow/endRowis a hard boundary: dense packing never backfills a hole above it, so a section keeps its own cards. colStartwithnewRowputs the component at that column in the new section.
Grid settings
| Setting | Default | Meaning |
| ---------------- | ------- | ------------------------------------------------------------------- |
| enabled | false | Turns the grid on. Off means the regular layout. |
| columns | 3 | Column count, max 12. |
| rowHeight | 132 | px per row unit. A cell spanning n rows is n*rowHeight + (n-1)*gap tall. |
| gap | 12 | px between cells. |
| packing | "dense" | "dense" fills holes with later cards that fit; "ordered" keeps strict reading order. |
| fillRows | false | Cards that end a row grow into the empty columns after them (sharing them in proportion to their spans), never wider than same-size cards on fuller lines. Always on while cards are widened for a narrow screen. |
| equalHeights | true | Every row holding an "auto" card (KPI cards by default) is the same height, the tallest such card's, so KPI cards match across the whole grid. false sizes each row on its own. |
| matchRowHeights | true | A defaultHeight / "fit" card stretches to the tallest card in its row, so side-by-side cards (a bar chart beside a pie) share one height; the shorter card's chart centres in the extra space. The row keeps its height. false keeps each fit card at its own content height. |
| items | {} | Per-component settings, see above. |
| extraWidgets | [] | Host-owned components, see below. |
Responsiveness
The config is shown as-is whenever it fits. The column count always stays at
columns, and while every card is at least its minWidth, every component keeps
its configured colSpan, rowSpan, order and row breaks. A 6-column layout is
6 columns.
On a smaller screen, cards are protected instead of squeezed. The grid
measures its container (not the window). A card that would get narrower than
its minWidth is widened to the fewest columns that keep it at that width, and
the layout is packed again, so cards wrap onto the next free line (e.g. 5 KPI
cards → 3 + 2 → 2 + 2 + 1 → stacked). While any card is widened, the cards that
end each row grow into the columns left after them, so lines stay full, and a
tall graph whose neighbors no longer fit beside it takes the full width.
Same-size cards stay the same width, though: a leftover line (fewer cards than
the fullest line of that size) grows no wider than the cards above it. Five KPI
cards at two per line give 2 + 2 + 1 with the last one half-width, and only go
full width once all of them do.
Heights never leak between cards. Every grid row is at least rowHeight,
so a card with a numeric rowSpan is exactly its row units tall and never
stretches the rows it crosses: a KPI card sharing a row with a 4-row table is
the same height as every other KPI card. With equalHeights (the default),
every row holding an "auto" card shares one height, the tallest such card's,
so a KPI card with a CTA button, a subtitle or a wrapping delta makes all KPI
cards that tall, not just the ones in its row. Tall cards crossing those rows
stretch with them, so edges stay aligned. Charts are
given the cell's measured height, so they fill it. A card with
defaultHeight: true opts out: it is as tall as its content and its row can be
shorter than rowHeight, e.g. { colSpan: 4, order: 8, endRow: true,
defaultHeight: true } for a full-width table that shows every row without
scrolling.
Cards keep their own styling. The grid only sizes cells; each card renders with its normal styles. Charts size from the cell's pixel height, and content taller than a fixed-height cell scrolls inside the cell.
Adding components
Host widgets. Pass extraWidgets to place your own components. A widget
whose key matches a built-in replaces it, and the rest are appended after
the built-ins:
layout={{
enabled: true,
columns: 3,
extraWidgets: [
{
key: "custom:alerts",
aliases: ["alerts"],
title: "Alerts",
type: "alerts",
defaultColSpan: 1,
defaultRowSpan: 3,
// Either a static `node`, or `render` to size from the cell.
render: ({ height, width }) => <AlertsPanel height={height ?? 300} />,
},
],
items: { alerts: { order: 1 } },
}}render receives { colSpan, rowSpan, height, width }: colSpan is the
rendered span after responsive scaling, height is the cell's measured px
height (at least its rowSpan height; null for "auto" and "fit" rows), and
width is its measured px width (null before the first measurement).
New builder types. When the UI config grows a new component type, add it in
two places: a branch in buildDashboardWidgets
(DashboardGrid/buildDashboardWidgets.ts)
that pushes one widget per config entry with a <type>:<key> id and defaults,
and a matching renderer in ExecutiveDashboard. Key matching, ordering and
sizing then work for it with no further changes.
The pieces are exported for hosts that lay out their own dashboards:
import {
DashboardGrid, // <DashboardGrid widgets={…} config={…} />
resolveLayout, // (config, widgets) → { columns, rowHeight, gap, items }
buildDashboardWidgets, // ({ uiData, renderers, extraWidgets }) → DashboardWidget[]
buildWidgetIndex, // key lookup used by resolveLayout
packLayout, // (items, columns, gap, width, { packing, fillRows }) → { items: positions, rows: row sizing }
placeItems, // same arguments → just the positions (colStart/colSpan/rowStart/rowSpan)
cellHeight, // (rowSpan, rowHeight, gap) → px | null
DEFAULT_COLUMNS, MAX_COLUMNS, DEFAULT_ROW_HEIGHT, DEFAULT_GAP,
DEFAULT_MIN_WIDTH, MAX_ROW_SPAN,
} from "@e-llm-studio/federated-dashboard-components";5. The tables prop in depth
The UI config's tableBuilder defines which tables exist and their raw
columns. The tables prop lets the host app decide how each of those tables
renders — custom cell JSX, headers, per-column styles, and the surrounding
card chrome (icon + title). It's optional and partial: any table key you don't
provide falls back to auto-generated plain-text columns.
type DashboardTablesConfig = Record<string, DashboardTableRenderConfig>;The keys must match tableBuilder[].key from the UI config.
DashboardTableRenderConfig
| Field | Type | Description |
| ------------ | --------------------------- | ------------------------------------------------------------------------------------------------------- |
| columns | FTableColumn[] | Required. Column render definitions (see below). |
| tableProps | Record<string, any> | Passthrough props merged onto the underlying PrimeReact DataTable (e.g. { showGridlines: true }). |
| card | DashboardTableCardStyles | Styling for the card wrapping the table (shell, header, title, icon). |
FTableColumn
| Field | Type | Description |
| ---------------- | ------------------------------------ | ----------------------------------------------------------------------- |
| key | string | Row field / column identity. |
| header | React.ReactNode | Header cell content (string or JSX). |
| body | (row: TableRow) => React.ReactNode | Cell renderer for each row. TableRow is Record<string, any>. |
| style | React.CSSProperties | Applied to both header and body cells of the column. |
| headerStyle | React.CSSProperties | Header-cell (<th>) only; overrides style for the header. |
| bodyStyle | React.CSSProperties | Body-cell (<td>) only; overrides style for rows. |
| headerClassName| string | Class on the header cell. |
| bodyClassName | string | Class on body cells. |
DashboardTableCardStyles
| Field | Type | Description |
| -------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| root | React.CSSProperties | The card shell. Set width here to size the whole card — the table fills it. |
| header | React.CSSProperties | The header row (icon + title + search/expand controls). |
| title | React.CSSProperties | The title text (font, colour, padding). |
| icon | { size?: number; color?: string; background?: string; style?: CSSProperties } | Glyph size/colour, chip background, and an escape hatch for other icon-container CSS. |
Sizing tip: To make a table narrower, set
card.root.width(e.g."60%"). Sizing only the table leaves the card full-width.
Full table example
import type { DashboardTablesConfig, FTableColumn, TableRow } from "@e-llm-studio/federated-dashboard-components";
const money = (v: any) => (Number.isFinite(Number(v)) ? `$${v}` : String(v ?? ""));
const linkCol = (field: string, header: string): FTableColumn => ({
key: field,
header,
body: (row) => <a href="#" style={{ color: "#7c3aed" }}>{row[field]}</a>,
});
const TABLES: DashboardTablesConfig = {
claim_cost_by_incident_type: {
tableProps: { showGridlines: true },
card: {
header: { padding: "10px" },
title: { fontSize: 16, color: "#111827" },
icon: { size: 18, color: "#059669" },
},
columns: [
linkCol("incident_type", "Incident Type"),
{ key: "total_claim_cost", header: "Total Claim Cost", body: (r) => money(r.total_claim_cost) },
{ key: "litigation_rate", header: "Litigation Rate", body: (r) => `${r.litigation_rate} %` },
],
},
};5.1 Table grid layout (tableLayout)
When the UI config has more than one table, a small toolbar appears above the tables section with one icon per layout — 1 / 2 / 3 tables per row. Picking an icon re-flows the tables into that many columns.
The layout is always responsive: minColumnWidth is the narrowest a table card
may become, and the grid drops columns automatically as the container (not the
window) gets narrower — 3-up becomes 2-up, then 1-up. The user's choice is
remembered, so widening the container restores it. An option that can't fit at
the current width is dimmed in the toolbar.
<ExecutiveDashboardWrapper
configId="6a579569dc07b01447bd9797"
tables={TABLES}
tableLayout={{
options: [1, 2, 3], // icons offered in the toolbar
defaultColumns: 2, // start 2-up
minColumnWidth: 420, // drop a column below this per-card width
gap: 16,
toggleLabel: "Layout",
}}
/>| Field | Type | Default | Description |
| ----------------- | ------------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| spans | Record<string, TableSpan> | TableSpan[] | — | Per-table width — see below. |
| totalColumns | number | 12 | Grid columns the spans values are expressed in. |
| defaultSpan | TableSpan | full width | Width for tables missing from spans. |
| options | number[] | [1, 2, 3] | Column counts offered as toolbar icons (1–4 have dedicated icons). |
| defaultColumns | number \| "custom" | see note | Layout on first render. Defaults to "custom" when spans is set, else 1. |
| columns | number \| "custom" | — | Controlled layout. Pair with onColumnsChange to own the state. |
| onColumnsChange | (columns: number \| "custom") => void | — | Fires when the user picks a layout. |
| showToggle | boolean | true | Hide the toolbar and keep the configured column count. Auto-hidden for 1 table. |
| toggleLabel | string | — | Caption rendered left of the toggle. |
| minColumnWidth | number | 380 | Narrowest a card may get before a column is dropped — this drives the reflow. |
| gap | number | 12 | Gap between cards, in px. |
| equalHeights | boolean | true | Stretch every card in a row to the tallest one. false lets each card hug its own content. |
Cards in the same row are height-matched by default, so a 5-row table beside a
10-row one no longer renders short — the shorter table's card fills the row
height. To cap how tall a row can get, give the table a scroll height via
tables[key].tableProps, e.g. { scrollHeight: "360px" }.
Per-table widths (spans)
Uniform 1/2/3-up is the user's control. spans is the host's control: it
sets the width of each table individually, so table 1 can own a full row while
tables 2 and 3 sit side by side and table 4 goes full width again.
tableLayout={{
spans: {
revenue_summary: "full", // row 1 — on its own
claims_by_type: "1/2", // row 2 — side by side
claims_by_plant: "1/2", //
litigation_log: "full", // row 3 — on its own
},
}}Keys are the table's key from the UI config's tableBuilder. An array works
too and is read in table order — handy when the layout is positional:
tableLayout={{ spans: ["full", "1/2", "1/2", "full"] }}Accepted values (TableSpan): "full", any fraction string ("1/2", "1/3",
"2/3", "1/4", "3/4", …), or a raw column count against totalColumns
(6 is half of the default 12-column grid). Rows are formed by CSS grid
auto-placement, so spans that overflow a row simply wrap.
This stays responsive. No table is ever rendered narrower than
minColumnWidth; spans widen instead. With the four-table layout above at
minColumnWidth: 380:
| Container width | Result |
| --------------- | --------------------------------------------------- |
| ≥ 772px | [T1] / [T2 + T3] / [T4] — the configured shape |
| < 772px | [T1] / [T2] / [T3] / [T4] — all full width |
(772px = two 380px cards plus the 12px gap.) Raise minColumnWidth to make it
break to one column earlier, lower it to hold two columns longer.
When spans is set, the toolbar grows an extra icon for the configured layout
and starts on it. The user can still switch to a uniform 1/2/3-up and click
back to the configured one. Pass showToggle: false to lock the layout down.
The same grid is exported standalone as TableGrid if you're composing table
cards yourself:
import { TableGrid, TableCard } from "@e-llm-studio/federated-dashboard-components";
<TableGrid defaultColumns={2} minColumnWidth={400}>
<TableCard title="Claims" table={{ rows, columns }} />
<TableCard title="Providers" table={{ rows: rows2, columns: columns2 }} />
</TableGrid>;6. Backend API contract
The wrapper (via src/api/dashboardApi.ts) calls three
POST JSON endpoints, all relative to baseUrl. Each responds with an envelope:
{
"success": true,
"request_id": "…",
"data": { /* … */ },
"error": null | { "errorMessage": "…", "errorType": "…" }
}A non-2xx status, or success: false, throws a DashboardApiError (surfaced in
the wrapper's error UI).
6.1 POST /federated/filters/ui — structure & labels
Request: { "config_id": "<configId>" }
Returns the dashboard layout: dateFilters, sidebarFilters, graphBuilder
(tiers → plot options → series), kpiCards, and optional tableBuilder. See
DashboardUIData in §9.
6.2 POST /federated/filters/distinct-value — filter options
Request: { "config_id": "<configId>" }
Returns { "sidebarFilters": { "<schemaFilterName>": string[] } }. The wrapper
merges these arrays into each sidebar filter definition as its options.
UI config (6.1) and distinct values (6.2) are fetched in parallel and merged into a single
DashboardUIConfig.
6.3 POST /federated/filters/get-data — the numbers
Request:
{
"config_id": "<configId>",
"dateFilters": { "startDate": "<ISO>", "endDate": "<ISO>" },
"sidebarFilters": { "<schemaFilterName>": ["value", …] }
}Filter normalisation: single-select values become one-element arrays; empty / unset filters are dropped from the payload.
Response data shape (DashboardDataResponse):
{
"graph": {
"<graphKey>": [
{ "key": "<tierKey>", "plotbyoptions": { "<plotOptionKey>": { "<seriesKey>": [ { "x": "D1", "y": 58 }, … ] } } }
]
},
"kpiCards": {
// either a bare value…
"Fill Rate Impact": "2.8k",
// …or an object carrying a baseline for indicator colouring:
"Total Claim Cost": { "currentValue": 8421316, "baselineValue": 7900000 }
},
"table": {
"<tableKey>": [ { /* row */ }, … ]
}
}7. Exported building blocks
If you're composing a dashboard by hand rather than using the wrapper, the following are exported from the package root (see src/index.ts):
Components
| Export | Notes |
| --------------------- | -------------------------------------------------------- |
| Graph | Multi-pane line chart. Props: GraphProps. |
| LineChart | Single line chart primitive. |
| PlotBySelector | Dropdown used to switch a tier's plotted metric. |
| DashboardDateFilter | Date-range picker with presets + refresh/filter buttons. |
| MetricCard | KPI card. Props: MetricCardProps. |
| FiltersSidebar | Slide-out filter panel. Props: FiltersSidebarProps. |
| FTable | Low-level table. Props: FTableProps. |
| TableCard | FTable wrapped in a titled/iconed card. Props: TableCardProps. |
| TableGrid | Responsive N-per-row grid + layout toggle for table cards. Props: TableGridProps. |
| TableLayoutToggle | The 1/2/3-per-row icon group on its own. Props: TableLayoutToggleProps. |
| useResponsiveColumns| Hook behind the grid — caps a desired column count to what the container fits. |
| DashboardCard | Rich content card (sections, tags, events, news, grid). |
| DashboardGrid | Read-only responsive grid behind the layout prop. Props: DashboardGridProps. See §4.2. |
Chart primitives (shadcn/Recharts port)
ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend,
ChartLegendContent, useChart, plus the ChartConfig type and the
DEFAULT_COLORS / colorForIndex palette helpers.
Date presets
DATE_PRESETS and related exports from datePreset — e.g. DAYS_7, DAYS_15,
DAYS_30, THIS_MONTH, CUSTOM. Each preset with a getRange() returns
{ startDate, endDate }.
8. KPI formatting utilities
Exported from the package root for formatting/colouring KPI values consistently (see src/utils/index.ts):
| Export | Signature / purpose |
| --------------------- | ----------------------------------------------------------------------------------------------------------- |
| formatKpiValue | Format a raw KPI value using a format string (compacts numbers, preserves suffixes like %). |
| getKpiDelta | Compute the delta (absolute + %) and sentiment between a current and baseline value. Returns KpiDelta. |
| getKpiSentiment | (current, baseline, indicatorLogic) → "positive" \| "negative" \| "neutral". |
| getKpiValueColor | Resolve a display colour from sentiment, with optional per-sentiment overrides. |
| isKpiValueObject | Type guard: is a raw value the { currentValue, baselineValue } object form? |
| KPI_SENTIMENT_COLORS| Default positive/negative colour map. |
| compactNumber | 8421316 → "8.4M" style compaction. |
IndicatorLogic is one of "higher_better" | "lower_better" | "neutral" — it
tells the sentiment helpers whether an increase is good or bad.
import {
getKpiDelta,
getKpiValueColor,
} from "@e-llm-studio/federated-dashboard-components";
const delta = getKpiDelta(8421316, 7900000, "higher_better");
// → { …, sentiment: "positive" }
const color = getKpiValueColor(8421316, 7900000, "higher_better");9. TypeScript types reference
All types below are exported (or defined) in src/types/index.ts and re-exported from the root where noted.
- Container:
DashboardContainerProps— the wrapper's prop bag (§4). - UI config:
DashboardUIConfig→DashboardUIData→{ dateFilters, sidebarFilters, graphBuilder, kpiCards, tableBuilder, sectionOrder? }.DateFiltersConfig—datepresets,defaultPreset,showFilterButton,showFilterCount,showLiveButton,liveDurationMs.SidebarFilterConfig—label,description,schemaFilterName,type.GraphBuilderConfig—title,subtitle,icon,axis,tiers[],legend.GraphTierConfig—key,title,displayOrder,plotOptions[],yAxis,series[].PlotOptionConfig,SeriesConfig,LegendConfig,KpiCardConfig.
- Data:
DashboardDataResponse,TierData,PlotByOptions,SeriesDataMap,DataPoint. - Query:
DashboardQueryParams—{ startDate?, endDate?, filters? }. - Tables:
DashboardTablesConfig,DashboardTableRenderConfig,DashboardTableCardStyles,DashboardTableLayoutConfig,TableBuilderConfig,TableColumnConfig,FTableColumn,TableRow. - Layout:
DashboardSectionKey— a section named by its UI config key ("graphBuilder" | "kpiCards" | "tableBuilder", plus any future one, since any string is accepted). Used bysectionOrder/hiddenSections(§4.1). - Grid layout:
DashboardLayoutConfig,DashboardItemConfig,DashboardRowSpan,DashboardWidget,DashboardWidgetRenderContext,ResolvedLayout,ResolvedLayoutItem(§4.2). - KPI:
KpiValueObject,KpiDelta,KpiSentiment,IndicatorLogic. - Fetchers:
FetchUIConfig,FetchDashboardData(function types for the pluggable fetchers).
10. Troubleshooting
| Symptom | Likely cause / fix |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| ExecutiveDashboardWrapper is undefined on import | Ensure you're on a build that exports it from the root; it is exported from src/index.ts. |
| Blank / unstyled layout | You didn't import dist/styles.css (and/or PrimeReact's theme CSS). See §1. |
| Requests hit the wrong host / 404 | baseUrl defaults to https://dev.appmod.ai. For another environment pass an explicit absolute baseUrl. |
| "Failed to load dashboard" with a Retry button | An API returned non-2xx or success: false. Check the three endpoints in §6 and the config_id. |
| Tables render as plain text | No matching entry in tables for that tableBuilder[].key. Keys must match exactly. See §5. |
| layout.items entry has no effect | The key didn't match a widget, or matched two. Check the dev console warning for the available widget ids and use the full id (e.g. "table:costs"). See §4.2. |
| Peer dependency warnings on install | Install the peers listed in §1 at compatible versions. |
Package: @e-llm-studio/federated-dashboard-components · Entry:
src/index.ts · Wrapper:
src/ExecutiveDashboardWrapper.tsx · API
layer: src/api/dashboardApi.ts
