mirror of
https://github.com/apache/superset.git
synced 2026-04-07 18:35:15 +00:00
feat(extensions): code-first frontend contributions (#38346)
This commit is contained in:
committed by
GitHub
parent
01d5245cd2
commit
a74d32ab44
@@ -39,7 +39,7 @@ Superset's core should remain minimal, with many features delegated to extension
|
||||
|
||||
### 2. Explicit Contribution Points
|
||||
|
||||
All extension points are clearly defined and documented. Extension authors know exactly where and how they can interact with the host system. Each extension declares its capabilities in a metadata file, enabling the host to:
|
||||
All extension points are clearly defined and documented. Extension authors know exactly where and how they can interact with the host system. Backend contributions are declared in `extension.json`. Frontend contributions are registered directly in code at module load time, giving the host clear visibility into what each extension provides:
|
||||
- Manage the extension lifecycle
|
||||
- Provide a consistent user experience
|
||||
- Validate extension compatibility
|
||||
@@ -122,7 +122,6 @@ The Superset host application serves as the runtime environment for extensions:
|
||||
|
||||
The extensions table contains:
|
||||
- Extension name, version, and author
|
||||
- Contributed features and exposed modules
|
||||
- Metadata and configuration
|
||||
- Built frontend and/or backend code
|
||||
|
||||
@@ -173,7 +172,7 @@ new ModuleFederationPlugin({
|
||||
|
||||
This configuration does several important things:
|
||||
|
||||
**`exposes`** - Declares which modules are available to the host application. The extension makes `./index` available as its entry point.
|
||||
**`exposes`** - Declares which modules are available to the host application. Superset always loads extensions by requesting the `./index` module from the remote container — this is a fixed convention, not a configurable value. Extensions must expose exactly `'./index': './src/index.tsx'` and place all API registrations (views, commands, menus, editors, event listeners) in that file. The module is executed as a side effect when the extension loads, so any call to `views.registerView`, `commands.registerCommand`, etc. made at the top level of `index.tsx` will run automatically.
|
||||
|
||||
**`externals` and `externalsType`** - Tell Webpack that when the extension imports `@apache-superset/core`, it should use `window.superset` at runtime instead of bundling its own copy. This ensures extensions use the host's implementation of shared packages.
|
||||
|
||||
|
||||
@@ -28,100 +28,83 @@ To facilitate the development of extensions, we define a set of well-defined con
|
||||
|
||||
## Frontend
|
||||
|
||||
Frontend contribution types allow extensions to extend Superset's user interface with new views, commands, and menu items.
|
||||
Frontend contribution types allow extensions to extend Superset's user interface with new views, commands, and menu items. Frontend contributions are registered directly in code from your extension's `index.tsx` entry point — they do not need to be declared in `extension.json`.
|
||||
|
||||
### Views
|
||||
|
||||
Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Each view is registered with a unique ID and can be activated or deactivated as needed. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
|
||||
Extensions can add new views or panels to the host application, such as custom SQL Lab panels, dashboards, or other UI components. Contribution areas are uniquely identified (e.g., `sqllab.panels` for SQL Lab panels), enabling seamless integration into specific parts of the application.
|
||||
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "my_extension.main",
|
||||
"name": "My Panel Name"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { views } from '@apache-superset/core';
|
||||
import MyPanel from './MyPanel';
|
||||
|
||||
views.registerView(
|
||||
{ id: 'my-extension.main', name: 'My Panel Name' },
|
||||
'sqllab.panels',
|
||||
() => <MyPanel />,
|
||||
);
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command can specify properties like a unique command identifier, an icon, a title, and a description. These commands can be invoked by users through menus, keyboard shortcuts, or other UI elements, enabling extensions to add rich, interactive functionality to Superset.
|
||||
Extensions can define custom commands that can be executed within the host application, such as context-aware actions or menu options. Each command specifies a unique identifier, a title, an optional icon, and a description. Commands can be invoked by users through menus, keyboard shortcuts, or other UI elements.
|
||||
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "my_extension.copy_query",
|
||||
"icon": "CopyOutlined",
|
||||
"title": "Copy Query",
|
||||
"description": "Copy the current query to clipboard"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```typescript
|
||||
import { commands } from '@apache-superset/core';
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: 'my-extension.copy-query',
|
||||
title: 'Copy Query',
|
||||
icon: 'CopyOutlined',
|
||||
description: 'Copy the current query to clipboard',
|
||||
},
|
||||
() => {
|
||||
// Command implementation
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Menus
|
||||
|
||||
Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item can specify properties such as the target view, the command to execute, its placement (primary, secondary, or context), and conditions for when it should be displayed. Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor), allowing extensions to seamlessly integrate their functionality into specific menus and workflows within Superset.
|
||||
Extensions can contribute new menu items or context menus to the host application, providing users with additional actions and options. Each menu item specifies the target area, the command to execute, and its placement (primary, secondary, or context). Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor).
|
||||
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"menus": {
|
||||
"sqllab": {
|
||||
"editor": {
|
||||
"primary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.copy_query"
|
||||
}
|
||||
],
|
||||
"secondary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.prettify"
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.clear"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```typescript
|
||||
import { menus } from '@apache-superset/core';
|
||||
|
||||
menus.addMenuItem('sqllab.editor', {
|
||||
placement: 'primary',
|
||||
command: 'my-extension.copy-query',
|
||||
});
|
||||
|
||||
menus.addMenuItem('sqllab.editor', {
|
||||
placement: 'secondary',
|
||||
command: 'my-extension.prettify',
|
||||
});
|
||||
|
||||
menus.addMenuItem('sqllab.editor', {
|
||||
placement: 'context',
|
||||
command: 'my-extension.clear',
|
||||
});
|
||||
```
|
||||
|
||||
### Editors
|
||||
|
||||
Extensions can replace Superset's default text editors with custom implementations. This enables enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions. When an extension registers an editor for a language, it replaces the default Ace editor in all locations that use that language (SQL Lab, Dashboard Properties, CSS editors, etc.).
|
||||
Extensions can replace Superset's default text editors with custom implementations. This enables enhanced editing experiences using alternative editor frameworks like Monaco, CodeMirror, or custom solutions. When an extension registers an editor for a language, it replaces the default editor in all locations that use that language (SQL Lab, Dashboard Properties, CSS editors, etc.).
|
||||
|
||||
```json
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"editors": [
|
||||
{
|
||||
"id": "my_extension.monaco_sql",
|
||||
"name": "Monaco SQL Editor",
|
||||
"languages": ["sql"],
|
||||
"description": "Monaco-based SQL editor with IntelliSense"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```typescript
|
||||
import { editors } from '@apache-superset/core';
|
||||
import MonacoSQLEditor from './MonacoSQLEditor';
|
||||
|
||||
editors.registerEditor(
|
||||
{
|
||||
id: 'my-extension.monaco-sql',
|
||||
name: 'Monaco SQL Editor',
|
||||
languages: ['sql'],
|
||||
},
|
||||
MonacoSQLEditor,
|
||||
);
|
||||
```
|
||||
|
||||
See [Editors Extension Point](./extension-points/editors) for implementation details.
|
||||
|
||||
@@ -91,39 +91,24 @@ The `README.md` file provides documentation and instructions for using the exten
|
||||
|
||||
## Extension Metadata
|
||||
|
||||
The `extension.json` file contains all metadata necessary for the host application to understand and manage the extension:
|
||||
The `extension.json` file contains the metadata necessary for the host application to identify and load the extension. Backend contributions (entry points and files) are declared here. Frontend contributions are registered directly in code from `frontend/src/index.tsx`.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "dataset-references",
|
||||
"name": "Dataset References",
|
||||
"publisher": "my-org",
|
||||
"name": "dataset-references",
|
||||
"displayName": "Dataset References",
|
||||
"version": "1.0.0",
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "dataset-references.main",
|
||||
"name": "Dataset References"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"moduleFederation": {
|
||||
"exposes": ["./index"],
|
||||
"name": "datasetReferences"
|
||||
}
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"backend": {
|
||||
"entryPoints": ["superset_extensions.dataset_references.entrypoint"],
|
||||
"files": ["backend/src/superset_extensions/dataset_references/**/*.py"]
|
||||
}
|
||||
},
|
||||
"permissions": []
|
||||
}
|
||||
```
|
||||
|
||||
The `contributions` section declares how the extension extends Superset's functionality through views, commands, menus, and other contribution types. The `backend` section specifies entry points and files to include in the bundle.
|
||||
The `backend` section specifies Python entry points to load eagerly when the extension starts, and glob patterns for source files to include in the bundle.
|
||||
|
||||
## Interacting with the Host
|
||||
|
||||
@@ -154,34 +139,38 @@ export const onDidQueryStop: Event<QueryContext>;
|
||||
The following code demonstrates more examples of the existing frontend APIs:
|
||||
|
||||
```typescript
|
||||
import { core, commands, sqlLab, authentication, Button } from '@apache-superset/core';
|
||||
import React from 'react';
|
||||
import { views, commands, sqlLab, authentication, Button } from '@apache-superset/core';
|
||||
import MyPanel from './MyPanel';
|
||||
|
||||
export function activate(context) {
|
||||
// Register a new panel (view) in SQL Lab and use shared UI components in your extension's React code
|
||||
const panelDisposable = core.registerView('my_extension.panel', <MyPanel><Button/></MyPanel>);
|
||||
// Register a new panel (view) in SQL Lab and use shared UI components in your extension's React code
|
||||
views.registerView(
|
||||
{ id: 'my-extension.panel', name: 'My Panel' },
|
||||
'sqllab.panels',
|
||||
() => <MyPanel><Button /></MyPanel>,
|
||||
);
|
||||
|
||||
// Register a custom command
|
||||
const commandDisposable = commands.registerCommand(
|
||||
'my_extension.copy_query',
|
||||
() => {
|
||||
// Command logic here
|
||||
},
|
||||
);
|
||||
// Register a custom command
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: 'my-extension.copy-query',
|
||||
title: 'Copy Query',
|
||||
description: 'Copy the current query to clipboard',
|
||||
},
|
||||
() => {
|
||||
// Command logic here
|
||||
},
|
||||
);
|
||||
|
||||
// Listen for query run events in SQL Lab
|
||||
const eventDisposable = sqlLab.onDidQueryRun(queryContext => {
|
||||
console.log('Query started on database:', queryContext.tab.databaseId);
|
||||
});
|
||||
// Listen for query run events in SQL Lab
|
||||
sqlLab.onDidQueryRun(queryContext => {
|
||||
console.log('Query started on database:', queryContext.tab.databaseId);
|
||||
});
|
||||
|
||||
// Access a CSRF token for secure API requests
|
||||
authentication.getCSRFToken().then(token => {
|
||||
// Use token as needed
|
||||
});
|
||||
|
||||
// Add all disposables for automatic cleanup on deactivation
|
||||
context.subscriptions.push(panelDisposable, commandDisposable, eventDisposable);
|
||||
}
|
||||
// Access a CSRF token for secure API requests
|
||||
authentication.getCSRFToken().then(token => {
|
||||
// Use token as needed
|
||||
});
|
||||
```
|
||||
|
||||
### Backend APIs
|
||||
|
||||
@@ -95,7 +95,7 @@ my-org.hello-world/
|
||||
|
||||
## Step 3: Configure Extension Metadata
|
||||
|
||||
The generated `extension.json` contains basic metadata. Update it to register your panel in SQL Lab:
|
||||
The generated `extension.json` contains the extension's metadata. It is used to identify the extension and declare its backend entry points. Frontend contributions are registered directly in code (see Step 5).
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -104,24 +104,6 @@ The generated `extension.json` contains basic metadata. Update it to register yo
|
||||
"displayName": "Hello World",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "my-org.hello-world.main",
|
||||
"name": "Hello World"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"moduleFederation": {
|
||||
"exposes": ["./index"],
|
||||
"name": "myOrg_helloWorld"
|
||||
}
|
||||
},
|
||||
"backend": {
|
||||
"entryPoints": ["superset_extensions.my_org.hello_world.entrypoint"],
|
||||
"files": ["backend/src/superset_extensions/my_org/hello_world/**/*.py"]
|
||||
@@ -130,15 +112,13 @@ The generated `extension.json` contains basic metadata. Update it to register yo
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: The `moduleFederation.name` uses collision-safe naming (`myOrg_helloWorld`), and backend entry points use the full nested Python namespace (`superset_extensions.my_org.hello_world`).
|
||||
|
||||
**Key fields:**
|
||||
|
||||
- `publisher`: Organizational namespace for the extension
|
||||
- `name`: Technical identifier (kebab-case)
|
||||
- `displayName`: Human-readable name shown to users
|
||||
- `frontend.contributions.views.sqllab.panels`: Registers your panel in SQL Lab
|
||||
- `backend.entryPoints`: Python modules to load eagerly when extension starts
|
||||
- `backend.entryPoints`: Python modules to load eagerly when the extension starts
|
||||
- `backend.files`: Glob patterns for Python source files to include in the bundle
|
||||
|
||||
## Step 4: Create Backend API
|
||||
|
||||
@@ -247,7 +227,9 @@ The `@apache-superset/core` package must be listed in both `peerDependencies` (t
|
||||
|
||||
**`frontend/webpack.config.js`**
|
||||
|
||||
The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting:
|
||||
The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting.
|
||||
|
||||
**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture#module-federation) for a full explanation.
|
||||
|
||||
```javascript
|
||||
const path = require("path");
|
||||
@@ -413,29 +395,26 @@ export default HelloWorldPanel;
|
||||
|
||||
**Update `frontend/src/index.tsx`**
|
||||
|
||||
Replace the generated code with the extension entry point:
|
||||
This file is the single entry point Superset loads from every extension. All registrations — views, commands, menus, editors, event listeners — must be made here (or imported and executed from here). Replace the generated code with:
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { core } from '@apache-superset/core';
|
||||
import { views } from '@apache-superset/core';
|
||||
import HelloWorldPanel from './HelloWorldPanel';
|
||||
|
||||
export const activate = (context: core.ExtensionContext) => {
|
||||
context.disposables.push(
|
||||
core.registerViewProvider('my-org.hello-world.main', () => <HelloWorldPanel />),
|
||||
);
|
||||
};
|
||||
|
||||
export const deactivate = () => {};
|
||||
views.registerView(
|
||||
{ id: 'my-org.hello-world.main', name: 'Hello World' },
|
||||
'sqllab.panels',
|
||||
() => <HelloWorldPanel />,
|
||||
);
|
||||
```
|
||||
|
||||
**Key patterns:**
|
||||
|
||||
- `activate` function is called when the extension loads
|
||||
- `core.registerViewProvider` registers the component with ID `my-org.hello-world.main` (matching `extension.json`)
|
||||
- `authentication.getCSRFToken()` retrieves the CSRF token for API calls
|
||||
- `views.registerView` is called at module load time — no `activate`/`deactivate` lifecycle needed
|
||||
- The first argument is a `{ id, name }` descriptor; the second is the contribution area (e.g., `sqllab.panels`); the third is a factory returning the React component
|
||||
- `authentication.getCSRFToken()` retrieves the CSRF token for API calls (used inside components)
|
||||
- Fetch calls to `/extensions/{publisher}/{name}/{endpoint}` reach your backend API
|
||||
- `context.disposables.push()` ensures proper cleanup
|
||||
|
||||
## Step 6: Install Dependencies
|
||||
|
||||
@@ -513,15 +492,15 @@ Superset will extract and validate the extension metadata, load the assets, regi
|
||||
|
||||
Here's what happens when your extension loads:
|
||||
|
||||
1. **Superset starts**: Reads `extension.json` and loads backend entrypoint
|
||||
1. **Superset starts**: Reads `extension.json` and loads the backend entrypoint
|
||||
2. **Backend registration**: `entrypoint.py` registers your API via `rest_api.add_extension_api()`
|
||||
3. **Frontend loads**: When SQL Lab opens, Superset fetches the remote entry file
|
||||
4. **Module Federation**: Webpack loads your extension code and resolves `@apache-superset/core` to `window.superset`
|
||||
5. **Activation**: `activate()` is called, registering your view provider
|
||||
4. **Module Federation**: Webpack loads your extension module and resolves `@apache-superset/core` to `window.superset`
|
||||
5. **Registration**: The module executes at load time, calling `views.registerView` to register your panel
|
||||
6. **Rendering**: When the user opens your panel, React renders `<HelloWorldPanel />`
|
||||
7. **API call**: Component fetches data from `/extensions/my-org/hello-world/message`
|
||||
7. **API call**: The component fetches data from `/extensions/my-org/hello-world/message`
|
||||
8. **Backend response**: Your Flask API returns the hello world message
|
||||
9. **Display**: Component shows the message to the user
|
||||
9. **Display**: The component shows the message to the user
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
Reference in New Issue
Block a user