Files
superset2/docs/developer_docs/extensions/extension-points/dashboards.md
Claude Code f1c7ec60ce feat(extensions): register the built-in dashboard renderer as the default provider
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>
2026-07-03 10:08:50 -07:00

130 lines
7.1 KiB
Markdown

---
title: Dashboards
sidebar_position: 4
---
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# 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 the `ENABLE_EXTENSIONS` feature 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 `Disposable` falls 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/data` with query contexts built from each chart's `form_data`).
- **Filter orchestration**: applying `initialDataMask`, reacting to filter interactions, and refreshing affected charts are the renderer's responsibility.
- **Layout interpretation**: `dashboard.layout` is the parsed `position_json` component 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:
```typescript
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:
```tsx
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):
```json
{
"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.
- `onDataMaskChange` and `onActiveTabsChange` are 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.