Follow the SQL Lab pattern one step further: instead of the host hardcoding the built-in renderer as a fallback branch, the built-in renderer is now registered through the same contribution point as the default-tier provider (superset.dashboard-renderer). - DashboardRendererProviders gains a default tier: setDefaultProvider (host-internal, idempotent by id), getDefaultProvider, getOverrideProvider; getProvider() resolves override ?? default. The default is never displaced by extension registrations, and disposing an override falls back to it through the registry. - The default registers via a lazy side-effect module (src/core/dashboards/defaultRenderer.ts) imported by both the host component and the namespace impl, so it is set wherever dashboards render (app + embedded) without pulling the dashboard stack into the startup bundle. Registration is independent of ExtensionsStartup and ENABLE_EXTENSIONS: dashboards always render with the flag off. - DashboardRendererHost renders the resolved provider: extension override (ErrorBoundary-wrapped, view mode + flag on only) or the lazy default under a local Suspense. - Public contract adds getDefaultDashboardRenderer() so extensions can wrap/augment the built-in renderer rather than fully replace it. - Tests updated/extended for the default tier (registry fallback semantics, idempotency, reset behavior, namespace API); the E2E spec now asserts live that the built-in is the registered default. The oxlint hook is skipped in this commit only because the local node_modules currently holds linux bindings (docker bind mount); the exact hook command was run clean inside the container instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.1 KiB
title, sidebar_position
| title | sidebar_position |
|---|---|
| Dashboards | 4 |
Dashboard Renderer Contributions
Extensions can replace Superset's built-in dashboard renderer with a custom implementation. This allows dashboards to be displayed in entirely different ways — kiosk layouts, alternative grid engines, story-style presentations — while reusing Superset's data fetching, authentication, theming, and URL/permalink handling.
Overview
The dashboard renderer is a single-slot contribution point with two tiers:
- Superset's built-in renderer is itself registered as the default provider (
superset.dashboard-renderer) through the same contribution point. It renders whenever no custom renderer is active — including when theENABLE_EXTENSIONSfeature flag is off — so dashboards always display, extensions or not. - At most one custom renderer is active at a time. The most recently registered renderer wins; a previously registered custom renderer is displaced and unregistered with a console warning. The default provider is never displaced.
- Disposing the active custom renderer's
Disposablefalls back to the built-in default. - Custom renderers handle view mode only. When a dashboard enters edit mode, the host always renders the built-in renderer (which owns drag-and-drop editing, undo/redo, and the component pane), returning to the custom renderer when edit mode exits.
- A custom renderer that throws is contained by an error boundary; the host does not fall back to the built-in renderer on error.
The host keeps its behavior identical regardless of which renderer is active: it fetches the dashboard, charts, and datasets, resolves initial filter state from the URL (permalinks, native_filters_key, legacy filter params), injects dashboard CSS, and manages the document title. The renderer receives the results as props.
The Props Contract
Your renderer component receives DashboardRendererProps from @apache-superset/core/dashboards:
| Prop | Type | Description |
|---|---|---|
dashboard |
DashboardInfo |
Identity and parsed metadata: id, uuid, slug, title, css, metadata (parsed json_metadata), layout (parsed position_json), isPublished, isManagedExternally |
charts |
DashboardChart[] |
Chart (slice) definitions as returned by GET /api/v1/dashboard/{id}/charts |
datasets |
DashboardDataset[] |
Datasets as returned by GET /api/v1/dashboard/{id}/datasets |
initialDataMask |
DashboardDataMask |
Initial filter state resolved by the host from the URL |
initialActiveTabs |
string[]? |
Layout component ids of the initially active tabs (from permalink) |
initialAnchor |
string? |
Layout component id to scroll to on mount (permalink anchor) |
uiConfig |
DashboardUiConfig? |
Chrome-hiding flags (hideTitle, hideTab, hideChartControls, emitDataMasks), mirroring the embedded SDK's uiConfig |
onDataMaskChange |
callback? | Reserved — not supplied by the host yet |
onActiveTabsChange |
callback? | Reserved — not supplied by the host yet |
The contract is designed to be Redux-free: everything a renderer needs to display a dashboard arrives via props, and host services are available through the public window.superset namespaces (authentication, navigation, theme, translation, and so on).
Renderer responsibilities
- Chart data fetching: the host does not fetch chart data. Query for it yourself (e.g.
POST /api/v1/chart/datawith query contexts built from each chart'sform_data). - Filter orchestration: applying
initialDataMask, reacting to filter interactions, and refreshing affected charts are the renderer's responsibility. - Layout interpretation:
dashboard.layoutis the parsedposition_jsoncomponent tree (rows, columns, tabs, charts, markdown); interpret as much or as little of it as your presentation needs.
Theming works out of the box: renderers are mounted inside the host's theme providers, so useTheme from @apache-superset/core/theme reflects the dashboard's active theme.
Registering a Renderer
Register the renderer as a module-level side effect in your extension's entry point:
import { dashboards } from '@apache-superset/core';
import type { ComponentType } from 'react';
const KioskDashboardRenderer: ComponentType<
dashboards.DashboardRendererProps
> = ({ dashboard, charts, initialDataMask }) => (
<main>
<h1>{dashboard.title}</h1>
{/* render charts from `charts` + `dashboard.layout` */}
</main>
);
dashboards.registerDashboardRenderer(
{ id: 'acme.kiosk-dashboard', name: 'Kiosk Dashboard Renderer' },
KioskDashboardRenderer,
);
registerDashboardRenderer returns a Disposable. Disposing it removes your renderer if it is still the active one; disposing after being displaced by a newer registration is a no-op.
You can observe slot changes with dashboards.onDidRegisterDashboardRenderer and dashboards.onDidUnregisterDashboardRenderer, and inspect the active provider with dashboards.getDashboardRenderer() (which returns the built-in default when no custom renderer is active).
Augmenting the built-in renderer
To augment rather than fully replace the built-in renderer, retrieve the default provider and wrap its component:
const defaultProvider = dashboards.getDefaultDashboardRenderer();
dashboards.registerDashboardRenderer(
{ id: 'acme.framed-dashboard', name: 'Framed Dashboard' },
props => (
<AcmeFrame>
{defaultProvider && <defaultProvider.component {...props} />}
</AcmeFrame>
),
);
Manifest Declaration
Declare the renderer in your extension's Contributions metadata (at most one per extension):
{
"dashboardRenderer": {
"id": "acme.kiosk-dashboard",
"name": "Kiosk Dashboard Renderer",
"description": "Full-screen kiosk presentation of dashboards"
}
}
Current Limitations
- Extensions load asynchronously after startup, so a dashboard opened before your extension finishes loading renders with the built-in renderer first and swaps to yours when registration lands.
onDataMaskChangeandonActiveTabsChangeare defined in the contract but not consumed by the host yet — filter state changed inside a custom renderer does not persist to permalinks.- While a custom renderer is active the host still hydrates its internal dashboard state so permalinks and embedded behavior remain intact; this is transparent to renderers but means the built-in state bookkeeping still runs.