msyavuz opened a new issue, #45248: URL: https://github.com/apache/superset/issues/45248
*Please make sure you are familiar with the SIP process documented* [here](https://github.com/apache/superset/issues/5602). The SIP will be numbered by a committer upon acceptance. > **Status: draft.** Opened to consolidate #44101 (Settings-menu extension point + `/extensions/view/` host page, a POC for SIP-96) with a proposal for main-navbar entries, so both land under one design instead of two overlapping ones. ## [SIP] Proposal for navbar extension points and extension-owned pages ### Motivation An extension that owns a surface of its own (an admin console, a catalog browser, a governance report, SIP-96's notification manager) has nowhere to live and no way to be reached. The only way to add a navigation entry today is `FLASK_APP_MUTATOR` + `appbuilder.add_link()` in `superset_config.py`: deployment config, not something an extension can install, enable, disable or version with itself. The `views` / `menus` registries in `src/core` already accept arbitrary locations, but SQL Lab is the only host surface that reads from them. Nothing in the app shell outside SQL Lab renders extension contributions, and there is no route an extension-registered view can be mounted on. #44101 adds the first two pieces of this (an "Extensions" group in the Settings dropdown and a generic `/extensions/view/:viewId` host page). This SIP keeps that work and extends it with the pieces an extension needs to behave like a first-class page: an entry in the main navbar, SPA navigation, active-tab highlighting and a stable item model. ### Proposed Change Two new host surfaces read from the existing registries, plus one route and two small SDK additions. **1. Navbar locations** (`main` and `settings`), rendered by `Menu.tsx` and `RightMenu.tsx`: - Items are appended after the built-ins, sorted by `order` then registration order. No extension can displace Dashboards / Charts / Datasets / SQL Lab. - `standalone` and `uiConfig.hideNav` early-returns apply as today. - Active-tab highlighting gets a case keyed on the item id when the current path is that item's page. - An item whose `view` / `command` target is not registered is skipped rather than rendered dead. - Items pop in via `useSyncExternalStore` once the extension bundle lands (bundles load in a `useEffect` after `userId` resolves). That late paint is accepted; see Rejected Alternatives. **2. Navbar item model.** An item carries its own label and icon and does exactly one of three things on click: - `view`: SPA-navigates to `/extensions/view/<viewId>` where the host renders the registered view. - `command`: runs a registered command without navigating (open a modal, kick off a job). - `href`: plain link to a URL Superset doesn't own (docs, an external console). Declaring more than one is a registration error. **3. Host page** `/extensions/view/:viewId+` (as in #44101). Resolves the view reactively through `useResolveView`, so a bookmark or refresh shows a placeholder until the extension finishes loading and then re-renders instead of permanently committing to "could not be loaded". An `extensions ready` signal exposed from `ExtensionsLoader` lets the page distinguish "still loading" from "not found" once initialization has settled. **4. Backend shell route.** Every SPA route needs an explicit Flask view rendering `spa.html`; a direct hit on an unregistered path 404s. `/extensions/view/<path:view_id>` is a login-gated shell page (same pattern as `welcome`). It returns no data, so it is not a security boundary: the extension's own REST endpoints are, and those already go through `@protect()`. **5. `navigation.navigateTo(path)`** in the public SDK, backed by SPA history, so a command handler (or the host's own navbar click) can move the user to a page without a full reload. Today the `navigation` namespace is read-only and #44101's menu items have to fall back to `window.location.assign`. **Extension author flow** is unchanged in scaffolding, build and install. The whole delta is two registration calls as module side effects in `frontend/src/index.tsx`: ```ts import { navigation, views } from '@apache-superset/core'; import CatalogPage from './CatalogPage'; views.registerView( { id: 'acme.catalog.main', name: 'Data catalog' }, 'global.page', CatalogPage, ); navigation.registerNavbarItem( { id: 'acme.catalog', label: 'Data catalog', icon: 'DatabaseOutlined', view: 'acme.catalog.main', }, 'main', ); ``` The page is then reachable at `/extensions/view/acme.catalog.main`, deep-linkable and refresh-safe, and the navbar entry highlights while it is open. Data comes from the extension's own `@api` endpoints mounted at `/extensions/{publisher}/{name}/{resource}/`; nothing host-specific sits in the middle. ### New or Changed Public Interfaces `@apache-superset/core`, `navigation` namespace: ```ts export type NavbarLocation = 'main' | 'settings'; export interface NavbarItem { /** Unique, publisher-namespaced. Doubles as React key and active-tab key. */ id: string; label: string; /** Icon name resolved against the host icon set. */ icon?: string; /** Tooltip / a11y description. */ description?: string; /** Sort weight among contributed items. Default Infinity. */ order?: number; /** Exactly one of: */ view?: string; command?: string; href?: string; } export function registerNavbarItem(item: NavbarItem, location: NavbarLocation): Disposable; export function getNavbarItems(location: NavbarLocation): NavbarItem[]; export function navigateTo(path: string): void; ``` - `Page` union gains `'extension'`, and the navigation tracker's path table gains the extension route, so `getPage()` stops reporting `'home'` on extension pages. - A `global.page` view location (name open, see below) exported from the SDK so extensions don't hardcode the string. #44101 introduces `global.settingsPanel` / `global.settingsMenu` as host-internal literals; this SIP proposes exporting the location constants. - `ExtensionContext.window` toast surface from #44101 is kept as-is. - Routes: `EXTENSION_VIEW: '/extensions/view/:viewId+'` behind `FeatureFlag.EnableExtensions`, plus a prefix check in `isFrontendRoute` so navbar links stay SPA navigations. - Backend: `extension_view` shell route on the core view, login-gated. - `extension.json`: a `navbar` contribution block is documented alongside the existing declarative types. As with the existing `views` / `menus` blocks, the host learns about contributions from the runtime registration calls; the manifest block is documentation and tooling input only. ### New dependencies None. ### Migration Plan and Compatibility No database migration. No change for deployments with `ENABLE_EXTENSIONS` off. Existing `FLASK_APP_MUTATOR` + `add_link()` menu entries keep working and keep rendering before contributed items. Extensions written against #44101's `global.settingsMenu` + command pattern continue to work; the navbar item model is additive. Known limitations, accepted for the first cut: - Late paint of contributed items (see above). - Contributed items exist only in the React shell; server-rendered FAB menu pages won't show them. - One route depth. Extension-internal sub-routing is out of scope; extensions can use query params or a hash. - No per-item RBAC. An item is visible iff its extension loaded, and the extensions API already filters by user. Per-item gating would need the user's permission set on the client, which bootstrap data does not expose. - The loader has no unload path, so disabling an extension removes its entry only after a refresh. ### Rejected Alternatives - **Dynamic route table that `RouteSwitch` subscribes to.** `routes.tsx` is evaluated at module load and extensions register asynchronously, so a hard refresh on an extension URL would render with no matching route and paint blank until the bundle lands. One static parameterised route plus a reactive resolver is simpler and refresh-safe. - **A separate `navigation.registerPage` API.** The `views` registry already provides location keying, `useSyncExternalStore` reactivity, `ErrorBoundary` wrapping and placeholder handling. Pages are views at a reserved location; no second registry. - **Separate `/extension/:extensionId/:pageId/` prefix.** Considered to avoid clashing with `/extensions/list/` and the extension REST prefix. #44101's `/extensions/view/:viewId+` works because the list route is registered first and view ids are already publisher-namespaced, so the extra segment is redundant. - **Serving contributions from the extensions API to avoid late paint.** The Python `Manifest` model has no `contributions` field and neither the CLI nor the API reads one from `extension.json`. Worth fixing, but independently of this SIP. - **`@has_access` with a dedicated `ExtensionView` permission on the shell route.** Gives operators a knob to withhold extension pages from a role, but the FAB-generated permission isn't backfilled onto existing custom roles by role sync, which breaks existing deployments on upgrade. Login-only gating on a data-less shell page matches `welcome`. - **Command-only navbar items (labelled by the command title).** Most navbar entries are navigational and have no command behind them; putting the label on the item keeps `command` for the modal / action case only. ### Open questions 1. Ship `main` and `settings` together, or `settings` first (as #44101 does) and `main` as a follow-up? 2. Flat items only, or allow children (submenu) in the first cut? 3. Name of the exported page location: `global.page`, `global.settingsPanel`, or one location per navbar location? 4. Do we want `href` in the first cut, or are `view` + `command` enough? 5. 404 at the Flask layer on an unknown `view_id` (leaks which extensions are installed to any authenticated user), or always render the shell and let the client show not-found? -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
