You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Please make sure you are familiar with the SIP process documented here. The SIP will be numbered by a committer upon acceptance.
[SIP-219] Proposal for registering built-in implementations as default-tier providers in extension contribution points
Motivation
SIP-151 gave Superset an extensions architecture with contribution points that let extensions replace built-in functionality — the SQL Lab editor can be swapped for a Monaco-based one, and with #41703 the entire dashboard renderer can be swapped as well. Extensions are gated behind the ENABLE_EXTENSIONS feature flag, which is off by default — and core surfaces like dashboard rendering and SQL editing must obviously keep working when it is off.
Today that guarantee is provided by hardcoded fallbacks inside host components. EditorHost checks the editor registry and, when no extension has registered a provider for the language, falls back to a hardcoded AceEditorProvider. This works, but it has structural drawbacks:
The built-in bypasses the contribution point it anchors. The extension path and the built-in path are two different code paths in the host component, so the contract is never exercised by Superset itself — extensions are the only consumers, and contract regressions only surface downstream.
Introspection is misleading.editors.getEditor('sql') returns undefined even though an editor is very much active — the API can't tell you what is actually rendering.
Extensions can only replace, never augment. There is no way for an extension to retrieve the built-in implementation and wrap it (add chrome around the default dashboard renderer, decorate the default editor) because the built-in isn't reachable through the API.
Fallback logic is duplicated per host and each host invents its own variant, rather than the registry owning resolution.
We want built-ins to be first-class citizens of the extension system — "Superset ships with a default dashboard-renderer extension" rather than "the host has an if-statement" — without ever making core UX dependent on the extensions feature flag or the extension loading machinery.
Proposed Change
Adopt a two-tier, single-slot provider registry pattern for contribution points that replace built-in functionality, as implemented for the dashboard renderer in #41703:
Default tier (host): the built-in implementation is registered through the contribution point itself, by the host, as the default provider (e.g. superset.dashboard-renderer). Registration happens as a lazy side-effect module import wherever the surface renders (app and embedded entry points alike), fully independent of ENABLE_EXTENSIONS, ExtensionsStartup, and the module-federation loader. The component itself is loaded via React.lazy, so registering the default does not pull the implementation into the startup bundle.
Override tier (extensions): extensions register through the existing public API (registerDashboardRenderer, registerEditor, ...). At most one override occupies the slot; the most recent registration wins and displaces the previous override — never the default.
Resolution lives in the registry:getProvider() returns override ?? default. Disposing the active override falls back to the default through the registry, not through a host code branch. The host component simply renders the resolved provider (extension overrides wrapped in an error boundary; edit-mode and feature-flag rules applied to overrides only).
The feature flag gates overrides, not defaults. With ENABLE_EXTENSIONS off, the loader never loads extensions and the host ignores overrides — the default renders unconditionally. A misbehaving or absent extension system can never take dashboards down.
Augmentation becomes possible: the public API exposes the default provider (e.g. dashboards.getDefaultDashboardRenderer()), so an extension can wrap the built-in component in its own and register the wrapper — replace or augment.
Registry sketch (from the dashboard-renderer implementation):
The dashboard renderer ships this pattern already (merged into the PR branch of #41703, with unit, integration, and Playwright E2E coverage — including a live assertion that getDashboardRenderer() reports superset.dashboard-renderer before any extension registers). This SIP proposes we:
Ratify the pattern as the standard for contribution points that replace built-in functionality, documented in the extensions developer docs.
Migrate the existing single-slot/keyed points to it: editors (register AceEditorProvider as the default provider per language, remove the hardcoded fallback from EditorHost) and chat (no built-in exists today; the default tier simply stays empty, which the pattern supports).
Apply it to future contribution points that wrap existing surfaces (e.g. chart renderers, filter bars) as they are proposed.
dashboards.getDashboardRenderer() semantics: returns the active provider — the extension override when one is active, otherwise the built-in default (previously undefined when no extension registered).
Proposed follow-ups in the same shape: editors.getDefaultEditor(language), with editors.getEditor(language) returning the active provider including the default.
Reserved, well-known provider ids for built-ins under the superset. prefix (e.g. superset.dashboard-renderer, superset.ace-editor).
No REST endpoints, models, CLI, or deployment changes. The setDefaultProvider registration API is host-internal and deliberately not exposed on window.superset.
New dependencies
None.
Migration Plan and Compatibility
No database migrations. The pattern is behavior-compatible: with no extensions registered, the default provider renders exactly what the hardcoded fallback rendered before. Migrating editors is an internal refactor of EditorHost + EditorProviders; the only observable change is getEditor() becoming truthful about the active editor, which is a strict improvement but should be noted in the extensions changelog since extensions may have used getEditor() === undefined as a "no custom editor" check (they should use the provider id or a new getOverride-style accessor instead).
Rejected Alternatives
Hardcoded fallback in the host component (the current SQL editor approach).EditorHost falls back to AceEditorProvider in a code branch, and the dashboard renderer initially shipped the same shape. Rejected as the long-term pattern for the reasons in Motivation: the contract isn't dogfooded, introspection APIs return undefined while a built-in renders, augmentation is impossible, and every host duplicates its own fallback logic. It does have the virtue of simplicity, and nothing breaks if a given contribution point stays on it temporarily — this SIP treats it as the migration starting point, not an error.
Shipping built-ins as real packaged extensions (bundled .supx files loaded through ExtensionsLoader / module federation). Maximally uniform, but it would route core UX through the extension loading machinery — remote-entry fetches, manifest parsing, and the ENABLE_EXTENSIONS gate — for code that is already statically bundled. A failure anywhere in that machinery (or simply the flag being off, which is the default) would take down dashboards and SQL Lab. Rejected: core surfaces must render even if the extension system is disabled or broken.
Enabling ENABLE_EXTENSIONS by default so registry-based built-ins always load. Rejected: it conflates two decisions (whether operators opt into third-party extensions vs. whether built-ins render), expands the default security surface, and still leaves built-ins dependent on loader machinery.
Please make sure you are familiar with the SIP process documented
here. The SIP will be numbered by a committer upon acceptance.
[SIP-219] Proposal for registering built-in implementations as default-tier providers in extension contribution points
Motivation
SIP-151 gave Superset an extensions architecture with contribution points that let extensions replace built-in functionality — the SQL Lab editor can be swapped for a Monaco-based one, and with #41703 the entire dashboard renderer can be swapped as well. Extensions are gated behind the
ENABLE_EXTENSIONSfeature flag, which is off by default — and core surfaces like dashboard rendering and SQL editing must obviously keep working when it is off.Today that guarantee is provided by hardcoded fallbacks inside host components.
EditorHostchecks the editor registry and, when no extension has registered a provider for the language, falls back to a hardcodedAceEditorProvider. This works, but it has structural drawbacks:editors.getEditor('sql')returnsundefinedeven though an editor is very much active — the API can't tell you what is actually rendering.We want built-ins to be first-class citizens of the extension system — "Superset ships with a default dashboard-renderer extension" rather than "the host has an if-statement" — without ever making core UX dependent on the extensions feature flag or the extension loading machinery.
Proposed Change
Adopt a two-tier, single-slot provider registry pattern for contribution points that replace built-in functionality, as implemented for the dashboard renderer in #41703:
superset.dashboard-renderer). Registration happens as a lazy side-effect module import wherever the surface renders (app and embedded entry points alike), fully independent ofENABLE_EXTENSIONS,ExtensionsStartup, and the module-federation loader. The component itself is loaded viaReact.lazy, so registering the default does not pull the implementation into the startup bundle.registerDashboardRenderer,registerEditor, ...). At most one override occupies the slot; the most recent registration wins and displaces the previous override — never the default.getProvider()returnsoverride ?? default. Disposing the active override falls back to the default through the registry, not through a host code branch. The host component simply renders the resolved provider (extension overrides wrapped in an error boundary; edit-mode and feature-flag rules applied to overrides only).ENABLE_EXTENSIONSoff, the loader never loads extensions and the host ignores overrides — the default renders unconditionally. A misbehaving or absent extension system can never take dashboards down.dashboards.getDefaultDashboardRenderer()), so an extension can wrap the built-in component in its own and register the wrapper — replace or augment.Registry sketch (from the dashboard-renderer implementation):
The dashboard renderer ships this pattern already (merged into the PR branch of #41703, with unit, integration, and Playwright E2E coverage — including a live assertion that
getDashboardRenderer()reportssuperset.dashboard-rendererbefore any extension registers). This SIP proposes we:editors(registerAceEditorProvideras the default provider per language, remove the hardcoded fallback fromEditorHost) andchat(no built-in exists today; the default tier simply stays empty, which the pattern supports).New or Changed Public Interfaces
dashboards.getDefaultDashboardRenderer(): DashboardRendererProvider | undefined(shipped with feat(extensions): add dashboard renderer contribution point #41703).dashboards.getDashboardRenderer()semantics: returns the active provider — the extension override when one is active, otherwise the built-in default (previouslyundefinedwhen no extension registered).editors.getDefaultEditor(language), witheditors.getEditor(language)returning the active provider including the default.superset.prefix (e.g.superset.dashboard-renderer,superset.ace-editor).setDefaultProviderregistration API is host-internal and deliberately not exposed onwindow.superset.New dependencies
None.
Migration Plan and Compatibility
No database migrations. The pattern is behavior-compatible: with no extensions registered, the default provider renders exactly what the hardcoded fallback rendered before. Migrating
editorsis an internal refactor ofEditorHost+EditorProviders; the only observable change isgetEditor()becoming truthful about the active editor, which is a strict improvement but should be noted in the extensions changelog since extensions may have usedgetEditor() === undefinedas a "no custom editor" check (they should use the provider id or a newgetOverride-style accessor instead).Rejected Alternatives
EditorHostfalls back toAceEditorProviderin a code branch, and the dashboard renderer initially shipped the same shape. Rejected as the long-term pattern for the reasons in Motivation: the contract isn't dogfooded, introspection APIs returnundefinedwhile a built-in renders, augmentation is impossible, and every host duplicates its own fallback logic. It does have the virtue of simplicity, and nothing breaks if a given contribution point stays on it temporarily — this SIP treats it as the migration starting point, not an error..supxfiles loaded throughExtensionsLoader/ module federation). Maximally uniform, but it would route core UX through the extension loading machinery — remote-entry fetches, manifest parsing, and theENABLE_EXTENSIONSgate — for code that is already statically bundled. A failure anywhere in that machinery (or simply the flag being off, which is the default) would take down dashboards and SQL Lab. Rejected: core surfaces must render even if the extension system is disabled or broken.ENABLE_EXTENSIONSby default so registry-based built-ins always load. Rejected: it conflates two decisions (whether operators opt into third-party extensions vs. whether built-ins render), expands the default security surface, and still leaves built-ins dependent on loader machinery.