diff --git a/docs/developer_docs/extensions/extension-points/dashboards.md b/docs/developer_docs/extensions/extension-points/dashboards.md
index e4c54e83bb5..2050bf3fa7e 100644
--- a/docs/developer_docs/extensions/extension-points/dashboards.md
+++ b/docs/developer_docs/extensions/extension-points/dashboards.md
@@ -28,10 +28,11 @@ Extensions can replace Superset's built-in dashboard renderer with a custom impl
## Overview
-The dashboard renderer is a **single-slot** contribution point:
+The dashboard renderer is a **single-slot** contribution point with two tiers:
-- At most one custom renderer is active at a time. The most recently registered renderer wins; a previously registered renderer is displaced and unregistered with a console warning.
-- When no custom renderer is registered, the host renders the built-in dashboard renderer.
+- **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.
@@ -88,7 +89,24 @@ dashboards.registerDashboardRenderer(
`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()`.
+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 => (
+
+ {defaultProvider && }
+
+ ),
+);
+```
## Manifest Declaration
diff --git a/superset-frontend/packages/superset-core/src/dashboards/index.ts b/superset-frontend/packages/superset-core/src/dashboards/index.ts
index 7a4d4388a7d..08d1f26e56a 100644
--- a/superset-frontend/packages/superset-core/src/dashboards/index.ts
+++ b/superset-frontend/packages/superset-core/src/dashboards/index.ts
@@ -30,8 +30,11 @@
*
* The dashboard renderer is a single-slot contribution point: at most one
* custom renderer is active at a time, and the most recently registered
- * renderer wins. When no custom renderer is registered, the host renders
- * the built-in dashboard renderer.
+ * renderer wins. The built-in dashboard renderer is itself registered as
+ * the default provider, so it renders whenever no custom renderer is
+ * active — including when the extensions feature flag is off — and it can
+ * be retrieved via {@link getDefaultDashboardRenderer} to wrap or augment
+ * it from a custom renderer.
*
* Custom renderers handle VIEW mode only. When a dashboard enters edit
* mode, the host always renders the built-in renderer, returning to the
@@ -241,8 +244,9 @@ export interface DashboardRendererUnregisteredEvent {
* The dashboard renderer is a single slot: the most recently registered
* renderer becomes active, displacing (and unregistering) any previously
* registered one with a console warning. Disposing the returned Disposable
- * removes the renderer if it is still the active one; disposing a displaced
- * renderer's Disposable is a no-op.
+ * removes the renderer if it is still the active one — the built-in
+ * default renderer takes over again; disposing a displaced renderer's
+ * Disposable is a no-op.
*
* Custom renderers handle view mode only — when a dashboard enters edit
* mode, the host always renders the built-in renderer.
@@ -265,15 +269,43 @@ export declare function registerDashboardRenderer(
): Disposable;
/**
- * Get the currently registered dashboard renderer provider.
+ * Get the active dashboard renderer provider: the extension-registered
+ * renderer when one is active, otherwise the built-in default provider.
*
- * @returns The active provider, or undefined if no extension has
- * registered a renderer (the host uses its built-in renderer).
+ * @returns The active provider, or undefined only if the host has not
+ * initialized a default renderer.
*/
export declare function getDashboardRenderer():
| DashboardRendererProvider
| undefined;
+/**
+ * Get the built-in default dashboard renderer provider.
+ *
+ * Useful for extensions that want to augment rather than fully replace
+ * the built-in renderer — retrieve the default component, wrap it, and
+ * register the wrapper.
+ *
+ * @example
+ * ```tsx
+ * const defaultProvider = dashboards.getDefaultDashboardRenderer();
+ * dashboards.registerDashboardRenderer(
+ * { id: 'acme.framed-dashboard', name: 'Framed Dashboard' },
+ * props => (
+ *
+ * {defaultProvider && }
+ *
+ * ),
+ * );
+ * ```
+ *
+ * @returns The default provider, or undefined if the host has not
+ * initialized one.
+ */
+export declare function getDefaultDashboardRenderer():
+ | DashboardRendererProvider
+ | undefined;
+
/**
* Event fired when a dashboard renderer is registered.
*/
diff --git a/superset-frontend/playwright/tests/dashboard/dashboard-renderer-extension.spec.ts b/superset-frontend/playwright/tests/dashboard/dashboard-renderer-extension.spec.ts
index 1f24ddefdb5..a38cbbcc94b 100644
--- a/superset-frontend/playwright/tests/dashboard/dashboard-renderer-extension.spec.ts
+++ b/superset-frontend/playwright/tests/dashboard/dashboard-renderer-extension.spec.ts
@@ -115,6 +115,20 @@ test.describe('Dashboard renderer extension point', () => {
),
).toBe('function');
+ // The built-in renderer is itself the registered default provider
+ expect(
+ await page.evaluate(() => ({
+ active: (window as any).superset.dashboards.getDashboardRenderer()
+ ?.renderer.id,
+ default: (
+ window as any
+ ).superset.dashboards.getDefaultDashboardRenderer()?.renderer.id,
+ })),
+ ).toEqual({
+ active: 'superset.dashboard-renderer',
+ default: 'superset.dashboard-renderer',
+ });
+
// Registering swaps the view in place, no navigation
await registerProbeRenderer(page);
await expect(page.getByText(CUSTOM_MARKER)).toBeVisible({
diff --git a/superset-frontend/src/core/dashboards/DashboardRendererHost.test.tsx b/superset-frontend/src/core/dashboards/DashboardRendererHost.test.tsx
index 1591b33f3a6..4095c2e3896 100644
--- a/superset-frontend/src/core/dashboards/DashboardRendererHost.test.tsx
+++ b/superset-frontend/src/core/dashboards/DashboardRendererHost.test.tsx
@@ -70,10 +70,12 @@ afterEach(() => {
cleanup();
});
-test('renders the built-in renderer when no custom renderer is registered', () => {
+test('renders the built-in renderer when no custom renderer is registered', async () => {
render();
- expect(screen.getByTestId('default-dashboard-renderer')).toBeInTheDocument();
+ expect(
+ await screen.findByTestId('default-dashboard-renderer'),
+ ).toBeInTheDocument();
expect(screen.getByTestId('default-renderer-dashboard-id')).toHaveTextContent(
'42',
);
@@ -115,7 +117,7 @@ test('renders a registered custom renderer with the contract props', () => {
).not.toBeInTheDocument();
});
-test('renders the built-in renderer in edit mode even with a custom renderer registered', () => {
+test('renders the built-in renderer in edit mode even with a custom renderer registered', async () => {
DashboardRendererProviders.getInstance().registerProvider(
{ id: 'test.custom', name: 'Custom Renderer' },
() =>
,
@@ -123,13 +125,15 @@ test('renders the built-in renderer in edit mode even with a custom renderer reg
render();
- expect(screen.getByTestId('default-dashboard-renderer')).toBeInTheDocument();
+ expect(
+ await screen.findByTestId('default-dashboard-renderer'),
+ ).toBeInTheDocument();
expect(
screen.queryByTestId('custom-dashboard-renderer'),
).not.toBeInTheDocument();
});
-test('renders the built-in renderer when the extensions feature flag is off', () => {
+test('renders the built-in renderer when the extensions feature flag is off', async () => {
mockIsFeatureEnabled.mockReturnValue(false);
DashboardRendererProviders.getInstance().registerProvider(
{ id: 'test.custom', name: 'Custom Renderer' },
@@ -138,7 +142,9 @@ test('renders the built-in renderer when the extensions feature flag is off', ()
render();
- expect(screen.getByTestId('default-dashboard-renderer')).toBeInTheDocument();
+ expect(
+ await screen.findByTestId('default-dashboard-renderer'),
+ ).toBeInTheDocument();
expect(
screen.queryByTestId('custom-dashboard-renderer'),
).not.toBeInTheDocument();
@@ -163,10 +169,12 @@ test('shows an error boundary when the custom renderer throws', () => {
consoleErrorSpy.mockRestore();
});
-test('swaps to a custom renderer registered after mount', () => {
+test('swaps to a custom renderer registered after mount', async () => {
render();
- expect(screen.getByTestId('default-dashboard-renderer')).toBeInTheDocument();
+ expect(
+ await screen.findByTestId('default-dashboard-renderer'),
+ ).toBeInTheDocument();
act(() => {
DashboardRendererProviders.getInstance().registerProvider(
diff --git a/superset-frontend/src/core/dashboards/DashboardRendererHost.tsx b/superset-frontend/src/core/dashboards/DashboardRendererHost.tsx
index 9b26a6afddb..e6643fa5826 100644
--- a/superset-frontend/src/core/dashboards/DashboardRendererHost.tsx
+++ b/superset-frontend/src/core/dashboards/DashboardRendererHost.tsx
@@ -21,18 +21,22 @@
* @fileoverview DashboardRendererHost component for dynamic dashboard
* renderer resolution.
*
- * This component resolves and renders the appropriate dashboard renderer
- * implementation. If an extension has registered a custom renderer (and the
- * dashboard is not in edit mode), it uses that; otherwise, it falls back to
- * the built-in dashboard renderer.
+ * This component resolves and renders the active dashboard renderer from
+ * the provider registry. Superset's built-in renderer is registered as the
+ * default provider (see ./defaultRenderer), so dashboards always render:
+ * an extension-registered override wins in view mode when the extensions
+ * feature flag is on; the built-in default renders otherwise — including
+ * in edit mode and when the flag is off.
*/
-import { useSyncExternalStore } from 'react';
+import { Suspense, useSyncExternalStore } from 'react';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import type { dashboards } from '@apache-superset/core';
+import { Loading } from '@superset-ui/core/components';
import { ErrorBoundary } from 'src/components/ErrorBoundary';
-import DefaultDashboardRenderer from 'src/dashboard/components/DashboardRenderer/DefaultDashboardRenderer';
import DashboardRendererProviders from './DashboardRendererProviders';
+// Registers the built-in renderer as the default provider (side effect)
+import './defaultRenderer';
export interface DashboardRendererHostProps
extends dashboards.DashboardRendererProps {
@@ -49,8 +53,8 @@ export interface DashboardRendererHostProps
* dashboard renderer.
*
* It checks whether an extension has registered a custom dashboard renderer
- * and uses that if available; otherwise, it falls back to the built-in
- * renderer. The built-in renderer is also used whenever the dashboard is in
+ * and uses that if available; otherwise, it renders the built-in default
+ * provider. The built-in renderer is also used whenever the dashboard is in
* edit mode, regardless of registration.
*/
const DashboardRendererHost = ({
@@ -58,21 +62,21 @@ const DashboardRendererHost = ({
...rendererProps
}: DashboardRendererHostProps) => {
const manager = DashboardRendererProviders.getInstance();
- const provider = useSyncExternalStore(
+ const override = useSyncExternalStore(
manager.subscribe,
- () => manager.getProvider(),
+ () => manager.getOverrideProvider(),
() => undefined,
);
// Extensions can only register while the feature flag is on (the loader is
// gated), but the dashboard blast radius warrants the extra check.
- const useCustomRenderer =
+ const useOverride =
!editMode &&
- provider !== undefined &&
+ override !== undefined &&
isFeatureEnabled(FeatureFlag.EnableExtensions);
- if (useCustomRenderer) {
- const RendererComponent = provider.component;
+ if (useOverride) {
+ const RendererComponent = override.component;
return (
@@ -80,7 +84,16 @@ const DashboardRendererHost = ({
);
}
- return ;
+ const defaultProvider = manager.getDefaultProvider();
+ if (!defaultProvider) {
+ return null;
+ }
+ const DefaultRenderer = defaultProvider.component;
+ return (
+ }>
+
+
+ );
};
export default DashboardRendererHost;
diff --git a/superset-frontend/src/core/dashboards/DashboardRendererProviders.test.ts b/superset-frontend/src/core/dashboards/DashboardRendererProviders.test.ts
index d809cb91ee8..3d128cf1d26 100644
--- a/superset-frontend/src/core/dashboards/DashboardRendererProviders.test.ts
+++ b/superset-frontend/src/core/dashboards/DashboardRendererProviders.test.ts
@@ -44,9 +44,10 @@ function createMockRendererComponent(): DashboardRendererComponent {
}
beforeEach(() => {
- // Reset the singleton instance before each test
+ // Reset the singleton instance before each test, including the default
+ // tier — this suite exercises the registry in isolation.
const manager = DashboardRendererProviders.getInstance();
- manager.reset();
+ manager.reset(true);
});
test('creates singleton instance', () => {
@@ -220,5 +221,87 @@ test('reset clears the provider slot', () => {
manager.reset();
- expect(manager.getProvider()).toBeUndefined();
+ expect(manager.getOverrideProvider()).toBeUndefined();
+});
+
+test('the default provider is active when no override is registered', () => {
+ const manager = DashboardRendererProviders.getInstance();
+ const defaultRenderer = createMockRenderer({ id: 'host.default' });
+ const component = createMockRendererComponent();
+
+ manager.setDefaultProvider(defaultRenderer, component);
+
+ expect(manager.getProvider()?.renderer.id).toBe('host.default');
+ expect(manager.getDefaultProvider()?.renderer.id).toBe('host.default');
+ expect(manager.getOverrideProvider()).toBeUndefined();
+});
+
+test('an override wins over the default without displacing it', () => {
+ const manager = DashboardRendererProviders.getInstance();
+ const unregisterListener = jest.fn();
+ manager.onDidUnregister(unregisterListener);
+
+ manager.setDefaultProvider(
+ createMockRenderer({ id: 'host.default' }),
+ createMockRendererComponent(),
+ );
+ manager.registerProvider(
+ createMockRenderer({ id: 'ext.override' }),
+ createMockRendererComponent(),
+ );
+
+ expect(manager.getProvider()?.renderer.id).toBe('ext.override');
+ expect(manager.getDefaultProvider()?.renderer.id).toBe('host.default');
+ // Registering over the default must not fire an unregister for it
+ expect(unregisterListener).not.toHaveBeenCalled();
+});
+
+test('disposing the override falls back to the default provider', () => {
+ const manager = DashboardRendererProviders.getInstance();
+
+ manager.setDefaultProvider(
+ createMockRenderer({ id: 'host.default' }),
+ createMockRendererComponent(),
+ );
+ const disposable = manager.registerProvider(
+ createMockRenderer({ id: 'ext.override' }),
+ createMockRendererComponent(),
+ );
+
+ disposable.dispose();
+
+ expect(manager.getProvider()?.renderer.id).toBe('host.default');
+ expect(manager.getOverrideProvider()).toBeUndefined();
+});
+
+test('setDefaultProvider is idempotent by renderer id', () => {
+ const manager = DashboardRendererProviders.getInstance();
+ const listener = jest.fn();
+ manager.subscribe(listener);
+
+ const renderer = createMockRenderer({ id: 'host.default' });
+ manager.setDefaultProvider(renderer, createMockRendererComponent());
+ expect(listener).toHaveBeenCalledTimes(1);
+
+ // Re-registering the same id (e.g. duplicate side-effect import) is a no-op
+ manager.setDefaultProvider(renderer, createMockRendererComponent());
+ expect(listener).toHaveBeenCalledTimes(1);
+});
+
+test('reset keeps the default provider', () => {
+ const manager = DashboardRendererProviders.getInstance();
+
+ manager.setDefaultProvider(
+ createMockRenderer({ id: 'host.default' }),
+ createMockRendererComponent(),
+ );
+ manager.registerProvider(
+ createMockRenderer({ id: 'ext.override' }),
+ createMockRendererComponent(),
+ );
+
+ manager.reset();
+
+ expect(manager.getOverrideProvider()).toBeUndefined();
+ expect(manager.getProvider()?.renderer.id).toBe('host.default');
});
diff --git a/superset-frontend/src/core/dashboards/DashboardRendererProviders.ts b/superset-frontend/src/core/dashboards/DashboardRendererProviders.ts
index c5562e754f5..0c63a8df00a 100644
--- a/superset-frontend/src/core/dashboards/DashboardRendererProviders.ts
+++ b/superset-frontend/src/core/dashboards/DashboardRendererProviders.ts
@@ -34,19 +34,28 @@ type Listener = (e: T) => void;
/**
* Singleton manager for the dashboard renderer slot.
*
- * The dashboard renderer is a single-slot contribution point: at most one
- * provider is active at a time. Registering while the slot is occupied
- * displaces (and unregisters) the previous provider with a console warning —
- * the most recent registration wins.
+ * The dashboard renderer is a single-slot contribution point with two
+ * tiers: the host registers the built-in renderer as the DEFAULT provider,
+ * and extensions register at most one OVERRIDE provider on top of it. The
+ * active provider is the override when present, otherwise the default —
+ * so dashboards always render, extensions or not. Registering an override
+ * while the slot is occupied displaces (and unregisters) the previous
+ * override with a console warning — the most recent registration wins;
+ * the default provider is never displaced.
*/
class DashboardRendererProviders {
private static instance: DashboardRendererProviders;
/**
- * The single active provider slot.
+ * The single extension-registered override slot.
*/
private provider: DashboardRendererProvider | undefined;
+ /**
+ * The host-registered built-in provider, active when no override is.
+ */
+ private defaultProvider: DashboardRendererProvider | undefined;
+
private registerEmitter =
createEventEmitter();
@@ -123,11 +132,47 @@ class DashboardRendererProviders {
}
/**
- * Get the active dashboard renderer provider.
- * @returns The provider or undefined if none is registered.
+ * Register the host's built-in renderer as the default provider.
+ * Host-internal — not exposed on the public `dashboards` namespace.
+ * Idempotent by id so duplicate side-effect imports are harmless.
+ *
+ * @param renderer The default renderer descriptor.
+ * @param component The React component implementing the renderer.
+ */
+ public setDefaultProvider(
+ renderer: DashboardRenderer,
+ component: DashboardRendererComponent,
+ ): void {
+ if (this.defaultProvider?.renderer.id === renderer.id) {
+ return;
+ }
+ this.defaultProvider = { renderer, component };
+ this.syncListeners.forEach(l => l());
+ }
+
+ /**
+ * Get the built-in default dashboard renderer provider.
+ * @returns The default provider or undefined if the host has not set one.
+ */
+ public getDefaultProvider(): DashboardRendererProvider | undefined {
+ return this.defaultProvider;
+ }
+
+ /**
+ * Get the extension-registered override provider, ignoring the default.
+ * @returns The override provider or undefined if none is registered.
+ */
+ public getOverrideProvider(): DashboardRendererProvider | undefined {
+ return this.provider;
+ }
+
+ /**
+ * Get the active dashboard renderer provider: the extension-registered
+ * override when present, otherwise the built-in default.
+ * @returns The active provider or undefined if neither is registered.
*/
public getProvider(): DashboardRendererProvider | undefined {
- return this.provider;
+ return this.provider ?? this.defaultProvider;
}
/**
@@ -156,9 +201,16 @@ class DashboardRendererProviders {
/**
* Reset the manager state (for testing purposes).
+ * The default provider is kept unless requested: it is registered by a
+ * one-time module side effect that will not re-run between tests.
+ *
+ * @param clearDefault When true, also clears the default provider.
*/
- public reset(): void {
+ public reset(clearDefault = false): void {
this.provider = undefined;
+ if (clearDefault) {
+ this.defaultProvider = undefined;
+ }
this.syncListeners.clear();
this.registerEmitter =
createEventEmitter();
diff --git a/superset-frontend/src/core/dashboards/defaultRenderer.ts b/superset-frontend/src/core/dashboards/defaultRenderer.ts
new file mode 100644
index 00000000000..445d0c2a1b8
--- /dev/null
+++ b/superset-frontend/src/core/dashboards/defaultRenderer.ts
@@ -0,0 +1,56 @@
+/**
+ * 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.
+ */
+
+/**
+ * @fileoverview Registers Superset's built-in dashboard renderer as the
+ * default provider in the dashboard renderer registry.
+ *
+ * The built-in renderer goes through the same contribution point as
+ * extension-contributed renderers: it occupies the default tier of the
+ * slot, renders whenever no extension override is active (including when
+ * the extensions feature flag is off), and is retrievable via the public
+ * `dashboards.getDefaultDashboardRenderer()` so extensions can wrap it.
+ *
+ * The component is imported lazily so that bundles importing the
+ * `dashboards` namespace (e.g. the app startup bundle) do not statically
+ * pull in the dashboard rendering stack.
+ */
+
+import { lazy } from 'react';
+import type { dashboards } from '@apache-superset/core';
+import DashboardRendererProviders from './DashboardRendererProviders';
+
+export const DEFAULT_DASHBOARD_RENDERER: dashboards.DashboardRenderer = {
+ id: 'superset.dashboard-renderer',
+ name: 'Superset Dashboard Renderer',
+ description: "Superset's built-in dashboard renderer",
+};
+
+const DefaultDashboardRenderer = lazy(
+ () =>
+ import(
+ /* webpackChunkName: "DefaultDashboardRenderer" */
+ 'src/dashboard/components/DashboardRenderer/DefaultDashboardRenderer'
+ ),
+);
+
+DashboardRendererProviders.getInstance().setDefaultProvider(
+ DEFAULT_DASHBOARD_RENDERER,
+ DefaultDashboardRenderer,
+);
diff --git a/superset-frontend/src/core/dashboards/index.test.ts b/superset-frontend/src/core/dashboards/index.test.ts
index b3eb5b47b43..a569510e277 100644
--- a/superset-frontend/src/core/dashboards/index.test.ts
+++ b/superset-frontend/src/core/dashboards/index.test.ts
@@ -28,8 +28,26 @@ beforeEach(() => {
DashboardRendererProviders.getInstance().reset();
});
-test('getDashboardRenderer returns undefined when nothing is registered', () => {
- expect(dashboards.getDashboardRenderer()).toBeUndefined();
+test('getDashboardRenderer returns the built-in default when nothing is registered', () => {
+ expect(dashboards.getDashboardRenderer()?.renderer.id).toBe(
+ 'superset.dashboard-renderer',
+ );
+});
+
+test('getDefaultDashboardRenderer always returns the built-in provider', () => {
+ expect(dashboards.getDefaultDashboardRenderer()?.renderer.id).toBe(
+ 'superset.dashboard-renderer',
+ );
+
+ // Registering an override does not change the default
+ const disposable = dashboards.registerDashboardRenderer(
+ { id: 'acme.renderer', name: 'Acme Renderer' },
+ component,
+ );
+ expect(dashboards.getDefaultDashboardRenderer()?.renderer.id).toBe(
+ 'superset.dashboard-renderer',
+ );
+ disposable.dispose();
});
test('registerDashboardRenderer makes the provider retrievable', () => {
@@ -59,15 +77,17 @@ test('the last-registered renderer wins when multiple are registered', () => {
jest.restoreAllMocks();
});
-test('disposing the registration clears the slot', () => {
+test('disposing the registration falls back to the built-in default', () => {
const disposable = dashboards.registerDashboardRenderer(
{ id: 'acme.renderer', name: 'Acme Renderer' },
component,
);
- expect(dashboards.getDashboardRenderer()).toBeDefined();
+ expect(dashboards.getDashboardRenderer()?.renderer.id).toBe('acme.renderer');
disposable.dispose();
- expect(dashboards.getDashboardRenderer()).toBeUndefined();
+ expect(dashboards.getDashboardRenderer()?.renderer.id).toBe(
+ 'superset.dashboard-renderer',
+ );
});
test('registration events fire through the public API', () => {
diff --git a/superset-frontend/src/core/dashboards/index.ts b/superset-frontend/src/core/dashboards/index.ts
index cf6d1369d92..c7b5495fd87 100644
--- a/superset-frontend/src/core/dashboards/index.ts
+++ b/superset-frontend/src/core/dashboards/index.ts
@@ -33,9 +33,12 @@
import { useSyncExternalStore } from 'react';
import { dashboards as dashboardsApi } from '@apache-superset/core';
import DashboardRendererProviders from './DashboardRendererProviders';
+// Registers the built-in renderer as the default provider (side effect)
+import './defaultRenderer';
export type { DashboardRendererHostProps } from './DashboardRendererHost';
export { default as DashboardRendererHost } from './DashboardRendererHost';
+export { DEFAULT_DASHBOARD_RENDERER } from './defaultRenderer';
const provider = DashboardRendererProviders.getInstance();
@@ -49,6 +52,7 @@ export const useDashboardRenderer = () =>
export const dashboards: typeof dashboardsApi = {
registerDashboardRenderer: provider.registerProvider.bind(provider),
getDashboardRenderer: provider.getProvider.bind(provider),
+ getDefaultDashboardRenderer: provider.getDefaultProvider.bind(provider),
onDidRegisterDashboardRenderer: provider.onDidRegister.bind(provider),
onDidUnregisterDashboardRenderer: provider.onDidUnregister.bind(provider),
};