mirror of
https://github.com/apache/superset.git
synced 2026-09-01 13:01:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8105a24b0d | ||
|
|
ac3c158c41 | ||
|
|
1f1539fe0e | ||
|
|
5bffceb295 | ||
|
|
b24cb2af8a | ||
|
|
040b33c450 | ||
|
|
9d1b5ebdb7 | ||
|
|
6e185ebce8 | ||
|
|
5b64150d0a | ||
|
|
b8b2e5ea8b | ||
|
|
f3985d88a1 | ||
|
|
6abab3c1cb | ||
|
|
b310f469c2 | ||
|
|
22891894d4 | ||
|
|
65fa1ce816 | ||
|
|
ee4a90892c | ||
|
|
bba4832946 | ||
|
|
2df06dcd57 | ||
|
|
88cd8bf575 | ||
|
|
5f67e85003 | ||
|
|
87fb7d18e1 | ||
|
|
96119ffefc | ||
|
|
74ea780eaa | ||
|
|
3d91ba3fde | ||
|
|
9151b5c6fb | ||
|
|
ec19a37e9b | ||
|
|
2ca3695f30 | ||
|
|
ac88acb14b | ||
|
|
46f17aa880 | ||
|
|
7bc44d5504 | ||
|
|
4444a89f26 | ||
|
|
7c2a3fa777 | ||
|
|
b38fa70256 | ||
|
|
1c8d58a77b | ||
|
|
de33efae98 | ||
|
|
b30d569028 | ||
|
|
6f69fc6dba | ||
|
|
b2a8842551 | ||
|
|
c7cc22f4fe | ||
|
|
058eaf78ab | ||
|
|
30402b412c | ||
|
|
782a57fbfc | ||
|
|
99a1f756cd | ||
|
|
0eda633b45 | ||
|
|
5c24f72d92 | ||
|
|
60479fb958 | ||
|
|
4ced5ca35a | ||
|
|
fc26991cd4 | ||
|
|
a5c68c8df9 | ||
|
|
53e76afd70 | ||
|
|
390be50f37 | ||
|
|
d997d363e3 | ||
|
|
9a6f6ee0c0 | ||
|
|
94dd3d049c | ||
|
|
b3f718da62 | ||
|
|
abf338d611 | ||
|
|
7aa6fcaf1e | ||
|
|
8dbdfb4fc6 | ||
|
|
e518b21994 | ||
|
|
e18f27e1ce | ||
|
|
2b1df8d462 | ||
|
|
9bf457dea6 | ||
|
|
cb7b790733 | ||
|
|
8bec85158c | ||
|
|
a30e4a4350 | ||
|
|
933dbbc2a2 | ||
|
|
feee3dea2f | ||
|
|
192ddf9a6d | ||
|
|
a59b96c4f5 | ||
|
|
12cd259c55 | ||
|
|
3ddc3b1d56 | ||
|
|
98ec6018df | ||
|
|
2ebd415b8a | ||
|
|
81b3e85522 | ||
|
|
fd64efd72d | ||
|
|
e39bfb255b | ||
|
|
b7301ac88a | ||
|
|
fa59b44cfe | ||
|
|
8c5be889d9 | ||
|
|
b733b57e9e | ||
|
|
13e97ba913 | ||
|
|
f9e43a37d8 | ||
|
|
5ff44c5ed6 | ||
|
|
7b145a520c | ||
|
|
9f62aace7e | ||
|
|
20be48c085 | ||
|
|
e1c7a51674 | ||
|
|
ecbd6578b0 | ||
|
|
ea3206b076 | ||
|
|
478f612f25 | ||
|
|
a16bc0d94f | ||
|
|
bcc6af6c5f | ||
|
|
94b59420b7 | ||
|
|
6814a89230 | ||
|
|
ebbe2e8714 | ||
|
|
b8acf4b207 | ||
|
|
ba5d5091b9 | ||
|
|
8c88c0d3ac | ||
|
|
e9e652d22d | ||
|
|
8f82d851cf | ||
|
|
83934c038e | ||
|
|
35fc36a97e | ||
|
|
88c4e689e3 | ||
|
|
29f4a05eb7 | ||
|
|
323741b043 | ||
|
|
5c3c6362ae | ||
|
|
c1b6ff340b | ||
|
|
05830ebc77 | ||
|
|
74dd690102 | ||
|
|
22027ec24c | ||
|
|
a3072c0bf6 | ||
|
|
6d8cbbfe8d | ||
|
|
926e0e6a92 | ||
|
|
d013bc7a21 | ||
|
|
87c48cc473 | ||
|
|
c2266f860c | ||
|
|
e8aea6b6df | ||
|
|
bde1c483b0 | ||
|
|
ff1d9f9e09 | ||
|
|
76151beade | ||
|
|
905a35d05f | ||
|
|
fde0fbd315 | ||
|
|
5879994e68 | ||
|
|
d1b7a4fdb4 | ||
|
|
5ea7d05d98 | ||
|
|
0ab64e305c | ||
|
|
7d355a254e | ||
|
|
ca63bb532d | ||
|
|
b71293acde | ||
|
|
0e172a6ff3 | ||
|
|
1c66b71e23 | ||
|
|
6b0aa8714d | ||
|
|
371e5e25e8 | ||
|
|
a140e74f5f | ||
|
|
b8308a44a3 | ||
|
|
13927f27e2 | ||
|
|
434511cb37 | ||
|
|
70c9203aa9 | ||
|
|
455e6603c1 | ||
|
|
0597f36bb6 | ||
|
|
53b88da3b9 | ||
|
|
3f4fdf5f07 | ||
|
|
5a6c1b977b | ||
|
|
e8577368d3 | ||
|
|
540f8cb2d0 | ||
|
|
aae997e546 |
+31
-8
@@ -5,6 +5,10 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
patterns: ["*"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -20,14 +24,6 @@ updates:
|
||||
- dependency-name: "@types/react-dom"
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "react-icons"
|
||||
# JSDOM v30 doesn't play well with Jest v30
|
||||
# Source: https://jestjs.io/blog#known-issues
|
||||
# GH thread: https://github.com/jsdom/jsdom/issues/3492
|
||||
- dependency-name: "jest-environment-jsdom"
|
||||
# `@swc/plugin-transform-imports` doesn't work with current Webpack-SWC hybrid setup
|
||||
# See https://github.com/apache/superset/pull/37384#issuecomment-3793991389
|
||||
# TODO: remove the plugin once Lodash usage has been migrated to a more readily tree-shakeable alternative
|
||||
- dependency-name: "@swc/plugin-transform-imports"
|
||||
# deck.gl and luma.gl share strict peer constraints across the root and
|
||||
# plugin workspaces, and root overrides pin their transitive versions.
|
||||
# Upgrade both families together in a manually validated change.
|
||||
@@ -61,6 +57,9 @@ updates:
|
||||
- npm
|
||||
- dependabot
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
patterns: ["*"]
|
||||
rjsf:
|
||||
patterns:
|
||||
- "@rjsf/*"
|
||||
@@ -80,6 +79,11 @@ updates:
|
||||
patterns:
|
||||
- "ag-grid-react"
|
||||
- "ag-grid-community"
|
||||
swc:
|
||||
patterns:
|
||||
- "@swc/core"
|
||||
- "@swc/plugin-emotion"
|
||||
- "@swc/plugin-transform-imports"
|
||||
open-pull-requests-limit: 30
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
@@ -98,6 +102,10 @@ updates:
|
||||
labels:
|
||||
- pip
|
||||
- dependabot
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
patterns: ["*"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -105,6 +113,10 @@ updates:
|
||||
directory: ".github/actions"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
patterns: ["*"]
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
@@ -115,6 +127,9 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
patterns: ["*"]
|
||||
storybook:
|
||||
patterns:
|
||||
- "@storybook/*"
|
||||
@@ -142,6 +157,10 @@ updates:
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
patterns: ["*"]
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
@@ -153,6 +172,10 @@ updates:
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
groups:
|
||||
security:
|
||||
applies-to: "security-updates"
|
||||
patterns: ["*"]
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -78,6 +78,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -66,6 +66,27 @@ jobs:
|
||||
|
||||
- name: "Set up liccheck"
|
||||
run: |
|
||||
# liccheck (as of 0.9.2) still does a bare `import pkg_resources`
|
||||
# without declaring setuptools as a dependency, relying on it
|
||||
# having historically been bundled. setuptools 81+ (installed
|
||||
# above via requirements/base.txt) dropped the pkg_resources
|
||||
# subpackage entirely, so liccheck's own import breaks outright.
|
||||
#
|
||||
# Reinstalling an older setuptools would restore pkg_resources but
|
||||
# would also downgrade the *real* setuptools install, which then
|
||||
# trips liccheck's own working_set.resolve() -- it cross-checks
|
||||
# requirements/base.txt's declared `setuptools==84.0.0` against
|
||||
# what's actually installed, and a downgrade makes those disagree.
|
||||
#
|
||||
# Instead, vendor just the pkg_resources/ package files from an
|
||||
# old setuptools wheel into site-packages, leaving the real
|
||||
# setuptools install (and its dist-info metadata) untouched. This
|
||||
# gives liccheck an importable pkg_resources whose own working-set
|
||||
# scan still correctly reports the real installed setuptools
|
||||
# version, so no conflict is raised.
|
||||
pip download "setuptools<81" --no-deps -d /tmp/old-setuptools
|
||||
python -m zipfile -e /tmp/old-setuptools/setuptools-*.whl /tmp/old-setuptools-extracted/
|
||||
cp -r /tmp/old-setuptools-extracted/pkg_resources "$(python -c 'import site; print(site.getsitepackages()[0])')/"
|
||||
uv pip install --system liccheck
|
||||
- name: "Run liccheck"
|
||||
run: |
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "11"
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "11"
|
||||
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "21"
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics (if triggered by integration tests)
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
path: docs/src/data/
|
||||
- name: Try to download latest diagnostics (for push/dispatch triggers)
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics from integration tests
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -97,6 +97,7 @@ jobs:
|
||||
mkdir -p ${{ github.workspace }}/superset-frontend/coverage
|
||||
docker run \
|
||||
-v ${{ github.workspace }}/superset-frontend/coverage:/app/superset-frontend/coverage \
|
||||
-e CI=true \
|
||||
--rm $TAG \
|
||||
bash -c \
|
||||
"npm run test -- --coverage --shard=${{ matrix.shard }}/8 --coverageReporters=json"
|
||||
|
||||
@@ -88,9 +88,9 @@ repos:
|
||||
language: system
|
||||
pass_filenames: true
|
||||
files: ^superset-frontend/.*\.(js|jsx|ts|tsx)$
|
||||
- id: eslint-docs
|
||||
name: eslint (docs)
|
||||
entry: bash -c 'cd docs && FILES=$(printf "%s\n" "$@" | sed "s|^docs/||" | tr "\n" " ") && yarn eslint --fix --quiet $FILES'
|
||||
- id: oxlint-docs
|
||||
name: oxlint (docs)
|
||||
entry: bash -c 'cd docs && FILES=$(printf "%s\n" "$@" | sed "s|^docs/||" | tr "\n" " ") && yarn lint --fix --quiet $FILES'
|
||||
language: system
|
||||
pass_filenames: true
|
||||
files: ^docs/.*\.(js|jsx|ts|tsx)$
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
.stylelintignore
|
||||
.flake8
|
||||
.nvmrc
|
||||
.npmrc
|
||||
.rat-excludes
|
||||
.swcrc
|
||||
.*log
|
||||
|
||||
@@ -25,6 +25,7 @@ assists people when migrating to a new version.
|
||||
## Next
|
||||
|
||||
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
|
||||
- The `cockroachdb` extra (`pip install apache-superset[cockroachdb]`) now installs `sqlalchemy-cockroachdb` instead of the abandoned `cockroachdb` package, whose SQLAlchemy dialect could not be imported under SQLAlchemy 2.0. Existing environments with the old package installed should `pip uninstall cockroachdb && pip install sqlalchemy-cockroachdb` (or simply reinstall the extra) to restore CockroachDB connectivity.
|
||||
|
||||
### MCP tool results preserve stored string values
|
||||
|
||||
|
||||
+3
-3
@@ -62,8 +62,8 @@ yarn version:remove:developer_docs <version> # Remove developer docs version
|
||||
yarn version:remove:components <version> # Remove components version
|
||||
|
||||
# Quality Checks
|
||||
yarn typecheck # TypeScript validation
|
||||
yarn eslint # Lint TypeScript/JavaScript files
|
||||
yarn typecheck # TypeScript validation
|
||||
yarn lint # Lint TypeScript/JavaScript files
|
||||
```
|
||||
|
||||
## 📁 Documentation Structure
|
||||
@@ -431,7 +431,7 @@ yarn build
|
||||
yarn typecheck
|
||||
|
||||
# Linting issues
|
||||
yarn eslint
|
||||
yarn lint
|
||||
```
|
||||
|
||||
### Version Issues
|
||||
|
||||
@@ -14,6 +14,20 @@ Users can configure automated alerts and reports to send dashboards or charts to
|
||||
|
||||
Alerts and reports are disabled by default. To turn them on, you'll need to change configuration settings and install a suitable headless browser in your environment.
|
||||
|
||||
## Content Format Options
|
||||
|
||||
When scheduling an alert or report, you can choose the format used to deliver the dashboard or chart:
|
||||
|
||||
- **PDF** – a full-page screenshot rendered as a PDF attachment. Available for both dashboards and charts.
|
||||
- **PNG** – a screenshot delivered as an attachment. Emails embed the image inline in the message body; Slack and webhook recipients receive it as an uploaded file. Available for both dashboards and charts.
|
||||
- **CSV** – chart data attached as a `.csv` file. Available for charts only.
|
||||
- **XLSX (Excel)** – chart data attached as a `.xlsx` file. Available for charts only. If the chart's data spans multiple server-paginated files, email delivery detects the bundle and renames the attachment to `.zip`; Slack and webhook deliveries always name the file with an `.xlsx` extension even when the contents are a multi-file ZIP archive.
|
||||
- **Text** – chart data embedded directly in the email or Slack message body. Available only for charts using a text-based visualization type (e.g. Table, Pivot Table, Paired t-test).
|
||||
|
||||
Dashboard reports and alerts are limited to the PDF and PNG formats; the CSV, XLSX, and Text options are only available when scheduling a report or alert for an individual chart.
|
||||
|
||||
For alerts (not scheduled reports), PNG/PDF screenshots and chart CSV/XLSX data are only generated when the `ALERTS_ATTACH_REPORTS` [feature flag](/admin-docs/configuration/configuring-superset#feature-flags) is enabled (the default); with it disabled, an alert notification is still sent, but without the attachment.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Commons
|
||||
|
||||
@@ -76,6 +76,27 @@ value defined in `DATA_CACHE_CONFIG`.
|
||||
Note, that by setting the cache timeout to `-1`, caching for charting data can be disabled, either
|
||||
per chart, dataset or database, or by default if set in `DATA_CACHE_CONFIG`.
|
||||
|
||||
Native filter option queries (the dropdown values for native filters) go through this same
|
||||
chart-data cache, but their freshness needs often differ from regular chart queries, especially for
|
||||
datasets whose visible values change frequently, including RLS-constrained datasets. Set
|
||||
`NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT` in `superset_config.py` to give these queries a dedicated
|
||||
timeout, checked before the chart/dataset/database chain and the `DATA_CACHE_CONFIG` default above:
|
||||
|
||||
```python
|
||||
NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT = 60 # seconds
|
||||
```
|
||||
|
||||
- `None` (default): native filter option queries fall through to the normal
|
||||
chart/dataset/database/`DATA_CACHE_CONFIG` resolution chain.
|
||||
- `-1`: disables caching for native filter option queries entirely.
|
||||
- `0`: passed directly to the cache backend; behavior is backend-specific, so use `-1` if the intent
|
||||
is to disable caching.
|
||||
- A positive integer: cache native filter option queries for that many seconds.
|
||||
|
||||
This setting only applies to requests detected as native filter option queries. It takes precedence
|
||||
over the per-chart/dataset/database timeouts, but not over an explicit per-request
|
||||
`custom_cache_timeout` override (e.g. "Force refresh").
|
||||
|
||||
## Limiting Cached Result Size
|
||||
|
||||
Very large chart or SQL query results can flood the cache backend (Redis/Memcached), evicting many
|
||||
|
||||
@@ -83,6 +83,26 @@ The optional username flag **-u** sets the user used for the datasource import.
|
||||
superset import_datasources -p <path / filename> -u 'admin'
|
||||
```
|
||||
|
||||
## Importing a Directory of Assets
|
||||
|
||||
The `import_directory` command imports a directory of exported assets (databases, datasets,
|
||||
dashboards, charts) in the same layout produced by the ZIP-based export. Saved queries and tags
|
||||
included in a full export are not imported by this command:
|
||||
|
||||
```bash
|
||||
superset import_directory <path / directory>
|
||||
```
|
||||
|
||||
As with `import_datasources`, the optional username flag **-u** sets the user assigned as the
|
||||
owner of the imported assets. The default is 'admin'. Example:
|
||||
|
||||
```bash
|
||||
superset import_directory <path / directory> -u 'admin'
|
||||
```
|
||||
|
||||
If the specified user does not exist, the command fails immediately with an error rather than
|
||||
importing the assets without an owner.
|
||||
|
||||
## Dashboard Import Overwrite Behavior
|
||||
|
||||
When importing a dashboard ZIP with the **overwrite** option enabled, any existing charts that are part of the dashboard are **replaced** rather than duplicated. This applies to:
|
||||
|
||||
@@ -505,6 +505,8 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
|
||||
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
|
||||
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
|
||||
| `MCP_DISABLED_TOOLS` | `set()` | Set of tool names to remove from the MCP server at startup. Disabled tools are never advertised to AI clients during tool discovery. Useful when a custom extension tool should replace a built-in Superset tool. See [Disabling built-in tools](#disabling-built-in-tools). |
|
||||
| `MCP_DISABLED_CHART_PLUGINS` | `frozenset()` | Set of chart type plugin names (e.g. `"handlebars"`) to hide from `generate_chart`. Does not affect `get_chart_type_schema`. See [Disabling chart type plugins](#disabling-chart-type-plugins). |
|
||||
| `MCP_CHART_PLUGIN_ENABLED_FUNC` | `None` | Callable `(chart_type: str) -> bool` evaluated per registry lookup for dynamic enable/disable decisions. Takes precedence over `MCP_DISABLED_CHART_PLUGINS` when set. See [Disabling chart type plugins](#disabling-chart-type-plugins). |
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -893,6 +895,39 @@ MCP_DISABLED_TOOLS = {"extensions.myorg.myextension.some_tool"}
|
||||
Specifying a tool name that does not exist logs a warning at startup and is otherwise ignored — it will not prevent the server from starting.
|
||||
:::
|
||||
|
||||
## Disabling chart type plugins
|
||||
|
||||
The `generate_chart` tool dispatches per chart type (`xy`, `table`, `pie`, `pivot_table`, `mixed_timeseries`, `handlebars`, `big_number`, `histogram`, `box_plot`, `waterfall`) to a registered chart type plugin. Two settings let operators enable or disable individual chart type plugins at runtime, without a code deploy.
|
||||
|
||||
### Static deny-list
|
||||
|
||||
Set `MCP_DISABLED_CHART_PLUGINS` in your `superset_config.py` to a set of chart type names:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
|
||||
# Emergency kill switch: hide "handlebars" from all callers
|
||||
MCP_DISABLED_CHART_PLUGINS = {"handlebars"}
|
||||
```
|
||||
|
||||
Disabled chart types stay registered but are filtered out at lookup time: they're never listed in `generate_chart`'s supported chart types, and `generate_chart` calls for them are rejected. `get_chart_type_schema` consults its own static schema/example map rather than the registry filter, so a disabled chart type's schema remains queryable through that tool even though `generate_chart` will reject it.
|
||||
|
||||
### Dynamic predicate
|
||||
|
||||
For per-request control (A/B tests, gradual rollout, entitlement checks), set `MCP_CHART_PLUGIN_ENABLED_FUNC` to a callable. It's evaluated as `enabled_func(chart_type: str) -> bool` on every registry lookup, and it takes precedence over `MCP_DISABLED_CHART_PLUGINS` when set:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
from flask import g
|
||||
|
||||
|
||||
def MCP_CHART_PLUGIN_ENABLED_FUNC(chart_type: str) -> bool:
|
||||
flags = getattr(g, "feature_flags", {})
|
||||
return flags.get(f"mcp_chart_{chart_type}", True)
|
||||
```
|
||||
|
||||
The callable must be cheap and in-process (consult already-loaded feature flags or request-local context) -- do not perform network I/O per call. If it raises, the registry fails closed (the plugin is hidden) and logs a warning.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Use TLS** for all production MCP endpoints -- place the server behind a reverse proxy with HTTPS
|
||||
|
||||
@@ -88,6 +88,27 @@ THEME_DARK = {
|
||||
# - OS preference detection is automatically enabled
|
||||
```
|
||||
|
||||
### Default Theme Mode
|
||||
|
||||
By default, Superset mimics the visitor's OS/browser preference (light or dark) for
|
||||
sessions that don't have a saved user preference. Use `THEME_DEFAULT_MODE` to override
|
||||
that starting point instance-wide for the standard application:
|
||||
|
||||
```python
|
||||
# Default theme mode for sessions without a saved user preference.
|
||||
# One of "default" (always light), "dark" (always dark), or "system" (mimic OS preference).
|
||||
THEME_DEFAULT_MODE = "dark"
|
||||
```
|
||||
|
||||
- `"system"` (the default) preserves the existing behavior of following the OS/browser
|
||||
preference, provided a dark theme is configured (`THEME_DARK` is not `None`). If no dark
|
||||
theme is available, Superset always starts in light mode regardless of this setting.
|
||||
- `"default"` or `"dark"` forces that starting mode for first-time visitors; users can still switch themes manually afterward if both `THEME_DEFAULT` and `THEME_DARK` are available.
|
||||
- A user's own saved preference, once they toggle the theme switcher, always takes precedence over `THEME_DEFAULT_MODE`.
|
||||
- `THEME_DEFAULT_MODE` has no effect on embedded dashboards: the embed SDK sets the
|
||||
starting mode via its own `themeMode` URL parameter, which takes precedence and falls
|
||||
back to light mode when the parameter is absent.
|
||||
|
||||
### App Branding
|
||||
|
||||
The application name shown in the browser title bar and navigation can be
|
||||
|
||||
@@ -243,6 +243,42 @@ Each subject in the response includes flat scalar ids (`user_id`, `role_id`, `gr
|
||||
than a nested object, so callers can match directly on whichever id they already have — only the
|
||||
id field matching the subject's `type` is populated; the others are `null`.
|
||||
|
||||
#### Extending Editorship with EXTRA_EDITORS_RESOLVER
|
||||
|
||||
Deployments that grant edit access to a dashboard or chart through a mechanism outside
|
||||
Superset's own Subject-based `editors` list — for example, a folder-permission system or an
|
||||
internal directory service — can plug that logic in with `EXTRA_EDITORS_RESOLVER`:
|
||||
|
||||
```python
|
||||
def extra_editors_resolver(resource):
|
||||
# `resource` is the Dashboard or Slice instance being checked.
|
||||
# Return Subject instances, raw subject ids, or dicts with an `id` key.
|
||||
return [...]
|
||||
|
||||
|
||||
EXTRA_EDITORS_RESOLVER = extra_editors_resolver
|
||||
```
|
||||
|
||||
The resolver's result is unioned with the resource's own `editors` for editorship checks: it
|
||||
feeds `is_editor`, `raise_for_editorship`, save-as, and soft-delete restore. When
|
||||
`EXTRA_EDITORS_RESOLVER` is configured, the usual lockout-prevention behavior — automatically
|
||||
re-adding a non-admin who removes themselves from a resource's editors — is skipped, since the
|
||||
deployment has its own way of keeping the resource editable. This skip is global to the setting,
|
||||
not per-resource: it still applies on a resource where the resolver currently returns no
|
||||
subjects, so a resolver that can't guarantee an alternate editor for every resource can let the
|
||||
last editor remove themselves and leave it uneditable by non-admins.
|
||||
|
||||
Resolved subject ids are also surfaced as `extra_editors` in the chart and dashboard `GET`
|
||||
responses, so API clients can distinguish resolver-granted editorship from the resource's own
|
||||
`editors` list. This field is attached after serialization and isn't part of the OpenAPI response
|
||||
schema, so generated API clients won't see it as a typed field.
|
||||
|
||||
Because the resolver is arbitrary per-deployment Python rather than a SQL-expressible condition,
|
||||
editorship it grants cannot be reflected in list-view filtering (for example, the soft-deleted
|
||||
archive is scoped to editors via a SQL query). It does still run once per row on chart and
|
||||
dashboard list responses to populate `extra_editors`, so a slow or unavailable external resolver
|
||||
affects ordinary list requests, not just direct per-object checks.
|
||||
|
||||
### Dashboard Access Control
|
||||
|
||||
Access to dashboards is managed via editors (subjects that have edit permissions to the dashboard).
|
||||
@@ -583,6 +619,28 @@ SELECT * FROM (
|
||||
queries run against tables that have associated datasets with RLS filters will then have
|
||||
the appropriate predicates injected automatically.
|
||||
|
||||
#### RLS Indicator in the Dataset List and Explore
|
||||
|
||||
When a dataset has one or more RLS filters that apply to it, Superset shows a lock
|
||||
icon badge next to the dataset name in the **Datasets** list and next to the dataset
|
||||
selector in **Explore**. Hovering over the badge shows a tooltip listing each
|
||||
applicable filter's name, filter type (Regular or Base), group key (if any), assigned
|
||||
subjects (labeled "Roles" in the tooltip, but may include users and groups too), and
|
||||
clause.
|
||||
|
||||
This badge also surfaces filters that are inherited from the physical tables
|
||||
referenced by a virtual (SQL-based) dataset, as described above. Inherited filters
|
||||
are marked "from underlying table" in the tooltip, and a summary note is shown
|
||||
whenever any of the listed filters are inherited rather than assigned directly to
|
||||
the dataset. Inherited-filter detection depends on Superset's SQL parser being able
|
||||
to identify the referenced tables and match them to a physical dataset by name,
|
||||
schema, and database, so it's best-effort: unparseable or unmatched references won't
|
||||
surface a filter on the badge even if one would apply at query time.
|
||||
|
||||
The badge is a visibility aid only — it does not change which filters are applied to
|
||||
a query. Use the RLS REST API described below if you need to confirm exactly which
|
||||
filters affect a dataset.
|
||||
|
||||
#### Checking RLS Filters via the API
|
||||
|
||||
You can use the RLS REST API to audit which filters are configured and which datasets
|
||||
|
||||
@@ -493,8 +493,8 @@ Frontend assets (TypeScript, JavaScript, CSS, and images) must be compiled in or
|
||||
|
||||
First, be sure you are using the following versions of Node.js and npm:
|
||||
|
||||
- `Node.js`: Version 22 (LTS)
|
||||
- `npm`: Version 10
|
||||
- `Node.js`: Version 24 (see `superset-frontend/.nvmrc` for the exact version)
|
||||
- `npm`: Version 11
|
||||
|
||||
We recommend using [nvm](https://github.com/nvm-sh/nvm) to manage your node environment:
|
||||
|
||||
@@ -507,8 +507,8 @@ export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
|
||||
|
||||
cd superset-frontend
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
nvm install
|
||||
nvm use
|
||||
```
|
||||
|
||||
Or if you use the default macOS starting with Catalina shell `zsh`, try:
|
||||
|
||||
@@ -114,8 +114,8 @@ function MyExtension() {
|
||||
|
||||
## Source Links
|
||||
|
||||
- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/Alert.stories.tsx)
|
||||
- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/ui/components/Alert/index.tsx)
|
||||
- [Story file](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/components/Alert/Alert.stories.tsx)
|
||||
- [Component source](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-core/src/components/Alert/index.tsx)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ export function MyExtensionPanel() {
|
||||
|
||||
Components in `@apache-superset/core/components` are automatically documented here. To add a new extension component:
|
||||
|
||||
1. Add the component to `superset-frontend/packages/superset-core/src/ui/components/`
|
||||
2. Export it from `superset-frontend/packages/superset-core/src/ui/components/index.ts`
|
||||
1. Add the component to `superset-frontend/packages/superset-core/src/components/`
|
||||
2. Export it from `superset-frontend/packages/superset-core/src/components/index.ts`
|
||||
3. Create a Storybook story with an `Interactive` export:
|
||||
|
||||
```tsx
|
||||
|
||||
@@ -406,6 +406,18 @@ ECharts option overrides bypass Superset's validation layer. Invalid option keys
|
||||
|
||||
When the **Search Box** is visible in a Table chart, the **Download** action exports only the rows currently visible after the search filter is applied — not the full underlying dataset. This matches the visual output and is intentional. To export the full dataset regardless of search state, use the **Download as CSV** option from the chart's three-dot menu in the dashboard or from the Explore chart toolbar before applying a search filter.
|
||||
|
||||
### Exporting a Chart as an Image or PDF
|
||||
|
||||
Alongside the raw-data export options (CSV, JSON, Excel), a chart's three-dot menu — in a dashboard or from the Explore chart toolbar — offers a few ways to export a visual snapshot of the chart:
|
||||
|
||||
- **Export screenshot (jpeg)** — a single-click JPEG screenshot of the chart.
|
||||
- **Export screenshot (png)** — opens a submenu with **Transparent background** and **Solid background** options. The solid option uses the current theme's background color. PNG produces a higher-quality image than the JPEG export.
|
||||
- **Export as PDF** — downloads the chart as a PDF file.
|
||||
|
||||
The dropdown menu is briefly hidden while the screenshot or PDF is being captured so it doesn't appear in the exported file. In Explore, these image and PDF options are available from the **Export All Data** submenu, and also from the **Export current view** submenu when the chart type supports current-view export.
|
||||
|
||||
These menu items respect your permissions: the dashboard export menu only appears if you can download, and the image/PDF options are disabled if you lack image-export permission.
|
||||
|
||||
### Sharing a Specific Tab
|
||||
|
||||
When a dashboard has tabs, each tab gets its own shareable URL. Navigate to the tab you want to share and copy the URL from your browser's address bar — the tab anchor is encoded in the URL so that anyone opening the link lands directly on that tab.
|
||||
|
||||
@@ -88,6 +88,21 @@ embedDashboard({
|
||||
|
||||
If the callback returns `null` or is not provided, Superset uses its own permalink URL as a fallback.
|
||||
|
||||
### Permalink origin rewriting
|
||||
|
||||
This rewrite only applies to the non-embedded permalink path — it has no effect on embedded dashboards. When Superset is not embedded, it rewrites the origin of any permalink URL it generates to `window.location.origin` before showing it to the user, which keeps a proxied or subdirectory-deployed Superset from handing out a permalink that points at an internal hostname the user's browser can't reach.
|
||||
|
||||
When Superset **is** embedded, this rewrite is skipped entirely regardless of the flag below: a `resolvePermalinkUrl` callback's return value is used as-is, and if no callback is provided (or it fails), the backend-supplied URL is also returned as-is.
|
||||
|
||||
If your reverse proxy correctly forwards `X-Forwarded-Host` and you'd rather non-embedded permalinks carry the backend's literal origin, opt out of the rewrite with `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE`:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True
|
||||
```
|
||||
|
||||
This defaults to `False` (rewrite enabled) and only affects non-embedded permalinks. Flipping the default would regress the common proxied/subdirectory deployment by exposing an unreachable internal host in copied permalinks.
|
||||
|
||||
---
|
||||
|
||||
## Feature Flags for Embedded Mode
|
||||
|
||||
@@ -78,6 +78,18 @@ Charts are **not saved by default**. The workflow is intentionally iterative:
|
||||
To skip the preview and save immediately, include "and save it" in your prompt.
|
||||
:::
|
||||
|
||||
:::info Deployment-specific chart types
|
||||
Use `get_chart_type_schema` before generating a chart to discover the types
|
||||
available on your Superset instance. Some deployments expose additional
|
||||
feature-gated visualizations. For example, a deployment with an AG Grid pivot
|
||||
extension enabled can expose `interactive_pivot`, which supports interactive
|
||||
row groups, pivot columns, totals, and period-over-period comparisons. Pair
|
||||
`comparison_period` (for example, `1 year ago`) with `comparison_type`
|
||||
(`values`, `difference`, `percentage`, or `ratio`). It is distinct from the
|
||||
built-in `pivot_table` chart type and is not offered when the host visualization
|
||||
is unavailable.
|
||||
:::
|
||||
|
||||
### Create Dashboards
|
||||
|
||||
Build dashboards from a collection of charts:
|
||||
@@ -248,11 +260,12 @@ Ask your admin for the MCP server URL and any authentication tokens you need.
|
||||
|
||||
### Datasets
|
||||
|
||||
| Tool | Description |
|
||||
| ------------------------ | ------------------------------------------------ |
|
||||
| `list_datasets` | List datasets with filtering and search |
|
||||
| `get_dataset_info` | Get dataset metadata (columns, metrics, filters) |
|
||||
| `create_virtual_dataset` | Create a virtual dataset from a SQL query |
|
||||
| Tool | Description |
|
||||
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `list_datasets` | List datasets with filtering and search |
|
||||
| `get_dataset_info` | Get dataset metadata (columns, metrics, schema details) |
|
||||
| `create_virtual_dataset` | Create a virtual dataset from a SQL query |
|
||||
| `update_dataset_metric` | Update a saved metric's expression, name, verbose_name, or format (affects every chart using it; requires dataset ownership) |
|
||||
|
||||
### Charts
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/* eslint-env node */
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const typescriptEslintParser = require('@typescript-eslint/parser');
|
||||
const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
|
||||
const js = require('@eslint/js');
|
||||
const ts = require('typescript-eslint');
|
||||
const react = require('eslint-plugin-react');
|
||||
const globals = require('globals');
|
||||
const { defineConfig, globalIgnores } = require('eslint/config');
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
},
|
||||
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
{
|
||||
files: ['eslint.config.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parser: typescriptEslintParser,
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
typescript: typescriptEslintPlugin,
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"typescript",
|
||||
"react"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "off"
|
||||
},
|
||||
"env": {
|
||||
"builtin": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"build/**/*",
|
||||
".docusaurus/**/*",
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "18.3.1"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"typeAware": true
|
||||
},
|
||||
"rules": {
|
||||
"constructor-super": "error",
|
||||
"for-direction": "error",
|
||||
"getter-return": "error",
|
||||
"no-async-promise-executor": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-class-assign": "error",
|
||||
"no-compare-neg-zero": "error",
|
||||
"no-cond-assign": "error",
|
||||
"no-const-assign": "error",
|
||||
"no-constant-binary-expression": "error",
|
||||
"no-constant-condition": "error",
|
||||
"no-control-regex": "error",
|
||||
"no-debugger": "error",
|
||||
"no-delete-var": "error",
|
||||
"no-dupe-class-members": "error",
|
||||
"no-dupe-else-if": "error",
|
||||
"no-dupe-keys": "error",
|
||||
"no-duplicate-case": "error",
|
||||
"no-empty": "error",
|
||||
"no-empty-character-class": "error",
|
||||
"no-empty-pattern": "error",
|
||||
"no-empty-static-block": "error",
|
||||
"no-ex-assign": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"no-fallthrough": "error",
|
||||
"no-func-assign": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-import-assign": "error",
|
||||
"no-invalid-regexp": "error",
|
||||
"no-irregular-whitespace": "error",
|
||||
"no-loss-of-precision": "error",
|
||||
"no-misleading-character-class": "error",
|
||||
"no-new-native-nonconstructor": "error",
|
||||
"no-nonoctal-decimal-escape": "error",
|
||||
"no-obj-calls": "error",
|
||||
"no-prototype-builtins": "error",
|
||||
"no-redeclare": "error",
|
||||
"no-regex-spaces": "error",
|
||||
"no-self-assign": "error",
|
||||
"no-setter-return": "error",
|
||||
"no-shadow-restricted-names": "error",
|
||||
"no-sparse-arrays": "error",
|
||||
"no-this-before-super": "error",
|
||||
"no-unexpected-multiline": "error",
|
||||
"no-unreachable": "error",
|
||||
"no-unsafe-finally": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"no-unsafe-optional-chaining": "error",
|
||||
"no-unused-labels": "error",
|
||||
"no-unused-private-class-members": "error",
|
||||
"no-unused-vars": "error",
|
||||
"no-useless-backreference": "error",
|
||||
"no-useless-catch": "error",
|
||||
"no-useless-escape": "error",
|
||||
"no-with": "error",
|
||||
"require-yield": "error",
|
||||
"use-isnan": "error",
|
||||
"valid-typeof": "error",
|
||||
"no-array-constructor": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/no-duplicate-enum-values": "error",
|
||||
"typescript/no-empty-object-type": "error",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"typescript/no-extra-non-null-assertion": "error",
|
||||
"typescript/no-misused-new": "error",
|
||||
"typescript/no-namespace": "error",
|
||||
"typescript/no-non-null-asserted-optional-chain": "error",
|
||||
"typescript/no-require-imports": "error",
|
||||
"typescript/no-this-alias": "error",
|
||||
"typescript/no-unnecessary-type-constraint": "error",
|
||||
"typescript/no-unsafe-declaration-merging": "error",
|
||||
"typescript/no-unsafe-function-type": "error",
|
||||
"typescript/no-wrapper-object-types": "error",
|
||||
"typescript/prefer-as-const": "error",
|
||||
"typescript/prefer-namespace-keyword": "error",
|
||||
"typescript/triple-slash-reference": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.mts",
|
||||
"**/*.cts"
|
||||
],
|
||||
"rules": {
|
||||
"constructor-super": "off",
|
||||
"getter-return": "off",
|
||||
"no-class-assign": "off",
|
||||
"no-const-assign": "off",
|
||||
"no-dupe-class-members": "off",
|
||||
"no-dupe-keys": "off",
|
||||
"no-func-assign": "off",
|
||||
"no-import-assign": "off",
|
||||
"no-new-native-nonconstructor": "off",
|
||||
"no-obj-calls": "off",
|
||||
"no-redeclare": "off",
|
||||
"no-setter-return": "off",
|
||||
"no-this-before-super": "off",
|
||||
"no-unreachable": "off",
|
||||
"no-unsafe-negation": "off",
|
||||
"no-var": "error",
|
||||
"no-with": "off",
|
||||
"prefer-const": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+9
-15
@@ -29,7 +29,7 @@
|
||||
"lint:db-metadata": "python3 ../superset/db_engine_specs/lint_metadata.py",
|
||||
"lint:db-metadata:report": "python3 ../superset/db_engine_specs/lint_metadata.py --markdown -o ../superset/db_engine_specs/METADATA_STATUS.md",
|
||||
"update:readme-db-logos": "node scripts/generate-database-docs.mjs --update-readme",
|
||||
"eslint": "eslint .",
|
||||
"lint": "oxlint --config oxlint.json",
|
||||
"lint:docs-links": "node scripts/lint-docs-links.mjs",
|
||||
"version:add": "node scripts/manage-versions.mjs add",
|
||||
"version:remove": "node scripts/manage-versions.mjs remove",
|
||||
@@ -58,11 +58,11 @@
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^10.5.9",
|
||||
"@storybook/addon-docs": "^10.5.10",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.16.1",
|
||||
"antd": "^6.6.1",
|
||||
"baseline-browser-mapping": "^2.11.15",
|
||||
"baseline-browser-mapping": "^2.11.18",
|
||||
"caniuse-lite": "^1.0.30001809",
|
||||
"docusaurus-plugin-openapi-docs": "^5.2.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.2.0",
|
||||
@@ -76,8 +76,8 @@
|
||||
"react-svg-pan-zoom": "^3.13.1",
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.2.0",
|
||||
"storybook": "^10.5.9",
|
||||
"reselect": "^5.3.0",
|
||||
"storybook": "^10.5.10",
|
||||
"swagger-ui-react": "^5.32.14",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
@@ -85,19 +85,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.10.2",
|
||||
"@docusaurus/tsconfig": "^3.10.2",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
||||
"@typescript-eslint/parser": "^8.67.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.11.0",
|
||||
"oxfmt": "^0.64.0",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.67.0",
|
||||
"webpack": "^5.109.2"
|
||||
"oxlint": "^1.80.0",
|
||||
"oxlint-tsgolint": "^7.0.2001",
|
||||
"typescript": "7.0.2",
|
||||
"webpack": "^5.110.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
Vendored
+6
@@ -120,6 +120,12 @@
|
||||
"lifecycle": "testing",
|
||||
"description": "Enables filter functionality in Alerts and Reports"
|
||||
},
|
||||
{
|
||||
"name": "ALERT_REPORTS_RETRY",
|
||||
"default": false,
|
||||
"lifecycle": "testing",
|
||||
"description": "Enables automatic retry functionality for failed report executions"
|
||||
},
|
||||
{
|
||||
"name": "ALERT_REPORT_SLACK_V2",
|
||||
"default": true,
|
||||
|
||||
+23
-6
@@ -1,14 +1,30 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
// "extends": "@docusaurus/tsconfig",
|
||||
// First compilerOptions section comes from above commented @docusaurus/tsconfig
|
||||
// We moved them here to help with TS v7 migration so whenever Docusaurus readily supports TS v7,
|
||||
// re-install @docusaurus/tsconfig and remove said section.
|
||||
// Commented options are overriden in the next section.
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"allowJs": true,
|
||||
// "esModuleInterop": true,
|
||||
// "jsx": "preserve",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
// "moduleResolution": "bundler",
|
||||
"module": "esnext",
|
||||
"noEmit": true,
|
||||
// "paths": {
|
||||
// "@site/*": ["./*"]
|
||||
// },
|
||||
// "skipLibCheck": true,
|
||||
|
||||
"ignoreDeprecations": "6.0",
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"strict": false,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["@docusaurus/module-type-aliases"],
|
||||
@@ -23,9 +39,10 @@
|
||||
// Runtime resolution uses webpack alias pointing to actual source (see src/webpack.extend.ts)
|
||||
// Using /ui path matches the established pattern used throughout the Superset codebase
|
||||
"@apache-superset/core/components": ["./src/types/apache-superset-core"],
|
||||
"*": ["src/*", "node_modules/*"]
|
||||
"@site/*": ["./*"],
|
||||
"*": ["./src/*", "./node_modules/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts"],
|
||||
"exclude": ["node_modules", "../superset-frontend/**/*", "src/shims/**"]
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.d.ts"],
|
||||
"exclude": ["./node_modules", "../superset-frontend/**/*", "src/shims/**"]
|
||||
}
|
||||
|
||||
+497
-1385
File diff suppressed because it is too large
Load Diff
+23
-12
@@ -16,7 +16,7 @@
|
||||
# under the License.
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=40.9.0", "wheel"]
|
||||
requires = ["setuptools>=84.0.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
@@ -67,7 +67,7 @@ dependencies = [
|
||||
"flask-sqlalchemy>=3.1.1, <4.0",
|
||||
"flask-wtf>=1.3.0, <2.0",
|
||||
"geopy",
|
||||
"greenlet<=3.5.4, >=3.5.4",
|
||||
"greenlet<=3.5.5, >=3.5.5",
|
||||
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
|
||||
"hashids>=1.3.1, <2",
|
||||
# holidays>=0.45 required for security fix
|
||||
@@ -94,7 +94,7 @@ dependencies = [
|
||||
"parsedatetime",
|
||||
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
|
||||
"pgsanity",
|
||||
"Pillow>=11.0.0, <13",
|
||||
"Pillow>=12.3.0, <13", # raise floor to match resolved pin; closes SCA false-positive on 11.x-range CVEs already fixed in 12.3.0
|
||||
"polyline>=2.0.4, <3.0",
|
||||
"pydantic>=2.8.0",
|
||||
"pyparsing>=3.3.2, <4",
|
||||
@@ -103,7 +103,7 @@ dependencies = [
|
||||
"pygeohash",
|
||||
"pyarrow>=25.0.1, <26", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyyaml>=6.0.3, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"PyJWT>=2.13.0, <3.0", # raise floor to match resolved pin; closes SCA false-positive on 2.4.x-range CVEs already fixed in 2.13.0
|
||||
"redis>=5.0.0, <9.0",
|
||||
"rison>=2.0.1, <3.0",
|
||||
|
||||
@@ -141,8 +141,18 @@ bigquery = [
|
||||
"sqlalchemy-bigquery>=1.17.2",
|
||||
"google-cloud-bigquery>=3.42.3",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.7.1, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
clickhouse = ["clickhouse-connect>=1.7.2, <2.0"]
|
||||
# The `cockroachdb` PyPI package (last released 2021) is abandoned and its
|
||||
# SQLAlchemy dialect cannot even import under SQLAlchemy 2.0 (it references
|
||||
# sqlalchemy.dialects.postgresql.psycopg2.PGCompiler_psycopg2, removed in
|
||||
# 2.0). sqlalchemy-cockroachdb is the actively maintained replacement,
|
||||
# already linked from CockroachDbEngineSpec.metadata's docs_url, and
|
||||
# registers the same `cockroachdb` SQLAlchemy dialect entry point.
|
||||
# sqlalchemy-cockroachdb depends only on SQLAlchemy itself, not on a DBAPI
|
||||
# driver, so psycopg2-binary is pinned alongside it (matching the `postgres`
|
||||
# extra) to keep this extra self-contained -- CockroachDB speaks the
|
||||
# PostgreSQL wire protocol, so psycopg2 is what actually opens connections.
|
||||
cockroachdb = ["sqlalchemy-cockroachdb>=2.0.0, <3", "psycopg2-binary==2.9.12"]
|
||||
crate = ["sqlalchemy-cratedb>=0.43.1, <1"]
|
||||
# sqlalchemy-d1's only release (0.1.0, Nov 2025) pins sqlalchemy<2,>=1.4,
|
||||
# explicitly excluding SQLAlchemy 2.0. See superset/db_engine_specs/d1.py's
|
||||
@@ -185,11 +195,12 @@ excel = ["xlrd>=2.0.2, <2.1"]
|
||||
# installing this extra is only required to actually run exports.
|
||||
excel-export = ["boto3"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.4.6,<4.0",
|
||||
"fastmcp>=3.4.7,<4.0",
|
||||
"mcp>=1.29.1,<3.0",
|
||||
# tiktoken backs the response-size-guard token estimator. Without
|
||||
# it, the middleware falls back to a coarser character-based
|
||||
# heuristic that under-counts JSON-heavy MCP responses.
|
||||
"tiktoken>=0.13.0,<1.0",
|
||||
"tiktoken>=0.14.0,<1.0",
|
||||
]
|
||||
# sqlalchemy-firebird >=2.0.0 unconditionally requires SQLAlchemy 2.0 on
|
||||
# Python >=3.8 (which covers Superset's >=3.11 floor), with no dual-compat
|
||||
@@ -235,7 +246,7 @@ pinot = ["pinotdb>=5.0.0, <10.0.0"]
|
||||
playwright = ["playwright>=1.62.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.12"]
|
||||
presto = ["pyhive[presto]>=0.6.5"]
|
||||
trino = ["trino>=0.338.0"]
|
||||
trino = ["trino>=0.339.0"]
|
||||
prophet = ["prophet>=1.4.0, <2"]
|
||||
# sqlalchemy-redshift cuts hard from SQLAlchemy 1.4-only (0.8.x) to 2.0-only
|
||||
# (>=1.0.0) with no dual-compat release. Bumped now that Superset's own
|
||||
@@ -249,7 +260,7 @@ risingwave = ["sqlalchemy-risingwave>=2.0.0"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.5, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"]
|
||||
sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
|
||||
sqlite = ["syntaqlite>=0.9.0,<0.10.0"]
|
||||
spark = [
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
"tableschema",
|
||||
@@ -259,7 +270,7 @@ tdengine = [
|
||||
"taospy>=2.8.10",
|
||||
"taos-ws-py>=0.7.0"
|
||||
]
|
||||
teradata = ["teradatasql>=20.0.0.65"]
|
||||
teradata = ["teradatasql>=20.0.0.66"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
|
||||
netezza = ["nzalchemy>= 11.1.2, < 11.2"]
|
||||
@@ -293,7 +304,7 @@ development = [
|
||||
"ruff",
|
||||
"sqloxide",
|
||||
"statsd",
|
||||
"syntaqlite>=0.7.0,<0.8.0",
|
||||
"syntaqlite>=0.9.0,<0.10.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -52,11 +52,11 @@ marshmallow-sqlalchemy>=1.5.0
|
||||
# needed for python 3.12 support
|
||||
openapi-schema-validator>=0.6.3
|
||||
|
||||
# Pin setuptools <81 until all dependencies migrate from pkg_resources to importlib.metadata
|
||||
# Pin setuptools <85 until all dependencies migrate from pkg_resources to importlib.metadata
|
||||
# pkg_resources is deprecated and will be removed in setuptools 81+ (around 2025-11-30)
|
||||
# Known affected packages: Preset's 'clients' package
|
||||
# See docs/docs/contributing/pkg-resources-migration.md for details
|
||||
setuptools<81
|
||||
setuptools<85
|
||||
|
||||
# google-auth 2.53+ dropped its transitive dependency on cachetools, which is
|
||||
# imported directly by superset.db_engine_specs.aws_iam. We declare cachetools
|
||||
|
||||
@@ -163,16 +163,16 @@ google-auth==2.53.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# shillelagh
|
||||
greenlet==3.5.4
|
||||
greenlet==3.5.5
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==26.0.0
|
||||
gunicorn==26.2.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
hashids==1.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
holidays==0.102
|
||||
holidays==0.103
|
||||
# via apache-superset (pyproject.toml)
|
||||
humanize==4.12.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -263,7 +263,6 @@ packaging==25.0
|
||||
# apache-superset (pyproject.toml)
|
||||
# apispec
|
||||
# deprecation
|
||||
# gunicorn
|
||||
# kombu
|
||||
# limits
|
||||
# shillelagh
|
||||
@@ -367,7 +366,7 @@ rpds-py==0.25.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
setuptools==80.9.0
|
||||
setuptools==84.0.0
|
||||
# via -r requirements/base.in
|
||||
shillelagh==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
-e .[development,bigquery,druid,duckdb,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
-e .[development,bigquery,cockroachdb,druid,duckdb,fastmcp,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
-e ./superset-extensions-cli[test]
|
||||
|
||||
@@ -375,7 +375,7 @@ googleapis-common-protos==1.66.0
|
||||
# via
|
||||
# google-api-core
|
||||
# grpcio-status
|
||||
greenlet==3.5.4
|
||||
greenlet==3.5.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -391,7 +391,7 @@ grpcio==1.83.0
|
||||
# grpcio-status
|
||||
grpcio-status==1.60.1
|
||||
# via google-api-core
|
||||
gunicorn==26.0.0
|
||||
gunicorn==26.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -403,7 +403,7 @@ hashids==1.3.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
holidays==0.102
|
||||
holidays==0.103
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -547,8 +547,10 @@ matplotlib==3.9.0
|
||||
# via prophet
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
mcp==1.24.0
|
||||
# via fastmcp-slim
|
||||
mcp==1.29.1
|
||||
# via
|
||||
# apache-superset
|
||||
# fastmcp-slim
|
||||
mdurl==0.1.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -622,7 +624,6 @@ packaging==25.0
|
||||
# duckdb-engine
|
||||
# fastmcp-slim
|
||||
# google-cloud-bigquery
|
||||
# gunicorn
|
||||
# kombu
|
||||
# limits
|
||||
# matplotlib
|
||||
@@ -662,7 +663,7 @@ pillow==12.3.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# matplotlib
|
||||
pip==25.1.1
|
||||
pip==26.2.1
|
||||
# via apache-superset
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
@@ -919,7 +920,7 @@ secretstorage==3.5.0
|
||||
# via keyring
|
||||
semver==3.0.4
|
||||
# via apache-superset-extensions-cli
|
||||
setuptools==80.9.0
|
||||
setuptools==84.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# nodeenv
|
||||
@@ -960,10 +961,13 @@ sqlalchemy==2.0.52
|
||||
# marshmallow-sqlalchemy
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
# sqlalchemy-cockroachdb
|
||||
# sqlalchemy-continuum
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.17.2
|
||||
# via apache-superset
|
||||
sqlalchemy-cockroachdb==2.0.4
|
||||
# via apache-superset
|
||||
sqlalchemy-continuum==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -993,13 +997,13 @@ starlette==1.3.1
|
||||
# mcp
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
syntaqlite==0.7.1
|
||||
syntaqlite==0.9.0
|
||||
# via apache-superset
|
||||
tabulate==0.10.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
tiktoken==0.13.0
|
||||
tiktoken==0.14.0
|
||||
# via apache-superset
|
||||
tomli-w==1.2.0
|
||||
# via apache-superset-extensions-cli
|
||||
@@ -1009,7 +1013,7 @@ tqdm==4.67.1
|
||||
# via
|
||||
# cmdstanpy
|
||||
# prophet
|
||||
trino==0.338.0
|
||||
trino==0.339.0
|
||||
# via apache-superset
|
||||
typing-extensions==4.16.0
|
||||
# via
|
||||
|
||||
@@ -390,19 +390,3 @@ def get_session() -> scoped_session:
|
||||
:returns: The SQLAlchemy scoped session instance.
|
||||
"""
|
||||
raise NotImplementedError("Function will be replaced during initialization")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Dataset",
|
||||
"Database",
|
||||
"Chart",
|
||||
"Dashboard",
|
||||
"User",
|
||||
"Role",
|
||||
"Group",
|
||||
"Tag",
|
||||
"KeyValue",
|
||||
"Subject",
|
||||
"CoreModel",
|
||||
"get_session",
|
||||
]
|
||||
|
||||
@@ -183,10 +183,3 @@ def prompt(
|
||||
"MCP prompt decorator not initialized. "
|
||||
"This decorator should be replaced during Superset startup."
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"tool",
|
||||
"prompt",
|
||||
"ToolAnnotations",
|
||||
]
|
||||
|
||||
@@ -55,9 +55,3 @@ class SavedQueryDAO(BaseDAO[SavedQuery]):
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QueryDAO",
|
||||
"SavedQueryDAO",
|
||||
]
|
||||
|
||||
@@ -71,9 +71,3 @@ class SavedQuery(CoreModel):
|
||||
database_id: int | None
|
||||
description: str | None
|
||||
user_id: int | None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Query",
|
||||
"SavedQuery",
|
||||
]
|
||||
|
||||
@@ -46,6 +46,3 @@ def get_sqlglot_dialect(database: "Database") -> Dialects:
|
||||
:returns: The SQLGlot dialect enum corresponding to the database.
|
||||
"""
|
||||
raise NotImplementedError("Function will be replaced during initialization")
|
||||
|
||||
|
||||
__all__ = ["get_sqlglot_dialect"]
|
||||
|
||||
@@ -165,13 +165,3 @@ class AsyncQueryHandle:
|
||||
:returns: True if cancellation was successful
|
||||
"""
|
||||
raise NotImplementedError("Method will be replaced during initialization")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"QueryStatus",
|
||||
"QueryOptions",
|
||||
"QueryResult",
|
||||
"StatementResult",
|
||||
"AsyncQueryHandle",
|
||||
"CacheOptions",
|
||||
]
|
||||
|
||||
@@ -27,6 +27,3 @@ class RestApi(BaseApi):
|
||||
"""
|
||||
|
||||
allow_browser_login = True
|
||||
|
||||
|
||||
__all__ = ["RestApi"]
|
||||
|
||||
@@ -98,6 +98,3 @@ def api(
|
||||
"API decorator not initialized. "
|
||||
"This decorator should be replaced during Superset startup."
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["api"]
|
||||
|
||||
@@ -164,6 +164,3 @@ class AbstractSemanticViewDAO(BaseDAO[SemanticViewModel]):
|
||||
:return: SemanticViewModel instance or None
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
__all__ = ["AbstractSemanticLayerDAO", "AbstractSemanticViewDAO"]
|
||||
|
||||
@@ -97,6 +97,3 @@ def semantic_layer(
|
||||
"Semantic layer decorator not initialized. "
|
||||
"This decorator should be replaced during Superset startup."
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["semantic_layer"]
|
||||
|
||||
@@ -21,6 +21,7 @@ from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from superset_core.semantic_layers.view import SemanticView
|
||||
|
||||
ConfigT = TypeVar("ConfigT", bound=BaseModel)
|
||||
|
||||
@@ -80,6 +80,3 @@ class SemanticViewModel(CoreModel):
|
||||
semantic_layer_uuid: UUID
|
||||
created_on: datetime | None
|
||||
changed_on: datetime | None
|
||||
|
||||
|
||||
__all__ = ["SemanticLayerModel", "SemanticViewModel"]
|
||||
|
||||
@@ -71,6 +71,3 @@ class TaskDAO(BaseDAO[Task]):
|
||||
:returns: Task instance or None if not found or not active
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
__all__ = ["TaskDAO"]
|
||||
|
||||
@@ -144,9 +144,3 @@ def get_context() -> TaskContext:
|
||||
)
|
||||
"""
|
||||
raise NotImplementedError("Function will be replaced during initialization")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"task",
|
||||
"get_context",
|
||||
]
|
||||
|
||||
@@ -161,9 +161,3 @@ class TaskSubscriber(CoreModel):
|
||||
changed_on: datetime | None
|
||||
created_by_fk: int | None
|
||||
changed_by_fk: int | None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Task",
|
||||
"TaskSubscriber",
|
||||
]
|
||||
|
||||
@@ -226,12 +226,3 @@ class TaskContext(ABC):
|
||||
cleanup_partial_work()
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TaskStatus",
|
||||
"TaskScope",
|
||||
"TaskProperties",
|
||||
"TaskContext",
|
||||
"TaskOptions",
|
||||
]
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../superset-frontend/.npmrc
|
||||
@@ -0,0 +1 @@
|
||||
min-release-age=3
|
||||
+54
-103
@@ -2086,14 +2086,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
@@ -2107,18 +2099,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
|
||||
"integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -3215,9 +3195,9 @@
|
||||
"integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -3834,9 +3814,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
|
||||
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "1.4.5",
|
||||
@@ -4317,18 +4301,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
@@ -4854,9 +4826,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -5586,9 +5558,19 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@@ -7848,11 +7830,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
@@ -8015,9 +7992,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -9842,7 +9819,7 @@
|
||||
"debug": "4.4.0",
|
||||
"execa": "4.1.0",
|
||||
"istanbul-lib-coverage": "^3.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.3.1",
|
||||
"nyc": "15.1.0",
|
||||
"tinyglobby": "^0.2.14"
|
||||
},
|
||||
@@ -10121,7 +10098,7 @@
|
||||
"requires": {
|
||||
"@eslint/object-schema": "^3.0.5",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^10.2.4"
|
||||
"minimatch": ">=10"
|
||||
}
|
||||
},
|
||||
"@eslint/config-helpers": {
|
||||
@@ -10213,18 +10190,10 @@
|
||||
"camelcase": "^5.3.1",
|
||||
"find-up": "^4.1.0",
|
||||
"get-package-type": "^0.1.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"js-yaml": "4.3.1",
|
||||
"resolve-from": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
@@ -10234,14 +10203,6 @@
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
|
||||
"integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -11185,9 +11146,9 @@
|
||||
"integrity": "sha512-Fc8Ne62jJlKHiG/ajlonC4Sd66Pq68fFwK4ihJGNZpGqboc324SQk+lRvMzpPRuJOmfrJefdG8/7JdWX4bzJ2Q=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
@@ -11630,9 +11591,9 @@
|
||||
}
|
||||
},
|
||||
"d3-color": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
|
||||
"integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
|
||||
},
|
||||
"d3-format": {
|
||||
"version": "1.4.5",
|
||||
@@ -11644,7 +11605,7 @@
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
|
||||
"integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
|
||||
"requires": {
|
||||
"d3-color": "1"
|
||||
"d3-color": "3.1.0"
|
||||
}
|
||||
},
|
||||
"d3-scale": {
|
||||
@@ -11892,7 +11853,7 @@
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"minimatch": "^10.2.4",
|
||||
"minimatch": ">=10",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -11981,11 +11942,6 @@
|
||||
"eslint-visitor-keys": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
|
||||
},
|
||||
"esquery": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
@@ -12334,7 +12290,7 @@
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"minimatch": "<10",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
@@ -12345,9 +12301,9 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -12358,7 +12314,7 @@
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "1.1.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12852,9 +12808,9 @@
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1"
|
||||
}
|
||||
@@ -13435,7 +13391,7 @@
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
"brace-expansion": ">=5.0.9"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
@@ -14383,11 +14339,6 @@
|
||||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
|
||||
},
|
||||
"sshpk": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
@@ -14494,7 +14445,7 @@
|
||||
"requires": {
|
||||
"@istanbuljs/schema": "^0.1.2",
|
||||
"glob": "^7.1.4",
|
||||
"minimatch": "^3.0.4"
|
||||
"minimatch": "<10"
|
||||
},
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
@@ -14503,9 +14454,9 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -14516,7 +14467,7 @@
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "1.1.18"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,14 +30,20 @@
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.6",
|
||||
"@cypress/code-coverage": {
|
||||
"js-yaml": "4.1.1"
|
||||
"js-yaml": "4.3.1"
|
||||
},
|
||||
"@cypress/request": "^3.0.0",
|
||||
"cypress": {
|
||||
"form-data": "^2.3.4"
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"d3-color": "3.1.0"
|
||||
},
|
||||
"minimatch@<10": {
|
||||
"brace-expansion": "1.1.18"
|
||||
},
|
||||
"minimatch@>=10": {
|
||||
"brace-expansion": ">=5.0.8"
|
||||
"brace-expansion": ">=5.0.9"
|
||||
},
|
||||
"qs": "^6.14.2",
|
||||
"uuid": "^11.1.1"
|
||||
|
||||
@@ -18,6 +18,19 @@
|
||||
*/
|
||||
// timezone for unit tests
|
||||
process.env.TZ = 'America/New_York';
|
||||
|
||||
const reporters = ['default'];
|
||||
|
||||
// HTML reporter is not used on CI so skipping its generation for saving time
|
||||
if (!process.env.CI) {
|
||||
reporters.push([
|
||||
'./node_modules/jest-html-reporter',
|
||||
{
|
||||
pageTitle: 'Test Report',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// [/\\] matches both path separators so the suite also collects on
|
||||
// native Windows, where jest hands the regex backslash-separated paths.
|
||||
@@ -77,7 +90,7 @@ module.exports = {
|
||||
// @ant-design/colors and @ant-design/fast-color are allowed through because
|
||||
// @ant-design/icons >= 6.3 deep-imports the ESM build of @ant-design/colors
|
||||
// from its CJS output, so babel-jest must transform those files.
|
||||
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format|selection)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|@x0k/.*|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|hastscript|refractor|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|character-reference-invalid|is-alphanumerical|is-alphabetical|is-decimal|is-hexadecimal|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact|@x0k/json-schema-merge)',
|
||||
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format|selection)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|@x0k/.*|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|hastscript|refractor|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|character-reference-invalid|is-alphanumerical|is-alphabetical|is-decimal|is-hexadecimal|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact|@x0k/json-schema-merge|content-disposition)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
@@ -88,14 +101,6 @@ module.exports = {
|
||||
__DEV__: true,
|
||||
caches: true,
|
||||
},
|
||||
reporters: [
|
||||
'default',
|
||||
[
|
||||
'./node_modules/jest-html-reporter',
|
||||
{
|
||||
pageTitle: 'Test Report',
|
||||
},
|
||||
],
|
||||
],
|
||||
reporters: reporters,
|
||||
testTimeout: 20000,
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
// === Import plugin rules ===
|
||||
"import/named": "error",
|
||||
"import/export": "error",
|
||||
"import/no-named-as-default": "error",
|
||||
"import/no-named-as-default": "warn",
|
||||
"import/no-named-as-default-member": "error",
|
||||
"import/no-mutable-exports": "error",
|
||||
"import/no-amd": "error",
|
||||
|
||||
Generated
+350
-347
File diff suppressed because it is too large
Load Diff
@@ -161,7 +161,7 @@
|
||||
"antd": "^6.6.1",
|
||||
"chrono-node": "^2.10.1",
|
||||
"classnames": "^2.2.5",
|
||||
"content-disposition": "^2.0.1",
|
||||
"content-disposition": "^3.0.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"dayjs": "^1.11.23",
|
||||
"dom-to-image-more": "^3.10.2",
|
||||
@@ -177,7 +177,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^11.0.2",
|
||||
"immer": "^11.1.17",
|
||||
"immer": "^11.1.18",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -185,7 +185,7 @@
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.28.1",
|
||||
"mapbox-gl": "^3.29.0",
|
||||
"markdown-to-jsx": "^9.10.2",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
@@ -257,9 +257,9 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.9",
|
||||
"@storybook/addon-links": "10.5.9",
|
||||
"@storybook/react-webpack5": "10.5.9",
|
||||
"@storybook/addon-docs": "10.5.10",
|
||||
"@storybook/addon-links": "10.5.10",
|
||||
"@storybook/react-webpack5": "10.5.10",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.16.1",
|
||||
@@ -295,13 +295,13 @@
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"baseline-browser-mapping": "^2.11.15",
|
||||
"baseline-browser-mapping": "^2.11.18",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.5",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.4",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint": "^10.9.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
@@ -311,8 +311,8 @@
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.9",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.2",
|
||||
"eslint-plugin-storybook": "10.5.10",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -326,13 +326,13 @@
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdom": "^30.0.1",
|
||||
"lerna": "^10.0.0",
|
||||
"lerna": "^10.0.1",
|
||||
"lightningcss": "^1.33.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.6.1",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxfmt": "^0.64.0",
|
||||
"oxlint": "^1.78.0",
|
||||
"oxlint": "^1.79.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"process": "^0.11.10",
|
||||
@@ -343,7 +343,7 @@
|
||||
"source-map": "^0.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.9",
|
||||
"storybook": "10.5.10",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"swc-loader": "^0.2.7",
|
||||
@@ -354,7 +354,7 @@
|
||||
"unzipper": "^0.12.5",
|
||||
"wait-on": "^9.1.0",
|
||||
"webpack": "^5.109.2",
|
||||
"webpack-bundle-analyzer": "^5.3.1",
|
||||
"webpack-bundle-analyzer": "^5.3.2",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
"webpack-manifest-plugin": "^6.0.1",
|
||||
@@ -383,6 +383,9 @@
|
||||
"@great-expectations/jsonforms-antd-renderers": {
|
||||
"antd": "$antd"
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
"js-yaml": "^3.15.1"
|
||||
},
|
||||
"@jest/globals": "^30.4.0",
|
||||
"@jest/types": "^30.4.0",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
@@ -392,7 +395,10 @@
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"core-js": "^3.38.1",
|
||||
"dompurify": "^3.4.11",
|
||||
"cosmiconfig": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"dompurify": "^3.4.13",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint-plugin-import": {
|
||||
"eslint": "$eslint"
|
||||
@@ -408,16 +414,22 @@
|
||||
"jest-mock": "^30.4.0",
|
||||
"jest-runtime": "^30.4.0",
|
||||
"jest-util": "^30.4.0",
|
||||
"js-yaml-loader": {
|
||||
"js-yaml": "^3.15.1"
|
||||
},
|
||||
"jspdf": "^4.2.0",
|
||||
"lerna": {
|
||||
"js-yaml": "^4.3.0"
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"minimatch@>=10": {
|
||||
"brace-expansion": ">=5.0.8"
|
||||
},
|
||||
"nanoid@>=3 <4": "3.3.18",
|
||||
"nwsapi": "^2.2.13",
|
||||
"nwsapi": "^2.2.24",
|
||||
"puppeteer": "^22.4.1",
|
||||
"react-diff-viewer-continued": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"tar": "^7.5.16",
|
||||
"typescript-json-schema": "^0.68.0",
|
||||
"underscore": "^1.13.7",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"dependencies": {
|
||||
"chalk": "^6.0.0",
|
||||
"lodash-es": "^4.18.1",
|
||||
"yeoman-generator": "^8.2.2",
|
||||
"yeoman-generator": "^8.3.0",
|
||||
"yosay": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.23",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.14",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
@@ -89,7 +89,7 @@
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"reselect": "^5.2.0",
|
||||
"reselect": "^5.3.0",
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"xss": "^1.0.15"
|
||||
|
||||
@@ -25,6 +25,7 @@ export enum VizType {
|
||||
BoxPlot = 'box_plot',
|
||||
Bubble = 'bubble_v2',
|
||||
Bullet = 'bullet',
|
||||
Butterfly = 'butterfly',
|
||||
Calendar = 'cal_heatmap',
|
||||
Cartodiagram = 'cartodiagram',
|
||||
Chord = 'chord',
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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 { useState } from 'react';
|
||||
import { fireEvent, render, screen } from '@superset-ui/core/spec';
|
||||
import { Input } from '../Input';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
const drag = (
|
||||
target: Element,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
) => {
|
||||
fireEvent.mouseDown(target, { clientX: from[0], clientY: from[1] });
|
||||
fireEvent.mouseMove(document, { clientX: to[0], clientY: to[1] });
|
||||
fireEvent.mouseUp(document);
|
||||
};
|
||||
|
||||
const isDragged = () => !!document.querySelector('.react-draggable-dragged');
|
||||
|
||||
describe('Modal draggable', () => {
|
||||
test('dragging from the title bar moves the modal', () => {
|
||||
render(
|
||||
<Modal show onHide={() => {}} title="Edit Dataset" draggable name="test">
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
|
||||
drag(trigger, [100, 50], [150, 90]);
|
||||
|
||||
expect(isDragged()).toBe(true);
|
||||
});
|
||||
|
||||
test('dragging inside modal content does not move the modal', () => {
|
||||
render(
|
||||
<Modal show onHide={() => {}} title="Edit Dataset" draggable name="test">
|
||||
<Input data-test="field" defaultValue="first_view_event" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('field');
|
||||
drag(input, [200, 400], [260, 430]);
|
||||
|
||||
expect(isDragged()).toBe(false);
|
||||
});
|
||||
|
||||
test('dragging inside modal content does not move the modal, even after an unrelated re-render while the title was hovered', () => {
|
||||
// Regression test: the title bar used to gate dragging with a
|
||||
// hover-tracked boolean (mouseover/mouseout on `.draggable-trigger`)
|
||||
// instead of react-draggable's own `handle` prop. Because the title
|
||||
// element was defined as an inline component recreated on every
|
||||
// render, any unrelated state change while the cursor was over the
|
||||
// title (e.g. typing in any field) force-remounted it without a real
|
||||
// mouseout ever firing, leaving dragging permanently enabled -- so
|
||||
// selecting text anywhere in the modal dragged the whole modal
|
||||
// instead.
|
||||
function Harness() {
|
||||
const [tick, setTick] = useState(0);
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
onHide={() => {}}
|
||||
title="Edit Dataset"
|
||||
draggable
|
||||
name="test"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-test="rerender"
|
||||
onClick={() => setTick(tick + 1)}
|
||||
>
|
||||
rerender
|
||||
</button>
|
||||
<Input data-test="field" defaultValue="first_view_event" />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Harness />);
|
||||
|
||||
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
|
||||
fireEvent.mouseOver(trigger);
|
||||
fireEvent.click(screen.getByTestId('rerender'));
|
||||
|
||||
const input = screen.getByTestId('field');
|
||||
drag(input, [200, 400], [260, 430]);
|
||||
|
||||
expect(isDragged()).toBe(false);
|
||||
});
|
||||
|
||||
test('dragging is disabled entirely when draggable is not set', () => {
|
||||
render(
|
||||
<Modal show onHide={() => {}} title="Edit Dataset" name="test">
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
expect(document.querySelector('.draggable-trigger')).toBeNull();
|
||||
});
|
||||
|
||||
test('draggableConfig cannot re-enable dragging on a non-draggable modal', () => {
|
||||
render(
|
||||
<Modal
|
||||
show
|
||||
onHide={() => {}}
|
||||
title="Edit Dataset"
|
||||
name="test"
|
||||
draggableConfig={{ disabled: false }}
|
||||
>
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
expect(document.querySelector('.draggable-trigger')).toBeNull();
|
||||
});
|
||||
|
||||
test('draggableConfig can still opt a draggable modal out of dragging', () => {
|
||||
render(
|
||||
<Modal
|
||||
show
|
||||
onHide={() => {}}
|
||||
title="Edit Dataset"
|
||||
draggable
|
||||
name="test"
|
||||
draggableConfig={{ disabled: true }}
|
||||
>
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
|
||||
drag(trigger, [100, 50], [150, 90]);
|
||||
|
||||
expect(isDragged()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -269,7 +269,6 @@ const CustomModal = ({
|
||||
);
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const [bounds, setBounds] = useState<DraggableBounds>({});
|
||||
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleOnHide = () => {
|
||||
@@ -339,19 +338,7 @@ const CustomModal = ({
|
||||
}, [hideFooter, resizableConfig]);
|
||||
|
||||
const ModalTitle = () =>
|
||||
draggable ? (
|
||||
<div
|
||||
className="draggable-trigger"
|
||||
onMouseOver={() => dragDisabled && setDragDisabled(false)}
|
||||
onMouseOut={() => !dragDisabled && setDragDisabled(true)}
|
||||
onFocus={() => dragDisabled && setDragDisabled(false)}
|
||||
onBlur={() => !dragDisabled && setDragDisabled(true)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<>{title}</>
|
||||
);
|
||||
draggable ? <div className="draggable-trigger">{title}</div> : <>{title}</>;
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
@@ -378,13 +365,19 @@ const CustomModal = ({
|
||||
modalRender={modal =>
|
||||
resizable || draggable ? (
|
||||
<Draggable
|
||||
disabled={!draggable || dragDisabled}
|
||||
bounds={bounds ?? false}
|
||||
onStart={(event, uiData) => onDragStart(event, uiData)}
|
||||
{...draggableConfig}
|
||||
// `disabled` and `handle` are applied after the spread so callers
|
||||
// can't use `draggableConfig` to re-enable dragging on a
|
||||
// non-draggable modal or move the drag handle off the title bar.
|
||||
// A caller opting a draggable modal out via
|
||||
// `draggableConfig.disabled` is still honored.
|
||||
disabled={!draggable || !!draggableConfig?.disabled}
|
||||
handle={draggable ? '.draggable-trigger' : undefined}
|
||||
// Pass nodeRef so react-draggable does not fall back to
|
||||
// ReactDOM.findDOMNode (deprecated in React 18+ Strict Mode).
|
||||
nodeRef={draggableRef}
|
||||
{...draggableConfig}
|
||||
>
|
||||
{resizable ? (
|
||||
<Resizable className="resizable" {...getResizableConfig}>
|
||||
|
||||
@@ -65,6 +65,7 @@ export type AntdExposedProps = Pick<
|
||||
| 'onOpenChange'
|
||||
| 'optionRender'
|
||||
| 'placeholder'
|
||||
| 'prefix'
|
||||
| 'showArrow'
|
||||
| 'showSearch'
|
||||
| 'tokenSeparators'
|
||||
|
||||
+28
@@ -246,6 +246,34 @@ test('wraps component with proper container div', () => {
|
||||
expect(wrapper).toHaveAttribute('data-themed-ag-grid', 'true');
|
||||
});
|
||||
|
||||
test('applies non-transparent backgrounds to native menus, tooltips and overlays', () => {
|
||||
const customTheme = {
|
||||
...supersetTheme,
|
||||
colorBgElevated: '#f2f2f2',
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeProvider theme={customTheme}>
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const agGrid = screen.getByTestId('ag-grid-react');
|
||||
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
|
||||
|
||||
// ag-grid's own context/column menus, side bar, tooltips and overlays are
|
||||
// rendered against these params rather than `backgroundColor` (which is
|
||||
// intentionally 'transparent' so the surrounding app shows through the
|
||||
// grid body). Without explicit values they inherit transparency too,
|
||||
// making native menus/popups unreadable.
|
||||
expect(theme.chromeBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.menuBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.menuBorder).toBe(true);
|
||||
expect(theme.sideBarBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.tooltipBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.modalOverlayBackgroundColor).toBe('#f2f2f2');
|
||||
});
|
||||
|
||||
test('handles missing theme gracefully', () => {
|
||||
const incompleteTheme = {
|
||||
...supersetTheme,
|
||||
|
||||
+11
@@ -104,6 +104,17 @@ export const ThemedAgGridReact = forwardRef<
|
||||
foregroundColor: theme.colorText,
|
||||
browserColorScheme: isDarkMode ? 'dark' : 'light',
|
||||
|
||||
// Native menus, popups, side bar, tooltips and loading/no-rows overlays
|
||||
// are rendered against these params rather than `backgroundColor`
|
||||
// (which is intentionally transparent). Without explicit values they
|
||||
// inherit transparency too, making them unreadable.
|
||||
chromeBackgroundColor: theme.colorBgElevated,
|
||||
menuBackgroundColor: theme.colorBgElevated,
|
||||
menuBorder: true,
|
||||
sideBarBackgroundColor: theme.colorBgElevated,
|
||||
tooltipBackgroundColor: theme.colorBgElevated,
|
||||
modalOverlayBackgroundColor: theme.colorBgElevated,
|
||||
|
||||
// Header styling
|
||||
headerBackgroundColor: theme.colorFillTertiary,
|
||||
headerTextColor: theme.colorTextHeading,
|
||||
|
||||
+14
-4
@@ -38,11 +38,21 @@ function formatMemory(
|
||||
: ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB'];
|
||||
const base = binary ? 1024 : 1000;
|
||||
|
||||
const i = Math.min(
|
||||
suffixes.length - 1,
|
||||
Math.floor(Math.log(absValue) / Math.log(base)),
|
||||
let i = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
suffixes.length - 1,
|
||||
Math.floor(Math.log(absValue) / Math.log(base)),
|
||||
),
|
||||
);
|
||||
formatted = `${sign}${parseFloat((absValue / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`;
|
||||
let scaled = parseFloat((absValue / Math.pow(base, i)).toFixed(decimals));
|
||||
|
||||
if (scaled >= base && i < suffixes.length - 1) {
|
||||
i += 1;
|
||||
scaled = parseFloat((absValue / Math.pow(base, i)).toFixed(decimals));
|
||||
}
|
||||
|
||||
formatted = `${sign}${scaled}${suffixes[i]}`;
|
||||
}
|
||||
|
||||
if (transfer) {
|
||||
|
||||
@@ -47,6 +47,8 @@ export default function extractQueryFields(
|
||||
metric: 'metrics',
|
||||
metric_2: 'metrics',
|
||||
secondary_metric: 'metrics',
|
||||
left_metric: 'metrics',
|
||||
right_metric: 'metrics',
|
||||
x: 'metrics',
|
||||
y: 'metrics',
|
||||
size: 'metrics',
|
||||
|
||||
@@ -119,6 +119,20 @@ export function retrieveErrorMessage(
|
||||
return statusError || parseStringResponse(str);
|
||||
}
|
||||
|
||||
function getFirstValidationError(message: JsonObject): string | undefined {
|
||||
const [firstError] = Object.values(message);
|
||||
|
||||
if (typeof firstError === 'string') {
|
||||
return firstError;
|
||||
}
|
||||
|
||||
if (Array.isArray(firstError)) {
|
||||
return firstError.find((item): item is string => typeof item === 'string');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
|
||||
let error = { ...responseJson };
|
||||
// Backwards compatibility for old error renderers with the new error object
|
||||
@@ -126,13 +140,12 @@ export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
|
||||
error.error = error.description = error.errors[0].message;
|
||||
error.link = error.errors[0]?.extra?.link;
|
||||
}
|
||||
// Marshmallow field validation returns the error message in the format
|
||||
// of { message: { field1: [msg1, msg2], field2: [msg], } }
|
||||
// Marshmallow field validation returns arrays for string messages, but
|
||||
// serializes lazy translation messages as strings instead.
|
||||
if (!error.error && error.message) {
|
||||
if (typeof error.message === 'object') {
|
||||
error.error =
|
||||
Object.values(error.message as Record<string, string[]>)[0]?.[0] ||
|
||||
t('Invalid input');
|
||||
getFirstValidationError(error.message) || t('Invalid input');
|
||||
}
|
||||
if (typeof error.message === 'string') {
|
||||
if (checkForHtml(error.message)) {
|
||||
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
QueryFormData,
|
||||
} from '../query';
|
||||
import type { JsonResponse } from '../connection';
|
||||
import type { MenuItem } from '../components/Menu';
|
||||
|
||||
/**
|
||||
* A function which returns text (or marked-up text)
|
||||
@@ -164,6 +165,13 @@ export interface SliceHeaderExtension {
|
||||
dashboardId: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for extensions to the Slice Header more-options menu
|
||||
*/
|
||||
export interface SliceHeaderMenuExtension extends SliceHeaderExtension {
|
||||
sliceName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for extensions to Embed Modal
|
||||
*/
|
||||
@@ -262,6 +270,9 @@ export type Extensions = Partial<{
|
||||
'sqleditor.extension.form': ComponentType<SQLFormExtensionProps>;
|
||||
'sqleditor.extension.resultTable': ComponentType<SQLResultTableExtensionProps>;
|
||||
'dashboard.slice.header': ComponentType<SliceHeaderExtension>;
|
||||
'dashboard.slice.header.menu': (
|
||||
context: SliceHeaderMenuExtension,
|
||||
) => MenuItem[];
|
||||
'sqleditor.extension.customAutocomplete': (
|
||||
args: CustomAutoCompleteArgs,
|
||||
) => CustomAutocomplete[] | undefined;
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum FeatureFlag {
|
||||
AlertReportSlackV2 = 'ALERT_REPORT_SLACK_V2',
|
||||
AlertReportWebhook = 'ALERT_REPORT_WEBHOOK',
|
||||
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
|
||||
AlertReportsRetry = 'ALERT_REPORTS_RETRY',
|
||||
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
|
||||
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
|
||||
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
|
||||
|
||||
+25
@@ -60,6 +60,31 @@ test('formats float bytes in human readable format with default options', () =>
|
||||
expect(formatter(1200.666)).toBe('1.2kB');
|
||||
});
|
||||
|
||||
test('formats values below one byte without dropping the unit', () => {
|
||||
const formatter = createMemoryFormatter();
|
||||
expect(formatter(0.5)).toBe('0.5B');
|
||||
expect(formatter(0.004)).toBe('0B');
|
||||
expect(formatter(-0.25)).toBe('-0.25B');
|
||||
|
||||
const binaryFormatter = createMemoryFormatter({ binary: true });
|
||||
expect(binaryFormatter(0.5)).toBe('0.5B');
|
||||
});
|
||||
|
||||
test('rolls over to the next unit when rounding reaches the base', () => {
|
||||
const formatter = createMemoryFormatter();
|
||||
expect(formatter(999999)).toBe('1MB');
|
||||
expect(formatter(999995)).toBe('1MB');
|
||||
expect(formatter(999994)).toBe('999.99kB');
|
||||
expect(formatter(-999999)).toBe('-1MB');
|
||||
|
||||
const binaryFormatter = createMemoryFormatter({ binary: true });
|
||||
expect(binaryFormatter(1024 * 1024 - 1)).toBe('1MiB');
|
||||
|
||||
// the largest unit has nothing to roll over into
|
||||
const largest = createMemoryFormatter();
|
||||
expect(largest(Math.pow(1000, 11))).toBe('1000QB');
|
||||
});
|
||||
|
||||
test('formats bytes in human readable format with additional binary option', () => {
|
||||
const formatter = createMemoryFormatter({ binary: true });
|
||||
expect(formatter(0)).toBe('0B');
|
||||
|
||||
@@ -59,6 +59,16 @@ describe('extractQueryFields', () => {
|
||||
).toEqual(['metric_1', 'metric_2', 'my_custom_metric']);
|
||||
});
|
||||
|
||||
test('should extract butterfly chart metrics', () => {
|
||||
expect(
|
||||
extractQueryFields({
|
||||
groupby: ['category'],
|
||||
left_metric: 'left_sum',
|
||||
right_metric: 'right_sum',
|
||||
}).metrics,
|
||||
).toEqual(['left_sum', 'right_sum']);
|
||||
});
|
||||
|
||||
test('should extract columns', () => {
|
||||
expect(extractQueryFields({ columns: 'col_1' })).toEqual({
|
||||
columns: ['col_1'],
|
||||
|
||||
@@ -244,6 +244,24 @@ test('parseErrorJson with message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('parseErrorJson preserves string-valued validation messages', () => {
|
||||
const calculatedColumnError =
|
||||
'Custom SQL fields cannot be parsed as a single SQL statement.';
|
||||
|
||||
expect(
|
||||
parseErrorJson({
|
||||
message: {
|
||||
'columns.0.expression': calculatedColumnError,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
message: {
|
||||
'columns.0.expression': calculatedColumnError,
|
||||
},
|
||||
error: calculatedColumnError,
|
||||
});
|
||||
});
|
||||
|
||||
test('parseErrorJson with HTML message', () => {
|
||||
expect(
|
||||
parseErrorJson({
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { allEventHandlers, type Event } from '../utils/eventHandlers';
|
||||
import Echart from '../components/Echart';
|
||||
import { EventHandlers } from '../types';
|
||||
import { ButterflyTransformedProps } from './types';
|
||||
|
||||
type ButterflyChartEvent = {
|
||||
name?: string;
|
||||
data?: { name?: string };
|
||||
event?: Event['event'];
|
||||
};
|
||||
|
||||
function getCategoryKey(params: ButterflyChartEvent): string {
|
||||
return params.data?.name ?? params.name ?? '';
|
||||
}
|
||||
|
||||
export default function Butterfly(props: ButterflyTransformedProps) {
|
||||
const {
|
||||
height,
|
||||
width,
|
||||
echartOptions,
|
||||
selectedValues,
|
||||
refs,
|
||||
onLegendStateChanged,
|
||||
formData,
|
||||
} = props;
|
||||
|
||||
const { click, contextmenu } = allEventHandlers(props);
|
||||
|
||||
const eventHandlers: EventHandlers = {
|
||||
click: (params: ButterflyChartEvent) => {
|
||||
click({ name: getCategoryKey(params) });
|
||||
},
|
||||
contextmenu: (params: ButterflyChartEvent) => {
|
||||
contextmenu({
|
||||
...params,
|
||||
name: getCategoryKey(params),
|
||||
});
|
||||
},
|
||||
legendselectchanged: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
},
|
||||
legendselectall: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
},
|
||||
legendinverseselect: payload => {
|
||||
onLegendStateChanged?.(payload.selected);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Echart
|
||||
refs={refs}
|
||||
height={height}
|
||||
width={width}
|
||||
echartOptions={echartOptions}
|
||||
eventHandlers={eventHandlers}
|
||||
selectedValues={selectedValues}
|
||||
vizType={formData.vizType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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 {
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
QueryFormData,
|
||||
QueryFormOrderBy,
|
||||
} from '@superset-ui/core';
|
||||
import { buildSortMetricOrderby } from '@superset-ui/chart-controls';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const columns = ensureIsArray(formData.groupby);
|
||||
const baseMetrics = [
|
||||
...ensureIsArray(formData.left_metric),
|
||||
...ensureIsArray(formData.right_metric),
|
||||
];
|
||||
|
||||
const { orderby, metrics } = buildSortMetricOrderby({
|
||||
metrics: baseMetrics,
|
||||
timeseriesLimitMetric: ensureIsArray(formData.orderby)[0],
|
||||
order_desc: formData.order_desc,
|
||||
});
|
||||
const resolvedOrderby: QueryFormOrderBy[] | undefined = orderby.length
|
||||
? orderby
|
||||
: columns.length
|
||||
? [[columns[0], true]]
|
||||
: undefined;
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
columns,
|
||||
metrics,
|
||||
orderby: resolvedOrderby,
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 {
|
||||
DEFAULT_LEGEND_FORM_DATA,
|
||||
DEFAULT_TITLE_FORM_DATA,
|
||||
} from '../constants';
|
||||
import { defaultXAxis } from '../defaults';
|
||||
|
||||
export const DEFAULT_FORM_DATA = {
|
||||
...DEFAULT_LEGEND_FORM_DATA,
|
||||
...DEFAULT_TITLE_FORM_DATA,
|
||||
xAxisLabelRotation: defaultXAxis.xAxisLabelRotation,
|
||||
};
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 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 { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
ControlSubSectionHeader,
|
||||
formatSelectOptions,
|
||||
getStandardizedControls,
|
||||
sections,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
legendSection,
|
||||
showValueControl,
|
||||
xAxisLabelRotation,
|
||||
} from '../controls';
|
||||
import { DEFAULT_FORM_DATA } from './constants';
|
||||
|
||||
const { xAxisTitleMargin, yAxisTitleMargin } = DEFAULT_FORM_DATA;
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['groupby'],
|
||||
[
|
||||
{
|
||||
name: 'left_metric',
|
||||
config: {
|
||||
...sharedControls.metric,
|
||||
label: t('Left metric'),
|
||||
description: t(
|
||||
'Metric displayed on the left side of the butterfly chart',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'right_metric',
|
||||
config: {
|
||||
...sharedControls.metric,
|
||||
label: t('Right metric'),
|
||||
description: t(
|
||||
'Metric displayed on the right side of the butterfly chart',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
['adhoc_filters'],
|
||||
['row_limit'],
|
||||
['orderby'],
|
||||
[
|
||||
{
|
||||
name: 'order_desc',
|
||||
config: {
|
||||
...sharedControls.order_desc,
|
||||
visibility: ({ controls }) => Boolean(controls.orderby.value),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [[showValueControl], ...legendSection],
|
||||
},
|
||||
{
|
||||
label: t('Series settings'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
<ControlSubSectionHeader>
|
||||
{t('Left series setting')}
|
||||
</ControlSubSectionHeader>,
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'left_color',
|
||||
config: {
|
||||
label: t('Left color'),
|
||||
type: 'ColorPickerControl',
|
||||
default: { r: 84, g: 112, b: 198, a: 1 },
|
||||
renderTrigger: true,
|
||||
description: t('Color for bars on the left side of the chart'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'left_label',
|
||||
config: {
|
||||
label: t('Left label'),
|
||||
type: 'TextControl',
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Customize the label for the left series in tooltips and legend',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
<ControlSubSectionHeader>
|
||||
{t('Right series setting')}
|
||||
</ControlSubSectionHeader>,
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'right_color',
|
||||
config: {
|
||||
label: t('Right color'),
|
||||
type: 'ColorPickerControl',
|
||||
default: { r: 145, g: 204, b: 117, a: 1 },
|
||||
renderTrigger: true,
|
||||
description: t('Color for bars on the right side of the chart'),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'right_label',
|
||||
config: {
|
||||
label: t('Right label'),
|
||||
type: 'TextControl',
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Customize the label for the right series in tooltips and legend',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('X Axis'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'x_axis_label',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('X Axis Label'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'x_axis_title_margin',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
clearable: true,
|
||||
label: t('X Axis title margin'),
|
||||
renderTrigger: true,
|
||||
default: xAxisTitleMargin,
|
||||
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||
},
|
||||
},
|
||||
],
|
||||
['x_axis_format'],
|
||||
['currency_format'],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Y Axis'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'y_axis_label',
|
||||
config: {
|
||||
type: 'TextControl',
|
||||
label: t('Y Axis Label'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'y_axis_title_margin',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
clearable: true,
|
||||
label: t('Y Axis title margin'),
|
||||
renderTrigger: true,
|
||||
default: yAxisTitleMargin,
|
||||
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: xAxisLabelRotation.name,
|
||||
config: {
|
||||
...xAxisLabelRotation.config,
|
||||
label: t('Rotate category label'),
|
||||
description: t(
|
||||
'Input field supports custom rotation. e.g. 30 for 30°',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
groupby: {
|
||||
label: t('Categories'),
|
||||
description: t('Dimension used for category labels on the vertical axis'),
|
||||
multi: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
groupby: ensureIsArray(getStandardizedControls().shiftColumn()),
|
||||
left_metric: getStandardizedControls().shiftMetric(),
|
||||
right_metric: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 { t } from '@apache-superset/core/translation';
|
||||
import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import { EchartsButterflyChartProps, EchartsButterflyFormData } from './types';
|
||||
import example from './images/example.png';
|
||||
import exampleDark from './images/example-dark.png';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
export default class EchartsButterflyChartPlugin extends ChartPlugin<
|
||||
EchartsButterflyFormData,
|
||||
EchartsButterflyChartProps
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('./Butterfly'),
|
||||
metadata: new ChartMetadata({
|
||||
behaviors: [
|
||||
Behavior.InteractiveChart,
|
||||
Behavior.DrillToDetail,
|
||||
Behavior.DrillBy,
|
||||
],
|
||||
credits: ['https://echarts.apache.org'],
|
||||
category: t('Comparison'),
|
||||
description: t(
|
||||
'A butterfly chart compares two metrics across categories using horizontal bars ' +
|
||||
'that extend left and right from a central axis.',
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Butterfly Chart'),
|
||||
tags: [
|
||||
t('Categorical'),
|
||||
t('Comparison'),
|
||||
t('ECharts'),
|
||||
t('Multi-Variables'),
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
}),
|
||||
transformProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* 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 {
|
||||
CurrencyFormatter,
|
||||
ensureIsArray,
|
||||
getColumnLabel,
|
||||
getMetricLabel,
|
||||
getNumberFormatter,
|
||||
NumberFormatter,
|
||||
rgbToHex,
|
||||
tooltipHtml,
|
||||
} from '@superset-ui/core';
|
||||
import type { ComposeOption } from 'echarts/core';
|
||||
import type { BarSeriesOption } from 'echarts/charts';
|
||||
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import { EchartsButterflyChartProps, ButterflyTransformedProps } from './types';
|
||||
import { DEFAULT_FORM_DATA } from './constants';
|
||||
import { defaultGrid } from '../defaults';
|
||||
import { getDefaultTooltip } from '../utils/tooltip';
|
||||
import { Refs } from '../types';
|
||||
import { OpacityEnum } from '../constants';
|
||||
import {
|
||||
getChartPadding,
|
||||
getLegendProps,
|
||||
getColtypesMapping,
|
||||
extractGroupbyLabel,
|
||||
} from '../utils/series';
|
||||
import { resolveLegendLayout } from '../utils/legendLayout';
|
||||
import { convertInteger } from '../utils/convertInteger';
|
||||
|
||||
type EChartsOption = ComposeOption<BarSeriesOption>;
|
||||
|
||||
const LABEL_LEFT = { position: 'left' as const };
|
||||
const LABEL_RIGHT = { position: 'right' as const };
|
||||
|
||||
function formatTooltip(
|
||||
params: CallbackDataParams[],
|
||||
formatter: NumberFormatter | CurrencyFormatter,
|
||||
categoryLabels: string[],
|
||||
categoryByKey: Map<string, string>,
|
||||
) {
|
||||
const axisParams = params.filter(
|
||||
param => param.seriesName && typeof param.value === 'number',
|
||||
);
|
||||
if (!axisParams.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { dataIndex, name } = axisParams[0];
|
||||
const title =
|
||||
(typeof dataIndex === 'number'
|
||||
? categoryLabels.at(dataIndex)
|
||||
: undefined) ??
|
||||
(typeof name === 'string' ? categoryByKey.get(name) : undefined) ??
|
||||
name;
|
||||
const rows = axisParams.map(param => [
|
||||
param.seriesName!,
|
||||
formatter(Math.abs(param.value as number)),
|
||||
]);
|
||||
|
||||
return tooltipHtml(rows, title);
|
||||
}
|
||||
|
||||
export default function transformProps(
|
||||
chartProps: EchartsButterflyChartProps,
|
||||
): ButterflyTransformedProps {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
formData,
|
||||
legendState,
|
||||
queriesData,
|
||||
hooks,
|
||||
theme,
|
||||
inContextMenu,
|
||||
filterState,
|
||||
emitCrossFilters,
|
||||
} = chartProps;
|
||||
const refs: Refs = {};
|
||||
const { data = [] } = queriesData[0];
|
||||
const { setDataMask = () => {}, onContextMenu, onLegendStateChanged } = hooks;
|
||||
|
||||
const {
|
||||
currencyFormat,
|
||||
groupby,
|
||||
leftMetric,
|
||||
rightMetric,
|
||||
leftColor = { r: 84, g: 112, b: 198, a: 1 },
|
||||
rightColor = { r: 145, g: 204, b: 117, a: 1 },
|
||||
leftLabel,
|
||||
rightLabel,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
xAxisFormat,
|
||||
xAxisTitleMargin,
|
||||
yAxisTitleMargin,
|
||||
showLegend,
|
||||
legendMargin,
|
||||
legendOrientation,
|
||||
legendType,
|
||||
legendSort,
|
||||
showValue,
|
||||
xAxisLabelRotation,
|
||||
}: EchartsButterflyChartProps['formData'] = {
|
||||
...DEFAULT_FORM_DATA,
|
||||
...formData,
|
||||
};
|
||||
|
||||
const leftMetricLabel = leftMetric ? getMetricLabel(leftMetric) : '';
|
||||
const rightMetricLabel = rightMetric ? getMetricLabel(rightMetric) : '';
|
||||
const leftSeriesName = leftLabel || leftMetricLabel;
|
||||
const rightSeriesName = rightLabel || rightMetricLabel;
|
||||
|
||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||
const groupbyColumns = ensureIsArray(groupby);
|
||||
const groupbyLabels = groupbyColumns.map(getColumnLabel);
|
||||
|
||||
const defaultFormatter = currencyFormat?.symbol
|
||||
? new CurrencyFormatter({ d3Format: xAxisFormat, currency: currencyFormat })
|
||||
: getNumberFormatter(xAxisFormat);
|
||||
|
||||
const categories = data.map(datum =>
|
||||
extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }),
|
||||
);
|
||||
const categoryKeys = data.map((datum, index) => {
|
||||
const label = categories.at(index) ?? '';
|
||||
return `${label}__${JSON.stringify(
|
||||
groupbyLabels.map(col =>
|
||||
Object.hasOwn(datum, col) ? datum[col] : undefined,
|
||||
),
|
||||
)}`;
|
||||
});
|
||||
const categoryByKey = new Map(
|
||||
categoryKeys.flatMap((key, index) => {
|
||||
const label = categories.at(index);
|
||||
return label === undefined ? [] : [[key, label] as const];
|
||||
}),
|
||||
);
|
||||
|
||||
const labelMap = data.reduce<Record<string, string[]>>(
|
||||
(acc, datum, index) => {
|
||||
const uniqueKey = categoryKeys.at(index);
|
||||
if (uniqueKey === undefined) {
|
||||
return acc;
|
||||
}
|
||||
acc[uniqueKey] = groupbyLabels.map(col =>
|
||||
Object.hasOwn(datum, col) ? (datum[col] as string) : '',
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const selectedValues = (filterState.selectedValues || []).reduce(
|
||||
(acc: Record<number, string>, value: string) => {
|
||||
const index = categoryKeys.indexOf(value);
|
||||
return index >= 0 ? { ...acc, [index]: value } : acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const getOpacity = (categoryKey: string) =>
|
||||
filterState.selectedValues?.length &&
|
||||
!filterState.selectedValues.includes(categoryKey)
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent;
|
||||
|
||||
const leftData = data.map((row, i) => ({
|
||||
name: categoryKeys[i],
|
||||
value: -Math.abs(Number(row[leftMetricLabel] ?? 0)),
|
||||
label: LABEL_LEFT,
|
||||
itemStyle: { opacity: getOpacity(categoryKeys[i]) },
|
||||
}));
|
||||
const rightData = data.map((row, i) => ({
|
||||
name: categoryKeys[i],
|
||||
value: Math.abs(Number(row[rightMetricLabel] ?? 0)),
|
||||
label: LABEL_RIGHT,
|
||||
itemStyle: { opacity: getOpacity(categoryKeys[i]) },
|
||||
}));
|
||||
|
||||
const labelFormatter = (params: CallbackDataParams) => {
|
||||
const value = Math.abs(params.value as number);
|
||||
|
||||
if (value === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return defaultFormatter(value);
|
||||
};
|
||||
|
||||
const series: BarSeriesOption[] = [
|
||||
{
|
||||
name: leftSeriesName,
|
||||
type: 'bar',
|
||||
stack: 'Total',
|
||||
label: {
|
||||
show: showValue,
|
||||
formatter: labelFormatter,
|
||||
color: theme.colorText,
|
||||
},
|
||||
itemStyle: {
|
||||
color: rgbToHex(leftColor.r, leftColor.g, leftColor.b),
|
||||
},
|
||||
data: leftData,
|
||||
},
|
||||
{
|
||||
name: rightSeriesName,
|
||||
type: 'bar',
|
||||
stack: 'Total',
|
||||
label: {
|
||||
show: showValue,
|
||||
formatter: labelFormatter,
|
||||
color: theme.colorText,
|
||||
},
|
||||
itemStyle: {
|
||||
color: rgbToHex(rightColor.r, rightColor.g, rightColor.b),
|
||||
},
|
||||
data: rightData,
|
||||
},
|
||||
];
|
||||
|
||||
const legendData = [leftSeriesName, rightSeriesName].sort((a, b) => {
|
||||
if (!legendSort) {
|
||||
return 0;
|
||||
}
|
||||
return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a);
|
||||
});
|
||||
|
||||
const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({
|
||||
chartHeight: height,
|
||||
chartWidth: width,
|
||||
legendItems: legendData,
|
||||
legendMargin,
|
||||
orientation: legendOrientation,
|
||||
show: showLegend,
|
||||
theme,
|
||||
type: legendType,
|
||||
});
|
||||
|
||||
const legendPadding = getChartPadding(
|
||||
showLegend,
|
||||
legendOrientation,
|
||||
effectiveLegendMargin,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const echartOptions: EChartsOption = {
|
||||
grid: {
|
||||
...defaultGrid,
|
||||
top:
|
||||
theme.sizeUnit * 5 +
|
||||
legendPadding.top +
|
||||
convertInteger(xAxisTitleMargin),
|
||||
bottom: theme.sizeUnit * 5 + legendPadding.bottom,
|
||||
left:
|
||||
theme.sizeUnit * 5 +
|
||||
legendPadding.left +
|
||||
convertInteger(yAxisTitleMargin),
|
||||
right: theme.sizeUnit * 5 + legendPadding.right,
|
||||
},
|
||||
legend: {
|
||||
...getLegendProps(
|
||||
effectiveLegendType,
|
||||
legendOrientation,
|
||||
showLegend,
|
||||
theme,
|
||||
false,
|
||||
legendState,
|
||||
),
|
||||
data: legendData,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
position: 'top',
|
||||
name: xAxisLabel,
|
||||
nameLocation: 'middle',
|
||||
nameGap: convertInteger(xAxisTitleMargin),
|
||||
nameTextStyle: {
|
||||
padding: [theme.sizeUnit * 4, 0, 0, 0],
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
formatter: (value: number) => defaultFormatter(Math.abs(value)),
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
name: yAxisLabel,
|
||||
nameLocation: 'middle',
|
||||
nameGap: convertInteger(yAxisTitleMargin),
|
||||
nameTextStyle: {
|
||||
padding: [0, theme.sizeUnit * 4, 0, 0],
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
axisLabel: {
|
||||
rotate: xAxisLabelRotation,
|
||||
},
|
||||
data: categories,
|
||||
},
|
||||
tooltip: {
|
||||
...getDefaultTooltip(refs),
|
||||
appendToBody: true,
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
show: !inContextMenu,
|
||||
formatter: (params: CallbackDataParams | CallbackDataParams[]) =>
|
||||
formatTooltip(
|
||||
ensureIsArray(params) as CallbackDataParams[],
|
||||
defaultFormatter,
|
||||
categories,
|
||||
categoryByKey,
|
||||
),
|
||||
},
|
||||
series,
|
||||
};
|
||||
|
||||
return {
|
||||
refs,
|
||||
formData,
|
||||
width,
|
||||
height,
|
||||
echartOptions,
|
||||
setDataMask,
|
||||
onContextMenu,
|
||||
onLegendStateChanged,
|
||||
groupby: groupbyColumns,
|
||||
labelMap,
|
||||
selectedValues,
|
||||
emitCrossFilters,
|
||||
coltypeMapping,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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 {
|
||||
ChartDataResponseResult,
|
||||
ChartProps,
|
||||
QueryFormColumn,
|
||||
QueryFormData,
|
||||
QueryFormMetric,
|
||||
RgbaColor,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
BaseTransformedProps,
|
||||
LegendFormData,
|
||||
TitleFormData,
|
||||
CrossFilterTransformedProps,
|
||||
} from '../types';
|
||||
|
||||
export type EchartsButterflyFormData = QueryFormData &
|
||||
LegendFormData &
|
||||
TitleFormData & {
|
||||
groupby: QueryFormColumn[];
|
||||
leftMetric: QueryFormMetric;
|
||||
rightMetric: QueryFormMetric;
|
||||
leftColor: RgbaColor;
|
||||
rightColor: RgbaColor;
|
||||
leftLabel?: string;
|
||||
rightLabel?: string;
|
||||
xAxisLabel: string;
|
||||
yAxisLabel: string;
|
||||
xAxisFormat: string;
|
||||
showValue: boolean;
|
||||
xAxisLabelRotation: number;
|
||||
};
|
||||
|
||||
export interface EchartsButterflyChartProps extends ChartProps {
|
||||
formData: EchartsButterflyFormData;
|
||||
queriesData: ChartDataResponseResult[];
|
||||
}
|
||||
|
||||
export type ButterflyTransformedProps =
|
||||
BaseTransformedProps<EchartsButterflyFormData> & CrossFilterTransformedProps;
|
||||
@@ -38,6 +38,8 @@ import { EchartsTimeseriesSeriesType } from '../Timeseries/types';
|
||||
import {
|
||||
legendSection,
|
||||
minorTicks,
|
||||
axisTicks,
|
||||
gridlines,
|
||||
richTooltipSection,
|
||||
truncateXAxis,
|
||||
xAxisBounds,
|
||||
@@ -201,6 +203,31 @@ function createCustomizeSection(
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: `label_position${controlSuffix}`,
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: false,
|
||||
label: t('Label Position'),
|
||||
choices: [
|
||||
['auto', t('Auto')],
|
||||
['top', t('Top')],
|
||||
['inside', t('Inside')],
|
||||
['bottom', t('Bottom')],
|
||||
['left', t('Left')],
|
||||
['right', t('Right')],
|
||||
],
|
||||
default: 'auto',
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Position of the data label relative to the data point',
|
||||
),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.[`show_value${controlSuffix}`]?.value),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: `only_total${controlSuffix}`,
|
||||
@@ -366,6 +393,8 @@ const config: ControlPanelConfig = {
|
||||
...createCustomizeSection(t('Query B'), 'B'),
|
||||
['zoomable'],
|
||||
[minorTicks],
|
||||
[axisTicks],
|
||||
[gridlines],
|
||||
...legendSection,
|
||||
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
|
||||
['x_axis_time_format'],
|
||||
|
||||
@@ -184,11 +184,15 @@ export default function transformProps(
|
||||
opacityB,
|
||||
minorSplitLine,
|
||||
minorTicks,
|
||||
gridlines,
|
||||
axisTicks,
|
||||
seriesType,
|
||||
seriesTypeB,
|
||||
showLegend,
|
||||
showValue,
|
||||
showValueB,
|
||||
labelPosition,
|
||||
labelPositionB,
|
||||
onlyTotal,
|
||||
onlyTotalB,
|
||||
stack,
|
||||
@@ -533,6 +537,7 @@ export default function transformProps(
|
||||
thresholdValues,
|
||||
timeShiftColor,
|
||||
theme,
|
||||
labelPosition,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -621,6 +626,7 @@ export default function transformProps(
|
||||
thresholdValues: thresholdValuesB,
|
||||
timeShiftColor,
|
||||
theme,
|
||||
labelPosition: labelPositionB,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -784,6 +790,8 @@ export default function transformProps(
|
||||
}),
|
||||
},
|
||||
minorTick: { show: minorTicks },
|
||||
axisTick: { show: axisTicks ? 'auto' : false },
|
||||
...(gridlines ? {} : { splitLine: { show: false } }),
|
||||
minInterval:
|
||||
xAxisType === AxisType.Time && resolvedTimeGrain && !forceMaxInterval
|
||||
? (TIMEGRAIN_TO_TIMESTAMP[
|
||||
@@ -814,6 +822,8 @@ export default function transformProps(
|
||||
min: yAxisMin,
|
||||
max: yAxisMax,
|
||||
minorTick: { show: minorTicks },
|
||||
axisTick: { show: axisTicks ? 'auto' : false },
|
||||
splitLine: { show: gridlines },
|
||||
minorSplitLine: { show: minorSplitLine },
|
||||
axisLabel: {
|
||||
formatter: getYAxisFormatter(
|
||||
@@ -836,6 +846,7 @@ export default function transformProps(
|
||||
min: minSecondary,
|
||||
max: maxSecondary,
|
||||
minorTick: { show: minorTicks },
|
||||
axisTick: { show: axisTicks ? 'auto' : false },
|
||||
splitLine: { show: false },
|
||||
minorSplitLine: { show: minorSplitLine },
|
||||
axisLabel: {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
ContextMenuTransformedProps,
|
||||
CrossFilterTransformedProps,
|
||||
EchartsTimeseriesSeriesType,
|
||||
LabelPositionEnum,
|
||||
LegendFormData,
|
||||
StackType,
|
||||
TitleFormData,
|
||||
@@ -47,6 +48,8 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & {
|
||||
// shared properties
|
||||
minorSplitLine: boolean;
|
||||
minorTicks: boolean;
|
||||
gridlines: boolean;
|
||||
axisTicks: boolean;
|
||||
logAxis: boolean;
|
||||
logAxisSecondary: boolean;
|
||||
yAxisFormat?: string;
|
||||
@@ -86,6 +89,18 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & {
|
||||
seriesTypeB: EchartsTimeseriesSeriesType;
|
||||
showValue: boolean;
|
||||
showValueB: boolean;
|
||||
/**
|
||||
* Where the data label sits relative to its data point on query A, applied
|
||||
* when `showValue` is on.
|
||||
*
|
||||
* `'auto'` keeps the orientation-aware default the chart used before this
|
||||
* control existed: `Right` for a horizontal chart, `Top` otherwise. It is
|
||||
* also the value every chart saved before then resolves to, so the default
|
||||
* must stay `'auto'` for those to keep rendering as they did.
|
||||
*/
|
||||
labelPosition?: LabelPositionEnum | 'auto';
|
||||
/** The same, for query B. Resolved independently of {@link labelPosition}. */
|
||||
labelPositionB?: LabelPositionEnum | 'auto';
|
||||
stack: StackType;
|
||||
stackB: StackType;
|
||||
yAxisIndex?: number;
|
||||
@@ -100,6 +115,8 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = {
|
||||
...DEFAULT_LEGEND_FORM_DATA,
|
||||
annotationLayers: [],
|
||||
minorSplitLine: TIMESERIES_DEFAULTS.minorSplitLine,
|
||||
gridlines: TIMESERIES_DEFAULTS.gridlines,
|
||||
axisTicks: TIMESERIES_DEFAULTS.axisTicks,
|
||||
truncateYAxis: TIMESERIES_DEFAULTS.truncateYAxis,
|
||||
truncateYAxisSecondary: TIMESERIES_DEFAULTS.truncateYAxis,
|
||||
logAxis: TIMESERIES_DEFAULTS.logAxis,
|
||||
@@ -130,6 +147,8 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = {
|
||||
seriesTypeB: TIMESERIES_DEFAULTS.seriesType,
|
||||
showValue: TIMESERIES_DEFAULTS.showValue,
|
||||
showValueB: TIMESERIES_DEFAULTS.showValue,
|
||||
labelPosition: 'auto',
|
||||
labelPositionB: 'auto',
|
||||
stack: TIMESERIES_DEFAULTS.stack,
|
||||
stackB: TIMESERIES_DEFAULTS.stack,
|
||||
yAxisIndex: 0,
|
||||
|
||||
@@ -44,6 +44,8 @@ import {
|
||||
truncateXAxis,
|
||||
xAxisBounds,
|
||||
minorTicks,
|
||||
axisTicks,
|
||||
gridlines,
|
||||
forceMaxInterval,
|
||||
} from '../../controls';
|
||||
import { AreaChartStackControlOptions } from '../../constants';
|
||||
@@ -174,6 +176,8 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
],
|
||||
[minorTicks],
|
||||
[axisTicks],
|
||||
[gridlines],
|
||||
['zoomable'],
|
||||
...legendSection,
|
||||
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
|
||||
|
||||
+2
@@ -133,6 +133,8 @@ const defaultFormData: EchartsTimeseriesFormData & {
|
||||
metrics: [],
|
||||
minorSplitLine: false,
|
||||
minorTicks: false,
|
||||
gridlines: true,
|
||||
axisTicks: true,
|
||||
opacity: 1,
|
||||
orderDesc: false,
|
||||
rowLimit: 0,
|
||||
|
||||
+4
@@ -40,6 +40,8 @@ import {
|
||||
import {
|
||||
legendSection,
|
||||
minorTicks,
|
||||
axisTicks,
|
||||
gridlines,
|
||||
richTooltipSection,
|
||||
seriesOrderSection,
|
||||
showValueSectionWithoutStream,
|
||||
@@ -388,6 +390,8 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
],
|
||||
[minorTicks],
|
||||
[axisTicks],
|
||||
[gridlines],
|
||||
['zoomable'],
|
||||
...legendSection,
|
||||
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
|
||||
|
||||
+4
@@ -37,6 +37,8 @@ import {
|
||||
import {
|
||||
legendSection,
|
||||
minorTicks,
|
||||
axisTicks,
|
||||
gridlines,
|
||||
richTooltipSection,
|
||||
seriesOrderSection,
|
||||
showValueSection,
|
||||
@@ -156,6 +158,8 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
['zoomable'],
|
||||
[minorTicks],
|
||||
[axisTicks],
|
||||
[gridlines],
|
||||
...legendSection,
|
||||
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
|
||||
[
|
||||
|
||||
+4
@@ -42,6 +42,8 @@ import {
|
||||
import {
|
||||
legendSection,
|
||||
minorTicks,
|
||||
axisTicks,
|
||||
gridlines,
|
||||
richTooltipSection,
|
||||
seriesOrderSection,
|
||||
showValueSection,
|
||||
@@ -480,6 +482,8 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
['zoomable'],
|
||||
[minorTicks],
|
||||
[axisTicks],
|
||||
[gridlines],
|
||||
...legendSection,
|
||||
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
|
||||
...createAxisControl('x'),
|
||||
|
||||
+4
@@ -37,6 +37,8 @@ import {
|
||||
import {
|
||||
legendSection,
|
||||
minorTicks,
|
||||
axisTicks,
|
||||
gridlines,
|
||||
richTooltipSection,
|
||||
seriesOrderSection,
|
||||
showValueSectionWithoutStack,
|
||||
@@ -105,6 +107,8 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
['zoomable'],
|
||||
[minorTicks],
|
||||
[axisTicks],
|
||||
[gridlines],
|
||||
...legendSection,
|
||||
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
|
||||
[
|
||||
|
||||
@@ -35,6 +35,8 @@ import { DEFAULT_FORM_DATA, TIME_SERIES_DESCRIPTION_TEXT } from '../constants';
|
||||
import {
|
||||
legendSection,
|
||||
minorTicks,
|
||||
axisTicks,
|
||||
gridlines,
|
||||
richTooltipSection,
|
||||
seriesOrderSection,
|
||||
showValueSection,
|
||||
@@ -157,6 +159,8 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
['zoomable'],
|
||||
[minorTicks],
|
||||
[axisTicks],
|
||||
[gridlines],
|
||||
...legendSection,
|
||||
[<ControlSubSectionHeader>{t('X Axis')}</ControlSubSectionHeader>],
|
||||
[
|
||||
|
||||
@@ -67,6 +67,8 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
|
||||
maxMarkerSize: 30,
|
||||
minMarkerSize: 5,
|
||||
minorSplitLine: false,
|
||||
gridlines: true,
|
||||
axisTicks: true,
|
||||
opacity: 0.2,
|
||||
orderDesc: true,
|
||||
rowLimit: 10000,
|
||||
@@ -86,6 +88,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
|
||||
xAxisLabelInterval: defaultXAxis.xAxisLabelInterval,
|
||||
groupby: [],
|
||||
showValue: false,
|
||||
labelPosition: 'auto',
|
||||
onlyTotal: false,
|
||||
percentageThreshold: 0,
|
||||
orientation: OrientationType.Vertical,
|
||||
|
||||
@@ -283,6 +283,8 @@ export default function transformProps(
|
||||
metrics,
|
||||
minorSplitLine,
|
||||
minorTicks,
|
||||
gridlines,
|
||||
axisTicks,
|
||||
onlyTotal,
|
||||
opacity,
|
||||
orientation,
|
||||
@@ -292,6 +294,7 @@ export default function transformProps(
|
||||
showLegend,
|
||||
showValue,
|
||||
size,
|
||||
labelPosition,
|
||||
colorByPrimaryAxis,
|
||||
sliceId,
|
||||
sortSeriesType,
|
||||
@@ -752,6 +755,7 @@ export default function transformProps(
|
||||
theme,
|
||||
hasDimensions: (groupBy?.length ?? 0) > 0,
|
||||
colorByPrimaryAxis,
|
||||
labelPosition,
|
||||
},
|
||||
);
|
||||
if (transformedSeries) {
|
||||
@@ -1272,12 +1276,19 @@ export default function transformProps(
|
||||
// at the axis boundary.
|
||||
...(showMaxLabel && {
|
||||
showMaxLabel: true,
|
||||
alignMaxLabel: 'right',
|
||||
showMinLabel: true,
|
||||
alignMinLabel: 'left',
|
||||
}),
|
||||
// The alignments assume the axis runs along the bottom; a horizontal
|
||||
// chart puts this axis on the side, where they misplace the labels.
|
||||
...(showMaxLabel &&
|
||||
!isHorizontal && {
|
||||
alignMaxLabel: 'right',
|
||||
alignMinLabel: 'left',
|
||||
}),
|
||||
},
|
||||
minorTick: { show: minorTicks },
|
||||
axisTick: { show: axisTicks ? 'auto' : false },
|
||||
...(gridlines ? {} : { splitLine: { show: false } }),
|
||||
minInterval:
|
||||
xAxisType === AxisType.Time && resolvedTimeGrain && !forceMaxInterval
|
||||
? (TIMEGRAIN_TO_TIMESTAMP[
|
||||
@@ -1322,7 +1333,7 @@ export default function transformProps(
|
||||
max: yAxisMax,
|
||||
minorTick: { show: isSmallChart ? false : minorTicks },
|
||||
minorSplitLine: { show: isSmallChart ? false : minorSplitLine },
|
||||
splitLine: { show: !isSmallChart },
|
||||
splitLine: { show: isSmallChart ? false : gridlines },
|
||||
axisLabel: {
|
||||
show: !isMicroChart,
|
||||
showMinLabel: !isMicroChart,
|
||||
@@ -1336,7 +1347,7 @@ export default function transformProps(
|
||||
yAxisFormat,
|
||||
),
|
||||
},
|
||||
axisTick: { show: !isSmallChart },
|
||||
axisTick: { show: isSmallChart ? false : axisTicks },
|
||||
scale: truncateYAxis,
|
||||
name: isSmallChart ? undefined : yAxisTitle,
|
||||
nameGap: convertInteger(yAxisTitleMargin),
|
||||
|
||||
@@ -51,6 +51,7 @@ import { extractForecastSeriesContext } from '../utils/forecast';
|
||||
import {
|
||||
EchartsTimeseriesSeriesType,
|
||||
ForecastSeriesEnum,
|
||||
LabelPositionEnum,
|
||||
LegendOrientation,
|
||||
OrientationType,
|
||||
StackType,
|
||||
@@ -170,6 +171,7 @@ export const getBaselineSeriesForStream = (
|
||||
export function transformNegativeLabelsPosition(
|
||||
series: SeriesOption,
|
||||
isHorizontal: boolean,
|
||||
labelPosition?: string,
|
||||
): TimeseriesDataRecord[] {
|
||||
/*
|
||||
* Adjusts label position for negative values in bar series
|
||||
@@ -185,7 +187,10 @@ export function transformNegativeLabelsPosition(
|
||||
? {
|
||||
value,
|
||||
label: {
|
||||
position: 'outside',
|
||||
position:
|
||||
labelPosition && labelPosition !== 'auto'
|
||||
? labelPosition
|
||||
: 'outside',
|
||||
},
|
||||
}
|
||||
: value;
|
||||
@@ -255,6 +260,7 @@ export function transformSeries(
|
||||
theme?: SupersetTheme;
|
||||
hasDimensions?: boolean;
|
||||
colorByPrimaryAxis?: boolean;
|
||||
labelPosition?: string;
|
||||
},
|
||||
): SeriesOption | undefined {
|
||||
const { name, data } = series;
|
||||
@@ -287,6 +293,7 @@ export function transformSeries(
|
||||
timeShiftColor,
|
||||
theme,
|
||||
colorByPrimaryAxis = false,
|
||||
labelPosition,
|
||||
} = opts;
|
||||
const contexts = seriesContexts[name || ''] || [];
|
||||
const hasForecast =
|
||||
@@ -406,7 +413,13 @@ export function transformSeries(
|
||||
),
|
||||
}
|
||||
: seriesType === 'bar' && !stack
|
||||
? { data: transformNegativeLabelsPosition(series, isHorizontal) }
|
||||
? {
|
||||
data: transformNegativeLabelsPosition(
|
||||
series,
|
||||
isHorizontal,
|
||||
labelPosition,
|
||||
),
|
||||
}
|
||||
: null
|
||||
: null),
|
||||
connectNulls,
|
||||
@@ -443,7 +456,12 @@ export function transformSeries(
|
||||
symbolSize: symbolSizeFn ?? markerSize,
|
||||
label: {
|
||||
show: !!showValue,
|
||||
position: isHorizontal ? 'right' : 'top',
|
||||
position: (labelPosition === 'auto' || !labelPosition
|
||||
? isHorizontal
|
||||
? LabelPositionEnum.Right
|
||||
: LabelPositionEnum.Top
|
||||
: labelPosition) as LabelPositionEnum,
|
||||
...(plotType === 'bar' ? { overflow: 'truncate' } : {}),
|
||||
color: theme?.colorText,
|
||||
textBorderWidth: 0,
|
||||
formatter: (params: any) => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
BaseTransformedProps,
|
||||
ContextMenuTransformedProps,
|
||||
CrossFilterTransformedProps,
|
||||
LabelPositionEnum,
|
||||
LegendFormData,
|
||||
StackType,
|
||||
TitleFormData,
|
||||
@@ -72,6 +73,8 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
||||
metrics: QueryFormMetric[];
|
||||
minorSplitLine: boolean;
|
||||
minorTicks: boolean;
|
||||
gridlines: boolean;
|
||||
axisTicks: boolean;
|
||||
opacity: number;
|
||||
orderDesc: boolean;
|
||||
rowLimit: number;
|
||||
@@ -99,6 +102,16 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
||||
xAxisLabelRotation: number;
|
||||
xAxisLabelInterval: number | string;
|
||||
showValue: boolean;
|
||||
/**
|
||||
* Where the data label sits relative to its data point, applied when
|
||||
* `showValue` is on.
|
||||
*
|
||||
* `'auto'` keeps the orientation-aware default the chart used before this
|
||||
* control existed: `Right` for a horizontal chart, `Top` otherwise. It is
|
||||
* also the value every chart saved before then resolves to, so the default
|
||||
* must stay `'auto'` for those to keep rendering as they did.
|
||||
*/
|
||||
labelPosition?: LabelPositionEnum | 'auto';
|
||||
onlyTotal: boolean;
|
||||
showExtraControls: boolean;
|
||||
percentageThreshold: number;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user