Skip to main content

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 filter drives 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 own surveyId);
  • runs a switch (widget.type) fetcher against the typed api client (e.g. npsapi.reportNps / api.nps, verbatim-bubbleapi.listCodings, survey-slotsapi.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.