| --- |
| title: Quick Start |
| sidebar_position: 2 |
| --- |
| |
| <!-- |
| 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. |
| --> |
| |
| # Quick Start |
| |
| This guide walks you through creating your first Superset extension - a simple "Hello World" panel that displays a message fetched from a backend API endpoint. You'll learn the essential structure and patterns for building full-stack Superset extensions. |
| |
| ## Prerequisites |
| |
| Before starting, ensure you have: |
| |
| - Node.js and npm compatible with your Superset version |
| - Python compatible with your Superset version |
| - A running Superset development environment |
| - Basic knowledge of React, TypeScript, and Flask |
| |
| ## Step 1: Install the Extensions CLI |
| |
| First, install the Apache Superset Extensions CLI: |
| |
| ```bash |
| pip install apache-superset-extensions-cli |
| ``` |
| |
| ## Step 2: Create a New Extension |
| |
| Use the CLI to scaffold a new extension project. Extensions can include frontend functionality, backend functionality, or both, depending on your needs. This quickstart demonstrates a full-stack extension with both frontend UI components and backend API endpoints to show the complete integration pattern. |
| |
| ```bash |
| superset-extensions init |
| ``` |
| |
| The CLI will prompt you for information using a three-step publisher workflow: |
| |
| ``` |
| Extension display name: Hello World |
| Extension name (hello-world): hello-world |
| Publisher (e.g., my-org): my-org |
| Initial version [0.1.0]: 0.1.0 |
| License [Apache-2.0]: Apache-2.0 |
| Include frontend? [Y/n]: Y |
| Include backend? [Y/n]: Y |
| ``` |
| |
| **Publisher Namespaces**: Extensions use organizational namespaces similar to VS Code extensions, providing collision-safe naming across organizations: |
| |
| - **NPM package**: `@my-org/hello-world` (scoped package for frontend distribution) |
| - **Module Federation name**: `myOrg_helloWorld` (collision-safe JavaScript identifier) |
| - **Backend package**: `my_org-hello_world` (collision-safe Python distribution name) |
| - **Python namespace**: `my_org.hello_world` |
| |
| This approach ensures that extensions from different organizations cannot conflict, even if they use the same technical name (e.g., both `acme.dashboard-widgets` and `corp.dashboard-widgets` can coexist). |
| |
| This creates a complete project structure: |
| |
| ``` |
| hello-world/ |
| ├── extension.json # Extension metadata and configuration |
| ├── backend/ # Backend Python code |
| │ ├── src/ |
| │ │ └── my_org/ |
| │ │ └── hello_world/ |
| │ │ └── entrypoint.py # Backend registration |
| │ └── pyproject.toml |
| └── frontend/ # Frontend TypeScript/React code |
| ├── src/ |
| │ └── index.tsx # Frontend entry point |
| ├── package.json |
| ├── tsconfig.json |
| └── webpack.config.js |
| ``` |
| |
| ## Step 3: Configure Extension Metadata |
| |
| The generated `extension.json` contains the extension's metadata. |
| |
| ```json |
| { |
| "publisher": "my-org", |
| "name": "hello-world", |
| "displayName": "Hello World", |
| "version": "0.1.0", |
| "license": "Apache-2.0", |
| "permissions": ["can_read"] |
| } |
| ``` |
| |
| **Key fields:** |
| |
| - `publisher`: Organizational namespace for the extension |
| - `name`: Technical identifier (kebab-case) |
| - `displayName`: Human-readable name shown to users |
| - `permissions`: List of permissions the extension requires |
| |
| ## Step 4: Create Backend API |
| |
| The CLI generated a basic `backend/src/my_org/hello_world/entrypoint.py`. We'll create an API endpoint. |
| |
| **Create `backend/src/my_org/hello_world/api.py`** |
| |
| ```python |
| from flask import Response |
| from flask_appbuilder.api import expose, protect, safe |
| from superset_core.rest_api.api import RestApi |
| from superset_core.rest_api.decorators import api |
| |
| |
| @api( |
| id="hello_world_api", |
| name="Hello World API", |
| description="API endpoints for the Hello World extension" |
| ) |
| class HelloWorldAPI(RestApi): |
| openapi_spec_tag = "Hello World" |
| class_permission_name = "hello_world" |
| |
| @expose("/message", methods=("GET",)) |
| @protect() |
| @safe |
| def get_message(self) -> Response: |
| """Gets a hello world message |
| --- |
| get: |
| description: >- |
| Get a hello world message from the backend |
| responses: |
| 200: |
| description: Hello world message |
| content: |
| application/json: |
| schema: |
| type: object |
| properties: |
| result: |
| type: object |
| properties: |
| message: |
| type: string |
| 401: |
| $ref: '#/components/responses/401' |
| """ |
| return self.response( |
| 200, |
| result={"message": "Hello from the backend!"} |
| ) |
| ``` |
| |
| **Key points:** |
| |
| - Uses [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator with automatic context detection |
| - Extends `RestApi` from `superset_core.rest_api.api` |
| - Uses Flask-AppBuilder decorators (`@expose`, `@protect`, `@safe`) |
| - Returns responses using `self.response(status_code, result=data)` |
| - The endpoint will be accessible at `/extensions/my-org/hello-world/message` (automatic extension context) |
| - OpenAPI docstrings are crucial - Flask-AppBuilder uses them to automatically generate interactive API documentation at `/swagger/v1`, allowing developers to explore endpoints, understand schemas, and test the API directly from the browser |
| |
| **Update `backend/src/my_org/hello_world/entrypoint.py`** |
| |
| Replace the generated print statement with API import to trigger registration: |
| |
| ```python |
| # Importing the API class triggers the @api decorator registration |
| from .api import HelloWorldAPI # noqa: F401 |
| ``` |
| |
| The [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator automatically detects extension context and registers your API with proper namespacing. |
| |
| ## Step 5: Create Frontend Component |
| |
| The CLI generates the frontend configuration files. Below are the key configurations that enable Module Federation integration with Superset. |
| |
| **`frontend/package.json`** |
| |
| The `@apache-superset/core` package must be listed in both `peerDependencies` (to declare runtime compatibility) and `devDependencies` (to provide TypeScript types during build): |
| |
| ```json |
| { |
| "name": "@my-org/hello-world", |
| "version": "0.1.0", |
| "private": true, |
| "license": "Apache-2.0", |
| "scripts": { |
| "start": "webpack serve --mode development", |
| "build": "webpack --stats-error-details --mode production" |
| }, |
| "peerDependencies": { |
| "@apache-superset/core": "^x.x.x", |
| "react": "^x.x.x", |
| "react-dom": "^x.x.x" |
| }, |
| "devDependencies": { |
| "@apache-superset/core": "^x.x.x", |
| "@types/react": "^x.x.x", |
| "ts-loader": "^x.x.x", |
| "typescript": "^x.x.x", |
| "webpack": "^5.x.x", |
| "webpack-cli": "^x.x.x", |
| "webpack-dev-server": "^x.x.x" |
| } |
| } |
| ``` |
| |
| **`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. |
| |
| **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'); |
| const { ModuleFederationPlugin } = require('webpack').container; |
| const packageConfig = require('./package'); |
| const extensionConfig = require('../extension.json'); |
| |
| module.exports = (env, argv) => { |
| const isProd = argv.mode === 'production'; |
| |
| return { |
| entry: isProd ? {} : './src/index.tsx', |
| mode: isProd ? 'production' : 'development', |
| devServer: { |
| port: 3000, |
| headers: { |
| 'Access-Control-Allow-Origin': '*', |
| }, |
| }, |
| output: { |
| clean: true, |
| filename: isProd ? undefined : '[name].[contenthash].js', |
| chunkFilename: '[name].[contenthash].js', |
| path: path.resolve(__dirname, 'dist'), |
| publicPath: `/api/v1/extensions/${extensionConfig.publisher}/${extensionConfig.name}/`, |
| }, |
| resolve: { |
| extensions: ['.ts', '.tsx', '.js', '.jsx'], |
| }, |
| // Map @apache-superset/core imports to window.superset at runtime |
| externalsType: 'window', |
| externals: { |
| '@apache-superset/core': 'superset', |
| }, |
| module: { |
| rules: [ |
| { |
| test: /\.tsx?$/, |
| use: 'ts-loader', |
| exclude: /node_modules/, |
| }, |
| ], |
| }, |
| plugins: [ |
| new ModuleFederationPlugin({ |
| name: 'myOrg_helloWorld', |
| filename: 'remoteEntry.[contenthash].js', |
| exposes: { |
| './index': './src/index.tsx', |
| }, |
| shared: { |
| react: { |
| singleton: true, |
| requiredVersion: packageConfig.peerDependencies.react, |
| import: false, // Use host's React, don't bundle |
| }, |
| 'react-dom': { |
| singleton: true, |
| requiredVersion: packageConfig.peerDependencies['react-dom'], |
| import: false, |
| }, |
| antd: { |
| singleton: true, |
| requiredVersion: packageConfig.peerDependencies['antd'], |
| import: false, |
| }, |
| }, |
| }), |
| ], |
| }; |
| }; |
| ``` |
| |
| **`frontend/tsconfig.json`** |
| |
| ```json |
| { |
| "compilerOptions": { |
| "target": "es5", |
| "module": "esnext", |
| "moduleResolution": "node10", |
| "jsx": "react", |
| "strict": true, |
| "esModuleInterop": true, |
| "skipLibCheck": true, |
| "forceConsistentCasingInFileNames": true |
| }, |
| "include": ["src"] |
| } |
| ``` |
| |
| **Create `frontend/src/HelloWorldPanel.tsx`** |
| |
| Create a new file for the component implementation: |
| |
| ```tsx |
| import React, { useEffect, useState } from 'react'; |
| import { authentication } from '@apache-superset/core'; |
| |
| const HelloWorldPanel: React.FC = () => { |
| const [message, setMessage] = useState<string>(''); |
| const [loading, setLoading] = useState(true); |
| const [error, setError] = useState<string>(''); |
| |
| useEffect(() => { |
| const fetchMessage = async () => { |
| try { |
| const csrfToken = await authentication.getCSRFToken(); |
| const response = await fetch('/extensions/my-org/hello-world/message', { |
| method: 'GET', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'X-CSRFToken': csrfToken!, |
| }, |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`Server returned ${response.status}`); |
| } |
| |
| const data = await response.json(); |
| setMessage(data.result.message); |
| } catch (err) { |
| setError(err instanceof Error ? err.message : 'An error occurred'); |
| } finally { |
| setLoading(false); |
| } |
| }; |
| |
| fetchMessage(); |
| }, []); |
| |
| if (loading) { |
| return ( |
| <div style={{ padding: '20px', textAlign: 'center' }}> |
| <p>Loading...</p> |
| </div> |
| ); |
| } |
| |
| if (error) { |
| return ( |
| <div style={{ padding: '20px', color: 'red' }}> |
| <strong>Error:</strong> {error} |
| </div> |
| ); |
| } |
| |
| return ( |
| <div style={{ padding: '20px' }}> |
| <h3>Hello World Extension</h3> |
| <div |
| style={{ |
| padding: '16px', |
| backgroundColor: '#f6ffed', |
| border: '1px solid #b7eb8f', |
| borderRadius: '4px', |
| marginBottom: '16px', |
| }} |
| > |
| <strong>{message}</strong> |
| </div> |
| <p>This message was fetched from the backend API! 🎉</p> |
| </div> |
| ); |
| }; |
| |
| export default HelloWorldPanel; |
| ``` |
| |
| **Update `frontend/src/index.tsx`** |
| |
| 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 { views } from '@apache-superset/core'; |
| import HelloWorldPanel from './HelloWorldPanel'; |
| |
| views.registerView( |
| { id: 'my-org.hello-world.main', name: 'Hello World' }, |
| 'sqllab.panels', |
| () => <HelloWorldPanel />, |
| ); |
| ``` |
| |
| **Key patterns:** |
| |
| - `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 |
| |
| ## Step 6: Install Dependencies |
| |
| Install the frontend dependencies: |
| |
| ```bash |
| cd frontend |
| npm install |
| cd .. |
| ``` |
| |
| ## Step 7: Package the Extension |
| |
| Create a `.supx` bundle for deployment: |
| |
| ```bash |
| superset-extensions bundle |
| ``` |
| |
| This command automatically: |
| |
| - Builds frontend assets using Webpack with Module Federation |
| - Collects backend Python source files |
| - Creates a `dist/` directory with: |
| - `manifest.json` - Build metadata and asset references |
| - `frontend/dist/` - Built frontend assets (remoteEntry.js, chunks) |
| - `backend/` - Python source files |
| - Packages everything into `my-org.hello-world-0.1.0.supx` - a zip archive with the specific structure required by Superset |
| |
| ## Step 8: Deploy to Superset |
| |
| To deploy your extension, you need to enable extensions support and configure where Superset should load them from. |
| |
| **Configure Superset** |
| |
| Add the following to your `superset_config.py`: |
| |
| ```python |
| # Enable extensions feature |
| FEATURE_FLAGS = { |
| "ENABLE_EXTENSIONS": True, |
| } |
| |
| # Set the directory where extensions are stored |
| EXTENSIONS_PATH = "/path/to/extensions/folder" |
| ``` |
| |
| **Copy Extension Bundle** |
| |
| Copy your `.supx` file to the configured extensions path: |
| |
| ```bash |
| cp my-org.hello-world-0.1.0.supx /path/to/extensions/folder/ |
| ``` |
| |
| **Restart Superset** |
| |
| Restart your Superset instance to load the extension: |
| |
| ```bash |
| # Restart your Superset server |
| superset run |
| ``` |
| |
| Superset will extract and validate the extension metadata, load the assets, register the extension with its capabilities, and make it available for use. |
| |
| ## Step 9: Test Your Extension |
| |
| 1. **Open SQL Lab** in Superset |
| 2. Look for the **"Hello World"** panel in the panels dropdown or sidebar |
| 3. Open the panel - it should display "Hello from the backend!" |
| 4. Check that the message was fetched from your API endpoint |
| |
| ## Understanding the Flow |
| |
| Here's what happens when your extension loads: |
| |
| 1. **Superset starts**: Reads `manifest.json` from the `.supx` bundle and loads the backend entrypoint |
| 2. **Backend registration**: `entrypoint.py` imports your API class, triggering the [`@api`](superset-core/src/superset_core/rest_api/decorators.py) decorator to register it automatically |
| 3. **Frontend loads**: When SQL Lab opens, Superset fetches the remote entry file |
| 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**: The component fetches data from `/extensions/my-org/hello-world/message` |
| 8. **Backend response**: Your Flask API returns the hello world message |
| 9. **Display**: The component shows the message to the user |
| |
| ## Next Steps |
| |
| Now that you have a working extension, explore: |
| |
| - **[Development](./development)** - Project structure, APIs, and development workflow |
| - **[Contribution Types](./contribution-types)** - Other contribution points beyond panels |
| - **[Deployment](./deployment)** - Packaging and deploying your extension |
| - **[Security](./security)** - Security best practices for extensions |
| |
| For a complete real-world example, examine the query insights extension in the Superset codebase. |