mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
chore(extensions): unified contribution api and automatic prefixing (#38412)
This commit is contained in:
@@ -115,22 +115,51 @@ Backend contribution types allow extensions to extend Superset's server-side cap
|
||||
|
||||
### REST API Endpoints
|
||||
|
||||
Extensions can register custom REST API endpoints under the `/api/v1/extensions/` namespace. This dedicated namespace prevents conflicts with built-in endpoints and provides a clear separation between core and extension functionality.
|
||||
|
||||
```json
|
||||
"backend": {
|
||||
"entryPoints": ["my_extension.entrypoint"],
|
||||
"files": ["backend/src/my_extension/**/*.py"]
|
||||
}
|
||||
```
|
||||
|
||||
The entry point module registers the API with Superset:
|
||||
Extensions can register custom REST API endpoints under the `/extensions/` namespace. This dedicated namespace prevents conflicts with built-in endpoints and provides a clear separation between core and extension functionality.
|
||||
|
||||
```python
|
||||
from superset_core.api.rest_api import add_extension_api
|
||||
from .api import MyExtensionAPI
|
||||
from superset_core.api.rest_api import RestApi, api
|
||||
from flask_appbuilder.api import expose, protect
|
||||
|
||||
add_extension_api(MyExtensionAPI)
|
||||
@api(
|
||||
id="my_extension_api",
|
||||
name="My Extension API",
|
||||
description="Custom API endpoints for my extension"
|
||||
)
|
||||
class MyExtensionAPI(RestApi):
|
||||
@expose("/hello", methods=("GET",))
|
||||
@protect()
|
||||
def hello(self) -> Response:
|
||||
return self.response(200, result={"message": "Hello from extension!"})
|
||||
|
||||
# Import the class in entrypoint.py to register it
|
||||
from .api import MyExtensionAPI
|
||||
```
|
||||
|
||||
**Note**: The [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator automatically detects context and generates appropriate paths:
|
||||
|
||||
- **Extension context**: `/extensions/{publisher}/{name}/` with ID prefixed as `extensions.{publisher}.{name}.{id}`
|
||||
- **Host context**: `/api/v1/` with original ID
|
||||
|
||||
For an extension with publisher `my-org` and name `dataset-tools`, the endpoint above would be accessible at:
|
||||
```
|
||||
/extensions/my-org/dataset-tools/hello
|
||||
```
|
||||
|
||||
You can also specify a `resource_name` parameter to add an additional path segment:
|
||||
|
||||
```python
|
||||
@api(
|
||||
id="analytics_api",
|
||||
name="Analytics API",
|
||||
resource_name="analytics" # Adds /analytics to the path
|
||||
)
|
||||
class AnalyticsAPI(RestApi):
|
||||
@expose("/insights", methods=("GET",))
|
||||
def insights(self):
|
||||
# This endpoint will be available at:
|
||||
# /extensions/my-org/dataset-tools/analytics/insights
|
||||
return self.response(200, result={"insights": []})
|
||||
```
|
||||
|
||||
### MCP Tools and Prompts
|
||||
|
||||
@@ -203,32 +203,52 @@ Extension endpoints are registered under a dedicated `/extensions` namespace to
|
||||
```python
|
||||
from superset_core.api.models import Database, get_session
|
||||
from superset_core.api.daos import DatabaseDAO
|
||||
from superset_core.api.rest_api import add_extension_api
|
||||
from .api import DatasetReferencesAPI
|
||||
from superset_core.api.rest_api import RestApi, api
|
||||
from flask_appbuilder.api import expose, protect
|
||||
|
||||
# Register a new extension REST API
|
||||
add_extension_api(DatasetReferencesAPI)
|
||||
|
||||
# Fetch Superset entities via the DAO to apply base filters that filter out entities
|
||||
# that the user doesn't have access to
|
||||
databases = DatabaseDAO.find_all()
|
||||
|
||||
# ..or apply simple filters on top of base filters
|
||||
databases = DatabaseDAO.filter_by(uuid=database.uuid)
|
||||
if not databases:
|
||||
raise Exception("Database not found")
|
||||
|
||||
return databases[0]
|
||||
|
||||
# Perform complex queries using SQLAlchemy Query, also filtering out
|
||||
# inaccessible entities
|
||||
session = get_session()
|
||||
databases_query = session.query(Database).filter(
|
||||
Database.database_name.ilike("%abc%")
|
||||
@api(
|
||||
id="dataset_references_api",
|
||||
name="Dataset References API",
|
||||
description="API for managing dataset references"
|
||||
)
|
||||
return DatabaseDAO.query(databases_query)
|
||||
class DatasetReferencesAPI(RestApi):
|
||||
@expose("/datasets", methods=("GET",))
|
||||
@protect()
|
||||
def get_datasets(self) -> Response:
|
||||
"""Get all accessible datasets."""
|
||||
# Fetch Superset entities via the DAO to apply base filters that filter out entities
|
||||
# that the user doesn't have access to
|
||||
databases = DatabaseDAO.find_all()
|
||||
|
||||
# ..or apply simple filters on top of base filters
|
||||
databases = DatabaseDAO.filter_by(uuid=database.uuid)
|
||||
if not databases:
|
||||
raise Exception("Database not found")
|
||||
|
||||
return self.response(200, result={"databases": databases})
|
||||
|
||||
@expose("/search", methods=("GET",))
|
||||
@protect()
|
||||
def search_databases(self) -> Response:
|
||||
"""Search databases with complex queries."""
|
||||
# Perform complex queries using SQLAlchemy Query, also filtering out
|
||||
# inaccessible entities
|
||||
session = get_session()
|
||||
databases_query = session.query(Database).filter(
|
||||
Database.database_name.ilike("%abc%")
|
||||
)
|
||||
databases = DatabaseDAO.query(databases_query)
|
||||
|
||||
return self.response(200, result={"databases": databases})
|
||||
```
|
||||
|
||||
### Automatic Context Detection
|
||||
|
||||
The [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator automatically detects whether it's being used in host or extension code:
|
||||
|
||||
- **Extension APIs**: Registered under `/extensions/{publisher}/{name}/` with IDs prefixed as `extensions.{publisher}.{name}.{id}`
|
||||
- **Host APIs**: Registered under `/api/v1/` with original IDs
|
||||
|
||||
In the future, we plan to expand the backend APIs to support configuring security models, database engines, SQL Alchemy dialects, etc.
|
||||
|
||||
## Development Mode
|
||||
|
||||
@@ -38,7 +38,7 @@ Extensions can provide:
|
||||
|
||||
- **Custom UI Components**: New panels, views, and interactive elements
|
||||
- **Commands and Menus**: Custom actions accessible via menus and keyboard shortcuts
|
||||
- **REST API Endpoints**: Backend services under the `/api/v1/extensions/` namespace
|
||||
- **REST API Endpoints**: Backend services under the `/extensions/` namespace
|
||||
- **MCP Tools and Prompts**: AI agent capabilities for enhanced user assistance
|
||||
|
||||
## UI Components for Extensions
|
||||
|
||||
@@ -129,11 +129,15 @@ The CLI generated a basic `backend/src/superset_extensions/my_org/hello_world/en
|
||||
```python
|
||||
from flask import Response
|
||||
from flask_appbuilder.api import expose, protect, safe
|
||||
from superset_core.api.rest_api import RestApi
|
||||
from superset_core.api.rest_api import RestApi, api
|
||||
|
||||
|
||||
@api(
|
||||
id="hello_world_api",
|
||||
name="Hello World API",
|
||||
description="API endpoints for the Hello World extension"
|
||||
)
|
||||
class HelloWorldAPI(RestApi):
|
||||
resource_name = "hello_world"
|
||||
openapi_spec_tag = "Hello World"
|
||||
class_permission_name = "hello_world"
|
||||
|
||||
@@ -170,25 +174,25 @@ class HelloWorldAPI(RestApi):
|
||||
|
||||
**Key points:**
|
||||
|
||||
- Extends `RestApi` from `superset_core.api.types.rest_api`
|
||||
- Uses [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator with automatic context detection
|
||||
- Extends `RestApi` from `superset_core.api.rest_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`
|
||||
- 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/superset_extensions/my_org/hello_world/entrypoint.py`**
|
||||
|
||||
Replace the generated print statement with API registration:
|
||||
Replace the generated print statement with API import to trigger registration:
|
||||
|
||||
```python
|
||||
from superset_core.api import rest_api
|
||||
|
||||
# Importing the API class triggers the @api decorator registration
|
||||
from .api import HelloWorldAPI
|
||||
|
||||
rest_api.add_extension_api(HelloWorldAPI)
|
||||
print("Hello World extension loaded successfully!")
|
||||
```
|
||||
|
||||
This registers your API with Superset when the extension loads.
|
||||
The [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator automatically detects extension context and registers your API with proper namespacing.
|
||||
|
||||
## Step 5: Create Frontend Component
|
||||
|
||||
@@ -328,16 +332,16 @@ const HelloWorldPanel: React.FC = () => {
|
||||
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!,
|
||||
},
|
||||
});
|
||||
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}`);
|
||||
@@ -493,7 +497,7 @@ 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 the backend entrypoint
|
||||
2. **Backend registration**: `entrypoint.py` registers your API via `rest_api.add_extension_api()`
|
||||
2. **Backend registration**: `entrypoint.py` imports your API class, triggering the [`@api`](superset-core/src/superset_core/api/rest_api.py:59) 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
|
||||
|
||||
Reference in New Issue
Block a user