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), };