mirror of
https://github.com/apache/superset.git
synced 2026-06-19 14:39:20 +00:00
Compare commits
61 Commits
fix-timezo
...
upgrade-sq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f4248b4d7 | ||
|
|
77148277b9 | ||
|
|
981b370fe9 | ||
|
|
b012b63e5b | ||
|
|
b0be47a4ac | ||
|
|
00d02cb2ea | ||
|
|
26a2e12779 | ||
|
|
5f0001affc | ||
|
|
255a0ada81 | ||
|
|
9089f30045 | ||
|
|
98ca599eef | ||
|
|
d640fe42c9 | ||
|
|
534fa48f1f | ||
|
|
c28729f944 | ||
|
|
88a14f2ba0 | ||
|
|
74e1607010 | ||
|
|
69c679be20 | ||
|
|
9a79dbf445 | ||
|
|
7e5ca83220 | ||
|
|
7d4a7f113c | ||
|
|
4eb8fc814a | ||
|
|
39ac96817a | ||
|
|
1388a62823 | ||
|
|
6a6b9b5386 | ||
|
|
b98b34a60f | ||
|
|
7ec5f1d7ec | ||
|
|
76aa91f5ea | ||
|
|
c41942a38a | ||
|
|
ae8d671fea | ||
|
|
c59d0a73d4 | ||
|
|
0f1278fa61 | ||
|
|
948b1d613b | ||
|
|
3af795af36 | ||
|
|
1cba53a043 | ||
|
|
8c6bc3eaea | ||
|
|
4d8ff84587 | ||
|
|
f370da5a87 | ||
|
|
2df60f9caf | ||
|
|
d078f18ff8 | ||
|
|
6ca028dee9 | ||
|
|
76351ff12c | ||
|
|
f6f96ecc49 | ||
|
|
59dd2fa385 | ||
|
|
6984e93171 | ||
|
|
f25d95be41 | ||
|
|
5125a67002 | ||
|
|
059b57d784 | ||
|
|
a1d65c7529 | ||
|
|
15b3c96f8e | ||
|
|
2b411b32ba | ||
|
|
cebdb9e0b7 | ||
|
|
ce872ddaf0 | ||
|
|
29aa69b779 | ||
|
|
ebee9bb3f9 | ||
|
|
82d6076804 | ||
|
|
3b75af9ac3 | ||
|
|
563d9f1a3f | ||
|
|
c4d2d42b3b | ||
|
|
7580bd1401 | ||
|
|
c4e7c3b03b | ||
|
|
3521f191b2 |
77
.github/dependabot.yml
vendored
77
.github/dependabot.yml
vendored
@@ -15,6 +15,7 @@ updates:
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
- dependency-name: "eslint-plugin-storybook"
|
||||
- dependency-name: "react-error-boundary"
|
||||
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
|
||||
- dependency-name: "remark-gfm"
|
||||
- dependency-name: "react-markdown"
|
||||
@@ -100,16 +101,6 @@ updates:
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-histogram/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
|
||||
schedule:
|
||||
@@ -210,16 +201,6 @@ updates:
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
|
||||
schedule:
|
||||
@@ -240,16 +221,6 @@ updates:
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-event-flow/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
|
||||
schedule:
|
||||
@@ -260,16 +231,6 @@ updates:
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
|
||||
schedule:
|
||||
@@ -281,7 +242,7 @@ updates:
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/preset-chart-xy/"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-ag-grid-table/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
@@ -291,7 +252,7 @@ updates:
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-heatmap/"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-cartodiagram/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
@@ -310,16 +271,6 @@ updates:
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-sunburst/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
|
||||
schedule:
|
||||
@@ -356,27 +307,7 @@ updates:
|
||||
# not until React >= 18.0.0
|
||||
- dependency-name: "react-markdown"
|
||||
- dependency-name: "remark-gfm"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-demo/"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 18.0.0
|
||||
- dependency-name: "@storybook*"
|
||||
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||
groups:
|
||||
storybook:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@storybook*"
|
||||
update-types:
|
||||
- "patch"
|
||||
- dependency-name: "react-error-boundary"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
|
||||
23
.github/workflows/bashlib.sh
vendored
23
.github/workflows/bashlib.sh
vendored
@@ -304,26 +304,3 @@ monitor_memory() {
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
cypress-run-applitools() {
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
|
||||
|
||||
local flasklog="${HOME}/flask.log"
|
||||
local port=8081
|
||||
local cypress="./node_modules/.bin/cypress run"
|
||||
local browser=${CYPRESS_BROWSER:-chrome}
|
||||
|
||||
export CYPRESS_BASE_URL="http://localhost:${port}"
|
||||
|
||||
nohup flask run --no-debugger -p $port >"$flasklog" 2>&1 </dev/null &
|
||||
local flaskProcessId=$!
|
||||
|
||||
$cypress --spec "cypress/applitools/**/*" --browser "$browser" --headless
|
||||
|
||||
say "::group::Flask log for default run"
|
||||
cat "$flasklog"
|
||||
say "::endgroup::"
|
||||
|
||||
# make sure the program exits
|
||||
kill $flaskProcessId
|
||||
}
|
||||
|
||||
6
.github/workflows/dependency-review.yml
vendored
6
.github/workflows/dependency-review.yml
vendored
@@ -39,13 +39,9 @@ jobs:
|
||||
# pkg:npm/store2@2.14.2
|
||||
# adding an exception for an ambigious license on store2, which has been resolved in
|
||||
# the latest version. It's MIT: https://github.com/nbubna/store/blob/master/LICENSE-MIT
|
||||
# pkg:npm/applitools/*
|
||||
# adding exception for all applitools modules (eyes-cypress and its dependencies),
|
||||
# which has an explicit OSS license approved by ASF
|
||||
# license: https://applitools.com/legal/open-source-terms-of-use/
|
||||
# pkg:npm/node-forge@1.3.1
|
||||
# selecting BSD-3-Clause licensing terms for node-forge to ensure compatibility with Apache
|
||||
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
|
||||
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
|
||||
|
||||
python-dependency-liccheck:
|
||||
# NOTE: Configuration for liccheck lives in our pyproject.yml.
|
||||
|
||||
91
.github/workflows/superset-applitool-cypress.yml
vendored
91
.github/workflows/superset-applitool-cypress.yml
vendored
@@ -1,91 +0,0 @@
|
||||
name: Applitools Cypress
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
- name: "Check for secrets"
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.APPLITOOLS_API_KEY != '' && secrets.APPLITOOLS_API_KEY != '') || '' }}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
cypress-applitools:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: ["chrome"]
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
REDIS_PORT: 16379
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
APPLITOOLS_APP_NAME: Superset
|
||||
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
|
||||
APPLITOOLS_BATCH_ID: ${{ github.sha }}
|
||||
APPLITOOLS_BATCH_NAME: Superset Cypress
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
POSTGRES_USER: superset
|
||||
POSTGRES_PASSWORD: superset
|
||||
ports:
|
||||
- 15432:5432
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: master
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
- name: Import test data
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Build javascript packages
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Setup Postgres
|
||||
if: steps.check.outcome == 'failure'
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Install cypress
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: cypress-install
|
||||
- name: Run Cypress
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ matrix.browser }}
|
||||
with:
|
||||
run: cypress-run-applitools
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Applitools Storybook
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
env:
|
||||
APPLITOOLS_APP_NAME: Superset
|
||||
APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }}
|
||||
APPLITOOLS_BATCH_ID: ${{ github.sha }}
|
||||
APPLITOOLS_BATCH_NAME: Superset Storybook
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
- name: "Check for secrets"
|
||||
id: check
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -n "${{ (secrets.APPLITOOLS_API_KEY != '' && secrets.APPLITOOLS_API_KEY != '') || '' }}" ]; then
|
||||
echo "has-secrets=1" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
cron:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: master
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install eyes-storybook dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: eyes-storybook-dependencies
|
||||
- name: Install NPM dependencies
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Run Applitools Eyes-Storybook
|
||||
working-directory: ./superset-frontend
|
||||
run: npx eyes-storybook -u https://superset-storybook.netlify.app/
|
||||
5
.github/workflows/superset-frontend.yml
vendored
5
.github/workflows/superset-frontend.yml
vendored
@@ -163,11 +163,6 @@ jobs:
|
||||
docker run --rm $TAG bash -c \
|
||||
"npm run plugins:build"
|
||||
|
||||
- name: Build Plugins Storybook
|
||||
run: |
|
||||
docker run --rm $TAG bash -c \
|
||||
"npm run plugins:build-storybook"
|
||||
|
||||
test-storybook:
|
||||
needs: frontend-build
|
||||
if: needs.frontend-build.outputs.should-run == 'true'
|
||||
|
||||
22
UPDATING.md
22
UPDATING.md
@@ -24,6 +24,28 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Signal Cache Backend
|
||||
|
||||
A new `SIGNAL_CACHE_CONFIG` configuration provides a unified Redis-based backend for real-time coordination features in Superset. This backend enables:
|
||||
|
||||
- **Pub/sub messaging** for real-time event notifications between workers
|
||||
- **Atomic distributed locking** using Redis SET NX EX (more performant than database-backed locks)
|
||||
- **Event-based coordination** for background task management
|
||||
|
||||
The signal cache is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments.
|
||||
|
||||
Example configuration in `superset_config.py`:
|
||||
```python
|
||||
SIGNAL_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_KEY_PREFIX": "signal_",
|
||||
"CACHE_REDIS_URL": "redis://localhost:6379/1",
|
||||
"CACHE_DEFAULT_TIMEOUT": 300,
|
||||
}
|
||||
```
|
||||
|
||||
See `superset/config.py` for complete configuration options.
|
||||
|
||||
### WebSocket config for GAQ with Docker
|
||||
|
||||
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
|
||||
|
||||
@@ -38,12 +38,14 @@ Extensions can add new views or panels to the host application, such as custom S
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab.panels": [
|
||||
{
|
||||
"id": "my_extension.main",
|
||||
"name": "My Panel Name"
|
||||
}
|
||||
]
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "my_extension.main",
|
||||
"name": "My Panel Name"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,25 +78,27 @@ Extensions can contribute new menu items or context menus to the host applicatio
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"menus": {
|
||||
"sqllab.editor": {
|
||||
"primary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.copy_query"
|
||||
}
|
||||
],
|
||||
"secondary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.prettify"
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.clear"
|
||||
}
|
||||
]
|
||||
"sqllab": {
|
||||
"editor": {
|
||||
"primary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.copy_query"
|
||||
}
|
||||
],
|
||||
"secondary": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.prettify"
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
{
|
||||
"view": "builtin.editor",
|
||||
"command": "my_extension.clear"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +92,14 @@ The `extension.json` file contains all metadata necessary for the host applicati
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab.panels": [
|
||||
{
|
||||
"id": "dataset_references.main",
|
||||
"name": "Dataset references"
|
||||
}
|
||||
]
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "dataset_references.main",
|
||||
"name": "Dataset references"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"moduleFederation": {
|
||||
|
||||
@@ -93,12 +93,14 @@ This example adds a "Data Profiler" panel to SQL Lab:
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab.panels": [
|
||||
{
|
||||
"id": "data_profiler.main",
|
||||
"name": "Data Profiler"
|
||||
}
|
||||
]
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "data_profiler.main",
|
||||
"name": "Data Profiler"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,25 +144,27 @@ This example adds primary, secondary, and context actions to the editor:
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,4 +51,5 @@ Extensions can provide:
|
||||
- **[Deployment](./deployment)** - Packaging and deploying extensions
|
||||
- **[MCP Integration](./mcp)** - Adding AI agent capabilities using extensions
|
||||
- **[Security](./security)** - Security considerations and best practices
|
||||
- **[Tasks](./tasks)** - Framework for creating and managing long running tasks
|
||||
- **[Community Extensions](./registry)** - Browse extensions shared by the community
|
||||
|
||||
@@ -94,12 +94,14 @@ The generated `extension.json` contains basic metadata. Update it to register yo
|
||||
"frontend": {
|
||||
"contributions": {
|
||||
"views": {
|
||||
"sqllab.panels": [
|
||||
{
|
||||
"id": "hello_world.main",
|
||||
"name": "Hello World"
|
||||
}
|
||||
]
|
||||
"sqllab": {
|
||||
"panels": [
|
||||
{
|
||||
"id": "hello_world.main",
|
||||
"name": "Hello World"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"moduleFederation": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Community Extensions
|
||||
sidebar_position: 10
|
||||
sidebar_position: 11
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
440
docs/developer_portal/extensions/tasks.md
Normal file
440
docs/developer_portal/extensions/tasks.md
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
title: Tasks
|
||||
sidebar_position: 10
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Global Task Framework
|
||||
|
||||
The Global Task Framework (GTF) provides a unified way to manage background tasks. It handles task execution, progress tracking, cancellation, and deduplication for both synchronous and asynchronous execution. The framework uses distributed locking internally to ensure race-free operations—you don't need to worry about concurrent task creation or cancellation conflicts.
|
||||
|
||||
## Enabling GTF
|
||||
|
||||
GTF is disabled by default and must be enabled via the `GLOBAL_TASK_FRAMEWORK` feature flag in your `superset_config.py`:
|
||||
|
||||
```python
|
||||
FEATURE_FLAGS = {
|
||||
"GLOBAL_TASK_FRAMEWORK": True,
|
||||
}
|
||||
```
|
||||
|
||||
When GTF is disabled:
|
||||
- The Task List UI menu item is hidden
|
||||
- The `/api/v1/task/*` endpoints return 404
|
||||
- Calling or scheduling a `@task`-decorated function raises `GlobalTaskFrameworkDisabledError`
|
||||
|
||||
:::note Future Migration
|
||||
When GTF is considered stable, it will replace legacy Celery tasks for built-in features like thumbnails and alerts & reports. Enabling this flag prepares your deployment for that migration.
|
||||
:::
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Define a Task
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import task, get_context
|
||||
|
||||
@task
|
||||
def process_data(dataset_id: int) -> None:
|
||||
ctx = get_context()
|
||||
|
||||
@ctx.on_cleanup
|
||||
def cleanup():
|
||||
logger.info("Processing complete")
|
||||
|
||||
data = fetch_dataset(dataset_id)
|
||||
process_and_cache(data)
|
||||
```
|
||||
|
||||
### Execute a Task
|
||||
|
||||
```python
|
||||
# Async execution - schedules on Celery worker
|
||||
task = process_data.schedule(dataset_id=123)
|
||||
print(task.status) # "pending"
|
||||
|
||||
# Sync execution - runs inline in current process
|
||||
task = process_data(dataset_id=123)
|
||||
# ... blocks until complete
|
||||
print(task.status) # "success"
|
||||
```
|
||||
|
||||
### Async vs Sync Execution
|
||||
|
||||
| Method | When to Use |
|
||||
|--------|-------------|
|
||||
| `.schedule()` | Long-running operations, background processing, when you need to return immediately |
|
||||
| Direct call | Short operations, when deduplication matters, when you need the result before responding |
|
||||
|
||||
Both execution modes provide the same task features: deduplication, progress tracking, cancellation, and visibility in the Task List UI. The difference is whether execution happens in a Celery worker (async) or inline (sync).
|
||||
|
||||
## Task Lifecycle
|
||||
|
||||
```
|
||||
PENDING ──→ IN_PROGRESS ────→ SUCCESS
|
||||
│ │
|
||||
│ ├──────────→ FAILURE
|
||||
│ ↓ ↑
|
||||
│ ABORTING ────────────┘
|
||||
│ │
|
||||
│ ├──────────→ TIMED_OUT (timeout)
|
||||
│ │
|
||||
└─────────────┴──────────→ ABORTED (user cancel)
|
||||
```
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| `PENDING` | Queued, awaiting execution |
|
||||
| `IN_PROGRESS` | Executing |
|
||||
| `ABORTING` | Abort/timeout triggered, abort handlers running |
|
||||
| `SUCCESS` | Completed successfully |
|
||||
| `FAILURE` | Failed with error or abort/cleanup handler exception |
|
||||
| `ABORTED` | Cancelled by user/admin |
|
||||
| `TIMED_OUT` | Exceeded configured timeout |
|
||||
|
||||
## Context API
|
||||
|
||||
Access task context via `get_context()` from within any `@task` function. The context provides methods for updating task metadata and registering handlers.
|
||||
|
||||
### Updating Task Metadata
|
||||
|
||||
Use `update_task()` to report progress and store custom payload data:
|
||||
|
||||
```python
|
||||
@task
|
||||
def my_task(items: list[int]) -> None:
|
||||
ctx = get_context()
|
||||
|
||||
for i, item in enumerate(items):
|
||||
result = process(item)
|
||||
ctx.update_task(
|
||||
progress=(i + 1, len(items)),
|
||||
payload={"last_result": result}
|
||||
)
|
||||
```
|
||||
|
||||
:::tip
|
||||
Call `update_task()` once per iteration for best performance. Frequent DB writes are throttled to limit metastore load, so batching progress and payload updates together in a single call ensures both are persisted at the same time.
|
||||
:::
|
||||
|
||||
#### Progress Formats
|
||||
|
||||
The `progress` parameter accepts three formats:
|
||||
|
||||
| Format | Example | Display |
|
||||
|--------|---------|---------|
|
||||
| `tuple[int, int]` | `progress=(3, 100)` | 3 of 100 (3%) with ETA |
|
||||
| `float` (0.0-1.0) | `progress=0.5` | 50% with ETA |
|
||||
| `int` | `progress=42` | 42 processed |
|
||||
|
||||
:::tip
|
||||
Use the tuple format `(current, total)` whenever possible. It provides the richest information to users: showing both the count and percentage, while still computing ETA automatically.
|
||||
:::
|
||||
|
||||
#### Payload
|
||||
|
||||
The `payload` parameter stores custom metadata that can help users understand what the task is doing. Each call to `update_task()` replaces the previous payload completely.
|
||||
|
||||
In the Task List UI, when a payload is defined, an info icon appears in the **Details** column. Users can hover over it to see the JSON content.
|
||||
|
||||
### Handlers
|
||||
|
||||
Register handlers to run cleanup logic or respond to abort requests:
|
||||
|
||||
| Handler | When it runs | Use case |
|
||||
|---------|--------------|----------|
|
||||
| `on_cleanup` | Always (success, failure, abort) | Release resources, close connections |
|
||||
| `on_abort` | When task is aborted | Set stop flag, cancel external operations |
|
||||
|
||||
```python
|
||||
@task
|
||||
def my_task() -> None:
|
||||
ctx = get_context()
|
||||
|
||||
@ctx.on_cleanup
|
||||
def cleanup():
|
||||
logger.info("Task ended, cleaning up")
|
||||
|
||||
@ctx.on_abort
|
||||
def handle_abort():
|
||||
logger.info("Abort requested")
|
||||
|
||||
# ... task logic
|
||||
```
|
||||
|
||||
Multiple handlers of the same type execute in LIFO order (last registered runs first). Abort handlers run first when abort is detected, then cleanup handlers run when the task ends.
|
||||
|
||||
#### Best-Effort Execution
|
||||
|
||||
**All registered handlers will always be attempted, even if one fails.** This ensures that a failure in one handler doesn't prevent other handlers from running their cleanup logic.
|
||||
|
||||
For example, if you have three cleanup handlers and the second one throws an exception:
|
||||
1. Handler 3 runs ✓
|
||||
2. Handler 2 throws an exception ✗ (logged, but execution continues)
|
||||
3. Handler 1 runs ✓
|
||||
|
||||
If any handler fails, the task is marked as `FAILURE` with combined error details showing all handler failures.
|
||||
|
||||
:::tip
|
||||
Write handlers to be independent and self-contained. Don't assume previous handlers succeeded, and don't rely on shared state between handlers.
|
||||
:::
|
||||
|
||||
## Making Tasks Abortable
|
||||
|
||||
When users click **Cancel** in the Task List, the system decides whether to **abort** (stop) the task or **unsubscribe** (remove the user from a shared task). Abort occurs when:
|
||||
- It's a private or system task
|
||||
- It's a shared task and the user is the last subscriber
|
||||
- An admin checks **Force abort** to stop the task for all subscribers
|
||||
|
||||
Pending tasks can always be aborted: they simply won't start. In-progress tasks require an abort handler to be abortable:
|
||||
|
||||
```python
|
||||
@task
|
||||
def abortable_task(items: list[str]) -> None:
|
||||
ctx = get_context()
|
||||
should_stop = False
|
||||
|
||||
@ctx.on_abort
|
||||
def handle_abort():
|
||||
nonlocal should_stop
|
||||
should_stop = True
|
||||
logger.info("Abort signal received")
|
||||
|
||||
@ctx.on_cleanup
|
||||
def cleanup():
|
||||
logger.info("Task ended, cleaning up")
|
||||
|
||||
for item in items:
|
||||
if should_stop:
|
||||
return # Exit gracefully
|
||||
process(item)
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Registering `on_abort` marks the task as abortable and starts the abort listener
|
||||
- The abort handler fires automatically when abort is triggered
|
||||
- Use a flag pattern to gracefully stop processing at safe points
|
||||
- Without an abort handler, in-progress tasks cannot be aborted: the Cancel button in the Task List UI will be disabled
|
||||
|
||||
The framework automatically skips execution if a task was aborted while pending: no manual check needed at task start.
|
||||
|
||||
:::tip
|
||||
Always implement an abort handler for long-running tasks. This allows users to cancel unneeded tasks and free up worker capacity for other operations.
|
||||
:::
|
||||
|
||||
## Timeouts
|
||||
|
||||
Set a timeout to automatically abort tasks that run too long:
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import task, get_context, TaskOptions
|
||||
|
||||
# Set default timeout in decorator
|
||||
@task(timeout=300) # 5 minutes
|
||||
def process_data(dataset_id: int) -> None:
|
||||
ctx = get_context()
|
||||
should_stop = False
|
||||
|
||||
@ctx.on_abort
|
||||
def handle_abort():
|
||||
nonlocal should_stop
|
||||
should_stop = True
|
||||
|
||||
for chunk in fetch_large_dataset(dataset_id):
|
||||
if should_stop:
|
||||
return
|
||||
process(chunk)
|
||||
|
||||
# Override timeout at call time
|
||||
task = process_data.schedule(
|
||||
dataset_id=123,
|
||||
options=TaskOptions(timeout=600) # Override to 10 minutes
|
||||
)
|
||||
```
|
||||
|
||||
### How Timeouts Work
|
||||
|
||||
The timeout timer starts when the task begins executing (status changes to `IN_PROGRESS`). When the timeout expires:
|
||||
|
||||
1. **With an abort handler registered:** The task transitions to `ABORTING`, abort handlers run, then cleanup handlers run. The final status depends on handler execution:
|
||||
- If handlers complete successfully → `TIMED_OUT` status
|
||||
- If handlers throw an exception → `FAILURE` status
|
||||
|
||||
2. **Without an abort handler:** The framework cannot forcibly terminate the task. A warning is logged, and the task continues running. The Task List UI shows a warning indicator (⚠️) in the Details column to alert users that the timeout cannot be enforced.
|
||||
|
||||
### Timeout Precedence
|
||||
|
||||
| Source | Priority | Example |
|
||||
|--------|----------|---------|
|
||||
| `TaskOptions.timeout` | Highest | `options=TaskOptions(timeout=600)` |
|
||||
| `@task(timeout=...)` | Default | `@task(timeout=300)` |
|
||||
| Not set | No timeout | Task runs indefinitely |
|
||||
|
||||
Call-time options always override decorator defaults, allowing tasks to have sensible defaults while permitting callers to extend or shorten the timeout for specific use cases.
|
||||
|
||||
:::warning
|
||||
Timeouts require an abort handler to be effective. Without one, the timeout triggers only a warning and the task continues running. Always implement an abort handler when using timeouts.
|
||||
:::
|
||||
|
||||
## Deduplication
|
||||
|
||||
Use `task_key` to prevent duplicate task execution:
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import TaskOptions
|
||||
|
||||
# Without key - creates new task each time (random UUID)
|
||||
task1 = my_task.schedule(x=1)
|
||||
task2 = my_task.schedule(x=1) # Different task
|
||||
|
||||
# With key - joins existing task if active
|
||||
task1 = my_task.schedule(x=1, options=TaskOptions(task_key="report_123"))
|
||||
task2 = my_task.schedule(x=1, options=TaskOptions(task_key="report_123")) # Returns same task
|
||||
```
|
||||
|
||||
When a task with matching key already exists, the user is added as a subscriber and the existing task is returned. This behavior is consistent across all scopes—private tasks naturally have only one subscriber since their deduplication key includes the user ID.
|
||||
|
||||
Deduplication only applies to active tasks (pending/in-progress). Once a task completes, a new task with the same key can be created.
|
||||
|
||||
### Sync Join-and-Wait
|
||||
|
||||
When a sync call joins an existing task, it blocks until the task completes:
|
||||
|
||||
```python
|
||||
# Schedule async task
|
||||
task = my_task.schedule(options=TaskOptions(task_key="report_123"))
|
||||
|
||||
# Later sync call with same key blocks until completion of the active task
|
||||
task2 = my_task(options=TaskOptions(task_key="report_123"))
|
||||
assert task.uuid == task2.uuid # True
|
||||
print(task2.status) # "success" (terminal status)
|
||||
```
|
||||
|
||||
## Task Scopes
|
||||
|
||||
```python
|
||||
from superset_core.api.tasks import task, TaskScope
|
||||
|
||||
@task # Private by default
|
||||
def private_task(): ...
|
||||
|
||||
@task(scope=TaskScope.SHARED) # Multiple users can subscribe
|
||||
def shared_task(): ...
|
||||
|
||||
@task(scope=TaskScope.SYSTEM) # Admin-only visibility
|
||||
def system_task(): ...
|
||||
```
|
||||
|
||||
| Scope | Visibility | Cancel Behavior |
|
||||
|-------|------------|-----------------|
|
||||
| `PRIVATE` | Creator only | Cancels immediately |
|
||||
| `SHARED` | All subscribers | Last subscriber cancels; others unsubscribe |
|
||||
| `SYSTEM` | Admins only | Admin cancels |
|
||||
|
||||
## Task Cleanup
|
||||
|
||||
Completed tasks accumulate in the database over time. Configure a scheduled prune job to automatically remove old tasks:
|
||||
|
||||
```python
|
||||
# In your superset_config.py, add to your Celery beat schedule:
|
||||
CELERY_CONFIG.beat_schedule["prune_tasks"] = {
|
||||
"task": "prune_tasks",
|
||||
"schedule": crontab(minute=0, hour=0), # Run daily at midnight
|
||||
"kwargs": {
|
||||
"retention_period_days": 90, # Keep tasks for 90 days
|
||||
"max_rows_per_run": 10000, # Limit deletions per run
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The prune job only removes tasks in terminal states (`SUCCESS`, `FAILURE`, `ABORTED`, `TIMED_OUT`). Active tasks (`PENDING`, `IN_PROGRESS`, `ABORTING`) are never pruned.
|
||||
|
||||
See `superset/config.py` for a complete example configuration.
|
||||
|
||||
:::tip Signal Cache for Faster Notifications
|
||||
By default, abort detection and sync join-and-wait use database polling. Configure `SIGNAL_CACHE_CONFIG` to enable Redis pub/sub for real-time notifications. See [Signal Cache Backend](/docs/configuration/cache#signal-cache-backend) for configuration details.
|
||||
:::
|
||||
|
||||
## API Reference
|
||||
|
||||
### @task Decorator
|
||||
|
||||
```python
|
||||
@task(
|
||||
name: str | None = None,
|
||||
scope: TaskScope = TaskScope.PRIVATE,
|
||||
timeout: int | None = None
|
||||
)
|
||||
```
|
||||
|
||||
- `name`: Task identifier (defaults to function name)
|
||||
- `scope`: `PRIVATE`, `SHARED`, or `SYSTEM`
|
||||
- `timeout`: Default timeout in seconds (can be overridden via `TaskOptions`)
|
||||
|
||||
### TaskContext Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `update_task(progress, payload)` | Update progress and/or custom payload |
|
||||
| `on_cleanup(handler)` | Register cleanup handler |
|
||||
| `on_abort(handler)` | Register abort handler (makes task abortable) |
|
||||
|
||||
### TaskOptions
|
||||
|
||||
```python
|
||||
TaskOptions(
|
||||
task_key: str | None = None,
|
||||
task_name: str | None = None,
|
||||
timeout: int | None = None
|
||||
)
|
||||
```
|
||||
|
||||
- `task_key`: Deduplication key (also used as display name if `task_name` is not set)
|
||||
- `task_name`: Human-readable display name for the Task List UI
|
||||
- `timeout`: Timeout in seconds (overrides decorator default)
|
||||
|
||||
:::tip
|
||||
Provide a descriptive `task_name` for better readability in the Task List UI. While `task_key` is used for deduplication and may be technical (e.g., `chart_export_123`), `task_name` can be user-friendly (e.g., `"Export Sales Chart 123"`).
|
||||
:::
|
||||
|
||||
## Error Handling
|
||||
|
||||
Let exceptions propagate: the framework captures them automatically and sets task status to `FAILURE`:
|
||||
|
||||
```python
|
||||
@task
|
||||
def risky_task() -> None:
|
||||
# No try/catch needed - framework handles it
|
||||
result = operation_that_might_fail()
|
||||
```
|
||||
|
||||
On failure, the framework records:
|
||||
- `error_message`: Exception message
|
||||
- `exception_type`: Exception class name
|
||||
- `stack_trace`: Full traceback (visible when `SHOW_STACKTRACE=True`)
|
||||
|
||||
In the Task List UI, failed tasks show error details when hovering over the status. When stack traces are enabled, a separate bug icon appears in the **Details** column for viewing the full traceback.
|
||||
|
||||
Cleanup handlers still run after an exception, so resources can be properly released as necessary.
|
||||
|
||||
:::tip
|
||||
Use descriptive exception messages. In environments where stack traces are hidden (`SHOW_STACKTRACE=False`), users see only the error message and exception type when hovering over failed tasks. Clear messages help users troubleshoot issues without administrator assistance.
|
||||
:::
|
||||
@@ -47,5 +47,5 @@ This is a list of statements that describe how we do frontend development in Sup
|
||||
- We do not debate code formatting style in PRs, instead relying on automated tooling to enforce it.
|
||||
- If there's not a linting rule, we don't have a rule!
|
||||
- See: [Linting How-Tos](../contributing/howtos#typescript--javascript)
|
||||
- We use [React Storybook](https://storybook.js.org/) and [Applitools](https://applitools.com/) to help preview/test and stabilize our components
|
||||
- We use [React Storybook](https://storybook.js.org/) to help preview/test and stabilize our components
|
||||
- A public Storybook with components from the `master` branch is available [here](https://apache-superset.github.io/superset-ui/?path=/story/*)
|
||||
|
||||
@@ -53,6 +53,7 @@ module.exports = {
|
||||
'extensions/deployment',
|
||||
'extensions/mcp',
|
||||
'extensions/security',
|
||||
'extensions/tasks',
|
||||
'extensions/registry',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -60,7 +60,6 @@ Superset embraces a testing pyramid approach:
|
||||
- **pytest**: Python testing framework with powerful fixtures and plugins
|
||||
- **SQLAlchemy Test Utilities**: Database testing and transaction management
|
||||
- **Flask Test Client**: API endpoint testing and request simulation
|
||||
- **Factory Boy**: Test data generation and model factories
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -157,7 +156,6 @@ npm run test:coverage
|
||||
- **React Testing Library** - Component testing utilities
|
||||
- **Playwright** - End-to-end testing (replacing Cypress)
|
||||
- **Storybook** - Component development and testing
|
||||
- **MSW** - API mocking for testing
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ version: 1
|
||||
|
||||
# Caching
|
||||
|
||||
:::note
|
||||
When a cache backend is configured, Superset expects it to remain available. Operations will
|
||||
fail if the configured backend becomes unavailable rather than silently degrading. This
|
||||
fail-fast behavior ensures operators are immediately aware of infrastructure issues.
|
||||
:::
|
||||
|
||||
Superset uses [Flask-Caching](https://flask-caching.readthedocs.io/) for caching purposes.
|
||||
Flask-Caching supports various caching backends, including Redis (recommended), Memcached,
|
||||
SimpleCache (in-memory), or the local filesystem.
|
||||
@@ -153,6 +159,84 @@ Then on configuration:
|
||||
WEBDRIVER_AUTH_FUNC = auth_driver
|
||||
```
|
||||
|
||||
## Signal Cache Backend
|
||||
|
||||
Superset supports an optional signal cache (`SIGNAL_CACHE_CONFIG`) for
|
||||
high-performance distributed operations. This configuration enables:
|
||||
|
||||
- **Distributed locking**: Moves lock operations from the metadata database to Redis, improving
|
||||
performance and reducing metastore load
|
||||
- **Real-time event notifications**: Enables instant pub/sub messaging for task abort signals and
|
||||
completion notifications instead of polling-based approaches
|
||||
|
||||
:::note
|
||||
This requires Redis or Valkey specifically—it uses Redis-specific features (pub/sub, `SET NX EX`)
|
||||
that are not available in general Flask-Caching backends.
|
||||
:::
|
||||
|
||||
### Configuration
|
||||
|
||||
The signal cache uses Flask-Caching style configuration for consistency with other cache
|
||||
backends. Configure `SIGNAL_CACHE_CONFIG` in `superset_config.py`:
|
||||
|
||||
```python
|
||||
SIGNAL_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_REDIS_HOST": "localhost",
|
||||
"CACHE_REDIS_PORT": 6379,
|
||||
"CACHE_REDIS_DB": 0,
|
||||
"CACHE_REDIS_PASSWORD": "", # Optional
|
||||
}
|
||||
```
|
||||
|
||||
For Redis Sentinel deployments:
|
||||
|
||||
```python
|
||||
SIGNAL_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisSentinelCache",
|
||||
"CACHE_REDIS_SENTINELS": [("sentinel1", 26379), ("sentinel2", 26379)],
|
||||
"CACHE_REDIS_SENTINEL_MASTER": "mymaster",
|
||||
"CACHE_REDIS_SENTINEL_PASSWORD": None, # Sentinel password (if different)
|
||||
"CACHE_REDIS_PASSWORD": "", # Redis password
|
||||
"CACHE_REDIS_DB": 0,
|
||||
}
|
||||
```
|
||||
|
||||
For SSL/TLS connections:
|
||||
|
||||
```python
|
||||
SIGNAL_CACHE_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_REDIS_HOST": "redis.example.com",
|
||||
"CACHE_REDIS_PORT": 6380,
|
||||
"CACHE_REDIS_SSL": True,
|
||||
"CACHE_REDIS_SSL_CERTFILE": "/path/to/client.crt",
|
||||
"CACHE_REDIS_SSL_KEYFILE": "/path/to/client.key",
|
||||
"CACHE_REDIS_SSL_CA_CERTS": "/path/to/ca.crt",
|
||||
}
|
||||
```
|
||||
|
||||
### Distributed Lock TTL
|
||||
|
||||
You can configure the default lock TTL (time-to-live) in seconds. Locks automatically expire after
|
||||
this duration to prevent deadlocks from crashed processes:
|
||||
|
||||
```python
|
||||
DISTRIBUTED_LOCK_DEFAULT_TTL = 30 # Default: 30 seconds
|
||||
```
|
||||
|
||||
Individual lock acquisitions can override this value when needed.
|
||||
|
||||
### Database-Only Mode
|
||||
|
||||
When `SIGNAL_CACHE_CONFIG` is not configured, Superset uses database-backed operations:
|
||||
|
||||
- **Locking**: Uses the KeyValue table with periodic cleanup of expired entries
|
||||
- **Event notifications**: Uses database polling instead of pub/sub
|
||||
|
||||
While database-backed operations work reliably, the Redis backend is recommended for production
|
||||
deployments where low latency and reduced database load are important.
|
||||
|
||||
:::resources
|
||||
- [Blog: The Data Engineer's Guide to Lightning-Fast Superset Dashboards](https://preset.io/blog/the-data-engineers-guide-to-lightning-fast-apache-superset-dashboards/)
|
||||
- [Blog: Accelerating Dashboards with Materialized Views](https://preset.io/blog/accelerating-apache-superset-dashboards-with-materialized-views/)
|
||||
|
||||
@@ -636,7 +636,6 @@ const config: Config = {
|
||||
copyright: `
|
||||
<div class="footer__ci-services">
|
||||
<span>CI powered by</span>
|
||||
<a href="https://applitools.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/applitools.png" alt="Applitools" title="Applitools - Visual Testing" /></a>
|
||||
<a href="https://www.netlify.com/" target="_blank" rel="nofollow noopener noreferrer"><img src="/img/netlify.png" alt="Netlify" title="Netlify - Deploy Previews" /></a>
|
||||
</div>
|
||||
<p>Copyright © ${new Date().getFullYear()},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"copyright": {
|
||||
"message": "\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://applitools.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/applitools.png\" alt=\"Applitools\" title=\"Applitools - Visual Testing\" /></a>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a> | \n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a> | \n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a> | \n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a> | \n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a> | \n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
|
||||
"message": "\n <div class=\"footer__ci-services\">\n <span>CI powered by</span>\n <a href=\"https://www.netlify.com/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\"><img src=\"/img/netlify.png\" alt=\"Netlify\" title=\"Netlify - Deploy Previews\" /></a>\n </div>\n <p>Copyright © 2026,\n The <a href=\"https://www.apache.org/\" target=\"_blank\" rel=\"noreferrer\">Apache Software Foundation</a>,\n Licensed under the Apache <a href=\"https://apache.org/licenses/LICENSE-2.0\" target=\"_blank\" rel=\"noreferrer\">License</a>.</p>\n <p><small>Apache Superset, Apache, Superset, the Superset logo, and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation. All other products or name brands are trademarks of their respective holders, including The Apache Software Foundation.\n <a href=\"https://www.apache.org/\" target=\"_blank\">Apache Software Foundation</a> resources</small></p>\n <img class=\"footer__divider\" src=\"/img/community/line.png\" alt=\"Divider\" />\n <p>\n <small>\n <a href=\"/docs/security/\" target=\"_blank\" rel=\"noreferrer\">Security</a> | \n <a href=\"https://www.apache.org/foundation/sponsorship.html\" target=\"_blank\" rel=\"noreferrer\">Donate</a> | \n <a href=\"https://www.apache.org/foundation/thanks.html\" target=\"_blank\" rel=\"noreferrer\">Thanks</a> | \n <a href=\"https://apache.org/events/current-event\" target=\"_blank\" rel=\"noreferrer\">Events</a> | \n <a href=\"https://apache.org/licenses/\" target=\"_blank\" rel=\"noreferrer\">License</a> | \n <a href=\"https://privacy.apache.org/policies/privacy-policy-public.html\" target=\"_blank\" rel=\"noreferrer\">Privacy</a>\n </small>\n </p>\n <!-- telemetry/analytics pixel: -->\n <img referrerPolicy=\"no-referrer-when-downgrade\" src=\"https://static.scarf.sh/a.png?x-pxid=39ae6855-95fc-4566-86e5-360d542b0a68\" />\n ",
|
||||
"description": "The footer copyright"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.11",
|
||||
"antd": "^6.2.3",
|
||||
"antd": "^6.3.0",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"caniuse-lite": "^1.0.30001769",
|
||||
"docusaurus-plugin-openapi-docs": "^4.6.0",
|
||||
@@ -94,7 +94,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.55.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
@@ -102,8 +102,8 @@
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
"webpack": "^5.105.0"
|
||||
"typescript-eslint": "^8.55.0",
|
||||
"webpack": "^5.105.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -97,6 +97,7 @@ const sidebars = {
|
||||
'extensions/deployment',
|
||||
'extensions/mcp',
|
||||
'extensions/security',
|
||||
'extensions/tasks',
|
||||
'extensions/registry',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -579,7 +579,7 @@ const DatabaseIndex: React.FC<DatabaseIndexProps> = ({ data }) => {
|
||||
columns={columns}
|
||||
rowKey={(record) => record.isCompatible ? `${record.compatibleWith}-${record.name}` : record.name}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
defaultPageSize: 20,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `${total} databases`,
|
||||
}}
|
||||
|
||||
BIN
docs/static/img/applitools.png
vendored
BIN
docs/static/img/applitools.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
372
docs/yarn.lock
372
docs/yarn.lock
@@ -195,19 +195,19 @@
|
||||
dependencies:
|
||||
"@ant-design/fast-color" "^3.0.0"
|
||||
|
||||
"@ant-design/cssinjs-utils@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz"
|
||||
integrity sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@ant-design/cssinjs" "^2.0.1"
|
||||
"@ant-design/cssinjs" "^2.1.0"
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@rc-component/util" "^1.4.0"
|
||||
|
||||
"@ant-design/cssinjs@^2.0.1", "@ant-design/cssinjs@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.0.3.tgz"
|
||||
integrity sha512-HAo8SZ3a6G8v6jT0suCz1270na6EA3obeJWM4uzRijBhdwdoMAXWK2f4WWkwB28yUufsfk3CAhN1coGPQq4kNQ==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.11.1"
|
||||
"@emotion/hash" "^0.8.0"
|
||||
@@ -2842,20 +2842,20 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.24.4"
|
||||
|
||||
"@rc-component/cascader@~1.11.0":
|
||||
version "1.11.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.11.0.tgz"
|
||||
integrity sha512-VDiEsskThWi8l0/1Nquc9I4ytcMKQYAb9Jkm6wiX5O5fpcMRsm+b8OulBMbr/b4rFTl/2y2y4GdKqQ+2whD+XQ==
|
||||
"@rc-component/cascader@~1.14.0":
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.14.0.tgz#74e1fca58cb14f8f75f6e4bf1debd90534aaea7c"
|
||||
integrity sha512-Ip9356xwZUR2nbW5PRVGif4B/bDve4pLa/N+PGbvBaTnjbvmN4PFMBGQSmlDlzKP1ovxaYMvwF/dI9lXNLT4iQ==
|
||||
dependencies:
|
||||
"@rc-component/select" "~1.5.0"
|
||||
"@rc-component/tree" "~1.1.0"
|
||||
"@rc-component/select" "~1.6.0"
|
||||
"@rc-component/tree" "~1.2.0"
|
||||
"@rc-component/util" "^1.4.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/checkbox@~1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-1.0.1.tgz"
|
||||
integrity sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ==
|
||||
"@rc-component/checkbox@~2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/checkbox/-/checkbox-2.0.0.tgz#90e0b30c276e507a5ab9942f626fe4572f5e4ff9"
|
||||
integrity sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==
|
||||
dependencies:
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
@@ -2870,10 +2870,10 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/color-picker@~3.0.3":
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.0.3.tgz"
|
||||
integrity sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@ant-design/fast-color" "^3.0.0"
|
||||
"@rc-component/util" "^1.3.0"
|
||||
@@ -2886,24 +2886,24 @@
|
||||
dependencies:
|
||||
"@rc-component/util" "^1.3.0"
|
||||
|
||||
"@rc-component/dialog@~1.8.2":
|
||||
version "1.8.3"
|
||||
resolved "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.8.3.tgz"
|
||||
integrity sha512-ek1ai9PYSyQ9/3RW6jE62lB8rGb9Qw/NTC8/03HbLbosNboknqTuyppbBvUwJFLWeW307F7xBQ6IJJV4OWWmXw==
|
||||
"@rc-component/dialog@~1.8.4":
|
||||
version "1.8.4"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.4.tgz#e1f05f311539852f40a5717bc3874ce0af64c6ff"
|
||||
integrity sha512-Ay6PM7phkTkquplG8fWfUGFZ2GTLx9diTl4f0d8Eqxd7W1u1KjE9AQooFQHOHnhZf0Ya3z51+5EKCWHmt/dNEw==
|
||||
dependencies:
|
||||
"@rc-component/motion" "^1.1.3"
|
||||
"@rc-component/portal" "^2.1.0"
|
||||
"@rc-component/util" "^1.7.0"
|
||||
"@rc-component/util" "^1.9.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/drawer@~1.4.1":
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.1.tgz"
|
||||
integrity sha512-kNJQie/QjJO5wGeWrZQwSGeuo8staxXx1nYN+dpK2UY7i8teo5PQdZ6ukKSnnW9vmPXsLn3F5nKYRbf43e8+5g==
|
||||
"@rc-component/drawer@~1.4.2":
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/drawer/-/drawer-1.4.2.tgz#eb13a6556fb67be28407295c89401b01549224e0"
|
||||
integrity sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==
|
||||
dependencies:
|
||||
"@rc-component/motion" "^1.1.4"
|
||||
"@rc-component/portal" "^2.1.3"
|
||||
"@rc-component/util" "^1.7.0"
|
||||
"@rc-component/util" "^1.9.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/dropdown@~1.0.0", "@rc-component/dropdown@~1.0.2":
|
||||
@@ -3082,21 +3082,10 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/select@~1.5.0":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/@rc-component/select/-/select-1.5.1.tgz"
|
||||
integrity sha512-ARXtwfCVnpDJj1bQjh1cimUlNQkZiN72hvtL2G4mKXIYfkokYdA2Vyu2deAfY7kuHSWpmZygVuohQt6TxOYjnA==
|
||||
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.5.2":
|
||||
version "1.5.2"
|
||||
resolved "https://registry.npmjs.org/@rc-component/select/-/select-1.5.2.tgz"
|
||||
integrity sha512-7wqD5D4I2+fc5XoB4nzDDK656QPlDnFAUaxLljkU1wwSpi4+MZxndv9vgg7NQfveuuf0/ilUdOjuPg7NPl7Mmg==
|
||||
"@rc-component/select@~1.6.0", "@rc-component/select@~1.6.5":
|
||||
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"
|
||||
@@ -3180,23 +3169,23 @@
|
||||
"@rc-component/util" "^1.7.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/tree-select@~1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.6.0.tgz"
|
||||
integrity sha512-UvEGmZT+gcVvRwImAZg3/sXw9nUdn4FmCs1rSIMWjEXEIAo0dTGmIyWuLCvs+1rGe9AZ7CHMPiQUEbdadwV0fw==
|
||||
"@rc-component/tree-select@~1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.8.0.tgz#480e84221befbd1fa93ab2034423e2b064e41981"
|
||||
integrity sha512-iYsPq3nuLYvGqdvFAW+l+I9ASRIOVbMXyA8FGZg2lGym/GwkaWeJGzI4eJ7c9IOEhRj0oyfIN4S92Fl3J05mjQ==
|
||||
dependencies:
|
||||
"@rc-component/select" "~1.5.0"
|
||||
"@rc-component/tree" "~1.1.0"
|
||||
"@rc-component/select" "~1.6.0"
|
||||
"@rc-component/tree" "~1.2.0"
|
||||
"@rc-component/util" "^1.4.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/tree@~1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/tree/-/tree-1.1.0.tgz"
|
||||
integrity sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA==
|
||||
"@rc-component/tree@~1.2.0", "@rc-component/tree@~1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.2.3.tgz#a70ae847a768763f4f461375620c1feccfcc933a"
|
||||
integrity sha512-mG8hF2ogQcKaEpfyxzPvMWqqkptofd7Sf+YiXOpPzuXLTLwNKfLDJtysc1/oybopbnzxNqWh2Vgwi+GYwNIb7w==
|
||||
dependencies:
|
||||
"@rc-component/motion" "^1.0.0"
|
||||
"@rc-component/util" "^1.2.1"
|
||||
"@rc-component/util" "^1.8.1"
|
||||
"@rc-component/virtual-list" "^1.0.1"
|
||||
clsx "^2.1.1"
|
||||
|
||||
@@ -3219,18 +3208,10 @@
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.npmjs.org/@rc-component/util/-/util-1.7.0.tgz"
|
||||
integrity sha512-tIvIGj4Vl6fsZFvWSkYw9sAfiCKUXMyhVz6kpKyZbwyZyRPqv2vxYZROdaO1VB4gqTNvUZFXh6i3APUiterw5g==
|
||||
dependencies:
|
||||
is-mobile "^5.0.0"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@rc-component/util@^1.8.1":
|
||||
version "1.8.2"
|
||||
resolved "https://registry.npmjs.org/@rc-component/util/-/util-1.8.2.tgz"
|
||||
integrity sha512-mx0F1VSqatbucxAcM2+/d4MAGJv/CpXxM9sAebUJkrgQLHsffY97o6gFx8cy+vqVTT/Hr8/6A8j3IYp6n5754g==
|
||||
"@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1", "@rc-component/util@^1.9.0":
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.9.0.tgz#ec5fe657a98554f26ef761345ca4b745be00af0e"
|
||||
integrity sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==
|
||||
dependencies:
|
||||
is-mobile "^5.0.0"
|
||||
react-is "^18.2.0"
|
||||
@@ -4735,100 +4716,100 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.54.0", "@typescript-eslint/eslint-plugin@^8.52.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz"
|
||||
integrity sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==
|
||||
"@typescript-eslint/eslint-plugin@8.55.0", "@typescript-eslint/eslint-plugin@^8.52.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz#086d2ef661507b561f7b17f62d3179d692a0765f"
|
||||
integrity sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.54.0"
|
||||
"@typescript-eslint/type-utils" "8.54.0"
|
||||
"@typescript-eslint/utils" "8.54.0"
|
||||
"@typescript-eslint/visitor-keys" "8.54.0"
|
||||
"@typescript-eslint/scope-manager" "8.55.0"
|
||||
"@typescript-eslint/type-utils" "8.55.0"
|
||||
"@typescript-eslint/utils" "8.55.0"
|
||||
"@typescript-eslint/visitor-keys" "8.55.0"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/parser@8.54.0", "@typescript-eslint/parser@^8.52.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz"
|
||||
integrity sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==
|
||||
"@typescript-eslint/parser@8.55.0", "@typescript-eslint/parser@^8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.55.0.tgz#6eace4e9e95f178d3447ed1f17f3d6a5dfdb345c"
|
||||
integrity sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.54.0"
|
||||
"@typescript-eslint/types" "8.54.0"
|
||||
"@typescript-eslint/typescript-estree" "8.54.0"
|
||||
"@typescript-eslint/visitor-keys" "8.54.0"
|
||||
"@typescript-eslint/scope-manager" "8.55.0"
|
||||
"@typescript-eslint/types" "8.55.0"
|
||||
"@typescript-eslint/typescript-estree" "8.55.0"
|
||||
"@typescript-eslint/visitor-keys" "8.55.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz"
|
||||
integrity sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==
|
||||
"@typescript-eslint/project-service@8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.55.0.tgz#b8a71c06a625bdad481c24d5614b68e252f3ae9b"
|
||||
integrity sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.54.0"
|
||||
"@typescript-eslint/types" "^8.54.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.55.0"
|
||||
"@typescript-eslint/types" "^8.55.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz"
|
||||
integrity sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==
|
||||
"@typescript-eslint/scope-manager@8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz#8a0752c31c788651840dc98f840b0c2ebe143b8c"
|
||||
integrity sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.54.0"
|
||||
"@typescript-eslint/visitor-keys" "8.54.0"
|
||||
"@typescript-eslint/types" "8.55.0"
|
||||
"@typescript-eslint/visitor-keys" "8.55.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.54.0", "@typescript-eslint/tsconfig-utils@^8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz"
|
||||
integrity sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==
|
||||
"@typescript-eslint/tsconfig-utils@8.55.0", "@typescript-eslint/tsconfig-utils@^8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz#62f1d005419985e09d37a040b2f1450e4e805afa"
|
||||
integrity sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==
|
||||
|
||||
"@typescript-eslint/type-utils@8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz"
|
||||
integrity sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==
|
||||
"@typescript-eslint/type-utils@8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz#195d854b3e56308ce475fdea2165313bb1190200"
|
||||
integrity sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.54.0"
|
||||
"@typescript-eslint/typescript-estree" "8.54.0"
|
||||
"@typescript-eslint/utils" "8.54.0"
|
||||
"@typescript-eslint/types" "8.55.0"
|
||||
"@typescript-eslint/typescript-estree" "8.55.0"
|
||||
"@typescript-eslint/utils" "8.55.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/types@8.54.0", "@typescript-eslint/types@^8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz"
|
||||
integrity sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==
|
||||
"@typescript-eslint/types@8.55.0", "@typescript-eslint/types@^8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.55.0.tgz#8449c5a7adac61184cac92dbf6315733569708c2"
|
||||
integrity sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz"
|
||||
integrity sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==
|
||||
"@typescript-eslint/typescript-estree@8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz#c83ac92c11ce79bedd984937c7780a65e7f7b2e3"
|
||||
integrity sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.54.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.54.0"
|
||||
"@typescript-eslint/types" "8.54.0"
|
||||
"@typescript-eslint/visitor-keys" "8.54.0"
|
||||
"@typescript-eslint/project-service" "8.55.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.55.0"
|
||||
"@typescript-eslint/types" "8.55.0"
|
||||
"@typescript-eslint/visitor-keys" "8.55.0"
|
||||
debug "^4.4.3"
|
||||
minimatch "^9.0.5"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/utils@8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz"
|
||||
integrity sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==
|
||||
"@typescript-eslint/utils@8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.55.0.tgz#c1744d94a3901deb01f58b09d3478d811f96d619"
|
||||
integrity sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.54.0"
|
||||
"@typescript-eslint/types" "8.54.0"
|
||||
"@typescript-eslint/typescript-estree" "8.54.0"
|
||||
"@typescript-eslint/scope-manager" "8.55.0"
|
||||
"@typescript-eslint/types" "8.55.0"
|
||||
"@typescript-eslint/typescript-estree" "8.55.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.54.0":
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz"
|
||||
integrity sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==
|
||||
"@typescript-eslint/visitor-keys@8.55.0":
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz#3d9a40fd4e3705c63d8fae3af58988add3ed464d"
|
||||
integrity sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.54.0"
|
||||
"@typescript-eslint/types" "8.55.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -5182,24 +5163,24 @@ 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.2.3:
|
||||
version "6.2.3"
|
||||
resolved "https://registry.npmjs.org/antd/-/antd-6.2.3.tgz"
|
||||
integrity sha512-q92r7/hcQAR2iv6CCysdz7c2Pdl/3nhslc3azF9e6AEl4knO6v+nlaeor1oF2jBanZ/tiw2m3NprOVUgPDvyhg==
|
||||
antd@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.3.0.tgz#0ea0596d2d8c19cb5860e3fa6ad20128f6c8ffda"
|
||||
integrity sha512-bbHJcASrRHp02wTpr940KtUHlTT6tvmaD4OAjqgOJXNmTQ/+qBDdBVWY/yeDV41p/WbWjTLlaqRGVbL3UEVpNw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.1"
|
||||
"@ant-design/cssinjs" "^2.0.3"
|
||||
"@ant-design/cssinjs-utils" "^2.0.2"
|
||||
"@ant-design/cssinjs" "^2.1.0"
|
||||
"@ant-design/cssinjs-utils" "^2.1.1"
|
||||
"@ant-design/fast-color" "^3.0.1"
|
||||
"@ant-design/icons" "^6.1.0"
|
||||
"@ant-design/react-slick" "~2.0.0"
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@rc-component/cascader" "~1.11.0"
|
||||
"@rc-component/checkbox" "~1.0.1"
|
||||
"@rc-component/cascader" "~1.14.0"
|
||||
"@rc-component/checkbox" "~2.0.0"
|
||||
"@rc-component/collapse" "~1.2.0"
|
||||
"@rc-component/color-picker" "~3.0.3"
|
||||
"@rc-component/dialog" "~1.8.2"
|
||||
"@rc-component/drawer" "~1.4.1"
|
||||
"@rc-component/color-picker" "~3.1.0"
|
||||
"@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/image" "~1.6.0"
|
||||
@@ -5217,7 +5198,7 @@ antd@^6.2.3:
|
||||
"@rc-component/rate" "~1.0.1"
|
||||
"@rc-component/resize-observer" "^1.1.1"
|
||||
"@rc-component/segmented" "~1.3.0"
|
||||
"@rc-component/select" "~1.5.2"
|
||||
"@rc-component/select" "~1.6.5"
|
||||
"@rc-component/slider" "~1.0.1"
|
||||
"@rc-component/steps" "~1.2.2"
|
||||
"@rc-component/switch" "~1.0.3"
|
||||
@@ -5226,11 +5207,11 @@ antd@^6.2.3:
|
||||
"@rc-component/textarea" "~1.1.2"
|
||||
"@rc-component/tooltip" "~1.4.0"
|
||||
"@rc-component/tour" "~2.3.0"
|
||||
"@rc-component/tree" "~1.1.0"
|
||||
"@rc-component/tree-select" "~1.6.0"
|
||||
"@rc-component/tree" "~1.2.3"
|
||||
"@rc-component/tree-select" "~1.8.0"
|
||||
"@rc-component/trigger" "^3.9.0"
|
||||
"@rc-component/upload" "~1.1.0"
|
||||
"@rc-component/util" "^1.8.1"
|
||||
"@rc-component/util" "^1.9.0"
|
||||
clsx "^2.1.1"
|
||||
dayjs "^1.11.11"
|
||||
scroll-into-view-if-needed "^3.1.0"
|
||||
@@ -5418,12 +5399,12 @@ available-typed-arrays@^1.0.7:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.12.2:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz"
|
||||
integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
|
||||
version "1.13.5"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43"
|
||||
integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.4"
|
||||
follow-redirects "^1.15.11"
|
||||
form-data "^4.0.5"
|
||||
proxy-from-env "^1.1.0"
|
||||
|
||||
babel-loader@^9.2.1:
|
||||
@@ -7207,14 +7188,6 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
enhanced-resolve@^5.17.4:
|
||||
version "5.18.4"
|
||||
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz"
|
||||
integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.2.0"
|
||||
|
||||
enhanced-resolve@^5.19.0:
|
||||
version "5.19.0"
|
||||
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz"
|
||||
@@ -7973,7 +7946,7 @@ flatted@^3.2.9:
|
||||
resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
|
||||
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.6:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.15.11:
|
||||
version "1.15.11"
|
||||
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz"
|
||||
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
|
||||
@@ -7995,10 +7968,10 @@ form-data-encoder@^2.1.2:
|
||||
resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz"
|
||||
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
|
||||
|
||||
form-data@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz"
|
||||
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
|
||||
form-data@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
|
||||
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
@@ -14099,7 +14072,7 @@ synckit@^0.11.12:
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
|
||||
tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0:
|
||||
tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz"
|
||||
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
|
||||
@@ -14396,15 +14369,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.54.0:
|
||||
version "8.54.0"
|
||||
resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz"
|
||||
integrity sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==
|
||||
typescript-eslint@^8.55.0:
|
||||
version "8.55.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.55.0.tgz#abae8295c5f0f82f816218113a46e89bc30c3de2"
|
||||
integrity sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.54.0"
|
||||
"@typescript-eslint/parser" "8.54.0"
|
||||
"@typescript-eslint/typescript-estree" "8.54.0"
|
||||
"@typescript-eslint/utils" "8.54.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.55.0"
|
||||
"@typescript-eslint/parser" "8.55.0"
|
||||
"@typescript-eslint/typescript-estree" "8.55.0"
|
||||
"@typescript-eslint/utils" "8.55.0"
|
||||
|
||||
typescript@~5.9.3:
|
||||
version "5.9.3"
|
||||
@@ -14887,14 +14860,6 @@ warning@^4.0.3:
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
watchpack@^2.4.4:
|
||||
version "2.4.4"
|
||||
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz"
|
||||
integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==
|
||||
dependencies:
|
||||
glob-to-regexp "^0.4.1"
|
||||
graceful-fs "^4.1.2"
|
||||
|
||||
watchpack@^2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz"
|
||||
@@ -15022,10 +14987,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.0:
|
||||
version "5.105.0"
|
||||
resolved "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz"
|
||||
integrity sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==
|
||||
webpack@^5.105.1, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.105.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.1.tgz#c05cb3621196c76fa3b3a9bea446d14616b83778"
|
||||
integrity sha512-Gdj3X74CLJJ8zy4URmK42W7wTZUJrqL+z8nyGEr4dTN0kb3nVs+ZvjbTOqRYPD7qX4tUmwyHL9Q9K6T1seW6Yw==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.8"
|
||||
@@ -15053,37 +15018,6 @@ webpack@^5.105.0:
|
||||
watchpack "^2.5.1"
|
||||
webpack-sources "^3.3.3"
|
||||
|
||||
webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.104.1"
|
||||
resolved "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz"
|
||||
integrity sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==
|
||||
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.17.4"
|
||||
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.4.4"
|
||||
webpack-sources "^3.3.3"
|
||||
|
||||
webpackbar@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz"
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.15.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.15.3 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 13.4.4
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -312,6 +312,12 @@ supersetNode:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "128Mi"
|
||||
|
||||
# -- Launch additional containers into supersetNode pod
|
||||
extraContainers: []
|
||||
@@ -410,6 +416,12 @@ supersetWorker:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "128Mi"
|
||||
# -- Launch additional containers into supersetWorker pod
|
||||
extraContainers: []
|
||||
# -- Annotations to be added to supersetWorker deployment
|
||||
@@ -492,6 +504,12 @@ supersetCeleryBeat:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "128Mi"
|
||||
# -- Launch additional containers into supersetCeleryBeat pods
|
||||
extraContainers: []
|
||||
# -- Annotations to be added to supersetCeleryBeat deployment
|
||||
@@ -585,6 +603,12 @@ supersetCeleryFlower:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "128Mi"
|
||||
# -- Launch additional containers into supersetCeleryFlower pods
|
||||
extraContainers: []
|
||||
# -- Annotations to be added to supersetCeleryFlower deployment
|
||||
@@ -749,6 +773,12 @@ init:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s
|
||||
resources:
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "128Mi"
|
||||
# -- A Superset init script
|
||||
# @default -- a script to create admin user and initialize roles
|
||||
initscript: |-
|
||||
|
||||
@@ -99,8 +99,8 @@ dependencies = [
|
||||
"simplejson>=3.15.0",
|
||||
"slack_sdk>=3.19.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy-utils>=0.38.3, <0.39",
|
||||
"sqlglot>=27.15.2, <28",
|
||||
"sqlalchemy-utils>=0.42.0, <0.43",
|
||||
"sqlglot>=28.10.0, <29",
|
||||
# newer pandas needs 0.9+
|
||||
"tabulate>=0.9.0, <1.0",
|
||||
"typing-extensions>=4, <5",
|
||||
|
||||
@@ -399,12 +399,12 @@ sqlalchemy==1.4.54
|
||||
# marshmallow-sqlalchemy
|
||||
# shillelagh
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-utils==0.38.3
|
||||
sqlalchemy-utils==0.42.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
# flask-appbuilder
|
||||
sqlglot==27.15.2
|
||||
sqlglot==28.10.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
|
||||
@@ -990,13 +990,13 @@ sqlalchemy==1.4.54
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.15.0
|
||||
# via apache-superset
|
||||
sqlalchemy-utils==0.38.3
|
||||
sqlalchemy-utils==0.42.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# apache-superset-core
|
||||
# flask-appbuilder
|
||||
sqlglot==27.15.2
|
||||
sqlglot==28.10.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-core"
|
||||
version = "0.0.1rc3"
|
||||
version = "0.0.1rc4"
|
||||
description = "Core Python package for building Apache Superset backend extensions and integrations"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
@@ -45,8 +45,8 @@ dependencies = [
|
||||
"flask-appbuilder>=5.0.2,<6",
|
||||
"pydantic>=2.8.0",
|
||||
"sqlalchemy>=1.4.0,<2.0",
|
||||
"sqlalchemy-utils>=0.38.0",
|
||||
"sqlglot>=27.15.2, <28",
|
||||
"sqlalchemy-utils>=0.42.0",
|
||||
"sqlglot>=28.10.0, <29",
|
||||
"typing-extensions>=4.0.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ from superset_core.api.models import (
|
||||
Query,
|
||||
SavedQuery,
|
||||
Tag,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
|
||||
@@ -248,6 +249,48 @@ 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",
|
||||
@@ -259,4 +302,5 @@ __all__ = [
|
||||
"SavedQueryDAO",
|
||||
"TagDAO",
|
||||
"KeyValueDAO",
|
||||
"TaskDAO",
|
||||
]
|
||||
|
||||
@@ -40,6 +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 (
|
||||
AsyncQueryHandle,
|
||||
QueryOptions,
|
||||
@@ -361,6 +362,132 @@ 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
|
||||
@@ -384,6 +511,8 @@ __all__ = [
|
||||
"SavedQuery",
|
||||
"Tag",
|
||||
"KeyValue",
|
||||
"Task",
|
||||
"TaskSubscriber",
|
||||
"CoreModel",
|
||||
"get_session",
|
||||
]
|
||||
|
||||
361
superset-core/src/superset_core/api/tasks.py
Normal file
361
superset-core/src/superset_core/api/tasks.py
Normal file
@@ -0,0 +1,361 @@
|
||||
# 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 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")
|
||||
|
||||
|
||||
class TaskStatus(str, Enum):
|
||||
"""
|
||||
Status of task execution.
|
||||
"""
|
||||
|
||||
PENDING = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
SUCCESS = "success"
|
||||
FAILURE = "failure"
|
||||
ABORTING = "aborting" # Abort/timeout requested, handlers running
|
||||
ABORTED = "aborted" # User/admin cancelled
|
||||
TIMED_OUT = "timed_out" # Timeout expired
|
||||
|
||||
|
||||
class TaskScope(str, Enum):
|
||||
"""
|
||||
Scope of task visibility and access control.
|
||||
"""
|
||||
|
||||
PRIVATE = "private" # User-specific tasks (default)
|
||||
SHARED = "shared" # Multi-user collaborative tasks
|
||||
SYSTEM = "system" # Admin-only background tasks
|
||||
|
||||
|
||||
class TaskProperties(TypedDict, total=False):
|
||||
"""
|
||||
TypedDict for task runtime state and execution config.
|
||||
|
||||
Stored as JSON in the database, accessed as a dict throughout the codebase.
|
||||
All fields are optional (total=False) - only set keys are present in the dict.
|
||||
|
||||
Usage:
|
||||
# Reading - always use .get() since keys may not be present
|
||||
if task.properties.get("is_abortable"):
|
||||
...
|
||||
|
||||
# Writing/updating - only include keys you want to set
|
||||
task.update_properties({"is_abortable": True, "progress_percent": 0.5})
|
||||
|
||||
Notes:
|
||||
- Sparse dict: only keys that are explicitly set are present
|
||||
- Unknown keys from JSON are preserved (forward compatibility)
|
||||
- Always use .get() for reads since keys may be absent
|
||||
"""
|
||||
|
||||
# Execution config - set at task creation
|
||||
execution_mode: Literal["async", "sync"]
|
||||
timeout: int
|
||||
|
||||
# Runtime state - set by framework during execution
|
||||
is_abortable: bool
|
||||
progress_percent: float
|
||||
progress_current: int
|
||||
progress_total: int
|
||||
|
||||
# Error info - set when task fails
|
||||
error_message: str
|
||||
exception_type: str
|
||||
stack_trace: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TaskOptions:
|
||||
"""
|
||||
Execution metadata for tasks.
|
||||
|
||||
NOTE: This is intentionally minimal for the initial implementation.
|
||||
Additional options (queue, priority, run_at, delay_s,
|
||||
max_retries, retry_backoff_s, tags, etc.) can be added later when needed.
|
||||
|
||||
Future enhancements will include:
|
||||
- Validation (e.g., run_at vs delay_s mutual exclusion)
|
||||
- Queue routing and priority management
|
||||
- Retry policies and backoff strategies
|
||||
|
||||
Example:
|
||||
from superset_core.api.tasks import TaskOptions, TaskScope
|
||||
|
||||
# Private task (default)
|
||||
task = my_task.schedule(arg1)
|
||||
|
||||
# Custom task with deduplication
|
||||
task = my_task.schedule(
|
||||
arg1,
|
||||
options=TaskOptions(
|
||||
task_key="custom_key",
|
||||
task_name="Custom Task Name"
|
||||
)
|
||||
)
|
||||
|
||||
# Task with custom name
|
||||
task = admin_task.schedule(
|
||||
options=TaskOptions(task_name="Admin Operation")
|
||||
)
|
||||
|
||||
# Task with timeout (overrides decorator default)
|
||||
task = long_task.schedule(
|
||||
options=TaskOptions(timeout=600) # 10 minute timeout
|
||||
)
|
||||
"""
|
||||
|
||||
task_key: str | None = None
|
||||
task_name: str | None = None
|
||||
timeout: int | None = None # Timeout in seconds
|
||||
|
||||
|
||||
class TaskContext(ABC):
|
||||
"""
|
||||
Abstract task context for write-only task state updates.
|
||||
|
||||
Tasks use this context to update their state (progress, payload) and
|
||||
check for cancellation. Tasks should not need to read their own state -
|
||||
they are the source of state, not consumers of it.
|
||||
|
||||
Host implementations will replace this abstract class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_task(
|
||||
self,
|
||||
progress: float | int | tuple[int, int] | None = None,
|
||||
payload: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Update task progress and/or payload atomically.
|
||||
|
||||
All parameters are optional. Payload is merged with existing data,
|
||||
not replaced. All updates occur in a single database transaction.
|
||||
|
||||
Progress can be specified in three ways:
|
||||
- float (0.0-1.0): Percentage only, e.g., 0.5 means 50%
|
||||
- int: Count only (total unknown), e.g., 42 means "42 items processed"
|
||||
- tuple[int, int]: Count and total, e.g., (3, 100) means "3 of 100"
|
||||
The percentage is automatically computed from count/total.
|
||||
|
||||
:param progress: Progress value, or None to leave unchanged
|
||||
:param payload: Payload data to merge (dict), or None to leave unchanged
|
||||
|
||||
Examples:
|
||||
# Percentage only - displays as "In progress: 50 %"
|
||||
ctx.update_task(progress=0.5)
|
||||
|
||||
# Count only (total unknown) - displays as "In progress: 42"
|
||||
ctx.update_task(progress=42)
|
||||
|
||||
# Count and total - displays as "In progress: 3 of 100 (3 %)"
|
||||
ctx.update_task(progress=(3, 100))
|
||||
|
||||
# Update payload only
|
||||
ctx.update_task(payload={"step": "processing"})
|
||||
|
||||
# Update both atomically
|
||||
ctx.update_task(
|
||||
progress=(80, 100),
|
||||
payload={"processed": 80, "total": 100}
|
||||
)
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_cleanup(self, handler: Callable[[], None]) -> Callable[[], None]:
|
||||
"""
|
||||
Register a cleanup handler that runs when the task ends.
|
||||
|
||||
Cleanup handlers are called when the task completes (success),
|
||||
fails with an error, or is cancelled. Multiple handlers can be
|
||||
registered and will execute in LIFO order (last registered runs first).
|
||||
|
||||
Can be used as a decorator:
|
||||
@ctx.on_cleanup
|
||||
def cleanup():
|
||||
logger.info("Task ended")
|
||||
|
||||
Or called directly:
|
||||
ctx.on_cleanup(lambda: logger.info("Task ended"))
|
||||
|
||||
:param handler: Cleanup function to register
|
||||
:returns: The handler (for decorator compatibility)
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def on_abort(self, handler: Callable[[], None]) -> Callable[[], None]:
|
||||
"""
|
||||
Register handler that runs when task is aborted.
|
||||
|
||||
When the first handler is registered, background polling starts
|
||||
automatically. The handler will be called when an abort is detected.
|
||||
|
||||
The handler executes in a background thread and the task code
|
||||
continues running unless the handler takes action to stop it.
|
||||
|
||||
:param handler: Callback function to execute when abort is detected
|
||||
:returns: The handler (for decorator compatibility)
|
||||
|
||||
Example:
|
||||
@ctx.on_abort
|
||||
def handle_abort():
|
||||
logger.info("Task was aborted!")
|
||||
cleanup_partial_work()
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
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",
|
||||
]
|
||||
@@ -56,19 +56,37 @@ class ModuleFederationConfig(BaseModel):
|
||||
|
||||
|
||||
class ContributionConfig(BaseModel):
|
||||
"""Configuration for frontend UI contributions."""
|
||||
"""Configuration for frontend UI contributions.
|
||||
|
||||
Views and menus use a nested structure: type -> scope -> location -> contributions.
|
||||
|
||||
Example:
|
||||
{
|
||||
"views": {
|
||||
"sqllab": {
|
||||
"panels": [{"id": "my-ext.panel", "name": "My Panel"}],
|
||||
"leftSidebar": [{"id": "my-ext.sidebar", "name": "Sidebar"}]
|
||||
}
|
||||
},
|
||||
"menus": {
|
||||
"sqllab": {
|
||||
"editor": {"primary": [...], "secondary": [...]}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
commands: list[dict[str, Any]] = Field(
|
||||
default_factory=list,
|
||||
description="Command contributions",
|
||||
)
|
||||
views: dict[str, list[dict[str, Any]]] = Field(
|
||||
views: dict[str, dict[str, list[dict[str, Any]]]] = Field(
|
||||
default_factory=dict,
|
||||
description="View contributions by location",
|
||||
description="View contributions by scope and location",
|
||||
)
|
||||
menus: dict[str, Any] = Field(
|
||||
menus: dict[str, dict[str, Any]] = Field(
|
||||
default_factory=dict,
|
||||
description="Menu contributions",
|
||||
description="Menu contributions by scope and location",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^11.1.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.8.2",
|
||||
"webpack": "^5.98.0",
|
||||
|
||||
@@ -135,7 +135,9 @@ module.exports = {
|
||||
'icons',
|
||||
'i18n-strings',
|
||||
'react-prefer-function-component',
|
||||
'react-you-might-not-need-an-effect',
|
||||
'prettier',
|
||||
'react-you-might-not-need-an-effect',
|
||||
],
|
||||
rules: {
|
||||
// === Essential Superset customizations ===
|
||||
@@ -235,12 +237,22 @@ module.exports = {
|
||||
'jsx-a11y/mouse-events-have-key-events': 0,
|
||||
'jsx-a11y/no-static-element-interactions': 0,
|
||||
|
||||
// React effect best practices
|
||||
'react-you-might-not-need-an-effect/no-empty-effect': 'error',
|
||||
'react-you-might-not-need-an-effect/no-pass-live-state-to-parent': 'error',
|
||||
'react-you-might-not-need-an-effect/no-initialize-state': 'error',
|
||||
|
||||
// Lodash
|
||||
'lodash/import-scope': [2, 'member'],
|
||||
|
||||
// File progress
|
||||
'file-progress/activate': 1,
|
||||
|
||||
// React effect rules
|
||||
'react-you-might-not-need-an-effect/no-adjust-state-on-prop-change':
|
||||
'error',
|
||||
'react-you-might-not-need-an-effect/no-pass-data-to-parent': 'error',
|
||||
|
||||
// Restricted imports
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
@@ -306,7 +318,6 @@ module.exports = {
|
||||
files: ['packages/**/src/**/*.js', 'packages/**/src/**/*.jsx'],
|
||||
excludedFiles: [
|
||||
'packages/generator-superset/**/*', // Yeoman generator templates run via Node
|
||||
'packages/superset-ui-demo/.storybook/**/*', // Storybook config files
|
||||
'packages/**/__mocks__/**/*', // Test mocks
|
||||
],
|
||||
rules: {
|
||||
@@ -350,7 +361,7 @@ module.exports = {
|
||||
],
|
||||
'@typescript-eslint/no-empty-function': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/no-use-before-define': 1,
|
||||
'@typescript-eslint/no-use-before-define': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 0,
|
||||
'@typescript-eslint/explicit-function-return-type': 0,
|
||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||
@@ -446,27 +457,13 @@ module.exports = {
|
||||
'**/spec/**/*',
|
||||
],
|
||||
excludedFiles: 'cypress-base/cypress/**/*',
|
||||
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
|
||||
env: {
|
||||
'jest/globals': true,
|
||||
},
|
||||
settings: {
|
||||
jest: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
'plugin:jest/recommended',
|
||||
'plugin:jest-dom/recommended',
|
||||
'plugin:testing-library/react',
|
||||
],
|
||||
plugins: ['jest-dom', 'no-only-tests', 'testing-library'],
|
||||
extends: ['plugin:jest-dom/recommended', 'plugin:testing-library/react'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: true },
|
||||
],
|
||||
'jest/consistent-test-it': 'error',
|
||||
'no-only-tests/no-only-tests': 'error',
|
||||
'prefer-promise-reject-errors': 0,
|
||||
'max-classes-per-file': 0,
|
||||
|
||||
|
||||
@@ -23,8 +23,13 @@ const customConfig = require('../webpack.config.js');
|
||||
// Filter out plugins that shouldn't be included in Storybook's static build
|
||||
// ReactRefreshWebpackPlugin adds Fast Refresh code that requires a dev server runtime,
|
||||
// which isn't available when serving the static storybook build
|
||||
// ForkTsCheckerWebpackPlugin causes TypeScript project reference errors in Storybook context
|
||||
const pluginsToExclude = [
|
||||
'ReactRefreshWebpackPlugin',
|
||||
'ForkTsCheckerWebpackPlugin',
|
||||
];
|
||||
const filteredPlugins = customConfig.plugins.filter(
|
||||
plugin => plugin.constructor.name !== 'ReactRefreshWebpackPlugin',
|
||||
plugin => !pluginsToExclude.includes(plugin.constructor.name),
|
||||
);
|
||||
|
||||
// Deep clone and modify rules to disable React Fast Refresh and dev mode in SWC loader
|
||||
@@ -73,9 +78,9 @@ const disableDevModeInRules = rules =>
|
||||
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../src/@(components|common|filters|explore|views|dashboard|features)/**/*.stories.@(tsx|jsx)',
|
||||
'../packages/superset-ui-demo/storybook/stories/**/*.*.@(tsx|jsx)',
|
||||
'../packages/superset-ui-core/src/components/**/*.stories.@(tsx|jsx)',
|
||||
'../src/**/*.stories.tsx',
|
||||
'../packages/superset-ui-core/src/**/*.stories.tsx',
|
||||
'../plugins/*/src/**/*.stories.tsx',
|
||||
],
|
||||
|
||||
addons: [
|
||||
@@ -102,6 +107,8 @@ module.exports = {
|
||||
...customConfig.resolve?.alias,
|
||||
// Fix for Storybook 8.6.x with React 17 - resolve ESM module paths
|
||||
'react-dom/test-utils': require.resolve('react-dom/test-utils'),
|
||||
// Shared storybook utilities
|
||||
'@storybook-shared': join(__dirname, 'shared'),
|
||||
},
|
||||
},
|
||||
plugins: [...config.plugins, ...filteredPlugins],
|
||||
|
||||
@@ -28,6 +28,27 @@ import { App, Layout, Space, Content } from 'antd';
|
||||
import 'src/theme.ts';
|
||||
import './storybook.css';
|
||||
|
||||
// Set up bootstrap data for components that check HTML_SANITIZATION config
|
||||
// (e.g., HandlebarsViewer). This allows <style> tags in Handlebars templates.
|
||||
if (typeof document !== 'undefined') {
|
||||
let appEl = document.getElementById('app');
|
||||
if (!appEl) {
|
||||
appEl = document.createElement('div');
|
||||
appEl.id = 'app';
|
||||
document.body.appendChild(appEl);
|
||||
}
|
||||
appEl.setAttribute(
|
||||
'data-bootstrap',
|
||||
JSON.stringify({
|
||||
common: {
|
||||
conf: {
|
||||
HTML_SANITIZATION: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export const GlobalStylesOverrides = () => (
|
||||
<Global
|
||||
styles={css`
|
||||
|
||||
@@ -61,10 +61,7 @@ export default function ResizableChartDemo({
|
||||
);
|
||||
}
|
||||
|
||||
export const withResizableChartDemo: Decorator<{
|
||||
width: number;
|
||||
height: number;
|
||||
}> = (storyFn, context) => {
|
||||
export const withResizableChartDemo: Decorator = (Story, context) => {
|
||||
const {
|
||||
parameters: { initialSize, panelPadding },
|
||||
} = context;
|
||||
@@ -73,7 +70,14 @@ export const withResizableChartDemo: Decorator<{
|
||||
initialSize={initialSize as Size | undefined}
|
||||
panelPadding={panelPadding}
|
||||
>
|
||||
{innerSize => storyFn({ ...context, ...context.args, ...innerSize })}
|
||||
{innerSize => (
|
||||
<Story
|
||||
args={{
|
||||
...context.args,
|
||||
...innerSize,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ResizableChartDemo>
|
||||
);
|
||||
};
|
||||
@@ -23,9 +23,25 @@ import {
|
||||
ResizableBoxProps,
|
||||
ResizeCallbackData,
|
||||
} from 'react-resizable';
|
||||
import { styled } from '@apache-superset/core/ui';
|
||||
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
const StyledResizableBox = styled(ResizableBox)`
|
||||
&.panel {
|
||||
overflow: hidden;
|
||||
background: ${({ theme }) => theme.colorBgContainer};
|
||||
border: 1px solid ${({ theme }) => theme.colorBorder};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export type Size = ResizeCallbackData['size'];
|
||||
|
||||
export default function ResizablePanel({
|
||||
@@ -41,7 +57,7 @@ export default function ResizablePanel({
|
||||
}) {
|
||||
const { width, height } = initialSize;
|
||||
return (
|
||||
<ResizableBox
|
||||
<StyledResizableBox
|
||||
className="panel"
|
||||
width={width}
|
||||
height={height}
|
||||
@@ -60,6 +76,6 @@ export default function ResizablePanel({
|
||||
{heading ? <div className="panel-heading">{heading}</div> : null}
|
||||
<div className="panel-body">{children}</div>
|
||||
</>
|
||||
</ResizableBox>
|
||||
</StyledResizableBox>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export default function createQueryStory({
|
||||
[key: string]: {
|
||||
chartType: string;
|
||||
formData: {
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export default function createQueryStory({
|
||||
mode: string | number,
|
||||
width: number,
|
||||
height: number,
|
||||
formData: any,
|
||||
formData: string,
|
||||
) => {
|
||||
const { chartType } = choices[mode];
|
||||
|
||||
@@ -17,12 +17,18 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { EchartsBoxPlotChartPlugin } from '@superset-ui/plugin-chart-echarts';
|
||||
|
||||
new EchartsBoxPlotChartPlugin().configure({ key: 'box-plot' }).register();
|
||||
|
||||
export default {
|
||||
title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/BoxPlot',
|
||||
};
|
||||
|
||||
export { basic } from './stories/basic';
|
||||
export { default as ErrorMessage } from './ErrorMessage';
|
||||
export { default as Expandable } from './Expandable';
|
||||
export { default as ResizablePanel, type Size } from './ResizablePanel';
|
||||
export {
|
||||
default as ResizableChartDemo,
|
||||
SupersetBody,
|
||||
withResizableChartDemo,
|
||||
} from './ResizableChartDemo';
|
||||
export {
|
||||
default as VerifyCORS,
|
||||
renderError,
|
||||
type Props as VerifyCORSProps,
|
||||
} from './VerifyCORS';
|
||||
export { default as createQueryStory } from './createQueryStory';
|
||||
export { default as dummyDatasource } from './dummyDatasource';
|
||||
@@ -1,27 +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.
|
||||
*/
|
||||
module.exports = {
|
||||
apiKey: process.env.APPLITOOLS_API_KEY,
|
||||
batchId: process.env.APPLITOOLS_BATCH_ID,
|
||||
batchName: process.env.APPLITOOLS_BATCH_NAME,
|
||||
puppeteerOptions: {
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
},
|
||||
};
|
||||
@@ -1,29 +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.
|
||||
*/
|
||||
module.exports = {
|
||||
apiKey: process.env.APPLITOOLS_API_KEY,
|
||||
batchId: process.env.APPLITOOLS_BATCH_ID,
|
||||
batchName: process.env.APPLITOOLS_BATCH_NAME,
|
||||
browser: [{ width: 1920, height: 1080, name: 'chrome' }],
|
||||
failCypressOnDiff: false,
|
||||
isDisabled: false,
|
||||
showLogs: false,
|
||||
testConcurrency: 10,
|
||||
ignoreCaret: true,
|
||||
};
|
||||
@@ -18,73 +18,67 @@
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { defineConfig } from 'cypress';
|
||||
import eyesPlugin from '@applitools/eyes-cypress';
|
||||
|
||||
const { verifyDownloadTasks } = require('cy-verify-downloads');
|
||||
|
||||
export default eyesPlugin(
|
||||
defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
defaultCommandTimeout: 8000,
|
||||
numTestsKeptInMemory: 3,
|
||||
// Disabled after realizing this MESSES UP rison encoding in intricate ways
|
||||
experimentalFetchPolyfill: false,
|
||||
experimentalMemoryManagement: true,
|
||||
requestTimeout: 10000,
|
||||
video: false,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 1024,
|
||||
projectId: 'ud5x2f',
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
export default defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
defaultCommandTimeout: 8000,
|
||||
numTestsKeptInMemory: 3,
|
||||
// Disabled after realizing this MESSES UP rison encoding in intricate ways
|
||||
experimentalFetchPolyfill: false,
|
||||
experimentalMemoryManagement: true,
|
||||
requestTimeout: 10000,
|
||||
video: false,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 1024,
|
||||
projectId: 'ud5x2f',
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
// ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
|
||||
// Check https://github.com/cypress-io/cypress/issues/27804 for context
|
||||
// TODO: This workaround should be removed when upgrading Cypress
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
launchOptions.args = launchOptions.args.map(arg => {
|
||||
if (arg === '--headless') {
|
||||
return '--headless=new';
|
||||
}
|
||||
|
||||
return arg;
|
||||
});
|
||||
|
||||
launchOptions.args.push(
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--no-sandbox',
|
||||
'--disable-software-rasterizer',
|
||||
'--memory-pressure-off',
|
||||
'--js-flags=--max-old-space-size=4096',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-renderer-backgrounding',
|
||||
);
|
||||
}
|
||||
return launchOptions;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line global-require
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
on('task', verifyDownloadTasks);
|
||||
// eslint-disable-next-line global-require,import/extensions
|
||||
return config;
|
||||
},
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
// ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
|
||||
// Check https://github.com/cypress-io/cypress/issues/27804 for context
|
||||
// TODO: This workaround should be removed when upgrading Cypress
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
launchOptions.args = launchOptions.args.map(arg => {
|
||||
if (arg === '--headless') {
|
||||
return '--headless=new';
|
||||
}
|
||||
|
||||
return arg;
|
||||
});
|
||||
|
||||
launchOptions.args.push(
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--no-sandbox',
|
||||
'--disable-software-rasterizer',
|
||||
'--memory-pressure-off',
|
||||
'--js-flags=--max-old-space-size=4096',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-renderer-backgrounding',
|
||||
);
|
||||
}
|
||||
return launchOptions;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line global-require
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
on('task', verifyDownloadTasks);
|
||||
// eslint-disable-next-line global-require,import/extensions
|
||||
return config;
|
||||
},
|
||||
baseUrl: 'http://localhost:8088',
|
||||
excludeSpecPattern: ['**/_skip.*'],
|
||||
experimentalRunAllSpecs: true,
|
||||
specPattern: [
|
||||
'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
'cypress/applitools/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
baseUrl: 'http://localhost:8088',
|
||||
excludeSpecPattern: ['**/_skip.*'],
|
||||
experimentalRunAllSpecs: true,
|
||||
specPattern: ['cypress/e2e/**/*.{js,jsx,ts,tsx}'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,45 +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.
|
||||
*/
|
||||
import { CHART_LIST } from 'cypress/utils/urls';
|
||||
|
||||
describe('charts list view', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(CHART_LIST);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.eyesClose();
|
||||
});
|
||||
|
||||
it('should load the Charts list', () => {
|
||||
cy.get('[aria-label="unordered-list"]').click();
|
||||
cy.eyesOpen({
|
||||
testName: 'Charts list-view',
|
||||
});
|
||||
cy.eyesCheckWindow('Charts list-view loaded');
|
||||
});
|
||||
|
||||
it('should load the Charts card list', () => {
|
||||
cy.get('[aria-label="appstore"]').click();
|
||||
cy.eyesOpen({
|
||||
testName: 'Charts card-view',
|
||||
});
|
||||
cy.eyesCheckWindow('Charts card-view loaded');
|
||||
});
|
||||
});
|
||||
@@ -1,53 +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.
|
||||
*/
|
||||
import { WORLD_HEALTH_DASHBOARD } from 'cypress/utils/urls';
|
||||
import { waitForChartLoad } from 'cypress/utils';
|
||||
import { WORLD_HEALTH_CHARTS } from '../e2e/dashboard/utils';
|
||||
|
||||
describe('Dashboard load', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(WORLD_HEALTH_DASHBOARD);
|
||||
WORLD_HEALTH_CHARTS.forEach(waitForChartLoad);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.eyesClose();
|
||||
});
|
||||
|
||||
it('should load the Dashboard', () => {
|
||||
cy.eyesOpen({
|
||||
testName: 'Dashboard page',
|
||||
});
|
||||
cy.eyesCheckWindow('Dashboard loaded');
|
||||
});
|
||||
|
||||
it('should load the Dashboard in edit mode', () => {
|
||||
cy.get('.header-with-actions')
|
||||
.find('[aria-label="Edit dashboard"]')
|
||||
.click();
|
||||
// wait for a chart to appear
|
||||
cy.get('[data-test="grid-container"]').find('.box_plot', {
|
||||
timeout: 10000,
|
||||
});
|
||||
cy.eyesOpen({
|
||||
testName: 'Dashboard edit mode',
|
||||
});
|
||||
cy.eyesCheckWindow('Dashboard edit mode loaded');
|
||||
});
|
||||
});
|
||||
@@ -1,45 +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.
|
||||
*/
|
||||
import { DASHBOARD_LIST } from 'cypress/utils/urls';
|
||||
|
||||
describe('dashboard list view', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(DASHBOARD_LIST);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.eyesClose();
|
||||
});
|
||||
|
||||
it('should load the Dashboards list', () => {
|
||||
cy.get('[aria-label="unordered-list"]').click();
|
||||
cy.eyesOpen({
|
||||
testName: 'Dashboards list-view',
|
||||
});
|
||||
cy.eyesCheckWindow('Dashboards list-view loaded');
|
||||
});
|
||||
|
||||
it('should load the Dashboards card list', () => {
|
||||
cy.get('[aria-label="appstore"]').click();
|
||||
cy.eyesOpen({
|
||||
testName: 'Dashboards card-view',
|
||||
});
|
||||
cy.eyesCheckWindow('Dashboards card-view loaded');
|
||||
});
|
||||
});
|
||||
@@ -1,46 +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.
|
||||
*/
|
||||
import {
|
||||
FORM_DATA_DEFAULTS,
|
||||
NUM_METRIC,
|
||||
} from '../e2e/explore/visualizations/shared.helper';
|
||||
|
||||
describe('explore view', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('POST', '**/superset/explore_json/**').as('getJson');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.eyesClose();
|
||||
});
|
||||
|
||||
it('should load Explore', () => {
|
||||
const LINE_CHART_DEFAULTS = {
|
||||
...FORM_DATA_DEFAULTS,
|
||||
viz_type: 'echarts_timeseries_line',
|
||||
};
|
||||
const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] };
|
||||
cy.visitChartByParams(formData);
|
||||
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
|
||||
cy.eyesOpen({
|
||||
testName: 'Explore page',
|
||||
});
|
||||
cy.eyesCheckWindow('Explore loaded');
|
||||
});
|
||||
});
|
||||
@@ -1,32 +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.
|
||||
*/
|
||||
|
||||
describe('SqlLab view', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/sqllab');
|
||||
});
|
||||
|
||||
it('should load the SqlLab', () => {
|
||||
cy.eyesOpen({
|
||||
testName: 'SqlLab page',
|
||||
});
|
||||
cy.eyesCheckWindow('SqlLab loaded');
|
||||
cy.eyesClose();
|
||||
});
|
||||
});
|
||||
@@ -1,57 +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.
|
||||
*/
|
||||
import { CHART_LIST } from 'cypress/utils/urls';
|
||||
import { setGridMode, clearAllInputs } from 'cypress/utils';
|
||||
import { setFilter } from '../explore/utils';
|
||||
|
||||
describe('Charts filters', () => {
|
||||
before(() => {
|
||||
cy.visit(CHART_LIST);
|
||||
setGridMode('card');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllInputs();
|
||||
});
|
||||
|
||||
it('should allow filtering by "Owner"', () => {
|
||||
setFilter('Owner', 'alpha user');
|
||||
setFilter('Owner', 'admin user');
|
||||
});
|
||||
|
||||
it('should allow filtering by "Modified by" correctly', () => {
|
||||
setFilter('Modified by', 'alpha user');
|
||||
setFilter('Modified by', 'admin user');
|
||||
});
|
||||
|
||||
it('should allow filtering by "Type" correctly', () => {
|
||||
setFilter('Type', 'Area Chart');
|
||||
setFilter('Type', 'Bubble Chart');
|
||||
});
|
||||
|
||||
it('should allow filtering by "Dataset" correctly', () => {
|
||||
setFilter('Dataset', 'birth_names');
|
||||
setFilter('Dataset', 'video_game_sales');
|
||||
});
|
||||
|
||||
it('should allow filtering by "Dashboards" correctly', () => {
|
||||
setFilter('Dashboard', 'USA Births Names');
|
||||
setFilter('Dashboard', 'Video Game Sales');
|
||||
});
|
||||
});
|
||||
@@ -23,12 +23,9 @@ import {
|
||||
interceptBulkDelete,
|
||||
interceptUpdate,
|
||||
interceptDelete,
|
||||
visitSampleChartFromList,
|
||||
saveChartToDashboard,
|
||||
interceptFiltering,
|
||||
interceptFavoriteStatus,
|
||||
} from '../explore/utils';
|
||||
import { interceptGet as interceptDashboardGet } from '../dashboard/utils';
|
||||
|
||||
function orderAlphabetical() {
|
||||
setFilter('Sort', 'Alphabetical');
|
||||
@@ -57,60 +54,6 @@ function visitChartList() {
|
||||
}
|
||||
|
||||
describe('Charts list', () => {
|
||||
describe('Cross-referenced dashboards', () => {
|
||||
beforeEach(() => {
|
||||
cy.createSampleDashboards([0, 1, 2, 3]);
|
||||
cy.createSampleCharts([0]);
|
||||
visitChartList();
|
||||
});
|
||||
|
||||
// Skipped: depends on "Supported Charts Dashboard" which requires specific example loading
|
||||
it.skip('should show the cross-referenced dashboards in the table cell', () => {
|
||||
interceptDashboardGet();
|
||||
cy.getBySel('table-row')
|
||||
.first()
|
||||
.find('[data-test="table-row-cell"]')
|
||||
.find('[data-test="crosslinks"]')
|
||||
.should('be.empty');
|
||||
cy.getBySel('table-row')
|
||||
.eq(10)
|
||||
.find('[data-test="table-row-cell"]')
|
||||
.find('[data-test="crosslinks"]')
|
||||
.contains('Supported Charts Dashboard')
|
||||
.invoke('removeAttr', 'target')
|
||||
.click();
|
||||
cy.wait('@get');
|
||||
});
|
||||
|
||||
it('should show the newly added dashboards in a tooltip', () => {
|
||||
interceptDashboardGet();
|
||||
visitSampleChartFromList('1 - Sample chart');
|
||||
saveChartToDashboard('1 - Sample chart', '1 - Sample dashboard');
|
||||
saveChartToDashboard('1 - Sample chart', '2 - Sample dashboard');
|
||||
saveChartToDashboard('1 - Sample chart', '3 - Sample dashboard');
|
||||
saveChartToDashboard('1 - Sample chart', '4 - Sample dashboard');
|
||||
visitChartList();
|
||||
|
||||
cy.getBySel('count-crosslinks').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('card mode', () => {
|
||||
before(() => {
|
||||
visitChartList();
|
||||
setGridMode('card');
|
||||
});
|
||||
|
||||
it('should preserve other filters when sorting', () => {
|
||||
// Check that we have some cards (count varies based on loaded examples)
|
||||
cy.getBySel('styled-card').should('have.length.at.least', 1);
|
||||
setFilter('Type', 'Big Number');
|
||||
setFilter('Sort', 'Least recently modified');
|
||||
// After filtering to Big Number type, we should have fewer cards
|
||||
cy.getBySel('styled-card').should('have.length.at.least', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('common actions', () => {
|
||||
beforeEach(() => {
|
||||
visitChartList();
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
* under the License.
|
||||
*/
|
||||
import '@cypress/code-coverage/support';
|
||||
import '@applitools/eyes-cypress/commands';
|
||||
import { expect } from 'chai';
|
||||
import rison from 'rison';
|
||||
|
||||
|
||||
5153
superset-frontend/cypress-base/package-lock.json
generated
5153
superset-frontend/cypress-base/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@applitools/eyes-cypress": "^3.44.9",
|
||||
"@cypress/code-coverage": "^3.10.4",
|
||||
"@superset-ui/core": "^2.1.0",
|
||||
"brace": "^0.11.1",
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
"strict": true,
|
||||
"target": "es2019",
|
||||
"lib": ["es2019", "DOM"],
|
||||
"types": ["cypress", "@applitools/eyes-cypress", "node"],
|
||||
"types": ["cypress", "node"],
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/types/index.d.ts"],
|
||||
"files": ["cypress/support/index.d.ts"],
|
||||
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ module.exports = {
|
||||
'src/**/*.{js,jsx,ts,tsx}',
|
||||
'{packages,plugins}/**/src/**/*.{js,jsx,ts,tsx}',
|
||||
'!**/*.stories.*',
|
||||
'!packages/superset-ui-demo/**/*',
|
||||
],
|
||||
coverageDirectory: '<rootDir>/coverage/',
|
||||
coveragePathIgnorePatterns: [
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["import", "react", "jsx-a11y", "typescript", "unicorn"],
|
||||
"plugins": ["import", "react", "jest", "jsx-a11y", "typescript", "unicorn"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2020": true
|
||||
"es2020": true,
|
||||
"jest": true
|
||||
},
|
||||
"globals": {
|
||||
"__webpack_public_path__": "writable",
|
||||
@@ -27,7 +28,8 @@
|
||||
// === Core ESLint rules ===
|
||||
// Error prevention
|
||||
"no-console": "warn",
|
||||
"no-alert": "warn",
|
||||
"no-alert": "error",
|
||||
"constructor-super": "error",
|
||||
"no-debugger": "error",
|
||||
"no-unused-vars": "off",
|
||||
"no-undef": "error",
|
||||
@@ -147,7 +149,8 @@
|
||||
],
|
||||
"react/no-array-index-key": "off",
|
||||
"react/no-children-prop": "error",
|
||||
"react/no-danger": "warn",
|
||||
"react/no-danger": "error",
|
||||
"react/forbid-foreign-prop-types": "error",
|
||||
"react/no-danger-with-children": "error",
|
||||
"react/no-deprecated": "error",
|
||||
"react/no-did-update-set-state": "error",
|
||||
@@ -230,7 +233,7 @@
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-use-before-define": "warn",
|
||||
"@typescript-eslint/no-use-before-define": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
@@ -249,6 +252,8 @@
|
||||
],
|
||||
|
||||
// === Unicorn rules (bonus coverage) ===
|
||||
"unicorn/no-new-array": "error",
|
||||
"unicorn/no-invalid-remove-event-listener": "error",
|
||||
"unicorn/filename-case": "off",
|
||||
"unicorn/prevent-abbreviations": "off",
|
||||
"unicorn/no-null": "off",
|
||||
@@ -256,18 +261,26 @@
|
||||
"unicorn/no-array-for-each": "off",
|
||||
"unicorn/prefer-module": "off",
|
||||
"unicorn/prefer-node-protocol": "off",
|
||||
"unicorn/no-useless-undefined": "off"
|
||||
"unicorn/no-useless-undefined": "off",
|
||||
|
||||
// === Jest rules ===
|
||||
"jest/consistent-test-it": ["error", { "fn": "test" }],
|
||||
"jest/no-focused-tests": "error",
|
||||
"jest/no-disabled-tests": "error",
|
||||
"jest/expect-expect": [
|
||||
"error",
|
||||
{
|
||||
"assertFunctionNames": [
|
||||
"expect",
|
||||
"expect*",
|
||||
"runTimezoneTest",
|
||||
"compareURI",
|
||||
"test*WithInitialValues"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"*.test.{js,ts,jsx,tsx}",
|
||||
"*.spec.{js,ts,jsx,tsx}",
|
||||
"**/__tests__/**",
|
||||
"**/__mocks__/**",
|
||||
"**/test/**",
|
||||
"**/tests/**",
|
||||
"**/spec/**",
|
||||
"plugins/**/test/**/*",
|
||||
"packages/**/test/**/*",
|
||||
"packages/generator-superset/**/*",
|
||||
"cypress-base/**",
|
||||
"node_modules/**",
|
||||
|
||||
14596
superset-frontend/package-lock.json
generated
14596
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@
|
||||
"build-translation": "scripts/po2json.sh",
|
||||
"bundle-stats": "cross-env BUNDLE_ANALYZER=true npm run build && npx open-cli ../superset/static/stats/statistics.html",
|
||||
"clear-npm": "mkdir -p /tmp/empty && rsync -a --delete /tmp/empty/ node_modules/ && rmdir node_modules /tmp/empty",
|
||||
"core:cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-ui-demo/**/*\", \"!packages/superset-core/**/*\"]' packages",
|
||||
"core:cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage --coverageThreshold='{\"global\":{\"statements\":100,\"branches\":100,\"functions\":100,\"lines\":100}}' --collectCoverageFrom='[\"packages/**/src/**/*.{js,ts}\", \"!packages/superset-core/**/*\"]' packages",
|
||||
"cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage",
|
||||
"dev": "webpack --mode=development --color --watch",
|
||||
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
|
||||
@@ -57,18 +57,17 @@
|
||||
"lint-fix:all": "npx oxlint --config oxlint.json --fix",
|
||||
"lint:full": "npm run lint && npm run check:custom-rules",
|
||||
"check:custom-rules": "node scripts/check-custom-rules.js",
|
||||
"check:storybook-coverage": "node scripts/check-storybook-coverage.js",
|
||||
"ensure-oxc": "echo 'OXC linter is ready' && npx oxlint --version",
|
||||
"lint-stats": "node ./scripts/oxlint-metrics-uploader.js",
|
||||
"plugins:build": "node ./scripts/build.js",
|
||||
"plugins:build-assets": "node ./scripts/copyAssets.js",
|
||||
"plugins:build-storybook": "cd packages/superset-ui-demo && npm run build-storybook",
|
||||
"plugins:create-conventional-version": "npm run prune && lerna version --conventional-commits --create-release github --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
|
||||
"plugins:create-minor-version": "npm run prune && lerna version minor --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
|
||||
"plugins:create-patch-version": "npm run prune && lerna version patch --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
|
||||
"plugins:publish-all": "npm run prune && npm run plugins:build && lerna publish from-package --force-publish --yes",
|
||||
"plugins:release-conventional": "npm run prune && npm run plugins:build && lerna publish --conventional-commits --create-release github --yes",
|
||||
"plugins:release-from-tag": "npm run prune && npm run plugins:build && lerna publish from-package --yes",
|
||||
"plugins:storybook": "cd packages/superset-ui-demo && npm run storybook",
|
||||
"playwright:test": "playwright test",
|
||||
"playwright:ui": "playwright test --ui",
|
||||
"playwright:headed": "playwright test --headed",
|
||||
@@ -110,6 +109,12 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.24.13",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
@@ -170,24 +175,18 @@
|
||||
"geostyler-style": "7.5.0",
|
||||
"geostyler-wfs-parser": "^2.0.3",
|
||||
"googleapis": "^171.4.0",
|
||||
"immer": "^11.1.3",
|
||||
"immer": "^11.1.4",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^2.0.0",
|
||||
"lodash": "^4.17.23",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"mapbox-gl": "^3.18.1",
|
||||
"markdown-to-jsx": "^9.7.3",
|
||||
"match-sorter": "^6.3.4",
|
||||
"memoize-one": "^5.2.1",
|
||||
"pretty-ms": "^9.3.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.6",
|
||||
@@ -235,7 +234,6 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.63.10",
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/compat-data": "^7.28.4",
|
||||
"@babel/core": "^7.29.0",
|
||||
@@ -257,7 +255,7 @@
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||
"@playwright/test": "^1.58.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-actions": "^8.6.15",
|
||||
"@storybook/addon-controls": "^8.6.15",
|
||||
@@ -271,9 +269,9 @@
|
||||
"@storybook/test": "^8.6.15",
|
||||
"@storybook/test-runner": "^0.17.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.14.0",
|
||||
"@swc/plugin-emotion": "^12.0.0",
|
||||
"@swc/plugin-transform-imports": "^10.0.0",
|
||||
"@swc/core": "^1.15.11",
|
||||
"@swc/plugin-emotion": "^14.5.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
@@ -285,7 +283,7 @@
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^25.2.1",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/react": "^17.0.83",
|
||||
"@types/react-dom": "^17.0.26",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
@@ -321,11 +319,9 @@
|
||||
"eslint-plugin-file-progress": "^1.5.0",
|
||||
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest": "^27.8.0",
|
||||
"eslint-plugin-jest-dom": "^5.5.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
@@ -344,6 +340,7 @@
|
||||
"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.0.0",
|
||||
"lerna": "^8.2.3",
|
||||
"lightningcss": "^1.31.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.0.1-rc10",
|
||||
"version": "0.0.1-rc11",
|
||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
@@ -16,8 +16,6 @@
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"install": "^0.13.0",
|
||||
"npm": "^11.8.0",
|
||||
"typescript": "^5.0.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/lodash": "^4.17.23",
|
||||
|
||||
@@ -93,20 +93,55 @@ export interface EditorContribution {
|
||||
*/
|
||||
export type EditorLanguage = 'sql' | 'json' | 'yaml' | 'markdown' | 'css';
|
||||
|
||||
/**
|
||||
* Valid locations within SQL Lab.
|
||||
*/
|
||||
export type SqlLabLocation =
|
||||
| 'leftSidebar'
|
||||
| 'rightSidebar'
|
||||
| 'panels'
|
||||
| 'editor'
|
||||
| 'statusBar'
|
||||
| 'results'
|
||||
| 'queryHistory';
|
||||
|
||||
/**
|
||||
* Nested structure for view contributions by scope and location.
|
||||
* @example
|
||||
* {
|
||||
* sqllab: {
|
||||
* panels: [{ id: "my-ext.panel", name: "My Panel" }],
|
||||
* leftSidebar: [{ id: "my-ext.sidebar", name: "My Sidebar" }]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export interface ViewContributions {
|
||||
sqllab?: Partial<Record<SqlLabLocation, ViewContribution[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested structure for menu contributions by scope and location.
|
||||
* @example
|
||||
* {
|
||||
* sqllab: {
|
||||
* editor: { primary: [...], secondary: [...] }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export interface MenuContributions {
|
||||
sqllab?: Partial<Record<SqlLabLocation, MenuContribution>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates all contributions (commands, menus, views, and editors) provided by an extension or module.
|
||||
*/
|
||||
export interface Contributions {
|
||||
/** List of command contributions. */
|
||||
commands: CommandContribution[];
|
||||
/** Mapping of menu contributions by menu key. */
|
||||
menus: {
|
||||
[key: string]: MenuContribution;
|
||||
};
|
||||
/** Mapping of view contributions by view key. */
|
||||
views: {
|
||||
[key: string]: ViewContribution[];
|
||||
};
|
||||
/** Nested mapping of menu contributions by scope and location. */
|
||||
menus: MenuContributions;
|
||||
/** Nested mapping of view contributions by scope and location. */
|
||||
views: ViewContributions;
|
||||
/** List of editor contributions. */
|
||||
editors?: EditorContribution[];
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should pipe to `console` methods', () => {
|
||||
test('should pipe to `console` methods', () => {
|
||||
const { logging } = require('@apache-superset/core');
|
||||
|
||||
jest.spyOn(logging, 'debug').mockImplementation();
|
||||
@@ -49,7 +49,7 @@ it('should pipe to `console` methods', () => {
|
||||
expect(() => logging.trace()).toThrow('Trace:');
|
||||
});
|
||||
|
||||
it('should use noop functions when console unavailable', () => {
|
||||
test('should use noop functions when console unavailable', () => {
|
||||
Object.assign(window, { console: undefined });
|
||||
const { logging } = require('@apache-superset/core');
|
||||
|
||||
|
||||
@@ -22,16 +22,21 @@ import { ReactElement, ReactNode, ReactText, ComponentType } from 'react';
|
||||
import type {
|
||||
AdhocColumn,
|
||||
Column,
|
||||
CurrencyFormatter,
|
||||
Currency,
|
||||
DatasourceType,
|
||||
DataRecordValue,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
Metric,
|
||||
NumberFormatter,
|
||||
QueryFormColumn,
|
||||
QueryFormData,
|
||||
QueryFormMetric,
|
||||
QueryResponse,
|
||||
TimeFormatter,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/api/core';
|
||||
import { sharedControls, sharedControlComponents } from './shared-controls';
|
||||
|
||||
export type { Metric } from '@superset-ui/core';
|
||||
@@ -608,3 +613,78 @@ export type ControlFormItemSpec<T extends ControlType = ControlType> = {
|
||||
defaultValue?: Currency;
|
||||
}
|
||||
: {});
|
||||
|
||||
export enum ColorSchemeEnum {
|
||||
Green = 'Green',
|
||||
Red = 'Red',
|
||||
}
|
||||
|
||||
/** ----------------------------------------------
|
||||
* Shared Table Chart Types
|
||||
* Used by plugin-chart-table and plugin-chart-ag-grid-table
|
||||
* --------------------------------------------- */
|
||||
|
||||
export type CustomFormatter = (value: DataRecordValue) => string;
|
||||
|
||||
export type BasicColorFormatterType = {
|
||||
backgroundColor: string;
|
||||
arrowColor: string;
|
||||
mainArrow: string;
|
||||
};
|
||||
|
||||
export type SortByItem = {
|
||||
id: string;
|
||||
key: string;
|
||||
desc?: boolean;
|
||||
};
|
||||
|
||||
export type SearchOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface ServerPaginationData {
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
sortBy?: SortByItem[];
|
||||
searchText?: string;
|
||||
searchColumn?: string;
|
||||
}
|
||||
|
||||
export type TableColumnConfig = {
|
||||
d3NumberFormat?: string;
|
||||
d3SmallNumberFormat?: string;
|
||||
d3TimeFormat?: string;
|
||||
columnWidth?: number;
|
||||
horizontalAlign?: 'left' | 'right' | 'center';
|
||||
showCellBars?: boolean;
|
||||
alignPositiveNegative?: boolean;
|
||||
colorPositiveNegative?: boolean;
|
||||
truncateLongCells?: boolean;
|
||||
currencyFormat?: Currency;
|
||||
visible?: boolean;
|
||||
customColumnName?: string;
|
||||
displayTypeIcon?: boolean;
|
||||
};
|
||||
|
||||
export interface DataColumnMeta {
|
||||
// `key` is what is called `label` in the input props
|
||||
key: string;
|
||||
// `label` is verbose column name used for rendering
|
||||
label: string;
|
||||
// `originalLabel` preserves the original label when time comparison transforms the labels
|
||||
originalLabel?: string;
|
||||
dataType: GenericDataType;
|
||||
formatter?:
|
||||
| TimeFormatter
|
||||
| NumberFormatter
|
||||
| CustomFormatter
|
||||
| CurrencyFormatter;
|
||||
isMetric?: boolean;
|
||||
isPercentMetric?: boolean;
|
||||
isNumeric?: boolean;
|
||||
config?: TableColumnConfig;
|
||||
isChildColumn?: boolean;
|
||||
description?: string;
|
||||
currencyCodeColumn?: string;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('metricColumnFilter', () => {
|
||||
}) as SqlaFormData;
|
||||
|
||||
describe('shouldSkipMetricColumn', () => {
|
||||
it('should skip unprefixed percent metric columns if prefixed version exists', () => {
|
||||
test('should skip unprefixed percent metric columns if prefixed version exists', () => {
|
||||
const colnames = ['metric1', '%metric1'];
|
||||
const formData = createFormData([], ['metric1']);
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('metricColumnFilter', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should not skip if column is also a regular metric', () => {
|
||||
test('should not skip if column is also a regular metric', () => {
|
||||
const colnames = ['metric1', '%metric1'];
|
||||
const formData = createFormData(['metric1'], ['metric1']);
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('metricColumnFilter', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not skip if column starts with %', () => {
|
||||
test('should not skip if column starts with %', () => {
|
||||
const colnames = ['%metric1'];
|
||||
const formData = createFormData(['metric1'], []);
|
||||
|
||||
@@ -84,7 +84,7 @@ describe('metricColumnFilter', () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not skip if no prefixed version exists', () => {
|
||||
test('should not skip if no prefixed version exists', () => {
|
||||
const colnames = ['metric1'];
|
||||
const formData = createFormData([], ['metric1']);
|
||||
|
||||
@@ -99,35 +99,35 @@ describe('metricColumnFilter', () => {
|
||||
});
|
||||
|
||||
describe('isRegularMetric', () => {
|
||||
it('should return true for regular metrics', () => {
|
||||
test('should return true for regular metrics', () => {
|
||||
const formData = createFormData(['metric1', 'metric2'], []);
|
||||
expect(isRegularMetric('metric1', formData)).toBe(true);
|
||||
expect(isRegularMetric('metric2', formData)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-metrics', () => {
|
||||
test('should return false for non-metrics', () => {
|
||||
const formData = createFormData(['metric1'], []);
|
||||
expect(isRegularMetric('non_metric', formData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for percentage metrics', () => {
|
||||
test('should return false for percentage metrics', () => {
|
||||
const formData = createFormData([], ['percent_metric1']);
|
||||
expect(isRegularMetric('percent_metric1', formData)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPercentMetric', () => {
|
||||
it('should return true for percentage metrics', () => {
|
||||
test('should return true for percentage metrics', () => {
|
||||
const formData = createFormData([], ['percent_metric1']);
|
||||
expect(isPercentMetric('%percent_metric1', formData)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for non-percentage metrics', () => {
|
||||
test('should return false for non-percentage metrics', () => {
|
||||
const formData = createFormData(['regular_metric'], []);
|
||||
expect(isPercentMetric('regular_metric', formData)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for regular metrics', () => {
|
||||
test('should return false for regular metrics', () => {
|
||||
const formData = createFormData(['metric1'], []);
|
||||
expect(isPercentMetric('metric1', formData)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -33,34 +33,34 @@ describe('ColumnOption', () => {
|
||||
render(<ColumnTypeLabel {...props} {...overrides} />);
|
||||
}
|
||||
|
||||
it('is a valid element', () => {
|
||||
test('is a valid element', () => {
|
||||
expect(isValidElement(<ColumnTypeLabel {...defaultProps} />)).toBe(true);
|
||||
});
|
||||
it('string type shows ABC icon', () => {
|
||||
test('string type shows ABC icon', () => {
|
||||
renderColumnTypeLabel({ type: GenericDataType.String });
|
||||
expect(screen.getByLabelText('string type icon')).toBeVisible();
|
||||
});
|
||||
it('int type shows # icon', () => {
|
||||
test('int type shows # icon', () => {
|
||||
renderColumnTypeLabel({ type: GenericDataType.Numeric });
|
||||
expect(screen.getByLabelText('numeric type icon')).toBeVisible();
|
||||
});
|
||||
it('bool type shows 1|0 icon', () => {
|
||||
test('bool type shows 1|0 icon', () => {
|
||||
renderColumnTypeLabel({ type: GenericDataType.Boolean });
|
||||
expect(screen.getByLabelText('boolean type icon')).toBeVisible();
|
||||
});
|
||||
it('expression type shows function icon', () => {
|
||||
test('expression type shows function icon', () => {
|
||||
renderColumnTypeLabel({ type: 'expression' });
|
||||
expect(screen.getByLabelText('function type icon')).toBeVisible();
|
||||
});
|
||||
it('metric type shows sigma icon', () => {
|
||||
test('metric type shows sigma icon', () => {
|
||||
renderColumnTypeLabel({ type: 'metric' });
|
||||
expect(screen.getByLabelText('metric type icon')).toBeVisible();
|
||||
});
|
||||
it('unknown type shows question mark', () => {
|
||||
test('unknown type shows question mark', () => {
|
||||
renderColumnTypeLabel({ type: undefined });
|
||||
expect(screen.getByLabelText('unknown type icon')).toBeVisible();
|
||||
});
|
||||
it('datetime type displays', () => {
|
||||
test('datetime type displays', () => {
|
||||
renderColumnTypeLabel({ type: GenericDataType.Temporal });
|
||||
expect(screen.getByLabelText('temporal type icon')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { sections } from '../src';
|
||||
|
||||
describe('@superset-ui/chart-controls', () => {
|
||||
it('exports sections', () => {
|
||||
test('exports sections', () => {
|
||||
expect(sections).toBeDefined();
|
||||
expect(sections.datasourceAndVizType).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('aggregationOperator', () => {
|
||||
granularity: 'month',
|
||||
};
|
||||
|
||||
it('should return undefined for LAST_VALUE aggregation', () => {
|
||||
test('should return undefined for LAST_VALUE aggregation', () => {
|
||||
const formDataWithLastValue = {
|
||||
...formData,
|
||||
aggregation: 'LAST_VALUE',
|
||||
@@ -51,7 +51,7 @@ describe('aggregationOperator', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when metrics is empty', () => {
|
||||
test('should return undefined when metrics is empty', () => {
|
||||
const queryObjectWithoutMetrics = {
|
||||
...queryObject,
|
||||
metrics: [],
|
||||
@@ -67,7 +67,7 @@ describe('aggregationOperator', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should apply sum aggregation to all metrics', () => {
|
||||
test('should apply sum aggregation to all metrics', () => {
|
||||
const formDataWithSum = {
|
||||
...formData,
|
||||
aggregation: 'sum',
|
||||
@@ -91,7 +91,7 @@ describe('aggregationOperator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply mean aggregation to all metrics', () => {
|
||||
test('should apply mean aggregation to all metrics', () => {
|
||||
const formDataWithMean = {
|
||||
...formData,
|
||||
aggregation: 'mean',
|
||||
@@ -115,7 +115,7 @@ describe('aggregationOperator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default aggregation when not specified', () => {
|
||||
test('should use default aggregation when not specified', () => {
|
||||
expect(aggregationOperator(formData, queryObject)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { getColorControlsProps } from '../../src';
|
||||
|
||||
describe('getColorControlsProps', () => {
|
||||
it('should return default values when state is empty', () => {
|
||||
test('should return default values when state is empty', () => {
|
||||
const state = {};
|
||||
const result = getColorControlsProps(state);
|
||||
expect(result).toEqual({
|
||||
@@ -33,7 +33,7 @@ describe('getColorControlsProps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct values when state has form_data with dashboardId and color scheme', () => {
|
||||
test('should return correct values when state has form_data with dashboardId and color scheme', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
@@ -54,7 +54,7 @@ describe('getColorControlsProps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should detect custom label colors correctly', () => {
|
||||
test('should detect custom label colors correctly', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 123,
|
||||
@@ -74,7 +74,7 @@ describe('getColorControlsProps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return shared label colors when available', () => {
|
||||
test('should return shared label colors when available', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
shared_label_colors: ['#FF5733', '#33FF57'],
|
||||
@@ -92,7 +92,7 @@ describe('getColorControlsProps', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing form_data and slice properties', () => {
|
||||
test('should handle missing form_data and slice properties', () => {
|
||||
const state = {
|
||||
form_data: {
|
||||
dashboardId: 789,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { GenericDataType } from '@apache-superset/core/api/core';
|
||||
import { columnChoices } from '../../src';
|
||||
|
||||
describe('columnChoices()', () => {
|
||||
it('should convert columns to choices when source is a Dataset', () => {
|
||||
test('should convert columns to choices when source is a Dataset', () => {
|
||||
expect(
|
||||
columnChoices({
|
||||
id: 1,
|
||||
@@ -60,11 +60,11 @@ describe('columnChoices()', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array when no columns', () => {
|
||||
test('should return empty array when no columns', () => {
|
||||
expect(columnChoices(undefined)).toEqual([]);
|
||||
});
|
||||
|
||||
it('should convert columns to choices when source is a Query', () => {
|
||||
test('should convert columns to choices when source is a Query', () => {
|
||||
expect(columnChoices(testQueryResponse)).toEqual([
|
||||
['Column 1', 'Column 1'],
|
||||
['Column 2', 'Column 2'],
|
||||
@@ -72,12 +72,12 @@ describe('columnChoices()', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return choices of a specific type', () => {
|
||||
test('should return choices of a specific type', () => {
|
||||
expect(columnChoices(testQueryResponse, GenericDataType.Temporal)).toEqual([
|
||||
['Column 2', 'Column 2'],
|
||||
]);
|
||||
});
|
||||
it('should use name when verbose_name key exists but is not defined', () => {
|
||||
test('should use name when verbose_name key exists but is not defined', () => {
|
||||
expect(
|
||||
columnChoices({
|
||||
id: 1,
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { defineSavedMetrics } from '@superset-ui/chart-controls';
|
||||
|
||||
describe('defineSavedMetrics', () => {
|
||||
it('defines saved metrics if source is a Dataset', () => {
|
||||
test('defines saved metrics if source is a Dataset', () => {
|
||||
const dataset = {
|
||||
id: 1,
|
||||
metrics: [
|
||||
@@ -55,7 +55,7 @@ describe('defineSavedMetrics', () => {
|
||||
expect(defineSavedMetrics({ ...dataset, metrics: undefined })).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns default saved metrics if source is a Query', () => {
|
||||
test('returns default saved metrics if source is a Query', () => {
|
||||
expect(defineSavedMetrics(testQuery as QueryResponse)).toEqual(
|
||||
DEFAULT_METRICS,
|
||||
);
|
||||
|
||||
@@ -24,14 +24,14 @@ import {
|
||||
} from '../../src';
|
||||
|
||||
describe('expandControlConfig()', () => {
|
||||
it('expands shared control alias', () => {
|
||||
test('expands shared control alias', () => {
|
||||
expect(expandControlConfig('metrics')).toEqual({
|
||||
name: 'metrics',
|
||||
config: sharedControls.metrics,
|
||||
});
|
||||
});
|
||||
|
||||
it('expands control with overrides', () => {
|
||||
test('expands control with overrides', () => {
|
||||
expect(
|
||||
expandControlConfig({
|
||||
name: 'metrics',
|
||||
@@ -48,7 +48,7 @@ describe('expandControlConfig()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('leave full control untouched', () => {
|
||||
test('leave full control untouched', () => {
|
||||
const input = {
|
||||
name: 'metrics',
|
||||
config: {
|
||||
@@ -59,7 +59,7 @@ describe('expandControlConfig()', () => {
|
||||
expect(expandControlConfig(input)).toEqual(input);
|
||||
});
|
||||
|
||||
it('load shared components in chart-controls', () => {
|
||||
test('load shared components in chart-controls', () => {
|
||||
const input = {
|
||||
name: 'metrics',
|
||||
config: {
|
||||
@@ -72,18 +72,18 @@ describe('expandControlConfig()', () => {
|
||||
).toEqual(sharedControlComponents.RadioButtonControl);
|
||||
});
|
||||
|
||||
it('leave NULL and ReactElement untouched', () => {
|
||||
test('leave NULL and ReactElement untouched', () => {
|
||||
expect(expandControlConfig(null)).toBeNull();
|
||||
const input = <h1>Test</h1>;
|
||||
expect(expandControlConfig(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('leave unknown text untouched', () => {
|
||||
test('leave unknown text untouched', () => {
|
||||
const input = 'superset-ui';
|
||||
expect(expandControlConfig(input as never)).toBe(input);
|
||||
});
|
||||
|
||||
it('return null for invalid configs', () => {
|
||||
test('return null for invalid configs', () => {
|
||||
expect(
|
||||
expandControlConfig({ type: 'SelectControl', label: 'Hello' } as never),
|
||||
).toBeNull();
|
||||
|
||||
@@ -19,25 +19,25 @@
|
||||
import { mainMetric } from '../../src';
|
||||
|
||||
describe('mainMetric', () => {
|
||||
it('is null when no options', () => {
|
||||
test('is null when no options', () => {
|
||||
expect(mainMetric([])).toBeUndefined();
|
||||
expect(mainMetric(null)).toBeUndefined();
|
||||
});
|
||||
it('prefers the "count" metric when first', () => {
|
||||
test('prefers the "count" metric when first', () => {
|
||||
const metrics = [
|
||||
{ metric_name: 'count', uuid: '1' },
|
||||
{ metric_name: 'foo', uuid: '2' },
|
||||
];
|
||||
expect(mainMetric(metrics)).toBe('count');
|
||||
});
|
||||
it('prefers the "count" metric when not first', () => {
|
||||
test('prefers the "count" metric when not first', () => {
|
||||
const metrics = [
|
||||
{ metric_name: 'foo', uuid: '1' },
|
||||
{ metric_name: 'count', uuid: '2' },
|
||||
];
|
||||
expect(mainMetric(metrics)).toBe('count');
|
||||
});
|
||||
it('selects the first metric when "count" is not an option', () => {
|
||||
test('selects the first metric when "count" is not an option', () => {
|
||||
const metrics = [
|
||||
{ metric_name: 'foo', uuid: '2' },
|
||||
{ metric_name: 'not_count', uuid: '2' },
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { formatSelectOptions, formatSelectOptionsForRange } from '../../src';
|
||||
|
||||
describe('formatSelectOptions', () => {
|
||||
it('formats an array of options', () => {
|
||||
test('formats an array of options', () => {
|
||||
expect(formatSelectOptions([1, 5, 10, 25, 50, 'unlimited'])).toEqual([
|
||||
[1, '1'],
|
||||
[5, '5'],
|
||||
@@ -29,7 +29,7 @@ describe('formatSelectOptions', () => {
|
||||
['unlimited', 'unlimited'],
|
||||
]);
|
||||
});
|
||||
it('formats a mix of values and already formated options', () => {
|
||||
test('formats a mix of values and already formated options', () => {
|
||||
expect(
|
||||
formatSelectOptions<number | string>([
|
||||
[0, 'all'],
|
||||
@@ -53,7 +53,7 @@ describe('formatSelectOptions', () => {
|
||||
});
|
||||
|
||||
describe('formatSelectOptionsForRange', () => {
|
||||
it('generates select options from a range', () => {
|
||||
test('generates select options from a range', () => {
|
||||
expect(formatSelectOptionsForRange(1, 5)).toEqual([
|
||||
[1, '1'],
|
||||
[2, '2'],
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"react-error-boundary": "^6.1.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
@@ -78,14 +78,14 @@
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/node": "^25.2.1",
|
||||
"@types/node": "^25.2.3",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/seedrandom": "^3.0.8",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"jest-mock-console": "^2.0.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"timezone-mock": "1.3.6"
|
||||
"timezone-mock": "1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^5.26.0",
|
||||
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
wordCloudFormData,
|
||||
} from '../../../../superset-ui-core/test/chart/fixtures/formData';
|
||||
|
||||
import Expandable from '../../shared/components/Expandable';
|
||||
import VerifyCORS, { renderError } from '../../shared/components/VerifyCORS';
|
||||
import { Expandable } from '@storybook-shared';
|
||||
import { VerifyCORS, renderError } from '@storybook-shared';
|
||||
|
||||
const BIG_NUMBER = bigNumberFormData.viz_type;
|
||||
const SANKEY = sankeyFormData.viz_type;
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
BuggyChartPlugin,
|
||||
ChartKeys,
|
||||
} from '../../../../superset-ui-core/test/chart/components/MockChartPlugins';
|
||||
import ResizableChartDemo from '../../shared/components/ResizableChartDemo';
|
||||
import { ResizableChartDemo } from '@storybook-shared';
|
||||
|
||||
new DiligentChartPlugin().configure({ key: ChartKeys.DILIGENT }).register();
|
||||
new BuggyChartPlugin().configure({ key: ChartKeys.BUGGY }).register();
|
||||
@@ -18,17 +18,18 @@
|
||||
*/
|
||||
|
||||
.palette-label {
|
||||
margin: 4px 12px 4px 0;
|
||||
padding-right: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.palette-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border: 1px solid #eaeaea;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.palette-item {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -20,24 +20,24 @@ import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useJsonValidation } from './useJsonValidation';
|
||||
|
||||
describe('useJsonValidation', () => {
|
||||
it('returns empty array for valid JSON', () => {
|
||||
test('returns empty array for valid JSON', () => {
|
||||
const { result } = renderHook(() => useJsonValidation('{"key": "value"}'));
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when disabled', () => {
|
||||
test('returns empty array when disabled', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useJsonValidation('invalid json', { enabled: false }),
|
||||
);
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array for empty input', () => {
|
||||
test('returns empty array for empty input', () => {
|
||||
const { result } = renderHook(() => useJsonValidation(''));
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
it('extracts line and column from error message with parentheses', () => {
|
||||
test('extracts line and column from error message with parentheses', () => {
|
||||
// Since we can't control the exact error message from JSON.parse,
|
||||
// let's test with a mock that demonstrates the pattern matching
|
||||
const mockError = {
|
||||
@@ -52,7 +52,7 @@ describe('useJsonValidation', () => {
|
||||
expect(match![2]).toBe('2');
|
||||
});
|
||||
|
||||
it('returns error on first line when no line/column info in message', () => {
|
||||
test('returns error on first line when no line/column info in message', () => {
|
||||
const invalidJson = '{invalid}';
|
||||
const { result } = renderHook(() => useJsonValidation(invalidJson));
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('useJsonValidation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('uses custom error prefix', () => {
|
||||
test('uses custom error prefix', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useJsonValidation('{invalid}', { errorPrefix: 'Custom error' }),
|
||||
);
|
||||
|
||||
@@ -44,14 +44,14 @@ const AutoCompleteTest = () => {
|
||||
};
|
||||
|
||||
describe('AutoComplete Component', () => {
|
||||
it('renders input field', () => {
|
||||
test('renders input field', () => {
|
||||
render(<AutoCompleteTest />);
|
||||
expect(
|
||||
screen.getByPlaceholderText('Type to search...'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows options when user types', async () => {
|
||||
test('shows options when user types', async () => {
|
||||
render(<AutoCompleteTest />);
|
||||
const input = screen.getByPlaceholderText('Type to search...');
|
||||
await userEvent.type(input, 'test');
|
||||
@@ -63,7 +63,7 @@ describe('AutoComplete Component', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('selecting an option updates input value', async () => {
|
||||
test('selecting an option updates input value', async () => {
|
||||
render(<AutoCompleteTest />);
|
||||
const input = screen.getByPlaceholderText('Type to search...');
|
||||
await userEvent.type(input, 'test');
|
||||
|
||||
@@ -21,7 +21,7 @@ import '@testing-library/jest-dom';
|
||||
import { Breadcrumb } from '.';
|
||||
|
||||
describe('Breadcrumb Component', () => {
|
||||
it('renders breadcrumb items correctly', () => {
|
||||
test('renders breadcrumb items correctly', () => {
|
||||
render(
|
||||
<Breadcrumb>
|
||||
<Breadcrumb.Item>Home</Breadcrumb.Item>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { Meta, StoryFn } from '@storybook/react';
|
||||
import { CachedLabel } from '.';
|
||||
import type { CacheLabelProps } from './types';
|
||||
|
||||
export default {
|
||||
title: 'Components/CachedLabel',
|
||||
component: CachedLabel,
|
||||
} as Meta<typeof CachedLabel>;
|
||||
|
||||
// Interactive CachedLabel story
|
||||
export const InteractiveCachedLabel: StoryFn<CacheLabelProps> = args => (
|
||||
<div style={{ display: 'inline-block' }}>
|
||||
<CachedLabel {...args} />
|
||||
</div>
|
||||
);
|
||||
|
||||
InteractiveCachedLabel.args = {
|
||||
cachedTimestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), // 5 minutes ago
|
||||
onClick: action('refresh-clicked'),
|
||||
};
|
||||
|
||||
InteractiveCachedLabel.argTypes = {
|
||||
cachedTimestamp: {
|
||||
description: 'ISO timestamp of when the data was cached',
|
||||
control: { type: 'text' },
|
||||
},
|
||||
className: {
|
||||
description: 'Additional CSS class for the label',
|
||||
control: { type: 'text' },
|
||||
},
|
||||
};
|
||||
|
||||
// Show different cache ages
|
||||
export const CacheAges: StoryFn = () => {
|
||||
const now = Date.now();
|
||||
const timestamps = [
|
||||
{ label: 'Just now', timestamp: new Date(now - 30 * 1000).toISOString() },
|
||||
{
|
||||
label: '5 minutes ago',
|
||||
timestamp: new Date(now - 5 * 60 * 1000).toISOString(),
|
||||
},
|
||||
{
|
||||
label: '1 hour ago',
|
||||
timestamp: new Date(now - 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
{
|
||||
label: '1 day ago',
|
||||
timestamp: new Date(now - 24 * 60 * 60 * 1000).toISOString(),
|
||||
},
|
||||
{ label: 'No timestamp', timestamp: undefined },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{timestamps.map(({ label, timestamp }) => (
|
||||
<div
|
||||
key={label}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 16 }}
|
||||
>
|
||||
<span style={{ width: 120, color: '#666' }}>{label}:</span>
|
||||
<CachedLabel
|
||||
cachedTimestamp={timestamp}
|
||||
onClick={action('refresh-clicked')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<p style={{ marginTop: 16, color: '#888', fontSize: 12 }}>
|
||||
Hover over each label to see the tooltip with relative time
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CacheAges.parameters = {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Shows the CachedLabel with different cache timestamps. Hover to see the relative time in the tooltip.',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -30,11 +30,11 @@ const defaultProps = {
|
||||
const setup = (props: CacheLabelProps) => <CachedLabel {...props} />;
|
||||
|
||||
describe('CachedLabel', () => {
|
||||
it('is valid', () => {
|
||||
test('is valid', () => {
|
||||
expect(isValidElement(<CachedLabel {...defaultProps} />)).toBe(true);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
test('renders', () => {
|
||||
render(setup(defaultProps));
|
||||
|
||||
const label = screen.getByText(/cached/i);
|
||||
|
||||
@@ -39,37 +39,37 @@ describe('Checkbox Component', () => {
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render correctly', async () => {
|
||||
test('should render correctly', async () => {
|
||||
const { container } = await asyncRender();
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the label', async () => {
|
||||
test('should render the label', async () => {
|
||||
await asyncRender();
|
||||
expect(screen.getByText('Checkbox Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the checkbox', async () => {
|
||||
test('should render the checkbox', async () => {
|
||||
await asyncRender();
|
||||
expect(screen.getByRole('checkbox')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('States', () => {
|
||||
it('should render as unchecked when checked is false', async () => {
|
||||
test('should render as unchecked when checked is false', async () => {
|
||||
await asyncRender();
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should render as checked when checked is true', async () => {
|
||||
test('should render as checked when checked is true', async () => {
|
||||
const checkedProps = { ...mockedProps, checked: true };
|
||||
await asyncRender(checkedProps);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
it('should render as indeterminate when indeterminate is true', async () => {
|
||||
test('should render as indeterminate when indeterminate is true', async () => {
|
||||
const indeterminateProps = { ...mockedProps, indeterminate: true };
|
||||
await asyncRender(indeterminateProps);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
@@ -77,7 +77,7 @@ describe('Checkbox Component', () => {
|
||||
expect((checkbox as HTMLInputElement).indeterminate).toBe(true);
|
||||
});
|
||||
|
||||
it('should render as disabled when disabled prop is true', async () => {
|
||||
test('should render as disabled when disabled prop is true', async () => {
|
||||
const disabledProps = { ...mockedProps, disabled: true };
|
||||
await asyncRender(disabledProps);
|
||||
expect(screen.getByRole('checkbox')).toBeDisabled();
|
||||
@@ -85,14 +85,14 @@ describe('Checkbox Component', () => {
|
||||
});
|
||||
|
||||
describe('Interactions', () => {
|
||||
it('should call the onChange handler when clicked', async () => {
|
||||
test('should call the onChange handler when clicked', async () => {
|
||||
await asyncRender();
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
await userEvent.click(checkbox);
|
||||
expect(mockedProps.onChange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call the onChange handler when disabled and clicked', async () => {
|
||||
test('should not call the onChange handler when disabled and clicked', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
const disabledProps = {
|
||||
...mockedProps,
|
||||
@@ -108,7 +108,7 @@ describe('Checkbox Component', () => {
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onChange handler successfully', async () => {
|
||||
test('calls onChange handler successfully', async () => {
|
||||
const mockAction = jest.fn();
|
||||
render(<Checkbox checked={false} onChange={mockAction} />);
|
||||
const checkboxInput = screen.getByRole('checkbox');
|
||||
|
||||
@@ -58,20 +58,20 @@ jest.mock(
|
||||
);
|
||||
|
||||
describe('CodeSyntaxHighlighter', () => {
|
||||
it('renders code content', () => {
|
||||
test('renders code content', () => {
|
||||
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
|
||||
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with default SQL language', () => {
|
||||
test('renders with default SQL language', () => {
|
||||
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
|
||||
|
||||
// Should show content (the important thing is content is visible)
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with specified language', () => {
|
||||
test('renders with specified language', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="json">
|
||||
{`{ "key": "value" }`}
|
||||
@@ -82,7 +82,7 @@ describe('CodeSyntaxHighlighter', () => {
|
||||
expect(screen.getByText('{ "key": "value" }')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports all expected languages', () => {
|
||||
test('supports all expected languages', () => {
|
||||
const languages = ['sql', 'json', 'htmlbars', 'markdown'] as const;
|
||||
|
||||
languages.forEach(language => {
|
||||
@@ -101,7 +101,7 @@ describe('CodeSyntaxHighlighter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders fallback pre element initially', () => {
|
||||
test('renders fallback pre element initially', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql">
|
||||
SELECT COUNT(*) FROM table;
|
||||
@@ -112,7 +112,7 @@ describe('CodeSyntaxHighlighter', () => {
|
||||
expect(screen.getByText('SELECT COUNT(*) FROM table;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles special characters', () => {
|
||||
test('handles special characters', () => {
|
||||
const specialContent = "SELECT * FROM `users` WHERE name = 'O\\'Brien';";
|
||||
|
||||
render(
|
||||
@@ -124,7 +124,7 @@ describe('CodeSyntaxHighlighter', () => {
|
||||
expect(screen.getByText(specialContent)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts custom styles', () => {
|
||||
test('accepts custom styles', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql" customStyle={{ fontSize: '16px' }}>
|
||||
SELECT * FROM users;
|
||||
@@ -134,7 +134,7 @@ describe('CodeSyntaxHighlighter', () => {
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts showLineNumbers prop', () => {
|
||||
test('accepts showLineNumbers prop', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql" showLineNumbers>
|
||||
SELECT * FROM users;
|
||||
@@ -144,7 +144,7 @@ describe('CodeSyntaxHighlighter', () => {
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('accepts wrapLines prop', () => {
|
||||
test('accepts wrapLines prop', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql" wrapLines={false}>
|
||||
SELECT * FROM users;
|
||||
|
||||
@@ -24,14 +24,14 @@ const props = {
|
||||
overlay: <div>Test Overlay</div>,
|
||||
};
|
||||
describe('NoAnimationDropdown', () => {
|
||||
it('requires children', () => {
|
||||
test('requires children', () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error need to test the error case
|
||||
render(<NoAnimationDropdown {...props} />);
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
it('renders its children', () => {
|
||||
test('renders its children', () => {
|
||||
render(
|
||||
<NoAnimationDropdown {...props}>
|
||||
<button type="button">Test Button</button>
|
||||
@@ -40,7 +40,7 @@ describe('NoAnimationDropdown', () => {
|
||||
expect(screen.getByText('Test Button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onBlur when it loses focus', () => {
|
||||
test('calls onBlur when it loses focus', () => {
|
||||
const onBlur = jest.fn();
|
||||
render(
|
||||
<NoAnimationDropdown {...props} onBlur={onBlur}>
|
||||
@@ -51,7 +51,7 @@ describe('NoAnimationDropdown', () => {
|
||||
expect(onBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onKeyDown when a key is pressed', () => {
|
||||
test('calls onKeyDown when a key is pressed', () => {
|
||||
const onKeyDown = jest.fn();
|
||||
render(
|
||||
<NoAnimationDropdown {...props} onKeyDown={onKeyDown}>
|
||||
|
||||
@@ -29,13 +29,13 @@ const createProps = (overrides: Record<string, any> = {}) => ({
|
||||
});
|
||||
|
||||
describe('Chart editable title', () => {
|
||||
it('renders chart title', () => {
|
||||
test('renders chart title', () => {
|
||||
const props = createProps();
|
||||
render(<DynamicEditableTitle {...props} />);
|
||||
expect(screen.getByText('Chart title')).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders placeholder', () => {
|
||||
test('renders placeholder', () => {
|
||||
const props = createProps({
|
||||
title: '',
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('Chart editable title', () => {
|
||||
expect(screen.getByText('Add the name of the chart')).toBeVisible();
|
||||
});
|
||||
|
||||
it('click, edit and save title', async () => {
|
||||
test('click, edit and save title', async () => {
|
||||
const props = createProps();
|
||||
render(<DynamicEditableTitle {...props} />);
|
||||
const textboxElement = screen.getByRole('textbox');
|
||||
@@ -54,7 +54,7 @@ describe('Chart editable title', () => {
|
||||
expect(props.onSave).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders in non-editable mode', async () => {
|
||||
test('renders in non-editable mode', async () => {
|
||||
const props = createProps({ canEdit: false });
|
||||
render(<DynamicEditableTitle {...props} />);
|
||||
const titleElement = screen.getByLabelText('Chart title');
|
||||
|
||||
@@ -48,7 +48,7 @@ test('should not render an input if it is not editable', () => {
|
||||
});
|
||||
|
||||
describe('should handle click', () => {
|
||||
it('should enable editing mode on click', () => {
|
||||
test('should enable editing mode on click', () => {
|
||||
const { getByTestId, container } = render(<EditableTitle {...mockProps} />);
|
||||
|
||||
fireEvent.click(getByTestId('textarea-editable-title-input'));
|
||||
@@ -59,7 +59,7 @@ describe('should handle click', () => {
|
||||
});
|
||||
|
||||
describe('should handle change', () => {
|
||||
it('should change title', () => {
|
||||
test('should change title', () => {
|
||||
const { getByTestId } = render(<EditableTitle {...mockProps} editing />);
|
||||
const textarea = getByTestId('textarea-editable-title-input');
|
||||
fireEvent.change(textarea, mockEvent);
|
||||
@@ -74,7 +74,7 @@ describe('should handle blur', () => {
|
||||
return selectors;
|
||||
};
|
||||
|
||||
it('should trigger callback', () => {
|
||||
test('should trigger callback', () => {
|
||||
const callback = jest.fn();
|
||||
const { getByTestId } = setup({ onSaveTitle: callback });
|
||||
fireEvent.change(getByTestId('textarea-editable-title-input'), mockEvent);
|
||||
@@ -83,7 +83,7 @@ describe('should handle blur', () => {
|
||||
expect(callback).toHaveBeenCalledWith('new title');
|
||||
});
|
||||
|
||||
it('should not trigger callback', () => {
|
||||
test('should not trigger callback', () => {
|
||||
const callback = jest.fn();
|
||||
const { getByTestId } = setup({ onSaveTitle: callback });
|
||||
fireEvent.blur(getByTestId('textarea-editable-title-input'));
|
||||
@@ -91,7 +91,7 @@ describe('should handle blur', () => {
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not save empty title', () => {
|
||||
test('should not save empty title', () => {
|
||||
const callback = jest.fn();
|
||||
const { getByTestId } = setup({ onSaveTitle: callback });
|
||||
const textarea = getByTestId('textarea-editable-title-input');
|
||||
|
||||
@@ -35,7 +35,7 @@ const defaultProps: LabeledErrorBoundInputProps = {
|
||||
};
|
||||
|
||||
describe('LabeledErrorBoundInput', () => {
|
||||
it('renders a LabeledErrorBoundInput normally, without an error', () => {
|
||||
test('renders a LabeledErrorBoundInput normally, without an error', () => {
|
||||
render(<LabeledErrorBoundInput {...defaultProps} />);
|
||||
|
||||
const label = screen.getByText(/username/i);
|
||||
@@ -47,7 +47,7 @@ describe('LabeledErrorBoundInput', () => {
|
||||
expect(helperText).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders a LabeledErrorBoundInput with an error', () => {
|
||||
test('renders a LabeledErrorBoundInput with an error', () => {
|
||||
// Pass an error into props, causing errorText to replace helperText
|
||||
defaultProps.errorMessage = 'Example error message';
|
||||
render(<LabeledErrorBoundInput {...defaultProps} />);
|
||||
@@ -60,7 +60,7 @@ describe('LabeledErrorBoundInput', () => {
|
||||
expect(textboxInput).toBeVisible();
|
||||
expect(errorText).toBeVisible();
|
||||
});
|
||||
it('renders a LabeledErrorBoundInput with a InfoTooltip', async () => {
|
||||
test('renders a LabeledErrorBoundInput with a InfoTooltip', async () => {
|
||||
defaultProps.hasTooltip = true;
|
||||
render(<LabeledErrorBoundInput {...defaultProps} />);
|
||||
|
||||
@@ -76,14 +76,14 @@ describe('LabeledErrorBoundInput', () => {
|
||||
expect(await screen.findByText('This is a tooltip')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('becomes a password input if visibilityToggle prop is passed in', async () => {
|
||||
test('becomes a password input if visibilityToggle prop is passed in', async () => {
|
||||
defaultProps.visibilityToggle = true;
|
||||
render(<LabeledErrorBoundInput {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByTestId('icon-eye')).toBeVisible();
|
||||
});
|
||||
|
||||
it('becomes a password input if props.name === password (backwards compatibility)', async () => {
|
||||
test('becomes a password input if props.name === password (backwards compatibility)', async () => {
|
||||
defaultProps.name = 'password';
|
||||
render(<LabeledErrorBoundInput {...defaultProps} />);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ import '@testing-library/jest-dom';
|
||||
import { Col, Row } from '.';
|
||||
|
||||
describe('Grid Component', () => {
|
||||
it('should render the grid with rows and columns', async () => {
|
||||
test('should render the grid with rows and columns', async () => {
|
||||
render(
|
||||
<Row>
|
||||
<Col span={8}>Column 1</Col>
|
||||
|
||||
@@ -25,7 +25,7 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
describe('IconButton', () => {
|
||||
it('renders an IconButton with icon and text', () => {
|
||||
test('renders an IconButton with icon and text', () => {
|
||||
render(<IconButton {...defaultProps} />);
|
||||
|
||||
const icon = screen.getByRole('img');
|
||||
@@ -35,7 +35,7 @@ describe('IconButton', () => {
|
||||
expect(buttonText).toBeVisible();
|
||||
});
|
||||
|
||||
it('is keyboard accessible and has correct aria attributes', () => {
|
||||
test('is keyboard accessible and has correct aria attributes', () => {
|
||||
render(<IconButton {...defaultProps} />);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
@@ -44,7 +44,7 @@ describe('IconButton', () => {
|
||||
expect(button).toHaveAttribute('aria-label', defaultProps.buttonText);
|
||||
});
|
||||
|
||||
it('handles Enter and Space key presses', () => {
|
||||
test('handles Enter and Space key presses', () => {
|
||||
const mockOnClick = jest.fn();
|
||||
render(<IconButton {...defaultProps} onClick={mockOnClick} />);
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('IconButton', () => {
|
||||
expect(mockOnClick).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('uses custom alt text when provided', () => {
|
||||
test('uses custom alt text when provided', () => {
|
||||
const customAltText = 'Custom Alt Text';
|
||||
render(
|
||||
<IconButton
|
||||
@@ -71,14 +71,14 @@ describe('IconButton', () => {
|
||||
expect(icon).toBeVisible();
|
||||
});
|
||||
|
||||
it('displays tooltip with button text', () => {
|
||||
test('displays tooltip with button text', () => {
|
||||
render(<IconButton {...defaultProps} />);
|
||||
|
||||
const tooltipTrigger = screen.getByText(/this is the iconbutton text/i);
|
||||
expect(tooltipTrigger).toBeVisible();
|
||||
});
|
||||
|
||||
it('calls onClick handler when clicked', () => {
|
||||
test('calls onClick handler when clicked', () => {
|
||||
const mockOnClick = jest.fn();
|
||||
render(<IconButton {...defaultProps} onClick={mockOnClick} />);
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ import {
|
||||
PlusSquareOutlined,
|
||||
PlusOutlined,
|
||||
ProfileOutlined,
|
||||
PushpinOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
RightOutlined,
|
||||
@@ -263,6 +264,7 @@ const AntdIcons = {
|
||||
PlusSquareOutlined,
|
||||
PlusOutlined,
|
||||
ProfileOutlined,
|
||||
PushpinOutlined,
|
||||
ReloadOutlined,
|
||||
QuestionCircleOutlined,
|
||||
RightOutlined,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user