mirror of
https://github.com/apache/superset.git
synced 2026-04-30 05:24:31 +00:00
Compare commits
68 Commits
fdf19db5e6
...
fix/values
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e3466862a | ||
|
|
7704d4c811 | ||
|
|
82087d2fb5 | ||
|
|
b2c10f909b | ||
|
|
9215eb5e45 | ||
|
|
fe7f220c21 | ||
|
|
3bb9704cd5 | ||
|
|
eb77452857 | ||
|
|
6d7cfac8b2 | ||
|
|
31754a39c9 | ||
|
|
bde48e563e | ||
|
|
0cfd760a36 | ||
|
|
13fe88000a | ||
|
|
cc8ad23d6f | ||
|
|
5c2cbb58bc | ||
|
|
6342c4f338 | ||
|
|
5fa70bdbd8 | ||
|
|
2a876e8b86 | ||
|
|
0533ca9941 | ||
|
|
5f20d2e15a | ||
|
|
6d1d5d64d1 | ||
|
|
06d6b513cd | ||
|
|
afa51125de | ||
|
|
26c07b1ffb | ||
|
|
9ecca47e69 | ||
|
|
6c1df93215 | ||
|
|
06fd0658ae | ||
|
|
a17f38a4e2 | ||
|
|
6ef4794778 | ||
|
|
4cd3ce164d | ||
|
|
8e3e57c1c8 | ||
|
|
61fbfda501 | ||
|
|
9017b9a74f | ||
|
|
bc99b710bd | ||
|
|
bf55f1e438 | ||
|
|
dca41f9a7b | ||
|
|
62cebc8a0e | ||
|
|
e70c7944b7 | ||
|
|
577654cd02 | ||
|
|
c7a1f57487 | ||
|
|
9983e255f8 | ||
|
|
d9a91f99db | ||
|
|
60577bcd97 | ||
|
|
3cb00bf116 | ||
|
|
a6c0d6321f | ||
|
|
5fb9e17721 | ||
|
|
03ad1789f0 | ||
|
|
296bd7e56b | ||
|
|
5c4bf0f6ea | ||
|
|
db7665c0bc | ||
|
|
84a53eab31 | ||
|
|
3609cd9544 | ||
|
|
7d2efd8c1a | ||
|
|
0d5ade6dd3 | ||
|
|
17df85b5ed | ||
|
|
664c465d80 | ||
|
|
884db9347d | ||
|
|
6c359733e1 | ||
|
|
357e35dc62 | ||
|
|
5f0efd2be9 | ||
|
|
0dbd4c5b90 | ||
|
|
f0416eff78 | ||
|
|
a513406239 | ||
|
|
f6f734f0d1 | ||
|
|
a2c23a2a58 | ||
|
|
20cc3345d8 | ||
|
|
880cab58c3 | ||
|
|
4dfb0e66cb |
@@ -24,7 +24,9 @@ notifications:
|
||||
discussions: notifications@superset.apache.org
|
||||
|
||||
github:
|
||||
del_branch_on_merge: true
|
||||
pull_requests:
|
||||
del_branch_on_merge: true
|
||||
allow_update_branch: true
|
||||
description: "Apache Superset is a Data Visualization and Data Exploration Platform"
|
||||
homepage: https://superset.apache.org/
|
||||
labels:
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
# Scan for vulnerabilities in built container image after pushes to mainline branch.
|
||||
- name: Run Trivy container image vulnerabity scan
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'lean'
|
||||
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # v0.34.2
|
||||
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
|
||||
with:
|
||||
image-ref: ${{ env.IMAGE_TAG }}
|
||||
format: 'sarif'
|
||||
|
||||
10
.github/workflows/pre-commit.yml
vendored
10
.github/workflows/pre-commit.yml
vendored
@@ -64,11 +64,17 @@ jobs:
|
||||
restore-keys: |
|
||||
pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
uses: ./.github/actions/file-changes-action
|
||||
with:
|
||||
output: ' '
|
||||
|
||||
- name: pre-commit
|
||||
run: |
|
||||
set +e # Don't exit immediately on failure
|
||||
export SKIP=eslint-frontend,type-checking-frontend
|
||||
pre-commit run --all-files
|
||||
export SKIP=type-checking-frontend
|
||||
pre-commit run --files ${{ steps.changed_files.outputs.files }}
|
||||
PRE_COMMIT_EXIT_CODE=$?
|
||||
git diff --quiet --exit-code
|
||||
GIT_DIFF_EXIT_CODE=$?
|
||||
|
||||
@@ -625,6 +625,9 @@ categories:
|
||||
- name: Stockarea
|
||||
url: https://stockarea.io
|
||||
|
||||
- name: VTG
|
||||
url: https://www.vtg.de
|
||||
|
||||
Sports:
|
||||
- name: Club 25 de Agosto (Femenino / Women's Team)
|
||||
url: https://www.instagram.com/25deagosto.basketfemenino/
|
||||
|
||||
@@ -1 +1 @@
|
||||
v20.20.0
|
||||
v22.22.0
|
||||
|
||||
@@ -233,6 +233,20 @@ def alert_dynamic_minimal_interval(**kwargs) -> int:
|
||||
ALERT_MINIMUM_INTERVAL = alert_dynamic_minimal_interval
|
||||
```
|
||||
|
||||
## External Link Redirection
|
||||
|
||||
For security, Superset rewrites external links in alert/report email HTML so
|
||||
they go through a warning page before the user is navigated to the external
|
||||
site. Internal links (matching your configured base URL) are not affected.
|
||||
|
||||
```python
|
||||
# Disable external link redirection entirely (default: True)
|
||||
ALERT_REPORTS_ENABLE_LINK_REDIRECT = False
|
||||
```
|
||||
|
||||
The feature uses `WEBDRIVER_BASEURL_USER_FRIENDLY` (or `WEBDRIVER_BASEURL`)
|
||||
to determine which hosts are internal.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
There are many reasons that reports might not be working. Try these steps to check for specific issues.
|
||||
|
||||
@@ -33,13 +33,15 @@ The extension architecture is built on six core principles that guide all techni
|
||||
### 1. Lean Core
|
||||
|
||||
Superset's core should remain minimal, with many features delegated to extensions. Built-in features use the same APIs and extension mechanisms available to external developers. This approach:
|
||||
|
||||
- Reduces maintenance burden and complexity
|
||||
- Encourages modularity
|
||||
- Allows the community to innovate independently of the main codebase
|
||||
|
||||
### 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. 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:
|
||||
All extension points are clearly defined and documented. Extension authors know exactly where and how they can interact with the host system. Both backend and frontend contributions are registered directly in code — backend contributions via classes decorated with `@api` (and other decorators) imported from the auto-discovered entrypoint, frontend contributions via calls like `views.registerView` and `commands.registerCommand` executed at module load time in `index.tsx`. This gives the host clear visibility into what each extension provides:
|
||||
|
||||
- Manage the extension lifecycle
|
||||
- Provide a consistent user experience
|
||||
- Validate extension compatibility
|
||||
@@ -47,6 +49,7 @@ All extension points are clearly defined and documented. Extension authors know
|
||||
### 3. Versioned and Stable APIs
|
||||
|
||||
Public interfaces for extensions follow semantic versioning, allowing for:
|
||||
|
||||
- Safe evolution of the platform
|
||||
- Backward compatibility
|
||||
- Clear upgrade paths for extension authors
|
||||
@@ -54,6 +57,7 @@ Public interfaces for extensions follow semantic versioning, allowing for:
|
||||
### 4. Lazy Loading and Activation
|
||||
|
||||
Extensions are loaded and activated only when needed, which:
|
||||
|
||||
- Minimizes performance overhead
|
||||
- Reduces resource consumption
|
||||
- Improves startup time
|
||||
@@ -61,6 +65,7 @@ Extensions are loaded and activated only when needed, which:
|
||||
### 5. Composability and Reuse
|
||||
|
||||
The architecture encourages reusing extension points and patterns across different modules, promoting:
|
||||
|
||||
- Consistency across extensions
|
||||
- Reduced duplication
|
||||
- Shared best practices
|
||||
@@ -80,6 +85,7 @@ Two core packages provide the foundation for extension development:
|
||||
**Frontend: `@apache-superset/core`**
|
||||
|
||||
This package provides essential building blocks for frontend extensions and the host application:
|
||||
|
||||
- Shared UI components
|
||||
- Utility functions
|
||||
- APIs and hooks
|
||||
@@ -90,6 +96,7 @@ By centralizing these resources, both extensions and built-in features use the s
|
||||
**Backend: `apache-superset-core`**
|
||||
|
||||
This package exposes key classes and APIs for backend extensions:
|
||||
|
||||
- Database connectors
|
||||
- API extensions
|
||||
- Security manager customization
|
||||
@@ -102,6 +109,7 @@ It includes dependencies on critical libraries like Flask-AppBuilder and SQLAlch
|
||||
**`apache-superset-extensions-cli`**
|
||||
|
||||
The CLI provides comprehensive commands for extension development:
|
||||
|
||||
- Project scaffolding
|
||||
- Code generation
|
||||
- Building and bundling
|
||||
@@ -114,6 +122,7 @@ By standardizing these processes, the CLI ensures extensions are built consisten
|
||||
The Superset host application serves as the runtime environment for extensions:
|
||||
|
||||
**Extension Management**
|
||||
|
||||
- Exposes `/api/v1/extensions` endpoint for registration and management
|
||||
- Provides a dedicated UI for managing extensions
|
||||
- Stores extension metadata in the `extensions` database table
|
||||
@@ -121,6 +130,7 @@ The Superset host application serves as the runtime environment for extensions:
|
||||
**Extension Storage**
|
||||
|
||||
The extensions table contains:
|
||||
|
||||
- Extension name, version, and author
|
||||
- Metadata and configuration
|
||||
- Built frontend and/or backend code
|
||||
@@ -132,6 +142,7 @@ The following diagram illustrates how these components work together:
|
||||
<img width="955" height="586" alt="Extension System Architecture" src="https://github.com/user-attachments/assets/cc2a41df-55a4-48c8-b056-35f7a1e567c6" />
|
||||
|
||||
The diagram shows:
|
||||
|
||||
1. **Extension projects** depend on core packages for development
|
||||
2. **Core packages** provide APIs and type definitions
|
||||
3. **The host application** implements the APIs and manages extensions
|
||||
@@ -151,23 +162,25 @@ The architecture leverages Webpack's Module Federation to enable dynamic loading
|
||||
|
||||
Extensions configure Webpack to expose their entry points:
|
||||
|
||||
``` typescript
|
||||
new ModuleFederationPlugin({
|
||||
name: 'my_extension',
|
||||
filename: 'remoteEntry.[contenthash].js',
|
||||
exposes: {
|
||||
'./index': './src/index.tsx',
|
||||
},
|
||||
externalsType: 'window',
|
||||
externals: {
|
||||
'@apache-superset/core': 'superset',
|
||||
},
|
||||
shared: {
|
||||
react: { singleton: true },
|
||||
'react-dom': { singleton: true },
|
||||
'antd-v5': { singleton: true }
|
||||
}
|
||||
})
|
||||
```javascript
|
||||
externalsType: 'window',
|
||||
externals: {
|
||||
'@apache-superset/core': 'superset',
|
||||
},
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: 'my_extension',
|
||||
filename: 'remoteEntry.[contenthash].js',
|
||||
exposes: {
|
||||
'./index': './src/index.tsx',
|
||||
},
|
||||
shared: {
|
||||
react: { singleton: true, import: false },
|
||||
'react-dom': { singleton: true, import: false },
|
||||
antd: { singleton: true, import: false },
|
||||
},
|
||||
}),
|
||||
]
|
||||
```
|
||||
|
||||
This configuration does several important things:
|
||||
@@ -195,24 +208,12 @@ Here's what happens at runtime:
|
||||
|
||||
On the Superset side, the APIs are mapped to `window.superset` during application bootstrap:
|
||||
|
||||
``` typescript
|
||||
```typescript
|
||||
import * as supersetCore from '@apache-superset/core';
|
||||
import {
|
||||
authentication,
|
||||
core,
|
||||
commands,
|
||||
extensions,
|
||||
sqlLab,
|
||||
} from 'src/extensions';
|
||||
|
||||
export default function setupExtensionsAPI() {
|
||||
window.superset = {
|
||||
...supersetCore,
|
||||
authentication,
|
||||
core,
|
||||
commands,
|
||||
extensions,
|
||||
sqlLab,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -23,7 +23,7 @@ sidebar_label: Alert
|
||||
-->
|
||||
|
||||
import { StoryWithControls } from '../../../src/components/StorybookWrapper';
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
|
||||
# Alert
|
||||
|
||||
@@ -105,10 +105,10 @@ function Demo() {
|
||||
|
||||
## Usage in Extensions
|
||||
|
||||
This component is available in the `@apache-superset/core/ui` package, which is automatically available to Superset extensions.
|
||||
This component is available in the `@apache-superset/core/components` package, which is automatically available to Superset extensions.
|
||||
|
||||
```tsx
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
|
||||
function MyExtension() {
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,7 @@ sidebar_position: 1
|
||||
|
||||
# Extension Components
|
||||
|
||||
These UI components are available to Superset extension developers through the `@apache-superset/core/ui` package. They provide a consistent look and feel with the rest of Superset and are designed to be used in extension panels, views, and other UI elements.
|
||||
These UI components are available to Superset extension developers through the `@apache-superset/core/components` package. They provide a consistent look and feel with the rest of Superset and are designed to be used in extension panels, views, and other UI elements.
|
||||
|
||||
## Available Components
|
||||
|
||||
@@ -33,10 +33,10 @@ These UI components are available to Superset extension developers through the `
|
||||
|
||||
## Usage
|
||||
|
||||
All components are exported from the `@apache-superset/core/ui` package:
|
||||
All components are exported from the `@apache-superset/core/components` package:
|
||||
|
||||
```tsx
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
|
||||
export function MyExtensionPanel() {
|
||||
return (
|
||||
@@ -49,7 +49,7 @@ export function MyExtensionPanel() {
|
||||
|
||||
## Adding New Components
|
||||
|
||||
Components in `@apache-superset/core/ui` are automatically documented here. To add a new extension component:
|
||||
Components in `@apache-superset/core/components` are automatically documented here. To add a new extension component:
|
||||
|
||||
1. Add the component to `superset-frontend/packages/superset-core/src/ui/components/`
|
||||
2. Export it from `superset-frontend/packages/superset-core/src/ui/components/index.ts`
|
||||
|
||||
@@ -28,7 +28,7 @@ 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 contributions are registered directly in code from your extension's `index.tsx` entry point — they do not need to be declared in `extension.json`.
|
||||
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.
|
||||
|
||||
### Views
|
||||
|
||||
@@ -68,25 +68,28 @@ commands.registerCommand(
|
||||
|
||||
### Menus
|
||||
|
||||
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).
|
||||
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 view and command to execute, the target area, and the location (`primary`, `secondary`, or `context`). Menu contribution areas are uniquely identified (e.g., `sqllab.editor` for the SQL Lab editor).
|
||||
|
||||
```typescript
|
||||
import { menus } from '@apache-superset/core';
|
||||
|
||||
menus.addMenuItem('sqllab.editor', {
|
||||
placement: 'primary',
|
||||
command: 'my-extension.copy-query',
|
||||
});
|
||||
menus.registerMenuItem(
|
||||
{ view: 'sqllab.editor', command: 'my-extension.copy-query' },
|
||||
'sqllab.editor',
|
||||
'primary',
|
||||
);
|
||||
|
||||
menus.addMenuItem('sqllab.editor', {
|
||||
placement: 'secondary',
|
||||
command: 'my-extension.prettify',
|
||||
});
|
||||
menus.registerMenuItem(
|
||||
{ view: 'sqllab.editor', command: 'my-extension.prettify' },
|
||||
'sqllab.editor',
|
||||
'secondary',
|
||||
);
|
||||
|
||||
menus.addMenuItem('sqllab.editor', {
|
||||
placement: 'context',
|
||||
command: 'my-extension.clear',
|
||||
});
|
||||
menus.registerMenuItem(
|
||||
{ view: 'sqllab.editor', command: 'my-extension.clear' },
|
||||
'sqllab.editor',
|
||||
'context',
|
||||
);
|
||||
```
|
||||
|
||||
### Editors
|
||||
@@ -111,24 +114,31 @@ See [Editors Extension Point](./extension-points/editors) for implementation det
|
||||
|
||||
## Backend
|
||||
|
||||
Backend contribution types allow extensions to extend Superset's server-side capabilities with new API endpoints, MCP tools, and MCP prompts.
|
||||
Backend contribution types allow extensions to extend Superset's server-side capabilities. Backend contributions are registered at startup via classes and functions imported from the auto-discovered `entrypoint.py` file.
|
||||
|
||||
### REST API Endpoints
|
||||
|
||||
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 RestApi, api
|
||||
from flask_appbuilder.api import expose, protect
|
||||
from flask import Response
|
||||
from flask_appbuilder.api import expose, permission_name, protect, safe
|
||||
from superset_core.rest_api.api import RestApi
|
||||
from superset_core.rest_api.decorators import api
|
||||
|
||||
@api(
|
||||
id="my_extension_api",
|
||||
name="My Extension API",
|
||||
description="Custom API endpoints for my extension"
|
||||
description="Custom API endpoints for my extension",
|
||||
)
|
||||
class MyExtensionAPI(RestApi):
|
||||
openapi_spec_tag = "My Extension"
|
||||
class_permission_name = "my_extension_api"
|
||||
|
||||
@expose("/hello", methods=("GET",))
|
||||
@protect()
|
||||
@safe
|
||||
@permission_name("read")
|
||||
def hello(self) -> Response:
|
||||
return self.response(200, result={"message": "Hello from extension!"})
|
||||
|
||||
@@ -136,7 +146,7 @@ class MyExtensionAPI(RestApi):
|
||||
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:
|
||||
**Note**: The [`@api`](superset-core/src/superset_core/rest_api/decorators.py) 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
|
||||
@@ -152,16 +162,65 @@ You can also specify a `resource_name` parameter to add an additional path segme
|
||||
@api(
|
||||
id="analytics_api",
|
||||
name="Analytics API",
|
||||
resource_name="analytics" # Adds /analytics to the path
|
||||
resource_name="analytics", # Adds /analytics to the path
|
||||
)
|
||||
class AnalyticsAPI(RestApi):
|
||||
|
||||
@expose("/insights", methods=("GET",))
|
||||
def insights(self):
|
||||
@protect()
|
||||
@safe
|
||||
@permission_name("read")
|
||||
def insights(self) -> Response:
|
||||
# This endpoint will be available at:
|
||||
# /extensions/my-org/dataset-tools/analytics/insights
|
||||
return self.response(200, result={"insights": []})
|
||||
```
|
||||
|
||||
### MCP Tools and Prompts
|
||||
### MCP Tools
|
||||
|
||||
Extensions can contribute Model Context Protocol (MCP) tools and prompts that AI agents can discover and use. See [MCP Integration](./mcp) for detailed documentation.
|
||||
Extensions can register Python functions as MCP tools that AI agents can discover and call. Tools provide executable functionality such as data processing, custom analytics, or integration with external services. Each tool specifies a unique name and an optional description that helps AI agents decide when to use it.
|
||||
|
||||
```python
|
||||
from superset_core.mcp.decorators import tool
|
||||
|
||||
@tool(
|
||||
name="my-extension.get_summary",
|
||||
description="Get a summary of recent query activity",
|
||||
tags=["analytics", "queries"],
|
||||
)
|
||||
def get_summary() -> dict:
|
||||
"""Returns a summary of recent query activity."""
|
||||
return {"status": "success", "result": {"queries_today": 42}}
|
||||
```
|
||||
|
||||
See [MCP Integration](./mcp) for implementation details.
|
||||
|
||||
### MCP Prompts
|
||||
|
||||
Extensions can register MCP prompts that provide interactive guidance and context to AI agents. Prompts help agents understand domain-specific workflows, best practices, or troubleshooting steps for your extension's use cases.
|
||||
|
||||
```python
|
||||
from superset_core.mcp.decorators import prompt
|
||||
from fastmcp import Context
|
||||
|
||||
@prompt(
|
||||
"my-extension.analysis_guide",
|
||||
title="Analysis Guide",
|
||||
description="Step-by-step guidance for data analysis workflows",
|
||||
)
|
||||
async def analysis_guide(ctx: Context) -> str:
|
||||
"""Provides guidance for data analysis workflows."""
|
||||
return """
|
||||
# Data Analysis Guide
|
||||
|
||||
Follow these steps for effective analysis:
|
||||
|
||||
1. **Explore your data** - Review available datasets and schema
|
||||
2. **Build your query** - Use SQL Lab to craft and test queries
|
||||
3. **Visualize results** - Choose the right chart type for your data
|
||||
|
||||
What would you like to analyze today?
|
||||
"""
|
||||
```
|
||||
|
||||
See [MCP Integration](./mcp) for implementation details.
|
||||
|
||||
@@ -70,8 +70,8 @@ import { someInternalFunction } from 'src/explore/components/SomeComponent';
|
||||
|
||||
```python
|
||||
# ✅ Public API - stable
|
||||
from superset_core.api.models import Database
|
||||
from superset_core.api.daos import DatabaseDAO
|
||||
from superset_core.common.models import Database
|
||||
from superset_core.common.daos import DatabaseDAO
|
||||
|
||||
# ❌ Internal code - may break without notice
|
||||
from superset.views.core import SomeInternalClass
|
||||
@@ -117,7 +117,7 @@ Extension developers should depend on and use core libraries directly:
|
||||
|
||||
**Frontend (examples):**
|
||||
- [React](https://react.dev/) - UI framework
|
||||
- [Ant Design](https://ant.design/) - UI component library (prefer Superset components from `@apache-superset/core/ui` when available to preserve visual consistency)
|
||||
- [Ant Design](https://ant.design/) - UI component library (prefer Superset components from `@apache-superset/core/components` when available to preserve visual consistency)
|
||||
- [Emotion](https://emotion.sh/) - CSS-in-JS styling
|
||||
- ...
|
||||
|
||||
|
||||
@@ -38,12 +38,14 @@ superset-extensions build: Builds extension assets.
|
||||
superset-extensions bundle: Packages the extension into a .supx file.
|
||||
|
||||
superset-extensions dev: Automatically rebuilds the extension as files change.
|
||||
|
||||
superset-extensions validate: Validates the extension structure and metadata.
|
||||
```
|
||||
|
||||
When creating a new extension with `superset-extensions init`, the CLI generates a standardized folder structure:
|
||||
|
||||
```
|
||||
dataset-references/
|
||||
my-org.dataset-references/
|
||||
├── extension.json
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
@@ -52,9 +54,10 @@ dataset-references/
|
||||
│ └── package.json
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ └── superset_extensions/
|
||||
│ │ └── my_org/
|
||||
│ │ └── dataset_references/
|
||||
│ ├── tests/
|
||||
│ │ ├── api.py
|
||||
│ │ └── entrypoint.py
|
||||
│ ├── pyproject.toml
|
||||
│ └── requirements.txt
|
||||
├── dist/
|
||||
@@ -64,20 +67,20 @@ dataset-references/
|
||||
│ │ ├── remoteEntry.d7a9225d042e4ccb6354.js
|
||||
│ │ └── 900.038b20cdff6d49cfa8d9.js
|
||||
│ └── backend
|
||||
│ └── superset_extensions/
|
||||
│ └── my_org/
|
||||
│ └── dataset_references/
|
||||
│ ├── __init__.py
|
||||
│ ├── api.py
|
||||
│ └── entrypoint.py
|
||||
├── dataset-references-1.0.0.supx
|
||||
├── my-org.dataset-references-1.0.0.supx
|
||||
└── README.md
|
||||
```
|
||||
|
||||
**Note**: The extension ID (`dataset-references`) serves as the basis for all technical names:
|
||||
- Directory name: `dataset-references` (kebab-case)
|
||||
- Backend Python package: `dataset_references` (snake_case)
|
||||
- Frontend package name: `dataset-references` (kebab-case)
|
||||
- Module Federation name: `datasetReferences` (camelCase)
|
||||
**Note**: With publisher `my-org` and name `dataset-references`, the technical names are:
|
||||
- Directory name: `my-org.dataset-references` (kebab-case)
|
||||
- Backend Python namespace: `my_org.dataset_references`
|
||||
- Backend distribution package: `my_org-dataset_references`
|
||||
- Frontend package name: `@my-org/dataset-references` (scoped)
|
||||
- Module Federation name: `myOrg_datasetReferences` (camelCase)
|
||||
|
||||
The `extension.json` file serves as the declared metadata for the extension, containing the extension's name, version, author, description, and a list of capabilities. This file is essential for the host application to understand how to load and manage the extension.
|
||||
|
||||
@@ -108,7 +111,7 @@ The `extension.json` file contains the metadata necessary for the host applicati
|
||||
|
||||
Extensions use standardized entry point locations:
|
||||
|
||||
- **Backend**: `backend/src/superset_extensions/{publisher}/{name}/entrypoint.py`
|
||||
- **Backend**: `backend/src/{publisher}/{name}/entrypoint.py`
|
||||
- **Frontend**: `frontend/src/index.tsx`
|
||||
|
||||
### Build Configuration
|
||||
@@ -124,7 +127,7 @@ license = "Apache-2.0"
|
||||
[tool.apache_superset_extensions.build]
|
||||
# Files to include in the extension build/bundle
|
||||
include = [
|
||||
"src/superset_extensions/my_org/dataset_references/**/*.py",
|
||||
"src/my_org/dataset_references/**/*.py",
|
||||
]
|
||||
exclude = []
|
||||
```
|
||||
@@ -201,9 +204,10 @@ Backend APIs (via `apache-superset-core`) follow a similar pattern, providing ac
|
||||
Extension endpoints are registered under a dedicated `/extensions` namespace to avoid conflicting with built-in endpoints and also because they don't share the same version constraints. By grouping all extension endpoints under `/extensions`, Superset establishes a clear boundary between core and extension functionality, making it easier to manage, document, and secure both types of APIs.
|
||||
|
||||
```python
|
||||
from superset_core.api.models import Database, get_session
|
||||
from superset_core.api.daos import DatabaseDAO
|
||||
from superset_core.api.rest_api import RestApi, api
|
||||
from superset_core.common.models import Database, get_session
|
||||
from superset_core.common.daos import DatabaseDAO
|
||||
from superset_core.rest_api.api import RestApi
|
||||
from superset_core.rest_api.decorators import api
|
||||
from flask_appbuilder.api import expose, protect
|
||||
|
||||
@api(
|
||||
@@ -244,7 +248,7 @@ class DatasetReferencesAPI(RestApi):
|
||||
|
||||
### 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:
|
||||
The [`@api`](superset-core/src/superset_core/rest_api/decorators.py) 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
|
||||
@@ -262,7 +266,7 @@ LOCAL_EXTENSIONS = [
|
||||
]
|
||||
```
|
||||
|
||||
This instructs Superset to load and serve extensions directly from disk, so you can iterate quickly. Running `superset-extensions dev` watches for file changes and rebuilds assets automatically, while the Webpack development server (started separately with `npm run dev-server`) serves updated files as soon as they're modified. This enables immediate feedback for React components, styles, and other frontend code. Changes to backend files are also detected automatically and immediately synced, ensuring that both frontend and backend updates are reflected in your development environment.
|
||||
This instructs Superset to load and serve extensions directly from disk, so you can iterate quickly. Running `superset-extensions dev` watches for file changes and rebuilds assets automatically, while the Webpack development server (started separately with `npm run start`) serves updated files as soon as they're modified. This enables immediate feedback for React components, styles, and other frontend code. Changes to backend files are also detected automatically and immediately synced, ensuring that both frontend and backend updates are reflected in your development environment.
|
||||
|
||||
Example output when running in development mode:
|
||||
|
||||
|
||||
@@ -37,31 +37,11 @@ Superset uses text editors in various places throughout the application:
|
||||
| `css` | Dashboard Properties, CSS Template Modal |
|
||||
| `markdown` | Dashboard Markdown component |
|
||||
| `yaml` | Template Params Editor |
|
||||
| `javascript` | Custom JavaScript editor contexts |
|
||||
| `python` | Custom Python editor contexts |
|
||||
| `text` | Plain text editor contexts |
|
||||
|
||||
By registering an editor provider for a language, your extension replaces the default Ace editor in **all** locations that use that language.
|
||||
|
||||
## Manifest Configuration
|
||||
|
||||
Declare editor contributions in your `extension.json` manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "monaco-editor",
|
||||
"version": "1.0.0",
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"editors": [
|
||||
{
|
||||
"id": "monaco-editor.sql",
|
||||
"name": "Monaco SQL Editor",
|
||||
"languages": ["sql"],
|
||||
"description": "Monaco-based SQL editor with IntelliSense"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
By registering an editor for a language, your extension replaces the default Ace editor in **all** locations that use that language.
|
||||
|
||||
## Implementing an Editor
|
||||
|
||||
@@ -165,21 +145,22 @@ const MonacoSQLEditor = forwardRef<editors.EditorHandle, editors.EditorProps>(
|
||||
export default MonacoSQLEditor;
|
||||
```
|
||||
|
||||
### activate.ts
|
||||
### index.tsx
|
||||
|
||||
Register the editor at module load time from your extension's entry point:
|
||||
|
||||
```typescript
|
||||
import { editors } from '@apache-superset/core';
|
||||
import MonacoSQLEditor from './MonacoSQLEditor';
|
||||
|
||||
export function activate(context) {
|
||||
// Register the Monaco editor for SQL using the contribution ID from extension.json
|
||||
const disposable = editors.registerEditorProvider(
|
||||
'monaco-sql-editor.sql',
|
||||
MonacoSQLEditor,
|
||||
);
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
editors.registerEditor(
|
||||
{
|
||||
id: 'my-extension.monaco-sql',
|
||||
name: 'Monaco SQL Editor',
|
||||
languages: ['sql'],
|
||||
},
|
||||
MonacoSQLEditor,
|
||||
);
|
||||
```
|
||||
|
||||
## Handling Hotkeys
|
||||
|
||||
@@ -86,132 +86,73 @@ Extensions can replace the default SQL editor with custom implementations (Monac
|
||||
|
||||
This example adds a "Data Profiler" panel to SQL Lab:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "data_profiler",
|
||||
"version": "1.0.0",
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "data_profiler.main",
|
||||
"name": "Data Profiler"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { core } from '@apache-superset/core';
|
||||
import React from 'react';
|
||||
import { views } from '@apache-superset/core';
|
||||
import DataProfilerPanel from './DataProfilerPanel';
|
||||
|
||||
export function activate(context) {
|
||||
// Register the panel view with the ID declared in extension.json
|
||||
const disposable = core.registerView('data_profiler.main', <DataProfilerPanel />);
|
||||
context.subscriptions.push(disposable);
|
||||
}
|
||||
views.registerView(
|
||||
{ id: 'my-extension.data-profiler', name: 'Data Profiler' },
|
||||
'sqllab.panels',
|
||||
() => <DataProfilerPanel />,
|
||||
);
|
||||
```
|
||||
|
||||
### Adding Actions to the Editor
|
||||
|
||||
This example adds primary, secondary, and context actions to the editor:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "query_tools",
|
||||
"version": "1.0.0",
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "query_tools.format",
|
||||
"title": "Format Query",
|
||||
"icon": "FormatPainterOutlined"
|
||||
},
|
||||
{
|
||||
"command": "query_tools.explain",
|
||||
"title": "Explain Query"
|
||||
},
|
||||
{
|
||||
"command": "query_tools.copy_as_cte",
|
||||
"title": "Copy as CTE"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"sqllab": {
|
||||
"editor": {
|
||||
"primary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "query_tools.format"
|
||||
}
|
||||
],
|
||||
"secondary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "query_tools.explain"
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "query_tools.copy_as_cte"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { commands, sqlLab } from '@apache-superset/core';
|
||||
import { commands, menus, sqlLab } from '@apache-superset/core';
|
||||
|
||||
export function activate(context) {
|
||||
// Register the commands declared in extension.json
|
||||
const formatCommand = commands.registerCommand(
|
||||
'query_tools.format',
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Format the SQL query
|
||||
}
|
||||
},
|
||||
);
|
||||
commands.registerCommand(
|
||||
{ id: 'my-extension.format', title: 'Format Query', icon: 'FormatPainterOutlined' },
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Format the SQL query
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const explainCommand = commands.registerCommand(
|
||||
'query_tools.explain',
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Show query explanation
|
||||
}
|
||||
},
|
||||
);
|
||||
commands.registerCommand(
|
||||
{ id: 'my-extension.explain', title: 'Explain Query' },
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Show query explanation
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const copyAsCteCommand = commands.registerCommand(
|
||||
'query_tools.copy_as_cte',
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Copy selected text as CTE
|
||||
}
|
||||
},
|
||||
);
|
||||
commands.registerCommand(
|
||||
{ id: 'my-extension.copy-as-cte', title: 'Copy as CTE' },
|
||||
async () => {
|
||||
const tab = sqlLab.getCurrentTab();
|
||||
if (tab) {
|
||||
const editor = await tab.getEditor();
|
||||
// Copy selected text as CTE
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
context.subscriptions.push(formatCommand, explainCommand, copyAsCteCommand);
|
||||
}
|
||||
menus.registerMenuItem(
|
||||
{ view: 'builtin.editor', command: 'my-extension.format' },
|
||||
'sqllab.editor',
|
||||
'primary',
|
||||
);
|
||||
menus.registerMenuItem(
|
||||
{ view: 'builtin.editor', command: 'my-extension.explain' },
|
||||
'sqllab.editor',
|
||||
'secondary',
|
||||
);
|
||||
menus.registerMenuItem(
|
||||
{ view: 'builtin.editor', command: 'my-extension.copy-as-cte' },
|
||||
'sqllab.editor',
|
||||
'context',
|
||||
);
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
679
docs/developer_docs/extensions/mcp-server.md
Normal file
679
docs/developer_docs/extensions/mcp-server.md
Normal file
@@ -0,0 +1,679 @@
|
||||
---
|
||||
title: MCP Server Deployment & Authentication
|
||||
hide_title: true
|
||||
sidebar_position: 9
|
||||
version: 1
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# MCP Server Deployment & Authentication
|
||||
|
||||
Superset includes a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets AI assistants -- Claude, ChatGPT, and other MCP-compatible clients -- interact with your Superset instance. Through MCP, clients can list dashboards, query datasets, execute SQL, create charts, and more.
|
||||
|
||||
This guide covers how to run, secure, and deploy the MCP server.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["AI Client<br/>(Claude, ChatGPT, etc.)"] -- "MCP protocol<br/>(HTTP + JSON-RPC)" --> B["MCP Server<br/>(:5008/mcp)"]
|
||||
B -- "Superset context<br/>(app, db, RBAC)" --> C["Superset<br/>(:8088)"]
|
||||
C --> D[("Database<br/>(Postgres)")]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
Get the MCP server running locally and connect an AI client in three steps.
|
||||
|
||||
### 1. Start the MCP server
|
||||
|
||||
The MCP server runs as a separate process alongside Superset:
|
||||
|
||||
```bash
|
||||
superset mcp run --host 127.0.0.1 --port 5008
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--host` | `127.0.0.1` | Host to bind to |
|
||||
| `--port` | `5008` | Port to bind to |
|
||||
| `--debug` | off | Enable debug logging |
|
||||
|
||||
The endpoint is available at `http://<host>:<port>/mcp`.
|
||||
|
||||
### 2. Set a development user
|
||||
|
||||
For local development, tell the MCP server which Superset user to impersonate (the user must already exist in your database):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_DEV_USERNAME = "admin"
|
||||
```
|
||||
|
||||
### 3. Connect an AI client
|
||||
|
||||
Point your MCP client at the server. For **Claude Desktop**, edit the config file:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"url": "http://localhost:5008/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Claude Desktop. The hammer icon in the chat bar confirms the connection.
|
||||
|
||||
See [Connecting AI Clients](#connecting-ai-clients) for Claude Code, Claude Web, ChatGPT, and raw HTTP examples.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Apache Superset 5.0+ running and accessible
|
||||
- Python 3.10+
|
||||
- The `fastmcp` package (`pip install fastmcp`)
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
The MCP server supports multiple authentication methods depending on your deployment scenario.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
R["Incoming MCP Request"] --> F{"MCP_AUTH_FACTORY<br/>set?"}
|
||||
F -- Yes --> CF["Custom Auth Provider"]
|
||||
F -- No --> AE{"MCP_AUTH_ENABLED?"}
|
||||
AE -- "True" --> JWT["JWT Validation"]
|
||||
AE -- "False" --> DU["Dev Mode<br/>(MCP_DEV_USERNAME)"]
|
||||
|
||||
JWT --> ALG{"MCP_JWT_ALGORITHM"}
|
||||
ALG -- "RS256 + JWKS" --> JWKS["Fetch keys from<br/>MCP_JWKS_URI"]
|
||||
ALG -- "RS256 + static" --> PK["Use<br/>MCP_JWT_PUBLIC_KEY"]
|
||||
ALG -- "HS256" --> SEC["Use<br/>MCP_JWT_SECRET"]
|
||||
|
||||
JWKS --> V["Validate token<br/>(exp, iss, aud, scopes)"]
|
||||
PK --> V
|
||||
SEC --> V
|
||||
V --> UR["Resolve Superset user<br/>from token claims"]
|
||||
UR --> OK["Authenticated request"]
|
||||
CF --> OK
|
||||
DU --> OK
|
||||
```
|
||||
|
||||
### Development Mode (No Auth)
|
||||
|
||||
Disable authentication and use a fixed user:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = False
|
||||
MCP_DEV_USERNAME = "admin"
|
||||
```
|
||||
|
||||
All operations run as the configured user.
|
||||
|
||||
:::warning
|
||||
Never use development mode in production. Always enable authentication for any internet-facing deployment.
|
||||
:::
|
||||
|
||||
### JWT Authentication
|
||||
|
||||
For production, enable JWT-based authentication. The MCP server validates a Bearer token on every request.
|
||||
|
||||
#### Option A: RS256 with JWKS endpoint
|
||||
|
||||
The most common setup for OAuth 2.0 / OIDC providers that publish a JWKS (JSON Web Key Set) endpoint:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_JWT_ALGORITHM = "RS256"
|
||||
MCP_JWKS_URI = "https://your-identity-provider.com/.well-known/jwks.json"
|
||||
MCP_JWT_ISSUER = "https://your-identity-provider.com/"
|
||||
MCP_JWT_AUDIENCE = "your-superset-instance"
|
||||
```
|
||||
|
||||
#### Option B: RS256 with static public key
|
||||
|
||||
Use this when you have a fixed RSA key pair (e.g., self-signed tokens):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_JWT_ALGORITHM = "RS256"
|
||||
MCP_JWT_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
-----END PUBLIC KEY-----"""
|
||||
MCP_JWT_ISSUER = "your-issuer"
|
||||
MCP_JWT_AUDIENCE = "your-audience"
|
||||
```
|
||||
|
||||
#### Option C: HS256 with shared secret
|
||||
|
||||
Use this when both the token issuer and the MCP server share a symmetric secret:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
MCP_JWT_ALGORITHM = "HS256"
|
||||
MCP_JWT_SECRET = "your-shared-secret-key"
|
||||
MCP_JWT_ISSUER = "your-issuer"
|
||||
MCP_JWT_AUDIENCE = "your-audience"
|
||||
```
|
||||
|
||||
:::warning
|
||||
Store `MCP_JWT_SECRET` securely. Never commit it to version control. Use environment variables:
|
||||
```python
|
||||
import os
|
||||
MCP_JWT_SECRET = os.environ.get("MCP_JWT_SECRET")
|
||||
```
|
||||
:::
|
||||
|
||||
#### JWT claims
|
||||
|
||||
The MCP server validates these standard claims:
|
||||
|
||||
| Claim | Config Key | Description |
|
||||
|-------|-----------|-------------|
|
||||
| `exp` | -- | Expiration time (always validated) |
|
||||
| `iss` | `MCP_JWT_ISSUER` | Token issuer (optional but recommended) |
|
||||
| `aud` | `MCP_JWT_AUDIENCE` | Token audience (optional but recommended) |
|
||||
| `sub` | -- | Subject -- primary claim used to resolve the Superset user |
|
||||
|
||||
#### User resolution
|
||||
|
||||
After validating the token, the MCP server resolves a Superset username from the claims. It checks these in order, using the first non-empty value:
|
||||
|
||||
1. `subject` -- the standard `sub` claim (via the access token object)
|
||||
2. `client_id` -- for machine-to-machine tokens
|
||||
3. `payload["sub"]` -- fallback to raw payload
|
||||
4. `payload["email"]` -- email-based lookup
|
||||
5. `payload["username"]` -- explicit username claim
|
||||
|
||||
The resolved value must match a `username` in the Superset `ab_user` table.
|
||||
|
||||
#### Scoped access
|
||||
|
||||
Require specific scopes in the JWT to limit what MCP operations a token can perform:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_REQUIRED_SCOPES = ["mcp:read", "mcp:write"]
|
||||
```
|
||||
|
||||
Only tokens that include **all** required scopes are accepted.
|
||||
|
||||
### Custom Auth Provider
|
||||
|
||||
For advanced scenarios (e.g., a proprietary auth system), provide a factory function. This takes precedence over all built-in JWT configuration:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
def my_custom_auth_factory(app):
|
||||
"""Return a FastMCP auth provider instance."""
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
return JWTVerifier(
|
||||
jwks_uri="https://my-auth.example.com/.well-known/jwks.json",
|
||||
issuer="https://my-auth.example.com/",
|
||||
audience="superset-mcp",
|
||||
)
|
||||
|
||||
MCP_AUTH_FACTORY = my_custom_auth_factory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connecting AI Clients
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
**Local development (no auth):**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"url": "http://localhost:5008/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**With JWT authentication:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"mcp-remote@latest",
|
||||
"http://your-superset-host:5008/mcp",
|
||||
"--header",
|
||||
"Authorization: Bearer YOUR_TOKEN"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Code (CLI)
|
||||
|
||||
Add to your project's `.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"type": "url",
|
||||
"url": "http://localhost:5008/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With authentication:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"type": "url",
|
||||
"url": "http://localhost:5008/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer YOUR_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Web (claude.ai)
|
||||
|
||||
1. Open [claude.ai](https://claude.ai)
|
||||
2. Click the **+** button (or your profile icon)
|
||||
3. Select **Connectors**
|
||||
4. Click **Manage Connectors** > **Add custom connector**
|
||||
5. Enter a name and your MCP URL (e.g., `https://your-superset-host/mcp`)
|
||||
6. Click **Add**
|
||||
|
||||
:::info
|
||||
Custom connectors on Claude Web require a Pro, Max, Team, or Enterprise plan.
|
||||
:::
|
||||
|
||||
### ChatGPT
|
||||
|
||||
1. Click your profile icon > **Settings** > **Apps and Connectors**
|
||||
2. Enable **Developer Mode** in Advanced Settings
|
||||
3. In the chat composer, press **+** > **Add sources** > **App** > **Connect more** > **Create app**
|
||||
4. Enter a name and your MCP server URL
|
||||
5. Click **I understand and continue**
|
||||
|
||||
:::info
|
||||
ChatGPT MCP connectors require a Pro, Team, Enterprise, or Edu plan.
|
||||
:::
|
||||
|
||||
### Direct HTTP requests
|
||||
|
||||
Call the MCP server directly with any HTTP client:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5008/mcp \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer YOUR_JWT_TOKEN' \
|
||||
-d '{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Single Process
|
||||
|
||||
The simplest setup: run the MCP server alongside Superset on the same host.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph host["Host / VM"]
|
||||
direction TB
|
||||
S["Superset<br/>:8088"] --> DB[("Postgres")]
|
||||
M["MCP Server<br/>:5008"] --> DB
|
||||
end
|
||||
C["AI Client"] -- "HTTPS" --> P["Reverse Proxy<br/>(Nginx / Caddy)"]
|
||||
U["Browser"] -- "HTTPS" --> P
|
||||
P -- ":8088" --> S
|
||||
P -- ":5008/mcp" --> M
|
||||
```
|
||||
|
||||
**superset_config.py:**
|
||||
|
||||
```python
|
||||
MCP_SERVICE_HOST = "0.0.0.0"
|
||||
MCP_SERVICE_PORT = 5008
|
||||
MCP_DEV_USERNAME = "admin" # or enable JWT auth
|
||||
|
||||
# If behind a reverse proxy, set the public-facing URL so
|
||||
# MCP-generated links (chart previews, SQL Lab URLs) resolve correctly:
|
||||
MCP_SERVICE_URL = "https://superset.example.com"
|
||||
```
|
||||
|
||||
**Start both processes:**
|
||||
|
||||
```bash
|
||||
# Terminal 1 -- Superset web server
|
||||
superset run -h 0.0.0.0 -p 8088
|
||||
|
||||
# Terminal 2 -- MCP server
|
||||
superset mcp run --host 0.0.0.0 --port 5008
|
||||
```
|
||||
|
||||
**Nginx reverse proxy with TLS:**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name superset.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Superset web UI
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8088;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# MCP endpoint
|
||||
location /mcp {
|
||||
proxy_pass http://127.0.0.1:5008/mcp;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Authorization $http_authorization;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Run Superset and the MCP server as separate containers sharing the same config:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
superset:
|
||||
image: apache/superset:latest
|
||||
ports:
|
||||
- "8088:8088"
|
||||
volumes:
|
||||
- ./superset_config.py:/app/superset_config.py
|
||||
environment:
|
||||
- SUPERSET_CONFIG_PATH=/app/superset_config.py
|
||||
|
||||
mcp:
|
||||
image: apache/superset:latest
|
||||
command: ["superset", "mcp", "run", "--host", "0.0.0.0", "--port", "5008"]
|
||||
ports:
|
||||
- "5008:5008"
|
||||
volumes:
|
||||
- ./superset_config.py:/app/superset_config.py
|
||||
environment:
|
||||
- SUPERSET_CONFIG_PATH=/app/superset_config.py
|
||||
depends_on:
|
||||
- superset
|
||||
```
|
||||
|
||||
Both containers share the same `superset_config.py`, so authentication settings, database connections, and feature flags stay in sync.
|
||||
|
||||
### Multi-Pod (Kubernetes)
|
||||
|
||||
For high-availability deployments, configure Redis so that replicas share session state:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
LB["Load Balancer"] --> M1["MCP Pod 1"]
|
||||
LB --> M2["MCP Pod 2"]
|
||||
LB --> M3["MCP Pod 3"]
|
||||
M1 --> R[("Redis<br/>(session store)")]
|
||||
M2 --> R
|
||||
M3 --> R
|
||||
M1 --> DB[("Postgres")]
|
||||
M2 --> DB
|
||||
M3 --> DB
|
||||
```
|
||||
|
||||
**superset_config.py:**
|
||||
|
||||
```python
|
||||
MCP_STORE_CONFIG = {
|
||||
"enabled": True,
|
||||
"CACHE_REDIS_URL": "redis://redis-host:6379/0",
|
||||
"event_store_max_events": 100,
|
||||
"event_store_ttl": 3600,
|
||||
}
|
||||
```
|
||||
|
||||
When `CACHE_REDIS_URL` is set, the MCP server uses a Redis-backed EventStore for session management, allowing replicas to share state. Without Redis, each pod manages its own in-memory sessions and stateful MCP interactions may fail when requests hit different replicas.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
All MCP settings go in `superset_config.py`. Defaults are defined in `superset/mcp_service/mcp_config.py`.
|
||||
|
||||
### Core
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `MCP_SERVICE_HOST` | `"localhost"` | Host the MCP server binds to |
|
||||
| `MCP_SERVICE_PORT` | `5008` | Port the MCP server binds to |
|
||||
| `MCP_SERVICE_URL` | `None` | Public base URL for MCP-generated links (set this when behind a reverse proxy) |
|
||||
| `MCP_DEBUG` | `False` | Enable debug logging |
|
||||
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
|
||||
|
||||
### Authentication
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `MCP_AUTH_ENABLED` | `False` | Enable JWT authentication |
|
||||
| `MCP_JWT_ALGORITHM` | `"RS256"` | JWT signing algorithm (`RS256` or `HS256`) |
|
||||
| `MCP_JWKS_URI` | `None` | JWKS endpoint URL (RS256) |
|
||||
| `MCP_JWT_PUBLIC_KEY` | `None` | Static RSA public key string (RS256) |
|
||||
| `MCP_JWT_SECRET` | `None` | Shared secret string (HS256) |
|
||||
| `MCP_JWT_ISSUER` | `None` | Expected `iss` claim |
|
||||
| `MCP_JWT_AUDIENCE` | `None` | Expected `aud` claim |
|
||||
| `MCP_REQUIRED_SCOPES` | `[]` | Required JWT scopes |
|
||||
| `MCP_JWT_DEBUG_ERRORS` | `False` | Log detailed JWT errors server-side (never exposed in HTTP responses per RFC 6750) |
|
||||
| `MCP_AUTH_FACTORY` | `None` | Custom auth provider factory `(flask_app) -> auth_provider`. Takes precedence over built-in JWT |
|
||||
|
||||
### Response Size Guard
|
||||
|
||||
Limits response sizes to prevent exceeding LLM context windows:
|
||||
|
||||
```python
|
||||
MCP_RESPONSE_SIZE_CONFIG = {
|
||||
"enabled": True,
|
||||
"token_limit": 25000,
|
||||
"warn_threshold_pct": 80,
|
||||
"excluded_tools": [
|
||||
"health_check",
|
||||
"get_chart_preview",
|
||||
"generate_explore_link",
|
||||
"open_sql_lab_with_context",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `enabled` | `True` | Enable response size checking |
|
||||
| `token_limit` | `25000` | Maximum estimated token count per response |
|
||||
| `warn_threshold_pct` | `80` | Warn when response exceeds this percentage of the limit |
|
||||
| `excluded_tools` | See above | Tools exempt from size checking (e.g., tools that return URLs, not data) |
|
||||
|
||||
### Caching
|
||||
|
||||
Optional response caching for read-heavy workloads. Requires Redis when used with multiple replicas.
|
||||
|
||||
```python
|
||||
MCP_CACHE_CONFIG = {
|
||||
"enabled": False,
|
||||
"CACHE_KEY_PREFIX": None,
|
||||
"list_tools_ttl": 300, # 5 min
|
||||
"list_resources_ttl": 300,
|
||||
"list_prompts_ttl": 300,
|
||||
"read_resource_ttl": 3600, # 1 hour
|
||||
"get_prompt_ttl": 3600,
|
||||
"call_tool_ttl": 3600,
|
||||
"max_item_size": 1048576, # 1 MB
|
||||
"excluded_tools": [
|
||||
"execute_sql",
|
||||
"generate_dashboard",
|
||||
"generate_chart",
|
||||
"update_chart",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `enabled` | `False` | Enable response caching |
|
||||
| `CACHE_KEY_PREFIX` | `None` | Optional prefix for cache keys (useful for shared Redis) |
|
||||
| `list_tools_ttl` | `300` | Cache TTL in seconds for `tools/list` |
|
||||
| `list_resources_ttl` | `300` | Cache TTL for `resources/list` |
|
||||
| `list_prompts_ttl` | `300` | Cache TTL for `prompts/list` |
|
||||
| `read_resource_ttl` | `3600` | Cache TTL for `resources/read` |
|
||||
| `get_prompt_ttl` | `3600` | Cache TTL for `prompts/get` |
|
||||
| `call_tool_ttl` | `3600` | Cache TTL for `tools/call` |
|
||||
| `max_item_size` | `1048576` | Maximum cached item size in bytes (1 MB) |
|
||||
| `excluded_tools` | See above | Tools that are never cached (mutating or non-deterministic) |
|
||||
|
||||
### Redis Store (Multi-Pod)
|
||||
|
||||
Enables Redis-backed session and event storage for multi-replica deployments:
|
||||
|
||||
```python
|
||||
MCP_STORE_CONFIG = {
|
||||
"enabled": False,
|
||||
"CACHE_REDIS_URL": None,
|
||||
"event_store_max_events": 100,
|
||||
"event_store_ttl": 3600,
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `enabled` | `False` | Enable Redis-backed store |
|
||||
| `CACHE_REDIS_URL` | `None` | Redis connection URL (e.g., `redis://redis-host:6379/0`) |
|
||||
| `event_store_max_events` | `100` | Maximum events retained per session |
|
||||
| `event_store_ttl` | `3600` | Event TTL in seconds |
|
||||
|
||||
### Session & CSRF
|
||||
|
||||
These values are flat-merged into the Flask app config used by the MCP server process:
|
||||
|
||||
```python
|
||||
MCP_SESSION_CONFIG = {
|
||||
"SESSION_COOKIE_HTTPONLY": True,
|
||||
"SESSION_COOKIE_SECURE": False,
|
||||
"SESSION_COOKIE_SAMESITE": "Lax",
|
||||
"SESSION_COOKIE_NAME": "superset_session",
|
||||
"PERMANENT_SESSION_LIFETIME": 86400,
|
||||
}
|
||||
|
||||
MCP_CSRF_CONFIG = {
|
||||
"WTF_CSRF_ENABLED": True,
|
||||
"WTF_CSRF_TIME_LIMIT": None,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Server won't start
|
||||
|
||||
- Verify `fastmcp` is installed: `pip install fastmcp`
|
||||
- Check that `MCP_DEV_USERNAME` is set if auth is disabled -- the server requires a user identity
|
||||
- Confirm the port is not already in use: `lsof -i :5008`
|
||||
|
||||
### 401 Unauthorized
|
||||
|
||||
- Verify your JWT token has not expired (`exp` claim)
|
||||
- Check that `MCP_JWT_ISSUER` and `MCP_JWT_AUDIENCE` match the token's `iss` and `aud` claims exactly
|
||||
- For RS256 with JWKS: confirm the JWKS URI is reachable from the MCP server
|
||||
- For RS256 with static key: confirm the public key string includes the `BEGIN`/`END` markers
|
||||
- For HS256: confirm the secret matches between the token issuer and `MCP_JWT_SECRET`
|
||||
- Enable `MCP_JWT_DEBUG_ERRORS = True` for detailed server-side logging (errors are never leaked to the client)
|
||||
|
||||
### Tool not found
|
||||
|
||||
- Ensure the MCP server and Superset share the same `superset_config.py`
|
||||
- Check server logs at startup -- tool registration errors are logged with the tool name and reason
|
||||
|
||||
### Client can't connect
|
||||
|
||||
- Verify the MCP server URL is reachable from the client machine
|
||||
- For Claude Desktop: fully quit the app (not just close the window) and restart after config changes
|
||||
- For remote access: ensure your firewall and reverse proxy allow traffic to the MCP port
|
||||
- Confirm the URL path ends with `/mcp` (e.g., `http://localhost:5008/mcp`)
|
||||
|
||||
### Permission errors on tool calls
|
||||
|
||||
- The MCP server enforces Superset's RBAC permissions -- the authenticated user must have the required roles
|
||||
- In development mode, ensure `MCP_DEV_USERNAME` maps to a user with appropriate roles (e.g., Admin)
|
||||
- Check `superset/security/manager.py` for the specific permission tuples required by each tool domain (e.g., `("can_execute_sql_query", "SQLLab")`)
|
||||
|
||||
### Response too large
|
||||
|
||||
- If a tool call returns an error about exceeding token limits, the response size guard is blocking an oversized result
|
||||
- Reduce `page_size` or `limit` parameters, use `select_columns` to exclude large fields, or add filters to narrow results
|
||||
- To adjust the threshold, change `token_limit` in `MCP_RESPONSE_SIZE_CONFIG`
|
||||
- To disable the guard entirely, set `MCP_RESPONSE_SIZE_CONFIG = {"enabled": False}`
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Use TLS** for all production MCP endpoints -- place the server behind a reverse proxy with HTTPS
|
||||
- **Enable JWT authentication** for any internet-facing deployment
|
||||
- **RBAC enforcement** -- The MCP server respects Superset's role-based access control. Users can only access data their roles permit
|
||||
- **Secrets management** -- Store `MCP_JWT_SECRET`, database credentials, and API keys in environment variables or a secrets manager, never in config files committed to version control
|
||||
- **Scoped tokens** -- Use `MCP_REQUIRED_SCOPES` to limit what operations a token can perform
|
||||
- **Network isolation** -- In Kubernetes, restrict MCP pod network policies to only allow traffic from your AI client endpoints
|
||||
- Review the **[Security documentation](./security)** for additional extension security guidance
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[MCP Integration](./mcp)** -- Build custom MCP tools and prompts via Superset extensions
|
||||
- **[Security](./security)** -- Security best practices for extensions
|
||||
- **[Deployment](./deployment)** -- Package and deploy Superset extensions
|
||||
@@ -61,7 +61,7 @@ Prompts provide interactive guidance and context to AI agents. They help agents
|
||||
The simplest way to create an MCP tool is using the `@tool` decorator:
|
||||
|
||||
```python
|
||||
from superset_core.api.mcp import tool
|
||||
from superset_core.mcp.decorators import tool
|
||||
|
||||
@tool
|
||||
def hello_world() -> dict:
|
||||
@@ -94,7 +94,7 @@ Here's a more comprehensive example showing best practices:
|
||||
import random
|
||||
from datetime import datetime, timezone
|
||||
from pydantic import BaseModel, Field
|
||||
from superset_core.api.mcp import tool
|
||||
from superset_core.mcp.decorators import tool
|
||||
|
||||
class RandomNumberRequest(BaseModel):
|
||||
"""Request schema for random number generation."""
|
||||
@@ -253,7 +253,7 @@ The AI agent sees your tool's:
|
||||
Create interactive prompts using the `@prompt` decorator:
|
||||
|
||||
```python
|
||||
from superset_core.api.mcp import prompt
|
||||
from superset_core.mcp.decorators import prompt
|
||||
from fastmcp import Context
|
||||
|
||||
@prompt("my_extension.workflow_guide")
|
||||
|
||||
@@ -43,7 +43,7 @@ Extensions can provide:
|
||||
|
||||
## UI Components for Extensions
|
||||
|
||||
Extension developers have access to pre-built UI components via `@apache-superset/core/ui`. Browse all available components on the [UI Components](/docs/components/) page and filter by **Extension Compatible** to see components available to extensions.
|
||||
Extension developers have access to pre-built UI components via `@apache-superset/core/components`. Browse all available components on the [UI Components](/docs/components/) page and filter by **Extension Compatible** to see components available to extensions.
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -64,10 +64,11 @@ 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**: `superset_extensions.my_org.hello_world`
|
||||
- **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).
|
||||
|
||||
@@ -78,12 +79,9 @@ my-org.hello-world/
|
||||
├── extension.json # Extension metadata and configuration
|
||||
├── backend/ # Backend Python code
|
||||
│ ├── src/
|
||||
│ │ └── superset_extensions/
|
||||
│ │ └── my_org/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── hello_world/
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── entrypoint.py # Backend registration
|
||||
│ │ └── my_org/
|
||||
│ │ └── hello_world/
|
||||
│ │ └── entrypoint.py # Backend registration
|
||||
│ └── pyproject.toml
|
||||
└── frontend/ # Frontend TypeScript/React code
|
||||
├── src/
|
||||
@@ -95,7 +93,7 @@ my-org.hello-world/
|
||||
|
||||
## Step 3: Configure Extension Metadata
|
||||
|
||||
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).
|
||||
The generated `extension.json` contains the extension's metadata.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -104,10 +102,6 @@ The generated `extension.json` contains the extension's metadata. It is used to
|
||||
"displayName": "Hello World",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"backend": {
|
||||
"entryPoints": ["superset_extensions.my_org.hello_world.entrypoint"],
|
||||
"files": ["backend/src/superset_extensions/my_org/hello_world/**/*.py"]
|
||||
},
|
||||
"permissions": ["can_read"]
|
||||
}
|
||||
```
|
||||
@@ -117,19 +111,19 @@ The generated `extension.json` contains the extension's metadata. It is used to
|
||||
- `publisher`: Organizational namespace for the extension
|
||||
- `name`: Technical identifier (kebab-case)
|
||||
- `displayName`: Human-readable name shown to users
|
||||
- `backend.entryPoints`: Python modules to load eagerly when the extension starts
|
||||
- `backend.files`: Glob patterns for Python source files to include in the bundle
|
||||
- `permissions`: List of permissions the extension requires
|
||||
|
||||
## Step 4: Create Backend API
|
||||
|
||||
The CLI generated a basic `backend/src/superset_extensions/my_org/hello_world/entrypoint.py`. We'll create an API endpoint.
|
||||
The CLI generated a basic `backend/src/my_org/hello_world/entrypoint.py`. We'll create an API endpoint.
|
||||
|
||||
**Create `backend/src/superset_extensions/my_org/hello_world/api.py`**
|
||||
**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.api.rest_api import RestApi, api
|
||||
from superset_core.rest_api.api import RestApi
|
||||
from superset_core.rest_api.decorators import api
|
||||
|
||||
|
||||
@api(
|
||||
@@ -174,25 +168,23 @@ class HelloWorldAPI(RestApi):
|
||||
|
||||
**Key points:**
|
||||
|
||||
- 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 [`@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/superset_extensions/my_org/hello_world/entrypoint.py`**
|
||||
**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
|
||||
|
||||
print("Hello World extension loaded successfully!")
|
||||
from .api import HelloWorldAPI # noqa: F401
|
||||
```
|
||||
|
||||
The [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator automatically detects extension context and registers your API with proper namespacing.
|
||||
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
|
||||
|
||||
@@ -236,52 +228,53 @@ The webpack configuration requires specific settings for Module Federation. Key
|
||||
**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.json");
|
||||
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";
|
||||
const isProd = argv.mode === 'production';
|
||||
|
||||
return {
|
||||
entry: isProd ? {} : "./src/index.tsx",
|
||||
mode: isProd ? "production" : "development",
|
||||
entry: isProd ? {} : './src/index.tsx',
|
||||
mode: isProd ? 'production' : 'development',
|
||||
devServer: {
|
||||
port: 3001,
|
||||
port: 3000,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
filename: isProd ? undefined : "[name].[contenthash].js",
|
||||
chunkFilename: "[name].[contenthash].js",
|
||||
clean: true,
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
publicPath: `/api/v1/extensions/my-org/hello-world/`,
|
||||
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"],
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||
},
|
||||
// Map @apache-superset/core imports to window.superset at runtime
|
||||
externalsType: "window",
|
||||
externalsType: 'window',
|
||||
externals: {
|
||||
"@apache-superset/core": "superset",
|
||||
'@apache-superset/core': 'superset',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: "myOrg_helloWorld",
|
||||
filename: "remoteEntry.[contenthash].js",
|
||||
name: 'myOrg_helloWorld',
|
||||
filename: 'remoteEntry.[contenthash].js',
|
||||
exposes: {
|
||||
"./index": "./src/index.tsx",
|
||||
'./index': './src/index.tsx',
|
||||
},
|
||||
shared: {
|
||||
react: {
|
||||
@@ -289,9 +282,14 @@ module.exports = (env, argv) => {
|
||||
requiredVersion: packageConfig.peerDependencies.react,
|
||||
import: false, // Use host's React, don't bundle
|
||||
},
|
||||
"react-dom": {
|
||||
'react-dom': {
|
||||
singleton: true,
|
||||
requiredVersion: packageConfig.peerDependencies["react-dom"],
|
||||
requiredVersion: packageConfig.peerDependencies['react-dom'],
|
||||
import: false,
|
||||
},
|
||||
antd: {
|
||||
singleton: true,
|
||||
requiredVersion: packageConfig.peerDependencies['antd'],
|
||||
import: false,
|
||||
},
|
||||
},
|
||||
@@ -306,8 +304,9 @@ module.exports = (env, argv) => {
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node10",
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -332,16 +331,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}`);
|
||||
@@ -496,8 +495,8 @@ 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` imports your API class, triggering the [`@api`](superset-core/src/superset_core/api/rest_api.py:59) decorator to register it automatically
|
||||
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
|
||||
|
||||
@@ -30,15 +30,15 @@ This page serves as a registry of community-created Superset extensions. These e
|
||||
|
||||
| Name | Description | Author | Preview |
|
||||
| ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Extensions API Explorer](https://github.com/michael-s-molina/superset-extensions/tree/main/api_explorer) | A SQL Lab panel that demonstrates the Extensions API by providing an interactive explorer for testing commands like getTabs, getCurrentTab, and getDatabases. Useful for extension developers to understand and experiment with the available APIs. | Michael S. Molina | <a href="/img/extensions/api-explorer.png" target="_blank"><img src="/img/extensions/api-explorer.png" alt="Extensions API Explorer" width="120" /></a> |
|
||||
| [Extensions API Explorer](https://github.com/michael-s-molina/superset-extensions/tree/main/api-explorer) | A SQL Lab panel that demonstrates the Extensions API by providing an interactive explorer for testing commands like getTabs, getCurrentTab, and getDatabases. Useful for extension developers to understand and experiment with the available APIs. | Michael S. Molina | <a href="/img/extensions/api-explorer.png" target="_blank"><img src="/img/extensions/api-explorer.png" alt="Extensions API Explorer" width="120" /></a> |
|
||||
| [SQL Query Flow Visualizer](https://github.com/msyavuz/superset-sql-visualizer) | A SQL Lab panel that transforms SQL queries into interactive flow diagrams, helping developers and analysts understand query execution paths and data relationships. | Mehmet Salih Yavuz | <a href="/img/extensions/sql-flow-visualizer.png" target="_blank"><img src="/img/extensions/sql-flow-visualizer.png" alt="SQL Flow Visualizer" width="120" /></a> |
|
||||
| [SQL Lab Export to Google Sheets](https://github.com/michael-s-molina/superset-extensions/tree/main/sqllab_gsheets) | A Superset extension that allows users to export SQL Lab query results directly to Google Sheets. | Michael S. Molina | <a href="/img/extensions/gsheets-export.png" target="_blank"><img src="/img/extensions/gsheets-export.png" alt="SQL Lab Export to Google Sheets" width="120" /></a> |
|
||||
| [SQL Lab Export to Google Sheets](https://github.com/michael-s-molina/superset-extensions/tree/main/sqllab-gsheets) | A Superset extension that allows users to export SQL Lab query results directly to Google Sheets. | Michael S. Molina | <a href="/img/extensions/gsheets-export.png" target="_blank"><img src="/img/extensions/gsheets-export.png" alt="SQL Lab Export to Google Sheets" width="120" /></a> |
|
||||
| [SQL Lab Export to Parquet](https://github.com/rusackas/superset-extensions/tree/main/sqllab_parquet) | Export SQL Lab query results directly to Apache Parquet format with Snappy compression. | Evan Rusackas | <a href="/img/extensions/parquet-export.png" target="_blank"><img src="/img/extensions/parquet-export.png" alt="SQL Lab Export to Parquet" width="120" /></a> |
|
||||
| [SQL Lab Query Comparison](https://github.com/michael-s-molina/superset-extensions/tree/main/query_comparison) | A SQL Lab extension that enables side-by-side comparison of query results across different tabs, with GitHub-style diff visualization showing added/removed rows and columns. | Michael S. Molina | <a href="/img/extensions/query-comparison.png" target="_blank"><img src="/img/extensions/query-comparison.png" alt="Query Comparison" width="120" /></a> |
|
||||
| [SQL Lab Result Stats](https://github.com/michael-s-molina/superset-extensions/tree/main/result_stats) | A SQL Lab extension that automatically computes statistics for query results, providing type-aware analysis including numeric metrics (min, max, mean, median, std dev), string analysis (length, empty counts), and date range information. | Michael S. Molina | <a href="/img/extensions/result-stats.png" target="_blank"><img src="/img/extensions/result-stats.png" alt="Result Stats" width="120" /></a> |
|
||||
| [SQL Snippets](https://github.com/michael-s-molina/superset-extensions/tree/main/sql_snippets) | A SQL Lab extension that provides reusable SQL code snippets, enabling quick insertion of commonly used code blocks such as license headers, author information, and frequently used SQL patterns. | Michael S. Molina | <a href="/img/extensions/sql-snippets.png" target="_blank"><img src="/img/extensions/sql-snippets.png" alt="SQL Snippets" width="120" /></a> |
|
||||
| [SQL Lab Query Estimator](https://github.com/michael-s-molina/superset-extensions/tree/main/query_estimator) | A SQL Lab panel that analyzes query execution plans to estimate resource impact, detect performance issues like Cartesian products and high-cost operations, and visualize the query plan tree. | Michael S. Molina | <a href="/img/extensions/query-estimator.png" target="_blank"><img src="/img/extensions/query-estimator.png" alt="Query Estimator" width="120" /></a> |
|
||||
| [Editors Bundle](https://github.com/michael-s-molina/superset-extensions/tree/main/editors_bundle) | A Superset extension that demonstrates how to provide custom code editors for different languages. This extension showcases the editor contribution system by registering alternative editors that can replace Superset's default Ace editor. | Michael S. Molina | <a href="/img/extensions/editors-bundle.png" target="_blank"><img src="/img/extensions/editors-bundle.png" alt="Editors Bundle" width="120" /></a> |
|
||||
| [SQL Lab Query Comparison](https://github.com/michael-s-molina/superset-extensions/tree/main/query-comparison) | A SQL Lab extension that enables side-by-side comparison of query results across different tabs, with GitHub-style diff visualization showing added/removed rows and columns. | Michael S. Molina | <a href="/img/extensions/query-comparison.png" target="_blank"><img src="/img/extensions/query-comparison.png" alt="Query Comparison" width="120" /></a> |
|
||||
| [SQL Lab Result Stats](https://github.com/michael-s-molina/superset-extensions/tree/main/result-stats) | A SQL Lab extension that automatically computes statistics for query results, providing type-aware analysis including numeric metrics (min, max, mean, median, std dev), string analysis (length, empty counts), and date range information. | Michael S. Molina | <a href="/img/extensions/result-stats.png" target="_blank"><img src="/img/extensions/result-stats.png" alt="Result Stats" width="120" /></a> |
|
||||
| [Editor Snippets](https://github.com/michael-s-molina/superset-extensions/tree/main/editor-snippets) | A SQL Lab extension for managing and inserting reusable code snippets into the editor, with server-side persistence per user. | Michael S. Molina | <a href="/img/extensions/editor-snippets.png" target="_blank"><img src="/img/extensions/editor-snippets.png" alt="Editor Snippets" width="120" /></a> |
|
||||
| [SQL Lab Query Estimator](https://github.com/michael-s-molina/superset-extensions/tree/main/query-estimator) | A SQL Lab panel that analyzes query execution plans to estimate resource impact, detect performance issues like Cartesian products and high-cost operations, and visualize the query plan tree. | Michael S. Molina | <a href="/img/extensions/query-estimator.png" target="_blank"><img src="/img/extensions/query-estimator.png" alt="Query Estimator" width="120" /></a> |
|
||||
| [Editors Bundle](https://github.com/michael-s-molina/superset-extensions/tree/main/editors-bundle) | A Superset extension that demonstrates how to provide custom code editors for different languages. This extension showcases the editor contribution system by registering alternative editors that can replace Superset's default Ace editor. | Michael S. Molina | <a href="/img/extensions/editors-bundle.png" target="_blank"><img src="/img/extensions/editors-bundle.png" alt="Editors Bundle" width="120" /></a> |
|
||||
|
||||
## How to Add Your Extension
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ When GTF is considered stable, it will replace legacy Celery tasks for built-in
|
||||
### Define a Task
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import task, get_context
|
||||
from superset_core.tasks.decorators import task, get_context
|
||||
|
||||
@task
|
||||
def process_data(dataset_id: int) -> None:
|
||||
@@ -245,7 +245,8 @@ Always implement an abort handler for long-running tasks. This allows users to c
|
||||
Set a timeout to automatically abort tasks that run too long:
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import task, get_context, TaskOptions
|
||||
from superset_core.tasks.decorators import task, get_context
|
||||
from superset_core.tasks.types import TaskOptions
|
||||
|
||||
# Set default timeout in decorator
|
||||
@task(timeout=300) # 5 minutes
|
||||
@@ -299,7 +300,7 @@ Timeouts require an abort handler to be effective. Without one, the timeout trig
|
||||
Use `task_key` to prevent duplicate task execution:
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import TaskOptions
|
||||
from superset_core.tasks.types import TaskOptions
|
||||
|
||||
# Without key - creates new task each time (random UUID)
|
||||
task1 = my_task.schedule(x=1)
|
||||
@@ -331,7 +332,8 @@ print(task2.status) # "success" (terminal status)
|
||||
## Task Scopes
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import task, TaskScope
|
||||
from superset_core.tasks.decorators import task
|
||||
from superset_core.tasks.types import TaskScope
|
||||
|
||||
@task # Private by default
|
||||
def private_task(): ...
|
||||
|
||||
@@ -52,6 +52,7 @@ module.exports = {
|
||||
'extensions/development',
|
||||
'extensions/deployment',
|
||||
'extensions/mcp',
|
||||
'extensions/mcp-server',
|
||||
'extensions/security',
|
||||
'extensions/tasks',
|
||||
'extensions/registry',
|
||||
|
||||
@@ -55,20 +55,20 @@
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^8.6.17",
|
||||
"@storybook/addon-docs": "^8.6.18",
|
||||
"@storybook/blocks": "^8.6.15",
|
||||
"@storybook/channels": "^8.6.17",
|
||||
"@storybook/client-logger": "^8.6.17",
|
||||
"@storybook/components": "^8.6.17",
|
||||
"@storybook/core": "^8.6.17",
|
||||
"@storybook/core-events": "^8.6.17",
|
||||
"@storybook/channels": "^8.6.18",
|
||||
"@storybook/client-logger": "^8.6.18",
|
||||
"@storybook/components": "^8.6.18",
|
||||
"@storybook/core": "^8.6.18",
|
||||
"@storybook/core-events": "^8.6.18",
|
||||
"@storybook/csf": "^0.1.13",
|
||||
"@storybook/docs-tools": "^8.6.17",
|
||||
"@storybook/preview-api": "^8.6.17",
|
||||
"@storybook/docs-tools": "^8.6.18",
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.17",
|
||||
"antd": "^6.3.1",
|
||||
"antd": "^6.3.2",
|
||||
"baseline-browser-mapping": "^2.10.0",
|
||||
"caniuse-lite": "^1.0.30001775",
|
||||
"docusaurus-plugin-openapi-docs": "^4.6.0",
|
||||
@@ -85,7 +85,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.1.1",
|
||||
"storybook": "^8.6.17",
|
||||
"storybook": "^8.6.18",
|
||||
"swagger-ui-react": "^5.32.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
@@ -107,7 +107,7 @@
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"webpack": "^5.105.3"
|
||||
"webpack": "^5.105.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -152,8 +152,8 @@ const SOURCES = [
|
||||
{
|
||||
name: 'Extension Components',
|
||||
path: 'packages/superset-core/src',
|
||||
importPrefix: '@apache-superset/core/ui',
|
||||
docImportPrefix: '@apache-superset/core/ui',
|
||||
importPrefix: '@apache-superset/core/components',
|
||||
docImportPrefix: '@apache-superset/core/components',
|
||||
category: 'extension',
|
||||
enabled: true,
|
||||
extensionCompatible: true,
|
||||
@@ -1155,7 +1155,7 @@ Help improve it by [editing the story file](https://github.com/apache/superset/e
|
||||
const CATEGORY_LABELS = {
|
||||
ui: { title: 'Core Components', sidebarLabel: 'Core Components', description: 'Buttons, inputs, modals, selects, and other fundamental UI elements.' },
|
||||
'design-system': { title: 'Layout Components', sidebarLabel: 'Layout Components', description: 'Grid, Layout, Table, Flex, Space, and container components for page structure.' },
|
||||
extension: { title: 'Extension Components', sidebarLabel: 'Extension Components', description: 'Components available to extension developers via @apache-superset/core/ui.' },
|
||||
extension: { title: 'Extension Components', sidebarLabel: 'Extension Components', description: 'Components available to extension developers via @apache-superset/core/components.' },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1463,7 +1463,7 @@ function generateExtensionTypeDeclarations(extensionComponents) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type declarations for @apache-superset/core/ui
|
||||
* Type declarations for @apache-superset/core/components
|
||||
*
|
||||
* AUTO-GENERATED by scripts/generate-superset-components.mjs
|
||||
* Do not edit manually - regenerate by running: yarn generate:superset-components
|
||||
|
||||
@@ -39,7 +39,7 @@ function getComponentRegistry() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SupersetComponents = require('@superset/components');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const CoreUI = require('@apache-superset/core/ui');
|
||||
const CoreUI = require('@apache-superset/core/components');
|
||||
|
||||
// Build component registry with antd as base fallback layer.
|
||||
// Some Superset components (e.g., Typography) use styled-components that may
|
||||
@@ -65,7 +65,7 @@ function getProviders() {
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themeObject } = require('@apache-superset/core/ui');
|
||||
const { themeObject } = require('@apache-superset/core/theme');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { App, ConfigProvider } = require('antd');
|
||||
|
||||
|
||||
120
docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
Normal file
120
docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Swizzled from docusaurus-theme-openapi-docs to fix SSG crash.
|
||||
*
|
||||
* The original component calls useTypedSelector (Redux) at the top level,
|
||||
* which fails during static site generation because no Redux store is
|
||||
* available. This version moves the hook into a browser-only child component
|
||||
* so SSG can render the page without a store context.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
interface ServerVariable {
|
||||
default?: string;
|
||||
}
|
||||
|
||||
interface ServerValue {
|
||||
url: string;
|
||||
variables?: Record<string, ServerVariable>;
|
||||
}
|
||||
|
||||
interface StoreState {
|
||||
server: { value: ServerValue | null };
|
||||
}
|
||||
|
||||
function colorForMethod(method: string) {
|
||||
switch (method.toLowerCase()) {
|
||||
case "get":
|
||||
return "primary";
|
||||
case "post":
|
||||
return "success";
|
||||
case "delete":
|
||||
return "danger";
|
||||
case "put":
|
||||
return "info";
|
||||
case "patch":
|
||||
return "warning";
|
||||
case "head":
|
||||
return "secondary";
|
||||
case "event":
|
||||
return "secondary";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
method: string;
|
||||
path: string;
|
||||
context?: "endpoint" | "callback";
|
||||
}
|
||||
|
||||
// Inner component rendered only in the browser, where the Redux store exists.
|
||||
function ServerUrl() {
|
||||
const serverValue = useSelector((state: StoreState) => state.server.value);
|
||||
|
||||
if (serverValue && serverValue.variables) {
|
||||
let serverUrlWithVariables = serverValue.url.replace(/\/$/, "");
|
||||
Object.keys(serverValue.variables).forEach((variable) => {
|
||||
serverUrlWithVariables = serverUrlWithVariables.replace(
|
||||
`{${variable}}`,
|
||||
serverValue.variables?.[variable].default ?? ""
|
||||
);
|
||||
});
|
||||
return <>{serverUrlWithVariables}</>;
|
||||
}
|
||||
|
||||
if (serverValue && serverValue.url) {
|
||||
return <>{serverValue.url}</>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function MethodEndpoint({ method, path, context }: Props) {
|
||||
const renderServerUrl = () => {
|
||||
if (context === "callback") {
|
||||
return "";
|
||||
}
|
||||
return <BrowserOnly>{() => <ServerUrl />}</BrowserOnly>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<pre className="openapi__method-endpoint">
|
||||
<span className={"badge badge--" + colorForMethod(method)}>
|
||||
{method === "event" ? "Webhook" : method.toUpperCase()}
|
||||
</span>{" "}
|
||||
{method !== "event" && (
|
||||
<h2 className="openapi__method-endpoint-path">
|
||||
{renderServerUrl()}
|
||||
{`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`}
|
||||
</h2>
|
||||
)}
|
||||
</pre>
|
||||
<div className="openapi__divider" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MethodEndpoint;
|
||||
@@ -35,7 +35,7 @@ function getThemeWrapper() {
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { themeObject } = require('@apache-superset/core/ui');
|
||||
const { themeObject } = require('@apache-superset/core/theme');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { App } = require('antd');
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ if (isBrowser) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SupersetComponents = require('@superset/components');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { Alert } = require('@apache-superset/core/ui');
|
||||
const { Alert } = require('@apache-superset/core/components');
|
||||
|
||||
console.log('[ReactLiveScope] SupersetComponents keys:', Object.keys(SupersetComponents || {}).slice(0, 10));
|
||||
console.log('[ReactLiveScope] Has Button?', 'Button' in (SupersetComponents || {}));
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type declarations for @apache-superset/core/ui
|
||||
* Type declarations for @apache-superset/core/components
|
||||
*
|
||||
* AUTO-GENERATED by scripts/generate-superset-components.mjs
|
||||
* Do not edit manually - regenerate by running: yarn generate:superset-components
|
||||
|
||||
@@ -156,9 +156,9 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
// to source so the docs build doesn't depend on pre-built lib/ artifacts.
|
||||
// More specific sub-path aliases must come first; webpack matches the
|
||||
// longest prefix.
|
||||
'@apache-superset/core/ui': path.resolve(
|
||||
'@apache-superset/core/components': path.resolve(
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src/ui',
|
||||
'../../superset-frontend/packages/superset-core/src/components',
|
||||
),
|
||||
'@apache-superset/core/api/core': path.resolve(
|
||||
__dirname,
|
||||
|
||||
6
docs/static/feature-flags.json
vendored
6
docs/static/feature-flags.json
vendored
@@ -51,6 +51,12 @@
|
||||
"lifecycle": "development",
|
||||
"description": "Enable Superset extensions for custom functionality without modifying core"
|
||||
},
|
||||
{
|
||||
"name": "GRANULAR_EXPORT_CONTROLS",
|
||||
"default": false,
|
||||
"lifecycle": "development",
|
||||
"description": "Enable granular export controls (can_export_data, can_export_image, can_copy_clipboard) instead of the single can_csv permission"
|
||||
},
|
||||
{
|
||||
"name": "MATRIXIFY",
|
||||
"default": false,
|
||||
|
||||
BIN
docs/static/img/databases/datastore.png
vendored
Normal file
BIN
docs/static/img/databases/datastore.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 358 KiB After Width: | Height: | Size: 358 KiB |
@@ -14,10 +14,10 @@
|
||||
"paths": {
|
||||
"@superset-ui/core": ["../superset-frontend/packages/superset-ui-core/src"],
|
||||
"@superset-ui/core/*": ["../superset-frontend/packages/superset-ui-core/src/*"],
|
||||
// Types for @apache-superset/core/ui are auto-generated by scripts/generate-superset-components.mjs
|
||||
// Types for @apache-superset/core/components are auto-generated by scripts/generate-superset-components.mjs
|
||||
// Runtime resolution uses webpack alias pointing to actual source (see src/webpack.extend.ts)
|
||||
// Using /ui path matches the established pattern used throughout the Superset codebase
|
||||
"@apache-superset/core/ui": ["./src/types/apache-superset-core"],
|
||||
"@apache-superset/core/components": ["./src/types/apache-superset-core"],
|
||||
"*": ["src/*", "node_modules/*"]
|
||||
}
|
||||
},
|
||||
|
||||
271
docs/yarn.lock
271
docs/yarn.lock
@@ -195,19 +195,19 @@
|
||||
dependencies:
|
||||
"@ant-design/fast-color" "^3.0.0"
|
||||
|
||||
"@ant-design/cssinjs-utils@^2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.1.tgz#c70d86206204e882073a0fe4969a5ddf154c6915"
|
||||
integrity sha512-RKxkj5pGFB+FkPJ5NGhoX3DK3xsv0pMltha7Ei1AnY3tILeq38L7tuhaWDPQI/5nlPxOog44wvqpNyyGcUsNMg==
|
||||
"@ant-design/cssinjs-utils@^2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz#a4a57e02dd7e7c3732ab7f1df406df98b5542d12"
|
||||
integrity sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==
|
||||
dependencies:
|
||||
"@ant-design/cssinjs" "^2.1.0"
|
||||
"@ant-design/cssinjs" "^2.1.2"
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@rc-component/util" "^1.4.0"
|
||||
|
||||
"@ant-design/cssinjs@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-2.1.0.tgz#081394937f86aefe55e35198019d0483f405a484"
|
||||
integrity sha512-eZFrPCnrYrF3XtL7qA4L75P0qA3TtZta8H3Yggy7UYFh8gZgu5bSMNF+v4UVCzGxzYmx8ZvPdgOce0BJ6PsW9g==
|
||||
"@ant-design/cssinjs@^2.1.2":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz#0219e37afdd957248b10da366febae1e4001c952"
|
||||
integrity sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.1"
|
||||
"@emotion/hash" "^0.8.0"
|
||||
@@ -2964,12 +2964,12 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/color-picker@~3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/color-picker/-/color-picker-3.1.0.tgz#437586ea2fc27862e7429a754cf85e519e05f461"
|
||||
integrity sha512-o7Vavj7yyfVxFmeynXf0fCHVlC0UTE9al74c6nYuLck+gjuVdQNWSVXR8Efq/mmWFy7891SCOsfaPq6Eqe1s/g==
|
||||
"@rc-component/color-picker@~3.1.1":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/color-picker/-/color-picker-3.1.1.tgz#0a00411457e697cf9320e945762a4b08f71938f9"
|
||||
integrity sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==
|
||||
dependencies:
|
||||
"@ant-design/fast-color" "^3.0.0"
|
||||
"@ant-design/fast-color" "^3.0.1"
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
@@ -3009,10 +3009,10 @@
|
||||
"@rc-component/util" "^1.2.1"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/form@~1.6.2":
|
||||
version "1.6.2"
|
||||
resolved "https://registry.npmjs.org/@rc-component/form/-/form-1.6.2.tgz"
|
||||
integrity sha512-OgIn2RAoaSBqaIgzJf/X6iflIa9LpTozci1lagLBdURDFhGA370v0+T0tXxOi8YShMjTha531sFhwtnrv+EJaQ==
|
||||
"@rc-component/form@~1.7.1":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/form/-/form-1.7.1.tgz#baf18de01e649415c39e895a2c2fc9c61e1f2e23"
|
||||
integrity sha512-Uhw0FPvJ+Ko4xBxhvziqmqzIuO0YvVBzVyFGNAI9fMCz4r4DfrYK6PRIN6CkFqM0vdAX9sr4JGA1/h/VzpA1cA==
|
||||
dependencies:
|
||||
"@rc-component/async-validator" "^5.1.0"
|
||||
"@rc-component/util" "^1.6.2"
|
||||
@@ -3075,15 +3075,7 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.0"
|
||||
|
||||
"@rc-component/motion@^1.0.0", "@rc-component/motion@^1.1.3", "@rc-component/motion@^1.1.4":
|
||||
version "1.1.6"
|
||||
resolved "https://registry.npmjs.org/@rc-component/motion/-/motion-1.1.6.tgz"
|
||||
integrity sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ==
|
||||
dependencies:
|
||||
"@rc-component/util" "^1.2.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/motion@^1.3.1":
|
||||
"@rc-component/motion@^1.0.0", "@rc-component/motion@^1.1.3", "@rc-component/motion@^1.1.4", "@rc-component/motion@^1.3.1":
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/motion/-/motion-1.3.1.tgz#1e56b06841ee677261251e6e69fedc8d73e65b22"
|
||||
integrity sha512-Wo1mkd0tCcHtvYvpPOmlYJz546z16qlsiwaygmW7NPJpOZOF9GBjhGzdzZSsC2lEJ1IUkWLF4gMHlRA1aSA+Yw==
|
||||
@@ -3184,21 +3176,10 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/select@~1.6.0":
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.6.5.tgz#69276239c6ac0884a67597961b0224c4ad0bc4ca"
|
||||
integrity sha512-Cx+/OYEorXlPQ6ZFDro3HbalPZLlJWagvGtl8DGYO4losXM6gw43qbsxWqU1c3XOQVIOUDBlr7dSksSNMj8kXg==
|
||||
dependencies:
|
||||
"@rc-component/overflow" "^1.0.0"
|
||||
"@rc-component/trigger" "^3.0.0"
|
||||
"@rc-component/util" "^1.3.0"
|
||||
"@rc-component/virtual-list" "^1.0.1"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/select@~1.6.12":
|
||||
version "1.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.6.12.tgz#24312b31aad2a78ce1ec0062b15f56428bddab8f"
|
||||
integrity sha512-jYXAglYdOb54BrpWAcjjhdBP16NyCv/HbEaWFMbEHZQAJVmGHPAtmBqbFuPPuvInAVsIwLbCj4Agag9udOamiQ==
|
||||
"@rc-component/select@~1.6.0", "@rc-component/select@~1.6.14":
|
||||
version "1.6.14"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.6.14.tgz#61028c0abe02d2a909935b5cb586374968196c96"
|
||||
integrity sha512-T1IWeLlSas7Z/igZtPtJ/bweCxMMkXIGKQBtnigK+I/n1AVNjCs+ZdL3Fj42mq3uqm4sd1uzeQLZkdCqR26ADw==
|
||||
dependencies:
|
||||
"@rc-component/overflow" "^1.0.0"
|
||||
"@rc-component/trigger" "^3.0.0"
|
||||
@@ -3524,53 +3505,53 @@
|
||||
resolved "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz"
|
||||
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
|
||||
|
||||
"@storybook/addon-docs@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-8.6.17.tgz#3b7fdccebb60bcde62241a2ef2c9e493003003d5"
|
||||
integrity sha512-zvcSzoYvaZO4l9NxsviDr5vmuq8GVnH4Ap0v+5sSTq192yevm/iQcRnkWYBD9E/Lg5GBeyE+Ml2vjEOK+EPBEg==
|
||||
"@storybook/addon-docs@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-8.6.18.tgz#1910942ecdff4e5cda6352d22bc483f0c2058f61"
|
||||
integrity sha512-55ADer0yNmmeR928Y3UAv3r4i7bJSd9LwywsQ+lRol/FNe0ZcwLEz31xL+jVsqQFNnDh/imsDIp8aYapGMtfEQ==
|
||||
dependencies:
|
||||
"@mdx-js/react" "^3.0.0"
|
||||
"@storybook/blocks" "8.6.17"
|
||||
"@storybook/csf-plugin" "8.6.17"
|
||||
"@storybook/react-dom-shim" "8.6.17"
|
||||
"@storybook/blocks" "8.6.18"
|
||||
"@storybook/csf-plugin" "8.6.18"
|
||||
"@storybook/react-dom-shim" "8.6.18"
|
||||
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/blocks@8.6.17", "@storybook/blocks@^8.6.15":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/blocks/-/blocks-8.6.17.tgz#153a9e5ce2b1f2e769f2d095208a303266a85823"
|
||||
integrity sha512-zuYHH+0egovMrjWRKwOtgVGbz6KALGowPSWBzQ8deTBu6IXfkz6Ce1hRLJPn5S6/jDqqr9xx8vuAiypnRQ98tA==
|
||||
"@storybook/blocks@8.6.18", "@storybook/blocks@^8.6.15":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/blocks/-/blocks-8.6.18.tgz#d1bf7e9639a86cdf690bea1c53028be725afb1e8"
|
||||
integrity sha512-esZv4msPQ9LxgTb8YUIZhhxVMuI6BPi5bkXtk8c7w7sWuAsqsCe/RnVInn7ooUry2gjnD4hd9+8Eqj0b8oTVoA==
|
||||
dependencies:
|
||||
"@storybook/icons" "^1.2.12"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/channels@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-8.6.17.tgz#074930ccfc9ce4a6d798f274819b70d2852f0fbe"
|
||||
integrity sha512-3uwPYVia6MdyeTI2oq46ybpFIZCCjohvzI7zn6NmnRqC8WvZapngLY6OT590eFCrFdgxMszKORUvSsPgtjpnuA==
|
||||
"@storybook/channels@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/channels/-/channels-8.6.18.tgz#21bf4624badc41f343ac7e182ba7a88c5d682bff"
|
||||
integrity sha512-J/xabOEHfMYEWpdm4gR6HD5IdC0e7OsNvgUEspQjcUMhjMwtGm/EaahwNpRUIxO2tgzKj4zHnflGfPCfTd4PgQ==
|
||||
|
||||
"@storybook/client-logger@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-8.6.17.tgz#43decc0f507dd7daf9310994fd612b25fc6915a5"
|
||||
integrity sha512-l8vbDNyyR9YfWZzlsupxEeekA/eq4iibBo3gWwr+2G5QfNTGveTQdpgr2m5IL5k+Xjnii22AepmQ4NdjPbJXwA==
|
||||
"@storybook/client-logger@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/client-logger/-/client-logger-8.6.18.tgz#21b95c5ecb30475ad5a1fa68c0af603a4199c01b"
|
||||
integrity sha512-l7x3KkumMcTN+R1ozAqEyAkHpNBonIvicYoTgha/3Dh/tKiBYLLum2AEXbiu0TBJ7EEUfi4AG7eOBBfVdfWqvQ==
|
||||
|
||||
"@storybook/components@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-8.6.17.tgz#67c87f4a5b98999c81f17418cbe2396e6dd216f1"
|
||||
integrity sha512-0b8xkkuPCNbM8LTOzyfxuo2KdJCHIfu3+QxWBFllXap0eYNHwVeSxE5KERQ/bk2GDCiRzaUbwH9PeLorxOzJJQ==
|
||||
"@storybook/components@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/components/-/components-8.6.18.tgz#0e5431f9d84cae29a8b8a406c9ad99406bf2ccb4"
|
||||
integrity sha512-55yViiZzPS/cPBuOeW4QGxGqrusjXVyxuknmbYCIwDtFyyvI/CgbjXRHdxNBaIjz+IlftxvBmmSaOqFG5+/dkA==
|
||||
|
||||
"@storybook/core-events@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-8.6.17.tgz#e67d6308e61cd6d7be574c40c605eafe7bb04c74"
|
||||
integrity sha512-HiKVE2sSbJF6PVFt2DfJtLef1Mc35cN+sf2f8Ay2ibHy2gY1t3/7W1PhYVGt7UpJNOnVZfsmcE3yqGNojct3mw==
|
||||
"@storybook/core-events@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core-events/-/core-events-8.6.18.tgz#aaaf2a544fdb07036a08200692bb88a96d9df651"
|
||||
integrity sha512-eUVwrcppny/ZYyke/SPVZVuco8wxkQ/0K20nlevSiDkgWZSELii5Ju0/l9Ubnopr9dshoFCYbC7q6liTSpok7A==
|
||||
|
||||
"@storybook/core@8.6.17", "@storybook/core@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core/-/core-8.6.17.tgz#73af480521333e421413ffdda7a992b3c96b1afb"
|
||||
integrity sha512-lndZDYIvUddWk54HmgYwE4h2B0JtWt8ztIRAzHRt6ReZZ9QQbmM5b85Qpa+ng4dyQEKc2JAtYD3Du7RRFcpHlw==
|
||||
"@storybook/core@8.6.18", "@storybook/core@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/core/-/core-8.6.18.tgz#0ddbec8421715b372419ae5dfefef3df5848386c"
|
||||
integrity sha512-dRBP2TnX6fGdS0T2mXBHjkS/3Nlu1ra1huovZVFuM67CYMzrhM/3hX/zru1vWSC5rqY93ZaAhjMciPW4pK5mMQ==
|
||||
dependencies:
|
||||
"@storybook/theming" "8.6.17"
|
||||
"@storybook/theming" "8.6.18"
|
||||
better-opn "^3.0.2"
|
||||
browser-assert "^1.2.1"
|
||||
esbuild "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0"
|
||||
@@ -3582,10 +3563,10 @@
|
||||
util "^0.12.5"
|
||||
ws "^8.2.3"
|
||||
|
||||
"@storybook/csf-plugin@8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-8.6.17.tgz#004e25cd408d98a1514d0bf83e02f270c87a2091"
|
||||
integrity sha512-ouvF/izbKclZxpfnRUkyC5ZVDU7QA0cHhjQnXTDT4F8b0uciQUDw1LosDZy5MXf03BeIDdyBAtzd/ym3wzd+kw==
|
||||
"@storybook/csf-plugin@8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-8.6.18.tgz#f92cede49c71d4381187884d72e41ee44d324d3b"
|
||||
integrity sha512-x1ioz/L0CwaelCkHci3P31YtvwayN3FBftvwQOPbvRh9qeb4Cpz5IdVDmyvSxxYwXN66uAORNoqgjTi7B4/y5Q==
|
||||
dependencies:
|
||||
unplugin "^1.3.1"
|
||||
|
||||
@@ -3596,30 +3577,30 @@
|
||||
dependencies:
|
||||
type-fest "^2.19.0"
|
||||
|
||||
"@storybook/docs-tools@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/docs-tools/-/docs-tools-8.6.17.tgz#e0beafce8ad36dbadf2e0c3a6bb39ee50ead8c03"
|
||||
integrity sha512-lnGPEecD2nNrByIGhlJOJEi4/3PM+P5DElsFdJ9EhQwO0rwQhTL+4sdBMOXgwsJj4WrQTBXQ1jr/x0UYrl7Qzg==
|
||||
"@storybook/docs-tools@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/docs-tools/-/docs-tools-8.6.18.tgz#ba79b08a41131f97d9c6970c48651552763acbcf"
|
||||
integrity sha512-43ggjDA1ZV0FWjMlNBkKC1VWQ6zDQmSj0WWWqivGQdnBt4dufYQFXnbQeFr9Og+3OjZYmr3KTrLCjDiyCGOgjg==
|
||||
|
||||
"@storybook/icons@^1.2.12":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/@storybook/icons/-/icons-1.4.0.tgz"
|
||||
integrity sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==
|
||||
|
||||
"@storybook/preview-api@^8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-8.6.17.tgz#92fa66f495c520074c88c3373be73e57f2803a5c"
|
||||
integrity sha512-vpTCTkw11wXerYnlG5Q0y4SbFqG9O6GhR0hlYgCn3Z9kcHlNjK/xuwd3h4CvwNXxRNWZGT8qYYCLn5gSSrX6fA==
|
||||
"@storybook/preview-api@^8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-8.6.18.tgz#2f5eb75c7587035a07670457c09b67208aa16735"
|
||||
integrity sha512-joXRXh3GdVvzhbfIgmix1xs90p8Q/nja7AhEAC2egn5Pl7SKsIYZUCYI6UdrQANb2myg9P552LKXfPect8llKg==
|
||||
|
||||
"@storybook/react-dom-shim@8.6.17":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-8.6.17.tgz#68a2e279ac2ce2e37d3f7331a16c5b46cc1c5659"
|
||||
integrity sha512-bHLsR9b/tiwm9lXbN8kp9XlOgkRXeg84UFwXaWBPu3pOO7vRXukk23SQUpLW+HhjKtCJ3xClSi5uMpse5MpkVQ==
|
||||
"@storybook/react-dom-shim@8.6.18":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-8.6.18.tgz#34bdc010d3c3572fc74fa149f754d185df85044e"
|
||||
integrity sha512-N4xULcAWZQTUv4jy1/d346Tyb4gufuC3UaLCuU/iVSZ1brYF4OW3ANr+096btbMxY8pR/65lmtoqr5CTGwnBvA==
|
||||
|
||||
"@storybook/theming@8.6.17", "@storybook/theming@^8.6.15":
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.6.17.tgz#0175bbc22cdc262d171168af67fce6a5e3d76a7f"
|
||||
integrity sha512-IttFvRqozpuzN5MlQEWGOzUA2rZg86688Dyv1d+bjpYcFHtY1X4XyTCGwv1BPTaTsB959oM8R2yoNYWQkABbBA==
|
||||
"@storybook/theming@8.6.18", "@storybook/theming@^8.6.15":
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.6.18.tgz#18c66263868bfb00a419772b5460a5714c5e1181"
|
||||
integrity sha512-n6OEjEtHupa2PdTwWzRepr7cO8NkDd4rgF6BKLitRbujOspLxzMBEqdphs+QLcuiCIgf33SqmEA64QWnbSMhPw==
|
||||
|
||||
"@superset-ui/core@^0.20.4":
|
||||
version "0.20.4"
|
||||
@@ -5509,12 +5490,7 @@ acorn@^6.1.1:
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz"
|
||||
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
|
||||
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0:
|
||||
version "8.15.0"
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
|
||||
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
|
||||
|
||||
acorn@^8.16.0:
|
||||
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.16.0:
|
||||
version "8.16.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
|
||||
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
|
||||
@@ -5673,14 +5649,14 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^6.3.1:
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.1.tgz#ea035d7b0f836a20938945d5a0eaef172537d89b"
|
||||
integrity sha512-8pRjvxitZFyrYAtgwml93Km7fCXjw9IeqlmzpIsusRsmO3eWFVrOMum6+0TsGCtR/WrXVnPwfsgrFg3ChzGCeA==
|
||||
antd@^6.3.2:
|
||||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.2.tgz#ce1a33783d495fcfc77b58b73156ac6249e4fc0a"
|
||||
integrity sha512-IlMoqaXlq5Bgxi0ANERhAzmDREYyGwr/U7MCVihaUQbE/ZOB3r4ArakUxjA1ULYNDA6K00dawSrB8aalGnZlLA==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.1"
|
||||
"@ant-design/cssinjs" "^2.1.0"
|
||||
"@ant-design/cssinjs-utils" "^2.1.1"
|
||||
"@ant-design/cssinjs" "^2.1.2"
|
||||
"@ant-design/cssinjs-utils" "^2.1.2"
|
||||
"@ant-design/fast-color" "^3.0.1"
|
||||
"@ant-design/icons" "^6.1.0"
|
||||
"@ant-design/react-slick" "~2.0.0"
|
||||
@@ -5688,11 +5664,11 @@ antd@^6.3.1:
|
||||
"@rc-component/cascader" "~1.14.0"
|
||||
"@rc-component/checkbox" "~2.0.0"
|
||||
"@rc-component/collapse" "~1.2.0"
|
||||
"@rc-component/color-picker" "~3.1.0"
|
||||
"@rc-component/color-picker" "~3.1.1"
|
||||
"@rc-component/dialog" "~1.8.4"
|
||||
"@rc-component/drawer" "~1.4.2"
|
||||
"@rc-component/dropdown" "~1.0.2"
|
||||
"@rc-component/form" "~1.6.2"
|
||||
"@rc-component/form" "~1.7.1"
|
||||
"@rc-component/image" "~1.6.0"
|
||||
"@rc-component/input" "~1.1.2"
|
||||
"@rc-component/input-number" "~1.6.2"
|
||||
@@ -5708,7 +5684,7 @@ antd@^6.3.1:
|
||||
"@rc-component/rate" "~1.0.1"
|
||||
"@rc-component/resize-observer" "^1.1.1"
|
||||
"@rc-component/segmented" "~1.3.0"
|
||||
"@rc-component/select" "~1.6.12"
|
||||
"@rc-component/select" "~1.6.14"
|
||||
"@rc-component/slider" "~1.0.1"
|
||||
"@rc-component/steps" "~1.2.2"
|
||||
"@rc-component/switch" "~1.0.3"
|
||||
@@ -7710,10 +7686,10 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
enhanced-resolve@^5.19.0:
|
||||
version "5.19.0"
|
||||
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz"
|
||||
integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==
|
||||
enhanced-resolve@^5.20.0:
|
||||
version "5.20.0"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz#323c2a70d2aa7fb4bdfd6d3c24dfc705c581295d"
|
||||
integrity sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.3.0"
|
||||
@@ -13940,7 +13916,7 @@ serialize-error@^8.1.0:
|
||||
dependencies:
|
||||
type-fest "^0.20.2"
|
||||
|
||||
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1, serialize-javascript@^6.0.2:
|
||||
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz"
|
||||
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
|
||||
@@ -14332,12 +14308,12 @@ stop-iteration-iterator@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
internal-slot "^1.1.0"
|
||||
|
||||
storybook@^8.6.17:
|
||||
version "8.6.17"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-8.6.17.tgz#56299bf9e58622bb834fb100eac89c15f7d0de98"
|
||||
integrity sha512-krR/l680A6qVnkGiK9p8jY0ucX3+kFCs2f4zw+S3w2Cdq8EiM/tFebPcX2V4S3z2UsO0v0dwAJOJNpzbFPdmVg==
|
||||
storybook@^8.6.18:
|
||||
version "8.6.18"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-8.6.18.tgz#2a635a4b0c99693f43ba21b8eb511c5cc513a807"
|
||||
integrity sha512-p8seiSI6FiVY6P3V0pG+5v7c8pDMehMAFRWEhG5XqIBSQszzOjDnW2rNvm3odoLKfo3V3P6Cs6Hv9ILzymULyQ==
|
||||
dependencies:
|
||||
"@storybook/core" "8.6.17"
|
||||
"@storybook/core" "8.6.18"
|
||||
|
||||
string-convert@^0.2.0:
|
||||
version "0.2.1"
|
||||
@@ -14691,15 +14667,14 @@ tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0:
|
||||
resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz"
|
||||
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
|
||||
|
||||
terser-webpack-plugin@^5.3.16, terser-webpack-plugin@^5.3.9:
|
||||
version "5.3.16"
|
||||
resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz"
|
||||
integrity sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==
|
||||
terser-webpack-plugin@^5.3.17, terser-webpack-plugin@^5.3.9:
|
||||
version "5.3.17"
|
||||
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz#75ea98876297fbb190d2fbb395e982582b859a67"
|
||||
integrity sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "^0.3.25"
|
||||
jest-worker "^27.4.5"
|
||||
schema-utils "^4.3.0"
|
||||
serialize-javascript "^6.0.2"
|
||||
terser "^5.31.1"
|
||||
|
||||
terser@^5.10.0, terser@^5.15.1, terser@^5.31.1:
|
||||
@@ -15591,11 +15566,6 @@ webpack-merge@^6.0.1:
|
||||
flat "^5.0.2"
|
||||
wildcard "^2.0.1"
|
||||
|
||||
webpack-sources@^3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz"
|
||||
integrity sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==
|
||||
|
||||
webpack-sources@^3.3.4:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.3.4.tgz#a338b95eb484ecc75fbb196cbe8a2890618b4891"
|
||||
@@ -15606,10 +15576,10 @@ webpack-virtual-modules@^0.6.2:
|
||||
resolved "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz"
|
||||
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
|
||||
|
||||
webpack@^5.105.3:
|
||||
version "5.105.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.3.tgz#307ad95bafffd08bc81049d6519477b16e42e7ba"
|
||||
integrity sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==
|
||||
webpack@^5.105.4, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.105.4"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.4.tgz#1b77fcd55a985ac7ca9de80a746caffa38220169"
|
||||
integrity sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.8"
|
||||
@@ -15621,7 +15591,7 @@ webpack@^5.105.3:
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.28.1"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.19.0"
|
||||
enhanced-resolve "^5.20.0"
|
||||
es-module-lexer "^2.0.0"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
@@ -15633,41 +15603,10 @@ webpack@^5.105.3:
|
||||
neo-async "^2.6.2"
|
||||
schema-utils "^4.3.3"
|
||||
tapable "^2.3.0"
|
||||
terser-webpack-plugin "^5.3.16"
|
||||
terser-webpack-plugin "^5.3.17"
|
||||
watchpack "^2.5.1"
|
||||
webpack-sources "^3.3.4"
|
||||
|
||||
webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.105.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.2.tgz#f3b76f9fc36f1152e156e63ffda3bbb82e6739ea"
|
||||
integrity sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.8"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
"@webassemblyjs/ast" "^1.14.1"
|
||||
"@webassemblyjs/wasm-edit" "^1.14.1"
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
acorn "^8.15.0"
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.28.1"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.19.0"
|
||||
es-module-lexer "^2.0.0"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.2.11"
|
||||
json-parse-even-better-errors "^2.3.1"
|
||||
loader-runner "^4.3.1"
|
||||
mime-types "^2.1.27"
|
||||
neo-async "^2.6.2"
|
||||
schema-utils "^4.3.3"
|
||||
tapable "^2.3.0"
|
||||
terser-webpack-plugin "^5.3.16"
|
||||
watchpack "^2.5.1"
|
||||
webpack-sources "^3.3.3"
|
||||
|
||||
webpackbar@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz"
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
#
|
||||
# Security: CVE-2026-21441 - decompression bomb bypass on redirects
|
||||
urllib3>=2.6.3,<3.0.0
|
||||
# Security: GHSA-87hc-h4r5-73f7 - Windows path traversal fix
|
||||
werkzeug>=3.1.5,<4.0.0
|
||||
# Security: CVE-2026-27199 - Windows device name handling in safe_join
|
||||
werkzeug>=3.1.6,<4.0.0
|
||||
# Security: CVE-2025-68146 - TOCTOU symlink vulnerability
|
||||
filelock>=3.20.3,<4.0.0
|
||||
# Security: decompression bomb fix (required by aiohttp 3.13.3)
|
||||
|
||||
@@ -54,7 +54,7 @@ certifi==2025.6.15
|
||||
# via
|
||||
# requests
|
||||
# selenium
|
||||
cffi==1.17.1
|
||||
cffi==2.0.0
|
||||
# via
|
||||
# cryptography
|
||||
# pynacl
|
||||
@@ -86,7 +86,7 @@ cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==44.0.3
|
||||
cryptography==46.0.5
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# paramiko
|
||||
@@ -219,7 +219,7 @@ markupsafe==3.0.2
|
||||
# mako
|
||||
# werkzeug
|
||||
# wtforms
|
||||
marshmallow==3.26.1
|
||||
marshmallow==3.26.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
@@ -317,9 +317,9 @@ pyjwt==2.10.1
|
||||
# flask-appbuilder
|
||||
# flask-jwt-extended
|
||||
# redis
|
||||
pynacl==1.5.0
|
||||
pynacl==1.6.2
|
||||
# via paramiko
|
||||
pyopenssl==25.1.0
|
||||
pyopenssl==25.3.0
|
||||
# via shillelagh
|
||||
pyparsing==3.2.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -457,7 +457,7 @@ wcwidth==0.2.13
|
||||
# via prompt-toolkit
|
||||
websocket-client==1.8.0
|
||||
# via selenium
|
||||
werkzeug==3.1.5
|
||||
werkzeug==3.1.6
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# flask
|
||||
|
||||
@@ -48,7 +48,7 @@ attrs==25.3.0
|
||||
# referencing
|
||||
# requests-cache
|
||||
# trio
|
||||
authlib==1.6.5
|
||||
authlib==1.6.7
|
||||
# via fastmcp
|
||||
babel==2.17.0
|
||||
# via
|
||||
@@ -115,7 +115,7 @@ certifi==2025.6.15
|
||||
# httpx
|
||||
# requests
|
||||
# selenium
|
||||
cffi==1.17.1
|
||||
cffi==2.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# cryptography
|
||||
@@ -177,7 +177,7 @@ croniter==6.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
cryptography==44.0.3
|
||||
cryptography==46.0.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -526,7 +526,7 @@ markupsafe==3.0.2
|
||||
# mako
|
||||
# werkzeug
|
||||
# wtforms
|
||||
marshmallow==3.26.1
|
||||
marshmallow==3.26.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -703,7 +703,7 @@ proto-plus==1.25.0
|
||||
# via
|
||||
# google-api-core
|
||||
# google-cloud-bigquery-storage
|
||||
protobuf==4.25.5
|
||||
protobuf==4.25.8
|
||||
# via
|
||||
# google-api-core
|
||||
# google-cloud-bigquery-storage
|
||||
@@ -786,11 +786,11 @@ pyjwt==2.10.1
|
||||
# redis
|
||||
pylint==3.3.7
|
||||
# via apache-superset
|
||||
pynacl==1.5.0
|
||||
pynacl==1.6.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# paramiko
|
||||
pyopenssl==25.1.0
|
||||
pyopenssl==25.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# shillelagh
|
||||
@@ -1009,7 +1009,7 @@ sshtunnel==0.4.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
starlette==0.48.0
|
||||
starlette==0.49.1
|
||||
# via mcp
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
@@ -1111,7 +1111,7 @@ websocket-client==1.8.0
|
||||
# selenium
|
||||
websockets==15.0.1
|
||||
# via fastmcp
|
||||
werkzeug==3.1.5
|
||||
werkzeug==3.1.6
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask
|
||||
|
||||
@@ -31,70 +31,70 @@ The official core package for building Apache Superset backend extensions and in
|
||||
pip install apache-superset-core
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
## 🏗️ Package Structure
|
||||
|
||||
The package is organized into logical modules, each providing specific functionality:
|
||||
|
||||
- **`api`** - REST API base classes, models access, query utilities, and registration
|
||||
- **`api.models`** - Access to Superset's database models (datasets, databases, etc.)
|
||||
- **`api.query`** - Database query utilities and SQL dialect handling
|
||||
- **`api.rest_api`** - Extension API registration and management
|
||||
- **`api.types.rest_api`** - REST API base classes and type definitions
|
||||
```
|
||||
src/superset_core/
|
||||
├── common/
|
||||
├── extensions/
|
||||
├── mcp/
|
||||
├── queries/
|
||||
├── rest_api/
|
||||
├── tasks/
|
||||
└── __init__.py
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Basic Extension Structure
|
||||
### Basic Extension API
|
||||
|
||||
```python
|
||||
from flask import request, Response
|
||||
from flask_appbuilder.api import expose, permission_name, protect, safe
|
||||
from superset_core.api import models, query, rest_api
|
||||
from superset_core.api.rest_api import RestApi
|
||||
from superset_core.rest_api.api import RestApi
|
||||
from superset_core.rest_api.decorators import api
|
||||
|
||||
|
||||
@api(id="dataset_references", name="Dataset References API")
|
||||
class DatasetReferencesAPI(RestApi):
|
||||
"""Example extension API demonstrating core functionality."""
|
||||
|
||||
resource_name = "dataset_references"
|
||||
openapi_spec_tag = "Dataset references"
|
||||
class_permission_name = "dataset_references"
|
||||
|
||||
@expose("/metadata", methods=("POST",))
|
||||
@protect()
|
||||
@safe
|
||||
@permission_name("read")
|
||||
def metadata(self) -> Response:
|
||||
"""Get dataset metadata for tables referenced in SQL."""
|
||||
sql: str = request.json.get("sql")
|
||||
database_id: int = request.json.get("databaseId")
|
||||
|
||||
# Access Superset's models using core APIs
|
||||
databases = models.get_databases(id=database_id)
|
||||
if not databases:
|
||||
return self.response_404()
|
||||
|
||||
database = databases[0]
|
||||
dialect = query.get_sqlglot_dialect(database)
|
||||
|
||||
# Access datasets to get owner information
|
||||
datasets = models.get_datasets()
|
||||
owners_map = {
|
||||
dataset.table_name: [
|
||||
f"{owner.first_name} {owner.last_name}"
|
||||
for owner in dataset.owners
|
||||
]
|
||||
for dataset in datasets
|
||||
}
|
||||
|
||||
# Process SQL and return dataset metadata
|
||||
return self.response(200, result=owners_map)
|
||||
|
||||
# Register the extension API
|
||||
rest_api.add_extension_api(DatasetReferencesAPI)
|
||||
# ... endpoint implementation
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
### Background Tasks
|
||||
|
||||
We welcome contributions! Please see the [Developer Portal](https://superset.apache.org/developer_portal/) for details.
|
||||
```python
|
||||
from superset_core.tasks.decorators import task
|
||||
from superset_core.tasks.types import TaskScope
|
||||
|
||||
@task(name="generate_report", scope=TaskScope.SHARED)
|
||||
def generate_report(chart_id: int) -> None:
|
||||
# ... task implementation
|
||||
```
|
||||
|
||||
### MCP Tools
|
||||
|
||||
```python
|
||||
from superset_core.mcp.decorators import tool
|
||||
|
||||
@tool(name="my_tool", description="Custom business logic", tags=["extension"])
|
||||
def my_extension_tool(param: str) -> dict:
|
||||
# ... tool implementation
|
||||
```
|
||||
|
||||
### MCP Prompts
|
||||
|
||||
```python
|
||||
from superset_core.mcp.decorators import prompt
|
||||
|
||||
@prompt(name="my_prompt", title="My Prompt", description="Interactive prompt", tags={"extension"})
|
||||
async def my_prompt_handler(ctx: Context) -> str:
|
||||
# ... prompt implementation
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
@@ -102,12 +102,6 @@ Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Apache Superset](https://superset.apache.org/)
|
||||
- [Documentation](https://superset.apache.org/docs/)
|
||||
- [Community](https://superset.apache.org/community/)
|
||||
- [GitHub Repository](https://github.com/apache/superset)
|
||||
- [Extension Development Guide](https://superset.apache.org/docs/extensions/)
|
||||
|
||||
---
|
||||
|
||||
**Note**: This package is currently in release candidate status. APIs may change before the 1.0.0 release. Please check the [changelog](CHANGELOG.md) for breaking changes between versions.
|
||||
- [Extensions Documentation](https://superset.apache.org/developer-docs/extensions/overview)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-core"
|
||||
version = "0.0.1rc4"
|
||||
version = "0.1.0rc1"
|
||||
description = "Core Python package for building Apache Superset backend extensions and integrations"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Data Access Object API for superset-core.
|
||||
Common Data Access Object API for superset-core.
|
||||
|
||||
Provides dependency-injected DAO classes that will be replaced by
|
||||
host implementations during initialization.
|
||||
|
||||
Usage:
|
||||
from superset_core.api.daos import DatasetDAO, DatabaseDAO
|
||||
from superset_core.common.daos import DatasetDAO, DatabaseDAO
|
||||
|
||||
# Use standard BaseDAO methods
|
||||
datasets = DatasetDAO.find_all()
|
||||
@@ -36,17 +36,14 @@ from typing import Any, ClassVar, Generic, TypeVar
|
||||
from flask_appbuilder.models.filters import BaseFilter
|
||||
from sqlalchemy.orm import Query as SQLAQuery
|
||||
|
||||
from superset_core.api.models import (
|
||||
from superset_core.common.models import (
|
||||
Chart,
|
||||
CoreModel,
|
||||
Dashboard,
|
||||
Database,
|
||||
Dataset,
|
||||
KeyValue,
|
||||
Query,
|
||||
SavedQuery,
|
||||
Tag,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
|
||||
@@ -193,34 +190,6 @@ class UserDAO(BaseDAO[User]):
|
||||
id_column_name = "id"
|
||||
|
||||
|
||||
class QueryDAO(BaseDAO[Query]):
|
||||
"""
|
||||
Abstract Query DAO interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
# Class variables that will be set by host implementation
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
|
||||
|
||||
class SavedQueryDAO(BaseDAO[SavedQuery]):
|
||||
"""
|
||||
Abstract SavedQuery DAO interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
# Class variables that will be set by host implementation
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
|
||||
|
||||
class TagDAO(BaseDAO[Tag]):
|
||||
"""
|
||||
Abstract Tag DAO interface.
|
||||
@@ -249,48 +218,6 @@ class KeyValueDAO(BaseDAO[KeyValue]):
|
||||
id_column_name = "id"
|
||||
|
||||
|
||||
class TaskDAO(BaseDAO[Task]):
|
||||
"""
|
||||
Abstract Task DAO interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
# Class variables that will be set by host implementation
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
uuid_column_name = "uuid"
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def find_by_task_key(
|
||||
cls,
|
||||
task_type: str,
|
||||
task_key: str,
|
||||
scope: str = "private",
|
||||
user_id: int | None = None,
|
||||
) -> Task | None:
|
||||
"""
|
||||
Find active task by type, key, scope, and user.
|
||||
|
||||
Uses dedup_key internally for efficient querying with a unique index.
|
||||
Only returns tasks that are active (pending or in progress).
|
||||
|
||||
Uniqueness logic by scope:
|
||||
- private: scope + task_type + task_key + user_id
|
||||
- shared/system: scope + task_type + task_key (user-agnostic)
|
||||
|
||||
:param task_type: Task type to filter by
|
||||
:param task_key: Task identifier for deduplication
|
||||
:param scope: Task scope (private/shared/system)
|
||||
:param user_id: User ID (required for private tasks)
|
||||
:returns: Task instance or None if not found or not active
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseDAO",
|
||||
"DatasetDAO",
|
||||
@@ -298,9 +225,6 @@ __all__ = [
|
||||
"ChartDAO",
|
||||
"DashboardDAO",
|
||||
"UserDAO",
|
||||
"QueryDAO",
|
||||
"SavedQueryDAO",
|
||||
"TagDAO",
|
||||
"KeyValueDAO",
|
||||
"TaskDAO",
|
||||
]
|
||||
@@ -16,13 +16,13 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Model API for superset-core.
|
||||
Common model API for superset-core.
|
||||
|
||||
Provides model classes that will be replaced by host implementations
|
||||
Provides core model classes that will be replaced by host implementations
|
||||
during initialization for extension developers to use.
|
||||
|
||||
Usage:
|
||||
from superset_core.api.models import Dataset, Database, get_session
|
||||
from superset_core.common.models import Dataset, Database, get_session
|
||||
|
||||
# Use as regular model classes
|
||||
dataset = Dataset(name="My Dataset")
|
||||
@@ -40,8 +40,7 @@ from flask_appbuilder import Model
|
||||
from sqlalchemy.orm import scoped_session
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset_core.api.tasks import TaskProperties
|
||||
from superset_core.api.types import (
|
||||
from superset_core.queries.types import (
|
||||
AsyncQueryHandle,
|
||||
QueryOptions,
|
||||
QueryResult,
|
||||
@@ -88,8 +87,8 @@ class Database(CoreModel):
|
||||
def execute(
|
||||
self,
|
||||
sql: str,
|
||||
options: QueryOptions | None = None,
|
||||
) -> QueryResult:
|
||||
options: "QueryOptions | None" = None,
|
||||
) -> "QueryResult":
|
||||
"""
|
||||
Execute SQL synchronously.
|
||||
|
||||
@@ -103,8 +102,8 @@ class Database(CoreModel):
|
||||
:returns: QueryResult with status, data (DataFrame), and metadata
|
||||
|
||||
Example:
|
||||
from superset_core.api.daos import DatabaseDAO
|
||||
from superset_core.api.types import QueryOptions, QueryStatus
|
||||
from superset_core.common.daos import DatabaseDAO
|
||||
from superset_core.queries.types import QueryOptions, QueryStatus
|
||||
|
||||
db = DatabaseDAO.find_one_or_none(id=1)
|
||||
result = db.execute(
|
||||
@@ -136,8 +135,8 @@ class Database(CoreModel):
|
||||
def execute_async(
|
||||
self,
|
||||
sql: str,
|
||||
options: QueryOptions | None = None,
|
||||
) -> AsyncQueryHandle:
|
||||
options: "QueryOptions | None" = None,
|
||||
) -> "AsyncQueryHandle":
|
||||
"""
|
||||
Execute SQL asynchronously.
|
||||
|
||||
@@ -286,47 +285,6 @@ class User(CoreModel):
|
||||
active: bool
|
||||
|
||||
|
||||
class Query(CoreModel):
|
||||
"""
|
||||
Abstract Query model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected attributes (no actual field definitions)
|
||||
id: int
|
||||
client_id: str | None
|
||||
database_id: int | None
|
||||
sql: str | None
|
||||
status: str | None
|
||||
user_id: int | None
|
||||
progress: int
|
||||
error_message: str | None
|
||||
|
||||
|
||||
class SavedQuery(CoreModel):
|
||||
"""
|
||||
Abstract SavedQuery model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected attributes (no actual field definitions)
|
||||
id: int
|
||||
uuid: UUID | None
|
||||
label: str | None
|
||||
sql: str | None
|
||||
database_id: int | None
|
||||
description: str | None
|
||||
user_id: int | None
|
||||
|
||||
|
||||
class Tag(CoreModel):
|
||||
"""
|
||||
Abstract Tag model interface.
|
||||
@@ -362,132 +320,6 @@ class KeyValue(CoreModel):
|
||||
changed_by_fk: int | None
|
||||
|
||||
|
||||
class Task(CoreModel):
|
||||
"""
|
||||
Abstract Task model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
This model represents async tasks in the Global Task Framework (GTF).
|
||||
|
||||
Non-filterable fields (progress, error info, execution config) are stored
|
||||
in a `properties` JSON blob for schema flexibility.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected column attributes
|
||||
id: int
|
||||
uuid: UUID
|
||||
task_key: str # For deduplication
|
||||
task_type: str # e.g., 'sql_execution'
|
||||
task_name: str | None # Human readable name
|
||||
scope: str # private/shared/system
|
||||
status: str
|
||||
dedup_key: str # Computed deduplication key
|
||||
|
||||
# Timestamps (from AuditMixinNullable)
|
||||
created_on: datetime | None
|
||||
changed_on: datetime | None
|
||||
started_at: datetime | None
|
||||
ended_at: datetime | None
|
||||
|
||||
# User context
|
||||
created_by_fk: int | None
|
||||
user_id: int | None
|
||||
|
||||
# Task output data
|
||||
payload: str # JSON serialized task output data
|
||||
|
||||
def get_payload(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get payload as parsed JSON.
|
||||
|
||||
Payload contains task-specific output data set by task code.
|
||||
|
||||
Host implementations will replace this method during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
:returns: Dictionary containing payload data
|
||||
"""
|
||||
raise NotImplementedError("Method will be replaced during initialization")
|
||||
|
||||
def set_payload(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Update payload with new data (merges with existing).
|
||||
|
||||
Host implementations will replace this method during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
:param data: Dictionary of data to merge into payload
|
||||
"""
|
||||
raise NotImplementedError("Method will be replaced during initialization")
|
||||
|
||||
@property
|
||||
def properties(self) -> Any:
|
||||
"""
|
||||
Get typed properties (runtime state and execution config).
|
||||
|
||||
Properties contain:
|
||||
- is_abortable: bool | None - has abort handler registered
|
||||
- progress_percent: float | None - progress 0.0-1.0
|
||||
- progress_current: int | None - current iteration count
|
||||
- progress_total: int | None - total iterations
|
||||
- error_message: str | None - human-readable error message
|
||||
- exception_type: str | None - exception class name
|
||||
- stack_trace: str | None - full formatted traceback
|
||||
- timeout: int | None - timeout in seconds
|
||||
|
||||
Host implementations will replace this property during initialization.
|
||||
|
||||
:returns: TaskProperties dataclass instance
|
||||
"""
|
||||
raise NotImplementedError("Property will be replaced during initialization")
|
||||
|
||||
def update_properties(self, updates: "TaskProperties") -> None:
|
||||
"""
|
||||
Update specific properties fields (merge semantics).
|
||||
|
||||
Only updates fields present in the updates dict.
|
||||
|
||||
Host implementations will replace this method during initialization.
|
||||
|
||||
:param updates: TaskProperties dict with fields to update
|
||||
|
||||
Example:
|
||||
task.update_properties({"is_abortable": True})
|
||||
"""
|
||||
raise NotImplementedError("Method will be replaced during initialization")
|
||||
|
||||
|
||||
class TaskSubscriber(CoreModel):
|
||||
"""
|
||||
Abstract TaskSubscriber model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
This model tracks task subscriptions for multi-user shared tasks. When a user
|
||||
schedules a shared task with the same parameters as an existing task,
|
||||
they are subscribed to that task instead of creating a duplicate.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected attributes (no actual field definitions)
|
||||
id: int
|
||||
task_id: int
|
||||
user_id: int
|
||||
subscribed_at: datetime
|
||||
|
||||
# Audit fields from AuditMixinNullable
|
||||
created_on: datetime | None
|
||||
changed_on: datetime | None
|
||||
created_by_fk: int | None
|
||||
changed_by_fk: int | None
|
||||
|
||||
|
||||
def get_session() -> scoped_session:
|
||||
"""
|
||||
Retrieve the SQLAlchemy session to directly interface with the
|
||||
@@ -507,12 +339,8 @@ __all__ = [
|
||||
"Chart",
|
||||
"Dashboard",
|
||||
"User",
|
||||
"Query",
|
||||
"SavedQuery",
|
||||
"Tag",
|
||||
"KeyValue",
|
||||
"Task",
|
||||
"TaskSubscriber",
|
||||
"CoreModel",
|
||||
"get_session",
|
||||
]
|
||||
16
superset-core/src/superset_core/mcp/__init__.py
Normal file
16
superset-core/src/superset_core/mcp/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
@@ -22,7 +22,7 @@ This module provides a decorator interface to register MCP tools with the
|
||||
host application.
|
||||
|
||||
Usage:
|
||||
from superset_core.api.mcp import tool
|
||||
from superset_core.mcp.decorators import tool
|
||||
|
||||
@tool(name="my_tool", description="Custom business logic", tags=["extension"])
|
||||
def my_extension_tool(param: str) -> dict:
|
||||
16
superset-core/src/superset_core/queries/__init__.py
Normal file
16
superset-core/src/superset_core/queries/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
63
superset-core/src/superset_core/queries/daos.py
Normal file
63
superset-core/src/superset_core/queries/daos.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Query Data Access Object API for superset-core.
|
||||
|
||||
Provides query-related DAO classes that will be replaced by host implementations
|
||||
during initialization.
|
||||
|
||||
Usage:
|
||||
from superset_core.queries.daos import QueryDAO, SavedQueryDAO
|
||||
"""
|
||||
|
||||
from superset_core.common.daos import BaseDAO
|
||||
from superset_core.queries.models import Query, SavedQuery
|
||||
|
||||
|
||||
class QueryDAO(BaseDAO[Query]):
|
||||
"""
|
||||
Abstract Query DAO interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
# Class variables that will be set by host implementation
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
|
||||
|
||||
class SavedQueryDAO(BaseDAO[SavedQuery]):
|
||||
"""
|
||||
Abstract SavedQuery DAO interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
# Class variables that will be set by host implementation
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QueryDAO",
|
||||
"SavedQueryDAO",
|
||||
]
|
||||
79
superset-core/src/superset_core/queries/models.py
Normal file
79
superset-core/src/superset_core/queries/models.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Query model API for superset-core.
|
||||
|
||||
Provides query-related model classes that will be replaced by host implementations
|
||||
during initialization for extension developers to use.
|
||||
|
||||
Usage:
|
||||
from superset_core.queries.models import Query, SavedQuery
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from superset_core.common.models import CoreModel
|
||||
|
||||
|
||||
class Query(CoreModel):
|
||||
"""
|
||||
Abstract Query model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected attributes (no actual field definitions)
|
||||
id: int
|
||||
client_id: str | None
|
||||
database_id: int | None
|
||||
sql: str | None
|
||||
status: str | None
|
||||
user_id: int | None
|
||||
progress: int
|
||||
error_message: str | None
|
||||
|
||||
|
||||
class SavedQuery(CoreModel):
|
||||
"""
|
||||
Abstract SavedQuery model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected attributes (no actual field definitions)
|
||||
id: int
|
||||
uuid: UUID | None
|
||||
label: str | None
|
||||
sql: str | None
|
||||
database_id: int | None
|
||||
description: str | None
|
||||
user_id: int | None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Query",
|
||||
"SavedQuery",
|
||||
]
|
||||
@@ -22,7 +22,7 @@ Provides dependency-injected query utility functions that will be replaced by
|
||||
host implementations during initialization.
|
||||
|
||||
Usage:
|
||||
from superset_core.api.query import get_sqlglot_dialect
|
||||
from superset_core.queries.query import get_sqlglot_dialect
|
||||
|
||||
dialect = get_sqlglot_dialect(database)
|
||||
"""
|
||||
@@ -32,7 +32,7 @@ from typing import TYPE_CHECKING
|
||||
from sqlglot import Dialects
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset_core.api.models import Database
|
||||
from superset_core.common.models import Database
|
||||
|
||||
|
||||
def get_sqlglot_dialect(database: "Database") -> Dialects:
|
||||
16
superset-core/src/superset_core/rest_api/__init__.py
Normal file
16
superset-core/src/superset_core/rest_api/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
32
superset-core/src/superset_core/rest_api/api.py
Normal file
32
superset-core/src/superset_core/rest_api/api.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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.
|
||||
|
||||
from flask_appbuilder.api import BaseApi
|
||||
|
||||
|
||||
class RestApi(BaseApi):
|
||||
"""
|
||||
Base REST API class for Superset with browser login support.
|
||||
|
||||
This class extends Flask-AppBuilder's BaseApi and enables browser-based
|
||||
authentication by default.
|
||||
"""
|
||||
|
||||
allow_browser_login = True
|
||||
|
||||
|
||||
__all__ = ["RestApi"]
|
||||
@@ -16,15 +16,11 @@
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
REST API functions and decorators for superset-core.
|
||||
|
||||
Provides dependency-injected REST API utility functions and decorators that will be
|
||||
replaced by host implementations during initialization.
|
||||
REST API decorator for superset-core.
|
||||
|
||||
Usage:
|
||||
from superset_core.api.rest_api import api
|
||||
from superset_core.rest_api.decorators import api
|
||||
|
||||
# Unified decorator for both host and extension APIs
|
||||
@api(
|
||||
id="main_api",
|
||||
name="Main API",
|
||||
@@ -34,25 +30,15 @@ Usage:
|
||||
pass
|
||||
"""
|
||||
|
||||
from typing import Callable, TypeVar
|
||||
from typing import Callable, TYPE_CHECKING, TypeVar
|
||||
|
||||
from flask_appbuilder.api import BaseApi
|
||||
if TYPE_CHECKING:
|
||||
from superset_core.rest_api.api import RestApi
|
||||
|
||||
# Type variable for decorated API classes
|
||||
T = TypeVar("T", bound=type["RestApi"])
|
||||
|
||||
|
||||
class RestApi(BaseApi):
|
||||
"""
|
||||
Base REST API class for Superset with browser login support.
|
||||
|
||||
This class extends Flask-AppBuilder's BaseApi and enables browser-based
|
||||
authentication by default.
|
||||
"""
|
||||
|
||||
allow_browser_login = True
|
||||
|
||||
|
||||
def api(
|
||||
id: str,
|
||||
name: str,
|
||||
@@ -114,4 +100,4 @@ def api(
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["RestApi", "api"]
|
||||
__all__ = ["api"]
|
||||
16
superset-core/src/superset_core/tasks/__init__.py
Normal file
16
superset-core/src/superset_core/tasks/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# 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.
|
||||
76
superset-core/src/superset_core/tasks/daos.py
Normal file
76
superset-core/src/superset_core/tasks/daos.py
Normal file
@@ -0,0 +1,76 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Task Data Access Object API for superset-core.
|
||||
|
||||
Provides task-related DAO classes that will be replaced by host implementations
|
||||
during initialization.
|
||||
|
||||
Usage:
|
||||
from superset_core.tasks.daos import TaskDAO
|
||||
"""
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
from superset_core.common.daos import BaseDAO
|
||||
from superset_core.tasks.models import Task
|
||||
|
||||
|
||||
class TaskDAO(BaseDAO[Task]):
|
||||
"""
|
||||
Abstract Task DAO interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
# Class variables that will be set by host implementation
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
uuid_column_name = "uuid"
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def find_by_task_key(
|
||||
cls,
|
||||
task_type: str,
|
||||
task_key: str,
|
||||
scope: str = "private",
|
||||
user_id: int | None = None,
|
||||
) -> Task | None:
|
||||
"""
|
||||
Find active task by type, key, scope, and user.
|
||||
|
||||
Uses dedup_key internally for efficient querying with a unique index.
|
||||
Only returns tasks that are active (pending or in progress).
|
||||
|
||||
Uniqueness logic by scope:
|
||||
- private: scope + task_type + task_key + user_id
|
||||
- shared/system: scope + task_type + task_key (user-agnostic)
|
||||
|
||||
:param task_type: Task type to filter by
|
||||
:param task_key: Task identifier for deduplication
|
||||
:param scope: Task scope (private/shared/system)
|
||||
:param user_id: User ID (required for private tasks)
|
||||
:returns: Task instance or None if not found or not active
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
__all__ = ["TaskDAO"]
|
||||
152
superset-core/src/superset_core/tasks/decorators.py
Normal file
152
superset-core/src/superset_core/tasks/decorators.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# 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.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Generic, ParamSpec, TYPE_CHECKING, TypeVar
|
||||
|
||||
from superset_core.tasks.types import TaskContext, TaskScope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset_core.tasks.models import Task
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def task(
|
||||
name: str | None = None,
|
||||
scope: TaskScope = TaskScope.PRIVATE,
|
||||
timeout: int | None = None,
|
||||
) -> Callable[[Callable[P, R]], "TaskWrapper[P]"]:
|
||||
"""
|
||||
Decorator to register a task.
|
||||
|
||||
Host implementations will replace this function during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
:param name: Optional unique task name (e.g., "superset.generate_thumbnail").
|
||||
If not provided, uses the function name as the task name.
|
||||
:param scope: Task scope (TaskScope.PRIVATE, SHARED, or SYSTEM).
|
||||
Defaults to TaskScope.PRIVATE.
|
||||
:param timeout: Optional timeout in seconds. When the timeout is reached,
|
||||
abort handlers are triggered if registered. Can be overridden
|
||||
at call time via TaskOptions(timeout=...).
|
||||
:returns: TaskWrapper with .schedule() method
|
||||
|
||||
Note:
|
||||
Both direct calls and .schedule() return Task, regardless of the
|
||||
original function's return type. The decorated function's return value
|
||||
is discarded; only side effects and context updates matter.
|
||||
|
||||
Example:
|
||||
from superset_core.tasks.decorators import task, get_context
|
||||
from superset_core.tasks.types import TaskScope
|
||||
|
||||
# Private task (default scope)
|
||||
@task
|
||||
def generate_thumbnail(chart_id: int) -> None:
|
||||
ctx = get_context()
|
||||
# ... task implementation
|
||||
|
||||
# Named task with shared scope
|
||||
@task(name="generate_report", scope=TaskScope.SHARED)
|
||||
def generate_chart_thumbnail(chart_id: int) -> None:
|
||||
ctx = get_context()
|
||||
|
||||
# Update progress and payload atomically
|
||||
ctx.update_task(
|
||||
progress=0.5,
|
||||
payload={"chart_id": chart_id, "status": "processing"}
|
||||
)
|
||||
# ... task implementation
|
||||
|
||||
ctx.update_task(progress=1.0)
|
||||
|
||||
# System task (admin-only)
|
||||
@task(scope=TaskScope.SYSTEM)
|
||||
def cleanup_old_data() -> None:
|
||||
ctx = get_context()
|
||||
# ... cleanup implementation
|
||||
|
||||
# Task with timeout
|
||||
@task(timeout=300) # 5-minute timeout
|
||||
def long_running_task() -> None:
|
||||
ctx = get_context()
|
||||
|
||||
@ctx.on_abort
|
||||
def handle_abort():
|
||||
# Called when timeout or manual abort
|
||||
pass
|
||||
|
||||
# Schedule async execution
|
||||
task = generate_chart_thumbnail.schedule(chart_id=123) # Returns Task
|
||||
|
||||
# Direct call for sync execution (blocks until task is complete)
|
||||
task = generate_chart_thumbnail(chart_id=123) # Also returns Task
|
||||
"""
|
||||
raise NotImplementedError("Function will be replaced during initialization")
|
||||
|
||||
|
||||
class TaskWrapper(Generic[P]):
|
||||
"""
|
||||
Type stub for task wrapper returned by @task decorator.
|
||||
|
||||
Both __call__ and .schedule() return Task.
|
||||
"""
|
||||
|
||||
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> "Task":
|
||||
"""Execute the task synchronously."""
|
||||
raise NotImplementedError("Will be replaced during initialization")
|
||||
|
||||
def schedule(self, *args: P.args, **kwargs: P.kwargs) -> "Task":
|
||||
"""Schedule the task for async execution."""
|
||||
raise NotImplementedError("Will be replaced during initialization")
|
||||
|
||||
|
||||
def get_context() -> TaskContext:
|
||||
"""
|
||||
Get the current task context from ambient context.
|
||||
|
||||
Host implementations will replace this function during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
This function provides ambient access to the task context without
|
||||
requiring it to be passed as a parameter. It can only be called
|
||||
from within an async task execution.
|
||||
|
||||
:returns: The current TaskContext
|
||||
:raises RuntimeError: If called outside a task execution context
|
||||
|
||||
Example:
|
||||
@task("thumbnail_generation")
|
||||
def generate_chart_thumbnail(chart_id: int):
|
||||
ctx = get_context() # Access ambient context
|
||||
|
||||
# Update task state - no need to fetch task object
|
||||
ctx.update_task(
|
||||
progress=0.5,
|
||||
payload={"chart_id": chart_id}
|
||||
)
|
||||
"""
|
||||
raise NotImplementedError("Function will be replaced during initialization")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"task",
|
||||
"get_context",
|
||||
]
|
||||
169
superset-core/src/superset_core/tasks/models.py
Normal file
169
superset-core/src/superset_core/tasks/models.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Task model API for superset-core.
|
||||
|
||||
Provides task-related model classes that will be replaced by host implementations
|
||||
during initialization for extension developers to use.
|
||||
|
||||
Usage:
|
||||
from superset_core.tasks.models import Task, TaskSubscriber
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from superset_core.common.models import CoreModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset_core.tasks.types import TaskProperties
|
||||
|
||||
|
||||
class Task(CoreModel):
|
||||
"""
|
||||
Abstract Task model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
This model represents async tasks in the Global Task Framework (GTF).
|
||||
|
||||
Non-filterable fields (progress, error info, execution config) are stored
|
||||
in a `properties` JSON blob for schema flexibility.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected column attributes
|
||||
id: int
|
||||
uuid: UUID
|
||||
task_key: str # For deduplication
|
||||
task_type: str # e.g., 'sql_execution'
|
||||
task_name: str | None # Human readable name
|
||||
scope: str # private/shared/system
|
||||
status: str
|
||||
dedup_key: str # Computed deduplication key
|
||||
|
||||
# Timestamps (from AuditMixinNullable)
|
||||
created_on: datetime | None
|
||||
changed_on: datetime | None
|
||||
started_at: datetime | None
|
||||
ended_at: datetime | None
|
||||
|
||||
# User context
|
||||
created_by_fk: int | None
|
||||
user_id: int | None
|
||||
|
||||
# Task output data
|
||||
payload: str # JSON serialized task output data
|
||||
|
||||
def get_payload(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get payload as parsed JSON.
|
||||
|
||||
Payload contains task-specific output data set by task code.
|
||||
|
||||
Host implementations will replace this method during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
:returns: Dictionary containing payload data
|
||||
"""
|
||||
raise NotImplementedError("Method will be replaced during initialization")
|
||||
|
||||
def set_payload(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Update payload with new data (merges with existing).
|
||||
|
||||
Host implementations will replace this method during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
:param data: Dictionary of data to merge into payload
|
||||
"""
|
||||
raise NotImplementedError("Method will be replaced during initialization")
|
||||
|
||||
@property
|
||||
def properties(self) -> Any:
|
||||
"""
|
||||
Get typed properties (runtime state and execution config).
|
||||
|
||||
Properties contain:
|
||||
- is_abortable: bool | None - has abort handler registered
|
||||
- progress_percent: float | None - progress 0.0-1.0
|
||||
- progress_current: int | None - current iteration count
|
||||
- progress_total: int | None - total iterations
|
||||
- error_message: str | None - human-readable error message
|
||||
- exception_type: str | None - exception class name
|
||||
- stack_trace: str | None - full formatted traceback
|
||||
- timeout: int | None - timeout in seconds
|
||||
|
||||
Host implementations will replace this property during initialization.
|
||||
|
||||
:returns: TaskProperties dataclass instance
|
||||
"""
|
||||
raise NotImplementedError("Property will be replaced during initialization")
|
||||
|
||||
def update_properties(self, updates: "TaskProperties") -> None:
|
||||
"""
|
||||
Update specific properties fields (merge semantics).
|
||||
|
||||
Only updates fields present in the updates dict.
|
||||
|
||||
Host implementations will replace this method during initialization.
|
||||
|
||||
:param updates: TaskProperties dict with fields to update
|
||||
|
||||
Example:
|
||||
task.update_properties({"is_abortable": True})
|
||||
"""
|
||||
raise NotImplementedError("Method will be replaced during initialization")
|
||||
|
||||
|
||||
class TaskSubscriber(CoreModel):
|
||||
"""
|
||||
Abstract TaskSubscriber model interface.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with concrete implementation providing actual functionality.
|
||||
|
||||
This model tracks task subscriptions for multi-user shared tasks. When a user
|
||||
schedules a shared task with the same parameters as an existing task,
|
||||
they are subscribed to that task instead of creating a duplicate.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected attributes (no actual field definitions)
|
||||
id: int
|
||||
task_id: int
|
||||
user_id: int
|
||||
subscribed_at: datetime
|
||||
|
||||
# Audit fields from AuditMixinNullable
|
||||
created_on: datetime | None
|
||||
changed_on: datetime | None
|
||||
created_by_fk: int | None
|
||||
changed_by_fk: int | None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Task",
|
||||
"TaskSubscriber",
|
||||
]
|
||||
@@ -20,12 +20,7 @@ from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Generic, Literal, ParamSpec, TypedDict, TypeVar
|
||||
|
||||
from superset_core.api.models import Task
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
from typing import Any, Callable, Literal, TypedDict
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
@@ -104,7 +99,7 @@ class TaskOptions:
|
||||
- Retry policies and backoff strategies
|
||||
|
||||
Example:
|
||||
from superset_core.api.tasks import TaskOptions, TaskScope
|
||||
from superset_core.tasks.types import TaskOptions, TaskScope
|
||||
|
||||
# Private task (default)
|
||||
task = my_task.schedule(arg1)
|
||||
@@ -233,129 +228,10 @@ class TaskContext(ABC):
|
||||
...
|
||||
|
||||
|
||||
def task(
|
||||
name: str | None = None,
|
||||
scope: TaskScope = TaskScope.PRIVATE,
|
||||
timeout: int | None = None,
|
||||
) -> Callable[[Callable[P, R]], "TaskWrapper[P]"]:
|
||||
"""
|
||||
Decorator to register a task.
|
||||
|
||||
Host implementations will replace this function during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
:param name: Optional unique task name (e.g., "superset.generate_thumbnail").
|
||||
If not provided, uses the function name as the task name.
|
||||
:param scope: Task scope (TaskScope.PRIVATE, SHARED, or SYSTEM).
|
||||
Defaults to TaskScope.PRIVATE.
|
||||
:param timeout: Optional timeout in seconds. When the timeout is reached,
|
||||
abort handlers are triggered if registered. Can be overridden
|
||||
at call time via TaskOptions(timeout=...).
|
||||
:returns: TaskWrapper with .schedule() method
|
||||
|
||||
Note:
|
||||
Both direct calls and .schedule() return Task, regardless of the
|
||||
original function's return type. The decorated function's return value
|
||||
is discarded; only side effects and context updates matter.
|
||||
|
||||
Example:
|
||||
from superset_core.api.tasks import task, get_context, TaskScope
|
||||
|
||||
# Private task (default scope)
|
||||
@task
|
||||
def generate_thumbnail(chart_id: int) -> None:
|
||||
ctx = get_context()
|
||||
# ... task implementation
|
||||
|
||||
# Named task with shared scope
|
||||
@task(name="generate_report", scope=TaskScope.SHARED)
|
||||
def generate_chart_thumbnail(chart_id: int) -> None:
|
||||
ctx = get_context()
|
||||
|
||||
# Update progress and payload atomically
|
||||
ctx.update_task(
|
||||
progress=0.5,
|
||||
payload={"chart_id": chart_id, "status": "processing"}
|
||||
)
|
||||
# ... task implementation
|
||||
|
||||
ctx.update_task(progress=1.0)
|
||||
|
||||
# System task (admin-only)
|
||||
@task(scope=TaskScope.SYSTEM)
|
||||
def cleanup_old_data() -> None:
|
||||
ctx = get_context()
|
||||
# ... cleanup implementation
|
||||
|
||||
# Task with timeout
|
||||
@task(timeout=300) # 5-minute timeout
|
||||
def long_running_task() -> None:
|
||||
ctx = get_context()
|
||||
|
||||
@ctx.on_abort
|
||||
def handle_abort():
|
||||
# Called when timeout or manual abort
|
||||
pass
|
||||
|
||||
# Schedule async execution
|
||||
task = generate_chart_thumbnail.schedule(chart_id=123) # Returns Task
|
||||
|
||||
# Direct call for sync execution (blocks until task is complete)
|
||||
task = generate_chart_thumbnail(chart_id=123) # Also returns Task
|
||||
"""
|
||||
raise NotImplementedError("Function will be replaced during initialization")
|
||||
|
||||
|
||||
class TaskWrapper(Generic[P]):
|
||||
"""
|
||||
Type stub for task wrapper returned by @task decorator.
|
||||
|
||||
Both __call__ and .schedule() return Task.
|
||||
"""
|
||||
|
||||
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Task:
|
||||
"""Execute the task synchronously."""
|
||||
raise NotImplementedError("Will be replaced during initialization")
|
||||
|
||||
def schedule(self, *args: P.args, **kwargs: P.kwargs) -> Task:
|
||||
"""Schedule the task for async execution."""
|
||||
raise NotImplementedError("Will be replaced during initialization")
|
||||
|
||||
|
||||
def get_context() -> TaskContext:
|
||||
"""
|
||||
Get the current task context from ambient context.
|
||||
|
||||
Host implementations will replace this function during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
This function provides ambient access to the task context without
|
||||
requiring it to be passed as a parameter. It can only be called
|
||||
from within an async task execution.
|
||||
|
||||
:returns: The current TaskContext
|
||||
:raises RuntimeError: If called outside a task execution context
|
||||
|
||||
Example:
|
||||
@task("thumbnail_generation")
|
||||
def generate_chart_thumbnail(chart_id: int):
|
||||
ctx = get_context() # Access ambient context
|
||||
|
||||
# Update task state - no need to fetch task object
|
||||
ctx.update_task(
|
||||
progress=0.5,
|
||||
payload={"chart_id": chart_id}
|
||||
)
|
||||
"""
|
||||
raise NotImplementedError("Function will be replaced during initialization")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TaskStatus",
|
||||
"TaskScope",
|
||||
"TaskProperties",
|
||||
"TaskContext",
|
||||
"TaskOptions",
|
||||
"task",
|
||||
"get_context",
|
||||
]
|
||||
@@ -1 +1 @@
|
||||
v20.18.3
|
||||
v22.22.0
|
||||
|
||||
@@ -232,6 +232,7 @@ export async function embedDashboard({
|
||||
});
|
||||
iframe.src = `${supersetDomain}/embedded/${id}${urlParamsString}`;
|
||||
iframe.title = iframeTitle;
|
||||
iframe.style.background = 'transparent';
|
||||
if (iframeAllowExtras.length > 0) {
|
||||
iframe.setAttribute('allow', iframeAllowExtras.join('; '));
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ Official command-line interface for building, bundling, and managing Apache Supe
|
||||
## 🚀 Features
|
||||
|
||||
- **Extension Scaffolding** - Generate initial folder structure and scaffold new extension projects
|
||||
- **Validation** - Validate extension structure and configuration before building
|
||||
- **Development Server** - Automatically rebuild extensions as files change during development
|
||||
- **Build System** - Build extension assets for production deployment
|
||||
- **Bundle Packaging** - Package extensions into distributable .supx files
|
||||
@@ -43,68 +44,52 @@ pip install apache-superset-extensions-cli
|
||||
### Available Commands
|
||||
|
||||
```bash
|
||||
# Generate initial folder structure and scaffold a new extension project
|
||||
superset-extensions init <extension-name>
|
||||
# Scaffold a new extension project (interactive prompts, or pass options directly)
|
||||
superset-extensions init [--publisher <publisher>] [--name <name>] [--display-name <name>]
|
||||
[--version <version>] [--license <license>]
|
||||
[--frontend/--no-frontend] [--backend/--no-backend]
|
||||
|
||||
# Validate extension structure and configuration
|
||||
superset-extensions validate
|
||||
|
||||
# Build extension assets for production (runs validate first)
|
||||
superset-extensions build
|
||||
|
||||
# Package extension into a distributable .supx file (runs build first)
|
||||
superset-extensions bundle [--output/-o <path>]
|
||||
|
||||
# Automatically rebuild extension as files change during development
|
||||
superset-extensions dev
|
||||
|
||||
# Build extension assets for production
|
||||
superset-extensions build
|
||||
|
||||
# Package extension into a distributable .supx file
|
||||
superset-extensions bundle
|
||||
```
|
||||
|
||||
## 📋 Extension Structure
|
||||
|
||||
The CLI generates extensions with the following structure:
|
||||
The CLI scaffolds extensions with the following structure:
|
||||
|
||||
```
|
||||
extension_name/
|
||||
{publisher}.{name}/ # e.g., my-org.dashboard-widgets/
|
||||
├── extension.json # Extension configuration and metadata
|
||||
├── frontend/ # Frontend code
|
||||
│ ├── src/ # TypeScript/React source files
|
||||
│ ├── webpack.config.js # Frontend build configuration
|
||||
│ ├── tsconfig.json # TypeScript configuration
|
||||
│ └── package.json # Frontend dependencies
|
||||
├── backend/ # Backend code
|
||||
├── .gitignore
|
||||
├── frontend/ # Optional frontend code
|
||||
│ ├── src/
|
||||
│ │ └── dataset_references/ # Python package source
|
||||
│ ├── tests/ # Backend tests
|
||||
│ ├── pyproject.toml # Python package configuration
|
||||
│ └── requirements.txt # Python dependencies
|
||||
├── dist/ # Built extension files (generated)
|
||||
│ ├── manifest.json # Generated extension manifest
|
||||
│ ├── frontend/
|
||||
│ │ └── dist/ # Built frontend assets
|
||||
│ │ ├── remoteEntry.*.js # Module federation entry
|
||||
│ │ └── *.js # Additional frontend bundles
|
||||
│ └── backend/
|
||||
│ └── dataset_references/ # Built backend package
|
||||
│ ├── __init__.py
|
||||
│ ├── api.py
|
||||
│ └── entrypoint.py
|
||||
├── dataset_references-1.0.0.supx # Packaged extension file (generated)
|
||||
└── README.md # Extension documentation
|
||||
│ │ └── index.tsx # Frontend entry point
|
||||
│ ├── package.json
|
||||
│ ├── webpack.config.js
|
||||
│ └── tsconfig.json
|
||||
└── backend/ # Optional backend code
|
||||
├── src/
|
||||
│ └── {publisher}/ # e.g., my_org/
|
||||
│ └── {name}/ # e.g., dashboard_widgets/
|
||||
│ └── entrypoint.py
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please see the [Developer Portal](https://superset.apache.org/developer_portal/) for details.
|
||||
|
||||
## 📄 License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/apache/superset/blob/master/LICENSE.txt) for details.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Apache Superset](https://superset.apache.org/)
|
||||
- [Extension Development Guide](https://superset.apache.org/docs/extensions/)
|
||||
- [API Documentation](https://superset.apache.org/docs/api/)
|
||||
- [GitHub Repository](https://github.com/apache/superset)
|
||||
- [Community](https://superset.apache.org/community/)
|
||||
|
||||
---
|
||||
|
||||
**Note**: This package is currently in early development. APIs and commands may change before the 1.0.0 release. Please check the [changelog](CHANGELOG.md) for breaking changes between versions.
|
||||
- [GitHub Repository](https://github.com/apache/superset)
|
||||
- [Extensions Documentation](https://superset.apache.org/developer-docs/extensions/overview)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-extensions-cli"
|
||||
version = "0.0.1rc2"
|
||||
version = "0.1.0rc1"
|
||||
description = "Official command-line interface for building, bundling, and managing Apache Superset extensions"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
@@ -167,7 +167,7 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
|
||||
# Generate conventional entry point
|
||||
publisher_snake = kebab_to_snake_case(extension.publisher)
|
||||
name_snake = kebab_to_snake_case(extension.name)
|
||||
entrypoint = f"superset_extensions.{publisher_snake}.{name_snake}.entrypoint"
|
||||
entrypoint = f"{publisher_snake}.{name_snake}.entrypoint"
|
||||
backend = ManifestBackend(entrypoint=entrypoint)
|
||||
|
||||
return Manifest(
|
||||
@@ -344,12 +344,7 @@ def validate() -> None:
|
||||
publisher_snake = kebab_to_snake_case(extension.publisher)
|
||||
name_snake = kebab_to_snake_case(extension.name)
|
||||
expected_entry_file = (
|
||||
backend_dir
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ publisher_snake
|
||||
/ name_snake
|
||||
/ "entrypoint.py"
|
||||
backend_dir / "src" / publisher_snake / name_snake / "entrypoint.py"
|
||||
)
|
||||
|
||||
if not expected_entry_file.exists():
|
||||
@@ -359,7 +354,7 @@ def validate() -> None:
|
||||
fg="red",
|
||||
)
|
||||
click.secho(
|
||||
f" Convention requires: backend/src/superset_extensions/{publisher_snake}/{name_snake}/entrypoint.py",
|
||||
f" Convention requires: backend/src/{publisher_snake}/{name_snake}/entrypoint.py",
|
||||
fg="yellow",
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -713,23 +708,19 @@ def init(
|
||||
(frontend_src_dir / "index.tsx").write_text(index_tsx)
|
||||
click.secho("✅ Created frontend folder structure", fg="green")
|
||||
|
||||
# Initialize backend files with superset_extensions.publisher.name structure
|
||||
# Initialize backend files with publisher.name structure
|
||||
if include_backend:
|
||||
backend_dir = target_dir / "backend"
|
||||
backend_dir.mkdir()
|
||||
backend_src_dir = backend_dir / "src"
|
||||
backend_src_dir.mkdir()
|
||||
|
||||
# Create superset_extensions namespace directory
|
||||
namespace_dir = backend_src_dir / "superset_extensions"
|
||||
namespace_dir.mkdir()
|
||||
|
||||
# Create publisher directory (e.g., superset_extensions/my_org)
|
||||
# Create publisher directory (e.g., my_org)
|
||||
publisher_snake = kebab_to_snake_case(names["publisher"])
|
||||
publisher_dir = namespace_dir / publisher_snake
|
||||
publisher_dir = backend_src_dir / publisher_snake
|
||||
publisher_dir.mkdir()
|
||||
|
||||
# Create extension package directory (e.g., superset_extensions/my_org/dashboard_widgets)
|
||||
# Create extension package directory (e.g., my_org/dashboard_widgets)
|
||||
name_snake = kebab_to_snake_case(names["name"])
|
||||
extension_package_dir = publisher_dir / name_snake
|
||||
extension_package_dir.mkdir()
|
||||
@@ -738,13 +729,7 @@ def init(
|
||||
pyproject_toml = env.get_template("backend/pyproject.toml.j2").render(ctx)
|
||||
(backend_dir / "pyproject.toml").write_text(pyproject_toml)
|
||||
|
||||
# Namespace package __init__.py (empty for namespace)
|
||||
(namespace_dir / "__init__.py").write_text("")
|
||||
(publisher_dir / "__init__.py").write_text("")
|
||||
|
||||
# Extension package files
|
||||
init_py = env.get_template("backend/src/package/__init__.py.j2").render(ctx)
|
||||
(extension_package_dir / "__init__.py").write_text(init_py)
|
||||
entrypoint_py = env.get_template("backend/src/package/entrypoint.py.j2").render(
|
||||
ctx
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ class ExtensionNames(TypedDict):
|
||||
# Backend package name with hyphens for distribution (e.g., "my_org-dashboard_widgets")
|
||||
backend_package: str
|
||||
|
||||
# Full backend import path (e.g., "superset_extensions.my_org.dashboard_widgets")
|
||||
# Full backend import path (e.g., "my_org.dashboard_widgets")
|
||||
backend_path: str
|
||||
|
||||
# Backend entry point (e.g., "superset_extensions.my_org.dashboard_widgets.entrypoint")
|
||||
# Backend entry point (e.g., "my_org.dashboard_widgets.entrypoint")
|
||||
backend_entry: str
|
||||
|
||||
@@ -361,7 +361,7 @@ def generate_extension_names(
|
||||
publisher_snake = kebab_to_snake_case(publisher)
|
||||
name_snake = kebab_to_snake_case(technical_name)
|
||||
backend_package = f"{publisher_snake}-{name_snake}"
|
||||
backend_path = f"superset_extensions.{publisher_snake}.{name_snake}"
|
||||
backend_path = f"{publisher_snake}.{name_snake}"
|
||||
backend_entry = f"{backend_path}.entrypoint"
|
||||
|
||||
# Validate the generated names
|
||||
|
||||
@@ -133,14 +133,7 @@ def extension_setup_for_bundling():
|
||||
(frontend_dir / "main.js").write_text("// main js")
|
||||
|
||||
# Create some backend files - updated path structure
|
||||
backend_dir = (
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_extension"
|
||||
)
|
||||
backend_dir = dist_dir / "backend" / "src" / "test_org" / "test_extension"
|
||||
backend_dir.mkdir(parents=True)
|
||||
(backend_dir / "__init__.py").write_text("# init")
|
||||
|
||||
|
||||
@@ -56,13 +56,7 @@ def extension_with_build_structure():
|
||||
backend_dir.mkdir()
|
||||
|
||||
# Create conventional backend structure
|
||||
backend_src_dir = (
|
||||
backend_dir
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_extension"
|
||||
)
|
||||
backend_src_dir = backend_dir / "src" / "test_org" / "test_extension"
|
||||
backend_src_dir.mkdir(parents=True)
|
||||
|
||||
# Create conventional entry point file
|
||||
@@ -70,10 +64,7 @@ def extension_with_build_structure():
|
||||
(backend_src_dir / "__init__.py").write_text("")
|
||||
|
||||
# Create parent __init__.py files for namespace packages
|
||||
(backend_dir / "src" / "superset_extensions" / "__init__.py").write_text("")
|
||||
(
|
||||
backend_dir / "src" / "superset_extensions" / "test_org" / "__init__.py"
|
||||
).write_text("")
|
||||
(backend_dir / "src" / "test_org" / "__init__.py").write_text("")
|
||||
|
||||
# Create pyproject.toml matching the template structure
|
||||
pyproject_content = """[project]
|
||||
@@ -84,7 +75,7 @@ license = "Apache-2.0"
|
||||
[tool.apache_superset_extensions.build]
|
||||
# Files to include in the extension build/bundle
|
||||
include = [
|
||||
"src/superset_extensions/test_org/test_extension/**/*.py",
|
||||
"src/test_org/test_extension/**/*.py",
|
||||
]
|
||||
exclude = []
|
||||
"""
|
||||
@@ -133,11 +124,7 @@ def test_build_command_success_flow(
|
||||
"project": {"name": "test"},
|
||||
"tool": {
|
||||
"apache_superset_extensions": {
|
||||
"build": {
|
||||
"include": [
|
||||
"src/superset_extensions/test_org/test_extension/**/*.py"
|
||||
]
|
||||
}
|
||||
"build": {"include": ["src/test_org/test_extension/**/*.py"]}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -178,11 +165,7 @@ def test_build_command_handles_frontend_build_failure(
|
||||
"project": {"name": "test"},
|
||||
"tool": {
|
||||
"apache_superset_extensions": {
|
||||
"build": {
|
||||
"include": [
|
||||
"src/superset_extensions/test_org/test_extension/**/*.py"
|
||||
]
|
||||
}
|
||||
"build": {"include": ["src/test_org/test_extension/**/*.py"]}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -322,10 +305,7 @@ def test_build_manifest_creates_correct_manifest_structure(
|
||||
|
||||
# Verify backend section and conventional entrypoint
|
||||
assert manifest.backend is not None
|
||||
assert (
|
||||
manifest.backend.entrypoint
|
||||
== "superset_extensions.test_org.test_extension.entrypoint"
|
||||
)
|
||||
assert manifest.backend.entrypoint == "test_org.test_extension.entrypoint"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -477,7 +457,7 @@ def test_copy_backend_files_skips_non_files(isolated_filesystem):
|
||||
"""Test copy_backend_files skips directories and non-files."""
|
||||
# Create backend structure with directory
|
||||
backend_dir = isolated_filesystem / "backend"
|
||||
backend_src = backend_dir / "src" / "superset_extensions" / "test_org" / "test_ext"
|
||||
backend_src = backend_dir / "src" / "test_org" / "test_ext"
|
||||
backend_src.mkdir(parents=True)
|
||||
(backend_src / "__init__.py").write_text("# init")
|
||||
|
||||
@@ -493,7 +473,7 @@ license = "Apache-2.0"
|
||||
|
||||
[tool.apache_superset_extensions.build]
|
||||
include = [
|
||||
"src/superset_extensions/test_org/test_ext/**/*",
|
||||
"src/test_org/test_ext/**/*",
|
||||
]
|
||||
exclude = []
|
||||
"""
|
||||
@@ -517,25 +497,11 @@ exclude = []
|
||||
# Verify only files were copied, not directories
|
||||
dist_dir = isolated_filesystem / "dist"
|
||||
assert_file_exists(
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "__init__.py"
|
||||
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "__init__.py"
|
||||
)
|
||||
|
||||
# Directory should not be copied as a file
|
||||
copied_subdir = (
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "subdir"
|
||||
)
|
||||
copied_subdir = dist_dir / "backend" / "src" / "test_org" / "test_ext" / "subdir"
|
||||
# The directory might exist but should be empty since we skip non-files
|
||||
if copied_subdir.exists():
|
||||
assert list(copied_subdir.iterdir()) == []
|
||||
@@ -546,7 +512,7 @@ def test_copy_backend_files_copies_matched_files(isolated_filesystem):
|
||||
"""Test copy_backend_files copies files matching patterns from pyproject.toml."""
|
||||
# Create backend source files
|
||||
backend_dir = isolated_filesystem / "backend"
|
||||
backend_src = backend_dir / "src" / "superset_extensions" / "test_org" / "test_ext"
|
||||
backend_src = backend_dir / "src" / "test_org" / "test_ext"
|
||||
backend_src.mkdir(parents=True)
|
||||
(backend_src / "__init__.py").write_text("# init")
|
||||
(backend_src / "main.py").write_text("# main")
|
||||
@@ -559,7 +525,7 @@ license = "Apache-2.0"
|
||||
|
||||
[tool.apache_superset_extensions.build]
|
||||
include = [
|
||||
"src/superset_extensions/test_org/test_ext/**/*.py",
|
||||
"src/test_org/test_ext/**/*.py",
|
||||
]
|
||||
exclude = []
|
||||
"""
|
||||
@@ -583,22 +549,10 @@ exclude = []
|
||||
# Verify files were copied
|
||||
dist_dir = isolated_filesystem / "dist"
|
||||
assert_file_exists(
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "__init__.py"
|
||||
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "__init__.py"
|
||||
)
|
||||
assert_file_exists(
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "main.py"
|
||||
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "main.py"
|
||||
)
|
||||
|
||||
|
||||
@@ -607,7 +561,7 @@ def test_copy_backend_files_handles_various_glob_patterns(isolated_filesystem):
|
||||
"""Test copy_backend_files correctly handles different glob pattern formats."""
|
||||
# Create backend structure with files in different locations
|
||||
backend_dir = isolated_filesystem / "backend"
|
||||
backend_src = backend_dir / "src" / "superset_extensions" / "test_org" / "test_ext"
|
||||
backend_src = backend_dir / "src" / "test_org" / "test_ext"
|
||||
backend_src.mkdir(parents=True)
|
||||
|
||||
# Create files that should match different pattern types
|
||||
@@ -628,9 +582,9 @@ license = "Apache-2.0"
|
||||
|
||||
[tool.apache_superset_extensions.build]
|
||||
include = [
|
||||
"config.py", # No '/' - would break old logic
|
||||
"**/*.py", # Starts with '**' - would break old logic
|
||||
"src/superset_extensions/test_org/test_ext/main.py", # Specific file
|
||||
"config.py", # No '/' - would break old logic
|
||||
"**/*.py", # Starts with '**' - would break old logic
|
||||
"src/test_org/test_ext/main.py", # Specific file
|
||||
]
|
||||
exclude = []
|
||||
"""
|
||||
@@ -659,34 +613,15 @@ exclude = []
|
||||
|
||||
# All .py files should be included (pattern: "**/*.py")
|
||||
assert_file_exists(
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "__init__.py"
|
||||
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "__init__.py"
|
||||
)
|
||||
assert_file_exists(
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "utils"
|
||||
/ "helper.py"
|
||||
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "utils" / "helper.py"
|
||||
)
|
||||
|
||||
# Specific file (pattern: "src/superset_extensions/test_org/test_ext/main.py")
|
||||
# Specific file (pattern: "src/test_org/test_ext/main.py")
|
||||
assert_file_exists(
|
||||
dist_dir
|
||||
/ "backend"
|
||||
/ "src"
|
||||
/ "superset_extensions"
|
||||
/ "test_org"
|
||||
/ "test_ext"
|
||||
/ "main.py"
|
||||
dist_dir / "backend" / "src" / "test_org" / "test_ext" / "main.py"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -55,10 +55,7 @@ def test_bundle_command_creates_zip_with_default_name(
|
||||
assert "manifest.json" in file_list
|
||||
assert "frontend/dist/remoteEntry.abc123.js" in file_list
|
||||
assert "frontend/dist/main.js" in file_list
|
||||
assert (
|
||||
"backend/src/superset_extensions/test_org/test_extension/__init__.py"
|
||||
in file_list
|
||||
)
|
||||
assert "backend/src/test_org/test_extension/__init__.py" in file_list
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
|
||||
@@ -376,13 +376,10 @@ def test_generate_extension_names_complete_flow(
|
||||
assert (
|
||||
names["backend_package"] == f"{publisher.replace('-', '_')}-{expected_snake}"
|
||||
) # Collision-safe
|
||||
assert (
|
||||
names["backend_path"]
|
||||
== f"superset_extensions.{publisher.replace('-', '_')}.{expected_snake}"
|
||||
)
|
||||
assert names["backend_path"] == f"{publisher.replace('-', '_')}.{expected_snake}"
|
||||
assert (
|
||||
names["backend_entry"]
|
||||
== f"superset_extensions.{publisher.replace('-', '_')}.{expected_snake}.entrypoint"
|
||||
== f"{publisher.replace('-', '_')}.{expected_snake}.entrypoint"
|
||||
)
|
||||
|
||||
|
||||
@@ -476,8 +473,8 @@ def test_manual_technical_name_override():
|
||||
assert names["id"] == "acme.chart-builder" # Composite ID
|
||||
assert names["mf_name"] == "acme_chartBuilder" # Module Federation format
|
||||
assert names["backend_package"] == "acme-chart_builder" # Collision-safe
|
||||
assert names["backend_path"] == "superset_extensions.acme.chart_builder"
|
||||
assert names["backend_entry"] == "superset_extensions.acme.chart_builder.entrypoint"
|
||||
assert names["backend_path"] == "acme.chart_builder"
|
||||
assert names["backend_entry"] == "acme.chart_builder.entrypoint"
|
||||
|
||||
|
||||
def test_generate_names_uses_suggested_technical_names():
|
||||
|
||||
@@ -49,8 +49,8 @@ def template_context():
|
||||
"npm_name": "@test-org/test-extension",
|
||||
"mf_name": "testOrg_testExtension",
|
||||
"backend_package": "test_org-test_extension",
|
||||
"backend_path": "superset_extensions.test_org.test_extension",
|
||||
"backend_entry": "superset_extensions.test_org.test_extension.entrypoint",
|
||||
"backend_path": "test_org.test_extension",
|
||||
"backend_entry": "test_org.test_extension.entrypoint",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"include_frontend": True,
|
||||
@@ -197,8 +197,8 @@ def test_template_rendering_with_different_ids(
|
||||
"npm_name": f"@{publisher}/{technical_name}",
|
||||
"mf_name": get_module_federation_name(publisher, technical_name),
|
||||
"backend_package": f"{publisher_snake}-{name_snake}",
|
||||
"backend_path": f"superset_extensions.{publisher_snake}.{name_snake}",
|
||||
"backend_entry": f"superset_extensions.{publisher_snake}.{name_snake}.entrypoint",
|
||||
"backend_path": f"{publisher_snake}.{name_snake}",
|
||||
"backend_entry": f"{publisher_snake}.{name_snake}.entrypoint",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"include_frontend": True,
|
||||
@@ -274,8 +274,8 @@ def test_template_rendering_with_different_licenses(jinja_env, license_type):
|
||||
"npm_name": "@test-pub/test-ext",
|
||||
"mf_name": "testPub_testExt",
|
||||
"backend_package": "test_pub-test_ext",
|
||||
"backend_path": "superset_extensions.test_pub.test_ext",
|
||||
"backend_entry": "superset_extensions.test_pub.test_ext.entrypoint",
|
||||
"backend_path": "test_pub.test_ext",
|
||||
"backend_entry": "test_pub.test_ext.entrypoint",
|
||||
"version": "1.0.0",
|
||||
"license": license_type,
|
||||
"include_frontend": True,
|
||||
@@ -347,8 +347,8 @@ def test_template_context_edge_cases(jinja_env):
|
||||
"npm_name": "@min/minimal",
|
||||
"mf_name": "min_minimal",
|
||||
"backend_package": "min-minimal",
|
||||
"backend_path": "superset_extensions.min.minimal",
|
||||
"backend_entry": "superset_extensions.min.minimal.entrypoint",
|
||||
"backend_path": "min.minimal",
|
||||
"backend_entry": "min.minimal.entrypoint",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"include_frontend": False,
|
||||
|
||||
@@ -1 +1 @@
|
||||
v20.18.3
|
||||
v22.22.0
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { withJsx } from '@mihkeleidast/storybook-addon-source';
|
||||
import { themeObject, css, exampleThemes } from '@apache-superset/core/ui';
|
||||
import { themeObject, css, exampleThemes } from '@apache-superset/core/theme';
|
||||
import { combineReducers, createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { useState, ReactNode, SyntheticEvent } from 'react';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import type { Decorator } from '@storybook/react';
|
||||
import { ResizeCallbackData } from 'react-resizable';
|
||||
import ResizablePanel, { Size } from './ResizablePanel';
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
ResizableBoxProps,
|
||||
ResizeCallbackData,
|
||||
} from 'react-resizable';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { Component, ReactNode } from 'react';
|
||||
import { t } from '@apache-superset/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
SupersetClient,
|
||||
Method,
|
||||
|
||||
@@ -69,7 +69,7 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect)',
|
||||
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
@@ -237,8 +237,7 @@
|
||||
"jsx-a11y/no-noninteractive-tabindex": "error",
|
||||
"jsx-a11y/no-redundant-roles": "error",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
// TODO: Fix missing aria-selected on tab roles
|
||||
"jsx-a11y/role-has-required-aria-props": "warn",
|
||||
"jsx-a11y/role-has-required-aria-props": "error",
|
||||
"jsx-a11y/role-supports-aria-props": "error",
|
||||
"jsx-a11y/scope": "error",
|
||||
"jsx-a11y/tabindex-no-positive": "error",
|
||||
|
||||
1930
superset-frontend/package-lock.json
generated
1930
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -175,7 +175,7 @@
|
||||
"geostyler-openlayers-parser": "^4.3.0",
|
||||
"geostyler-style": "7.5.0",
|
||||
"geostyler-wfs-parser": "^2.0.3",
|
||||
"google-auth-library": "^10.5.0",
|
||||
"google-auth-library": "^10.6.1",
|
||||
"immer": "^11.1.4",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
@@ -197,7 +197,7 @@
|
||||
"react": "^17.0.2",
|
||||
"react-arborist": "^3.4.3",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^3.4.0",
|
||||
"react-diff-viewer-continued": "^4.2.0",
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^17.0.2",
|
||||
@@ -311,7 +311,7 @@
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.4",
|
||||
"css-minimizer-webpack-plugin": "^7.0.4",
|
||||
"css-minimizer-webpack-plugin": "^8.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
@@ -325,7 +325,7 @@
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.1",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.2",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"eslint-plugin-testing-library": "^7.16.0",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
@@ -335,20 +335,20 @@
|
||||
"html-webpack-plugin": "^5.6.6",
|
||||
"http-server": "^14.1.1",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest": "^30.3.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-html-reporter": "^4.3.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdom": "^28.1.0",
|
||||
"lerna": "^8.2.3",
|
||||
"lightningcss": "^1.31.1",
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.0",
|
||||
"open-cli": "^8.0.0",
|
||||
"oxlint": "^1.51.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.1",
|
||||
"prettier-plugin-packagejson": "^3.0.0",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"process": "^0.11.10",
|
||||
"react-refresh": "^0.18.0",
|
||||
"react-resizable": "^3.1.3",
|
||||
@@ -368,7 +368,7 @@
|
||||
"unzipper": "^0.12.3",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"wait-on": "^9.0.4",
|
||||
"webpack": "^5.105.3",
|
||||
"webpack": "^5.105.4",
|
||||
"webpack-bundle-analyzer": "^5.2.0",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.3",
|
||||
@@ -384,7 +384,7 @@
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.18.1",
|
||||
"node": "^22.22.0",
|
||||
"npm": "^10.8.1"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.3",
|
||||
"jest": "^30.2.0",
|
||||
"jest": "^30.3.0",
|
||||
"yeoman-test": "^11.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -30,74 +30,98 @@ The official core package for building Apache Superset extensions and integratio
|
||||
npm install @apache-superset/core
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
## 🏗️ Package Structure
|
||||
|
||||
The package is organized into logical namespaces, each providing specific functionality:
|
||||
The source is organized into focused namespaces, each in its own directory:
|
||||
|
||||
- **`authentication`** - User authentication and authorization APIs
|
||||
- **`commands`** - Command registration and execution system
|
||||
- **`contributions`** - UI contribution points and customization APIs
|
||||
- **`core`** - Fundamental types, utilities, and lifecycle management
|
||||
- **`environment`** - Environment detection and configuration APIs
|
||||
- **`extensions`** - Extension management and metadata APIs
|
||||
- **`sqlLab`** - SQL Lab integration and event handling
|
||||
```
|
||||
src/
|
||||
├── authentication/
|
||||
├── commands/
|
||||
├── common/
|
||||
├── components/
|
||||
├── contributions/
|
||||
├── editors/
|
||||
├── extensions/
|
||||
├── menus/
|
||||
├── sqlLab/
|
||||
├── theme/
|
||||
├── translation/
|
||||
├── utils/
|
||||
├── views/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Basic Extension Structure
|
||||
Frontend contributions are registered as module-level side effects from your extension's entry point.
|
||||
|
||||
```typescript
|
||||
import {
|
||||
core,
|
||||
commands,
|
||||
sqlLab,
|
||||
authentication,
|
||||
} from '@apache-superset/core';
|
||||
### Views
|
||||
|
||||
export function activate(context: core.ExtensionContext) {
|
||||
// Register a command to save current query
|
||||
const commandDisposable = commands.registerCommand(
|
||||
'my_extension.save_query',
|
||||
async () => {
|
||||
const currentTab = sqlLab.getCurrentTab();
|
||||
if (currentTab?.editor.content) {
|
||||
const token = await authentication.getCSRFToken();
|
||||
// Use token for secure API calls
|
||||
console.log('Saving query with CSRF token:', token);
|
||||
}
|
||||
},
|
||||
);
|
||||
Add custom panels or UI components at specific locations in the application:
|
||||
|
||||
// Listen for query execution events
|
||||
const eventDisposable = sqlLab.onDidQueryRun(editor => {
|
||||
console.log('Query executed:', editor.content.substring(0, 50) + '...');
|
||||
});
|
||||
```tsx
|
||||
import { views } from '@apache-superset/core';
|
||||
import MyPanel from './MyPanel';
|
||||
|
||||
// Register a simple view
|
||||
const viewDisposable = core.registerViewProvider(
|
||||
'my_extension.panel',
|
||||
() => (
|
||||
<div>
|
||||
<h3>My Extension</h3>
|
||||
<button onClick={() => commands.executeCommand('my_extension.save_query')}>
|
||||
Save Query
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
// Cleanup registration
|
||||
context.subscriptions.push(commandDisposable, eventDisposable, viewDisposable);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
// Cleanup handled automatically via disposables
|
||||
}
|
||||
views.registerView(
|
||||
{ id: 'my-extension.main', name: 'My Panel Name' },
|
||||
'sqllab.panels',
|
||||
() => <MyPanel />,
|
||||
);
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
### Commands
|
||||
|
||||
We welcome contributions! Please see the [Developer Portal](https://superset.apache.org/developer_portal/) for details.
|
||||
Define named actions that can be triggered from menus, keyboard shortcuts, or code:
|
||||
|
||||
```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',
|
||||
},
|
||||
() => {
|
||||
/* implementation */
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Menus
|
||||
|
||||
Attach commands to primary, secondary, or context menus at a given location:
|
||||
|
||||
```typescript
|
||||
import { menus } from '@apache-superset/core';
|
||||
|
||||
menus.registerMenuItem(
|
||||
{ view: 'sqllab.editor', command: 'my-extension.copy-query' },
|
||||
'sqllab.editor',
|
||||
'primary',
|
||||
);
|
||||
```
|
||||
|
||||
### Editors
|
||||
|
||||
Replace the default text editor for one or more languages:
|
||||
|
||||
```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,
|
||||
);
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
@@ -105,12 +129,6 @@ Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Apache Superset](https://superset.apache.org/)
|
||||
- [Documentation](https://superset.apache.org/docs/)
|
||||
- [Community](https://superset.apache.org/community/)
|
||||
- [GitHub Repository](https://github.com/apache/superset)
|
||||
- [Extension Development Guide](https://superset.apache.org/docs/extensions/)
|
||||
|
||||
---
|
||||
|
||||
**Note**: This package is currently in release candidate status. APIs may change before the 1.0.0 release. Please check the [changelog](CHANGELOG.md) for breaking changes between versions.
|
||||
- [Extensions Documentation](https://superset.apache.org/developer-docs/extensions/overview)
|
||||
|
||||
@@ -1,10 +1,72 @@
|
||||
{
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.0.1-rc11",
|
||||
"version": "0.1.0-rc1",
|
||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./common": {
|
||||
"types": "./lib/common/index.d.ts",
|
||||
"default": "./lib/common/index.js"
|
||||
},
|
||||
"./authentication": {
|
||||
"types": "./lib/authentication/index.d.ts",
|
||||
"default": "./lib/authentication/index.js"
|
||||
},
|
||||
"./commands": {
|
||||
"types": "./lib/commands/index.d.ts",
|
||||
"default": "./lib/commands/index.js"
|
||||
},
|
||||
"./editors": {
|
||||
"types": "./lib/editors/index.d.ts",
|
||||
"default": "./lib/editors/index.js"
|
||||
},
|
||||
"./extensions": {
|
||||
"types": "./lib/extensions/index.d.ts",
|
||||
"default": "./lib/extensions/index.js"
|
||||
},
|
||||
"./menus": {
|
||||
"types": "./lib/menus/index.d.ts",
|
||||
"default": "./lib/menus/index.js"
|
||||
},
|
||||
"./sqlLab": {
|
||||
"types": "./lib/sqlLab/index.d.ts",
|
||||
"default": "./lib/sqlLab/index.js"
|
||||
},
|
||||
"./views": {
|
||||
"types": "./lib/views/index.d.ts",
|
||||
"default": "./lib/views/index.js"
|
||||
},
|
||||
"./contributions": {
|
||||
"types": "./lib/contributions/index.d.ts",
|
||||
"default": "./lib/contributions/index.js"
|
||||
},
|
||||
"./theme": {
|
||||
"types": "./lib/theme/index.d.ts",
|
||||
"default": "./lib/theme/index.js"
|
||||
},
|
||||
"./translation": {
|
||||
"types": "./lib/translation/index.d.ts",
|
||||
"default": "./lib/translation/index.js"
|
||||
},
|
||||
"./components": {
|
||||
"types": "./lib/components/index.d.ts",
|
||||
"default": "./lib/components/index.js"
|
||||
},
|
||||
"./utils": {
|
||||
"types": "./lib/utils/index.d.ts",
|
||||
"default": "./lib/utils/index.js"
|
||||
},
|
||||
"./testing": {
|
||||
"types": "./lib/testing.d.ts",
|
||||
"default": "./lib/testing.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Main entry point for the Superset Extension API.
|
||||
*
|
||||
* This module exports all public APIs for Superset extensions, providing
|
||||
* a unified interface for extension developers to interact with the Superset
|
||||
* platform. The API includes:
|
||||
*
|
||||
* - `authentication`: Handle user authentication and authorization
|
||||
* - `commands`: Execute Superset commands and operations
|
||||
* - `contributions`: Register UI contributions and customizations
|
||||
* - `core`: Access fundamental Superset types and utilities
|
||||
* - `editors`: Register custom text editor implementations
|
||||
* - `extensions`: Manage extension lifecycle and metadata
|
||||
* - `sqlLab`: Integrate with SQL Lab functionality
|
||||
*/
|
||||
|
||||
export * as authentication from './authentication';
|
||||
export * as commands from './commands';
|
||||
export * as contributions from './contributions';
|
||||
export * as core from './core';
|
||||
export * as editors from './editors';
|
||||
export * as extensions from './extensions';
|
||||
export * as menus from './menus';
|
||||
export * as sqlLab from './sqlLab';
|
||||
export * as views from './views';
|
||||
@@ -25,7 +25,7 @@
|
||||
* via keyboard shortcuts, menu items, programmatic calls, or other user interactions.
|
||||
*/
|
||||
|
||||
import { Disposable } from './core';
|
||||
import { Disposable } from '../common';
|
||||
|
||||
/**
|
||||
* Describes a command that can be contributed to the application.
|
||||
@@ -38,7 +38,7 @@ export interface Command {
|
||||
/** The icon associated with the command. */
|
||||
icon?: string;
|
||||
/** A description of what the command does. */
|
||||
description: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,10 +26,10 @@
|
||||
* menus, editors) and re-exported here for the manifest schema.
|
||||
*/
|
||||
|
||||
import { Command } from './commands';
|
||||
import { View } from './views';
|
||||
import { Menu } from './menus';
|
||||
import { Editor } from './editors';
|
||||
import { Command } from '../commands';
|
||||
import { View } from '../views';
|
||||
import { Menu } from '../menus';
|
||||
import { Editor } from '../editors';
|
||||
|
||||
/**
|
||||
* Valid locations within SQL Lab.
|
||||
@@ -34,8 +34,8 @@
|
||||
*/
|
||||
|
||||
import { ForwardRefExoticComponent, RefAttributes } from 'react';
|
||||
import { Disposable, Event } from './core';
|
||||
import type { SupersetTheme } from '../ui';
|
||||
import { Disposable, Event } from '../common';
|
||||
import type { SupersetTheme } from '../theme';
|
||||
|
||||
/**
|
||||
* Supported editor languages.
|
||||
@@ -480,6 +480,18 @@ export interface EditorHandle {
|
||||
* @returns A Disposable that removes the provider when disposed
|
||||
*/
|
||||
registerCompletionProvider(provider: CompletionProvider): Disposable;
|
||||
|
||||
/**
|
||||
* Force the editor to recalculate its dimensions.
|
||||
* Called when the container size changes or when the editor becomes
|
||||
* visible after being hidden (e.g., in a tab).
|
||||
*
|
||||
* Each editor implementation maps this to their equivalent:
|
||||
* - Ace: editor.resize()
|
||||
* - Monaco: editor.layout()
|
||||
* - CodeMirror: editor.requestMeasure()
|
||||
*/
|
||||
resize(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,17 +513,17 @@ export interface EditorProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when an editor provider is registered.
|
||||
* Event fired when an editor is registered.
|
||||
*/
|
||||
export interface EditorProviderRegisteredEvent {
|
||||
/** The registered provider */
|
||||
provider: EditorProvider;
|
||||
export interface EditorRegisteredEvent {
|
||||
/** The descriptor of the editor that was registered */
|
||||
editor: Editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired when an editor provider is unregistered.
|
||||
* Event fired when an editor is unregistered.
|
||||
*/
|
||||
export interface EditorProviderUnregisteredEvent {
|
||||
export interface EditorUnregisteredEvent {
|
||||
/** The descriptor of the editor that was unregistered */
|
||||
editor: Editor;
|
||||
}
|
||||
@@ -545,7 +557,7 @@ export declare function registerEditor(
|
||||
* @param language The language to get an editor for
|
||||
* @returns The editor provider or undefined if no extension provides one
|
||||
*/
|
||||
export declare function getEditorProvider(
|
||||
export declare function getEditor(
|
||||
language: EditorLanguage,
|
||||
): EditorProvider | undefined;
|
||||
|
||||
@@ -555,21 +567,21 @@ export declare function getEditorProvider(
|
||||
* @param language The language to check
|
||||
* @returns True if an extension provides an editor for this language
|
||||
*/
|
||||
export declare function hasEditorProvider(language: EditorLanguage): boolean;
|
||||
export declare function hasEditor(language: EditorLanguage): boolean;
|
||||
|
||||
/**
|
||||
* Get all registered editor providers.
|
||||
*
|
||||
* @returns Array of all registered editor providers
|
||||
*/
|
||||
export declare function getAllEditorProviders(): EditorProvider[];
|
||||
export declare function getAllEditors(): EditorProvider[];
|
||||
|
||||
/**
|
||||
* Event fired when an editor provider is registered.
|
||||
* Event fired when an editor is registered.
|
||||
*/
|
||||
export declare const onDidRegisterEditorProvider: Event<EditorProviderRegisteredEvent>;
|
||||
export declare const onDidRegisterEditor: Event<EditorRegisteredEvent>;
|
||||
|
||||
/**
|
||||
* Event fired when an editor provider is unregistered.
|
||||
* Event fired when an editor is unregistered.
|
||||
*/
|
||||
export declare const onDidUnregisterEditorProvider: Event<EditorProviderUnregisteredEvent>;
|
||||
export declare const onDidUnregisterEditor: Event<EditorUnregisteredEvent>;
|
||||
@@ -26,7 +26,7 @@
|
||||
* in the extension ecosystem.
|
||||
*/
|
||||
|
||||
import { Extension } from './core';
|
||||
import { Extension } from '../common';
|
||||
|
||||
/**
|
||||
* Get an extension by its full identifier in the form of: `publisher.name`.
|
||||
@@ -16,6 +16,16 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './api';
|
||||
export * from './ui';
|
||||
export * from './utils';
|
||||
export * as common from './common';
|
||||
export * as authentication from './authentication';
|
||||
export * as commands from './commands';
|
||||
export * as editors from './editors';
|
||||
export * as extensions from './extensions';
|
||||
export * as menus from './menus';
|
||||
export * as sqlLab from './sqlLab';
|
||||
export * as views from './views';
|
||||
export * as contributions from './contributions';
|
||||
export * as theme from './theme';
|
||||
export * as translation from './translation';
|
||||
export * as components from './components';
|
||||
export * as utils from './utils';
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { Disposable } from './core';
|
||||
import { Disposable } from '../common';
|
||||
|
||||
/**
|
||||
* Represents a menu item that links a view to a command.
|
||||
@@ -47,6 +47,8 @@ export interface MenuItem {
|
||||
view: string;
|
||||
/** The command to execute when this menu item is selected. */
|
||||
command: string;
|
||||
/** Optional description of the menu item, for display in contribution manifests. */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,8 +29,8 @@
|
||||
* - Global APIs: Functions and events available across the entire SQL Lab interface
|
||||
*/
|
||||
|
||||
import { Event, Database, SupersetError, Column } from './core';
|
||||
import { EditorHandle } from './editors';
|
||||
import { Event, Database, SupersetError, Column } from '../common';
|
||||
import { EditorHandle } from '../editors';
|
||||
|
||||
/**
|
||||
* Provides imperative control over the code editor component.
|
||||
@@ -98,6 +98,17 @@ export const GlobalStyles = () => {
|
||||
[role='button'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Override geostyler CSS that hides AntD ColorPicker alpha input
|
||||
// See: https://github.com/apache/superset/issues/34721
|
||||
.ant-color-picker .ant-color-picker-alpha-input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-color-picker .ant-color-picker-slider-alpha {
|
||||
display: flex;
|
||||
margin-top: ${theme.marginXS}px;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user