Dashboard widgets
Dashboards are saved widget layouts. The widget module
(frontend/src/components/dashboard/widgets.tsx) is shared by the read-only canvas (the Dashboards
home, /) and the builder (/dashboards/builder), so a widget renders identically whether it's
being viewed or arranged.
The catalogue
WIDGET_TYPES is the single source of truth for the available widgets — each entry has a type, a
display label, and a survey flag (whether it needs a survey context):
export const WIDGET_TYPES = [
{ type: 'nps', label: 'NPS score', survey: true },
{ type: 'csat', label: 'CSAT', survey: true },
{ type: 'ces', label: 'CES', survey: true },
{ type: 'volume', label: 'Response volume', survey: true },
{ type: 'sentiment', label: 'Sentiment split', survey: true },
{ type: 'leaderboard', label: 'Staff leaderboard', survey: false },
{ type: 'agent-kpi', label: 'Agent KPI matrix', survey: true },
{ type: 'survey-trend', label: 'NPS trend', survey: true },
{ type: 'alerts-summary', label: 'Alerts summary', survey: false },
{ type: 'agent-activity', label: 'Agent activity', survey: false },
{ type: 'call-details', label: 'Recent calls/SMS', survey: true },
{ type: 'verbatim-bubble', label: 'Verbatim bubble (sentiment × volume)', survey: true },
{ type: 'survey-slots', label: 'Responses by hour', survey: true },
{ type: 'question-chart', label: 'Question chart', survey: true },
{ type: 'key-driver', label: 'Key driver analysis', survey: true },
{ type: 'comparison', label: 'Comparison (vs prior period)', survey: true },
{ type: 'text', label: 'Text / note', survey: false },
{ type: 'external-api', label: 'External API', survey: false },
] as const;
WIDGET_META maps each type to its lucide icon + accent tone (used for the header chip and the
add-widget tiles). COLORS holds the shared chart palette.
The persisted shape is DashboardWidget (lib/api.ts): grid placement (i, x, y, w, h), type,
optional title, optional surveyId, and — for the external-API widget — an api
(ExternalApiWidgetConfig).
DashboardCanvas — read-only
DashboardCanvas renders a saved layout without drag/resize/remove chrome. It uses
react-grid-layout (WidthProvider(GridLayout), imported from react-grid-layout/legacy) with every
item marked static: true, cols={12}, rowHeight={48}, isDraggable={false}, isResizable={false}.
An empty layout shows an empty-state prompting the user to open the builder.
Single-value KPI widgets (nps / csat / ces, the METRIC_TYPES set) render as borderless
MetricCard-style tiles; the rest get a header bar with an icon chip + title.
filterDays and filter (a ReportFilterParams) thread the global Analyse filter into the canvas —
see Filters.
WidgetBody — the renderer
WidgetBody is the per-widget fetch-and-render unit, shared by both canvas and builder. It:
- resolves the effective survey id — when a global
filterdrives the dashboard, metric widgets aggregate tenant-wide across the selected surveys, while per-survey widgets resolve to the first selected survey (else the widget's ownsurveyId); - runs a
switch (widget.type)fetcher against the typedapiclient (e.g.nps→api.reportNps/api.nps,verbatim-bubble→api.listCodings,survey-slots→api.surveySlots), keyed on[widget.type, resolvedSid, days, filterKey]; - shows a loading line, an error line (
Couldn't load data.), or the chart/table — most charts are Recharts (LineChart,PieChart,BarChart,ScatterChart).
The external-API widget
external-api is special: ExternalApiWidget fetches an allowlisted host through the server proxy
(api.proxyFetch) — never directly from the browser — using its own ExternalApiWidgetConfig
(hostId, method, path, body, dataPath, labelKey / valueKey, chart, cacheTtlSeconds).
It resolves dataPath into the JSON (getPath) and renders the result as a number, table, or
line/bar/pie chart. It has its own loading/error state and is short-circuited before the shared fetch
path.
Builder vs canvas
The builder (DashboardsPage, /dashboards/builder) renders its own editable grid (drag, resize,
add from WIDGET_TYPES, configure, remove) and persists via api.createDashboard /
api.updateDashboard. The Dashboards home renders the saved layout through DashboardCanvas. Because
both use the same WidgetBody, what you arrange is exactly what you view.