Compare commits

..
Author SHA1 Message Date
rusackasandClaude Opus 4.8 44d27e84c9 docs(theming): correct Button size-token fallback values
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 07:15:56 -07:00
rusackasandClaude Opus 4.8 8b64351f41 docs(theming): correct HIDE_NAVBAR_LOGO and dashboard tile defaults
HIDE_NAVBAR_LOGO hides the whole brand area, not just the logo, and
dashboard chart tiles default to a bordered look with the box-shadow
only during fade-out, not on every tile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 22:58:31 -07:00
rusackasandClaude Sonnet 5 7e460b3b93 docs(theming): document new component sizing tokens and HIDE_NAVBAR_LOGO
PR #40985 adds button/label/select/dashboard-tile sizing tokens, a
per-instance styleConfig prop for Button/DropdownButton, and the
HIDE_NAVBAR_LOGO config flag, none of which were documented.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 21:34:11 -07:00
bucketbase26andJoe Li e39bfb255b fix(explore): prevent duplicate Date Range tooltips (#43425)
Co-authored-by: Joe Li <joe@preset.io>
2026-08-27 21:02:57 -07:00
b7301ac88a fix: Dashboard export with charts from multiple databases (#37120)
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 20:27:54 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> fa59b44cfe chore(deps): bump mapbox-gl from 3.28.1 to 3.29.0 in /superset-frontend (#43619)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 19:58:55 -07:00
8c5be889d9 fix(cockroachdb): replace abandoned cockroachdb package with sqlalchemy-cockroachdb (#43501)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 19:48:13 -07:00
Evan RusackasandClaude b733b57e9e docs(mcp): document chart type plugin filtering config (#43597)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 19:47:07 -07:00
Joe Li 13e97ba913 fix(sql-lab): fix transparent background in ag-grid native menus (#43368) 2026-08-27 16:56:36 -07:00
Evan RusackasandClaude Sonnet 5 f9e43a37d8 docs(theming): document THEME_DEFAULT_MODE config setting (#43601)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 16:19:40 -07:00
Evan RusackasandClaude 5ff44c5ed6 docs(cli): document import_directory --username option (#43602)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 16:19:25 -07:00
Elizabeth ThompsonandClaude Opus 4.8 7b145a520c fix(tags): catch TemplateError when validating access for tagged SQL Lab queries (#43423)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 15:06:46 -07:00
Elizabeth Thompson 9f62aace7e fix(examples): replace deprecated granularity_sqla with granularity (#43432) 2026-08-27 15:06:42 -07:00
Elizabeth ThompsonandClaude Opus 4.8 20be48c085 fix(explore): catch TemplateError when validating access for permalinked query datasources (#43605)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 15:06:38 -07:00
Aurimas Navardauskas e1c7a51674 fix(mcp): flag failed tool calls with isError (#43374) 2026-08-27 14:59:19 -07:00
Gabriel Torres Ruiz ecbd6578b0 fix(db_engine_specs): always mask OAuth2 client secret in encrypted extra (#43491) 2026-08-27 18:42:51 -03:00
Amin Ghadersohi ea3206b076 fix(mcp): surface rejected filter columns in get_chart_data (#43598) 2026-08-27 17:36:50 -04:00
Amin Ghadersohi 478f612f25 fix(mcp): add open-world tool annotations (#43529) 2026-08-27 17:36:30 -04:00
Amin Ghadersohi a16bc0d94f fix(mcp): apply time grain overrides from extra_form_data (#43599) 2026-08-27 17:35:23 -04:00
Amin GhadersohiandClaude bcc6af6c5f fix(dao): don't mask transient OperationalError as a "not found" result (#43479)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 17:34:53 -04:00
Evan RusackasandClaude 94b59420b7 docs(embedding): document EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE (#43600)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 13:15:08 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Joe Li
6814a89230 chore(deps): bump gunicorn from 26.0.0 to 26.2.0 (#43609)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-27 13:08:37 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ebbe2e8714 chore(deps): bump the storybook group in /docs with 2 updates (#43610)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:32 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b8acf4b207 chore(deps-dev): bump syntaqlite from 0.7.1 to 0.9.0 (#43612)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 13:08:28 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Joe Li
ba5d5091b9 chore(deps): bump holidays from 0.102 to 0.103 (#43614)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-08-27 13:08:24 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8c88c0d3ac chore(deps-dev): update teradatasql requirement from >=20.0.0.65 to >=20.0.0.66 (#43615)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:18 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
e9e652d22d chore(deps-dev): bump trino from 0.338.0 to 0.339.0 (#43616)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 13:08:14 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8f82d851cf chore(deps-dev): bump tiktoken from 0.13.0 to 0.14.0 (#43617)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-27 13:08:09 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 83934c038e chore(deps-dev): bump the storybook group in /superset-frontend with 5 updates (#43618)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:05 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 35fc36a97e chore(deps-dev): bump baseline-browser-mapping from 2.11.15 to 2.11.16 in /superset-frontend (#43621)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 13:08:00 -07:00
Evan RusackasandClaude Opus 4.8 88c4e689e3 fix(pivot-table): keep D3_FORMAT-styled numbers intact in pivoted Excel export (#42601)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-27 12:55:57 -07:00
Evan RusackasandClaude Sonnet 5 29f4a05eb7 docs(security): document the RLS indicator badge on dataset list/Explore (#43591)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 12:54:21 -07:00
Evan RusackasandClaude 323741b043 docs(alerts-reports): document report/alert content format options (#43594)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:31 -07:00
Evan RusackasandClaude 5c3c6362ae docs(cache): document NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT (#43592)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:27 -07:00
Evan RusackasandClaude c1b6ff340b docs(mcp): document the update_dataset_metric tool (#43596)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:19 -07:00
Evan RusackasandClaude 05830ebc77 docs: document PNG/PDF chart export options (#43593)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 12:53:09 -07:00
Evan RusackasandClaude Sonnet 5 74dd690102 docs(security): document EXTRA_EDITORS_RESOLVER (#43595)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 12:52:56 -07:00
Joe Li 22027ec24c fix(explore): require a value for adhoc filters before save (#43317) 2026-08-27 12:17:46 -07:00
a3072c0bf6 fix(deckgl): give handlebars tooltips an opaque background (#43195)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:17:32 -07:00
6d8cbbfe8d fix(api): stop export downloads inheriting the one-year Cache-Control (#43185)
Co-authored-by: bikashJMV <bikash@jmv.co.in>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-27 12:17:02 -07:00
Sepuri Sai KrishnaandEvan Rusackas 926e0e6a92 fix(chart-data): validate select()'s exclude option instead of raising 500s (#42410)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-27 12:14:33 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d013bc7a21 chore(deps): bump baseline-browser-mapping from 2.11.15 to 2.11.16 in /docs (#43613)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 12:11:32 -07:00
87c48cc473 feat(echarts): make bar chart label position user-configurable (#38695)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
2026-08-27 12:10:38 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c2266f860c chore(deps-dev): update clickhouse-connect requirement from <2.0,>=1.7.1 to >=1.7.2,<2.0 (#43608)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-27 12:06:41 -07:00
Viktor Högberg e8aea6b6df fix: remove redundant 'tooltip' tooltip in scoping modal (#43455) 2026-08-27 11:10:26 -07:00
Evan Rusackas bde1c483b0 chore(deps): group Dependabot security-update PRs per directory (#43604) 2026-08-27 11:08:21 -07:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>hainenber
ff1d9f9e09 chore(deps): bump dawidd6/action-download-artifact from 21 to 24 (#43441)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-08-27 10:24:38 -07:00
Amogh AtreyaandEvan Rusackas 76151beade fix(export): truncate exported filenames to prevent Windows path extraction errors (#42531) (#42541)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-08-27 08:48:58 -07:00
Amin GhadersohiandClaude 905a35d05f fix(mcp): resolve dashboard permalinks (#43482)
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-27 11:35:36 -04:00
Mehmet Salih Yavuz fde0fbd315 fix(ci): unblock lint-frontend after the oxlint 1.79.0 bump (#43587) 2026-08-27 17:32:04 +03:00
203 changed files with 4638 additions and 856 deletions
+26
View File
@@ -5,6 +5,10 @@ updates:
directory: "/"
schedule:
interval: "daily"
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
cooldown:
default-days: 7
@@ -61,6 +65,9 @@ updates:
- npm
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
rjsf:
patterns:
- "@rjsf/*"
@@ -98,6 +105,10 @@ updates:
labels:
- pip
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
cooldown:
default-days: 7
@@ -105,6 +116,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 +130,9 @@ updates:
schedule:
interval: "daily"
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
storybook:
patterns:
- "@storybook/*"
@@ -142,6 +160,10 @@ updates:
labels:
- npm
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
versioning-strategy: increase
cooldown:
default-days: 7
@@ -153,6 +175,10 @@ updates:
labels:
- npm
- dependabot
groups:
security:
applies-to: "security-updates"
patterns: ["*"]
open-pull-requests-limit: 10
versioning-strategy: increase
cooldown:
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 }}
+1
View File
@@ -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
@@ -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
+21
View File
@@ -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
+144
View File
@@ -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
@@ -117,6 +138,18 @@ The existing `APP_NAME` Python config key continues to work for backward compati
Email and alert/report notification subjects are driven by backend settings such as
`EMAIL_REPORTS_SUBJECT_PREFIX` and `APP_NAME`, not by this theme token.
To hide the entire brand area in the navbar (both the logo image and the
brand text), set `HIDE_NAVBAR_LOGO` in `superset_config.py`:
```python
# Hide the entire brand area in the navbar, including the logo image and the
# brand text (brandAppName / APP_NAME). Defaults to False.
HIDE_NAVBAR_LOGO = True
```
`HIDE_NAVBAR_LOGO` is a Python config flag rather than a theme token, so it
cannot be set through the theme CRUD UI or `THEME_DEFAULT`/`THEME_DARK`.
### Migration from Configuration to UI
When `ENABLE_UI_THEME_ADMINISTRATION = True`:
@@ -433,6 +466,117 @@ THEME_DEFAULT = {
This feature provides powerful theming capabilities while maintaining the flexibility of ECharts' extensive configuration options.
## Component Sizing & Style Tokens
:::note
Available since Superset 6.1
:::
Beyond colors and fonts, a handful of Superset-specific tokens let you tune the
sizing, radius, and outline behavior of individual UI components. All of these
tokens are optional — omit them and components fall back to their existing
defaults, so applying them is a zero-visual-change operation until you opt in.
### Button & DropdownButton Sizing
```python
THEME_DEFAULT = {
"token": {
# ... other tokens
"buttonControlHeight": 32, # default button height, in px
"buttonControlHeightSM": 30, # small/dropdown button height, in px
"buttonControlHeightXS": 22, # xsmall button height, in px
"buttonPaddingInline": 18, # horizontal padding, in px
"buttonPaddingInlineSM": 10, # horizontal padding for small buttons, in px
"buttonFontSize": 14,
"buttonBorderRadius": 4,
}
}
```
`buttonControlHeight` and `buttonBorderRadius` also drive the sizing of the
menu-trigger button used by `PageHeaderWithActions`, so a single pair of tokens
keeps page-header icon buttons visually consistent with regular buttons.
For one-off overrides that shouldn't apply to every button in the app, pass a
`styleConfig` prop directly to `Button` or `DropdownButton` instead of setting
a theme token:
```tsx
<Button
styleConfig={{
controlHeight: 40,
paddingInline: 20,
fontSize: 16,
fontWeight: 700,
borderRadius: 8,
ctaMinWidth: 120,
ctaMinHeight: 40,
iconGap: 8,
}}
>
Click me
</Button>
<DropdownButton
styleConfig={{
controlHeight: 32,
fontSize: 14,
fontWeight: 500,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
menu={menuProps}
>
Options
</DropdownButton>
```
`styleConfig` values take precedence over the equivalent theme tokens, which in
turn take precedence over the built-in defaults.
### Label Border Radius
```python
THEME_DEFAULT = {
"token": {
"labelBorderRadius": 4, # defaults to 8px
}
}
```
### Select Option Outline
By default, hovering or navigating to an option in a `Select` dropdown draws a
2px outline in `colorPrimary`. Set `selectOptionActiveOutline` to `False` for a
more subtle hover style with no outline:
```python
THEME_DEFAULT = {
"token": {
"selectOptionActiveOutline": False,
}
}
```
### Dashboard Tile Appearance
Chart tiles on a dashboard (not text/markdown tiles) can be restyled via
`dashboardTile*` tokens. All fall back to the existing look — a
`colorBgContainer` background, a `1px solid colorBorder` border, and a
hairline `box-shadow` while the tile is fading out (e.g. when a filter
makes it irrelevant):
```python
THEME_DEFAULT = {
"token": {
"dashboardTileBg": "#ffffff",
"dashboardTileBorder": "1px solid #e0e0e0",
"dashboardTileBorderRadius": 8,
"dashboardTileBoxShadow": "0 1px 2px rgba(0, 0, 0, 0.08)",
}
}
```
## Advanced Features
- **System Themes**: Manage system-wide default and dark themes via UI or configuration
+58
View File
@@ -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
@@ -284,8 +284,6 @@ automatically run whenever you make a Git commit.
#### Running Pre-commit Manually
Before pushing, run `pre-commit run --all-files` to catch issues locally before CI.
You can also run the pre-commit checks manually in various ways:
- **Run pre-commit on all files (same as CI):**
@@ -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.
+15
View File
@@ -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
@@ -248,11 +248,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
+3 -3
View File
@@ -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.16",
"caniuse-lite": "^1.0.30001809",
"docusaurus-plugin-openapi-docs": "^5.2.0",
"docusaurus-theme-openapi-docs": "^5.2.0",
@@ -77,7 +77,7 @@
"react-table": "^7.8.0",
"remark-import-partial": "^0.0.2",
"reselect": "^5.2.0",
"storybook": "^10.5.9",
"storybook": "^10.5.10",
"swagger-ui-react": "^5.32.14",
"swc-loader": "^0.2.7",
"tinycolor2": "^1.4.2",
+141 -134
View File
@@ -2145,10 +2145,10 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@emnapi/core@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.1.tgz#b9e1064f3a6b1631e241e638eb48d736bfd372a6"
integrity sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==
"@emnapi/core@1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.0.tgz#8a655042dbbb10d0266670c9903c34a7001c705b"
integrity sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==
dependencies:
"@emnapi/wasi-threads" "1.2.2"
tslib "^2.4.0"
@@ -2169,10 +2169,10 @@
"@emnapi/wasi-threads" "1.2.2"
tslib "^2.4.0"
"@emnapi/runtime@1.11.1":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.1.tgz#58f1f3d5d81a9b12f793ab688c96371901027c24"
integrity sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==
"@emnapi/runtime@1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.0.tgz#ce16b3674ff7266bbf50f9668bde8a04f3014d4e"
integrity sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==
dependencies:
tslib "^2.4.0"
@@ -2934,13 +2934,20 @@
"@emnapi/runtime" "^1.5.0"
"@tybys/wasm-util" "^0.10.1"
"@napi-rs/wasm-runtime@^1.1.4", "@napi-rs/wasm-runtime@^1.1.6":
"@napi-rs/wasm-runtime@^1.1.4":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz#ed33806d0f9be98dc76d0c3d4fd872fda701b5d5"
integrity sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==
dependencies:
"@tybys/wasm-util" "^0.10.3"
"@napi-rs/wasm-runtime@^1.1.5":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz#97e3d45d7424dc5da1d4e32f3bf3b292f6c1b44c"
integrity sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==
dependencies:
"@tybys/wasm-util" "^0.10.3"
"@noble/hashes@1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426"
@@ -3076,104 +3083,104 @@
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.127.0.tgz#8374fcdfb4a641861218daa5700c447c00b66663"
integrity sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==
"@oxc-resolver/binding-android-arm-eabi@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.23.0.tgz#31b3087c2c8a9d100ae22aced377c03befc87efa"
integrity sha512-8IJyWRLVAyhTfe9/TIEbQqSQnl5rUqYJrUOS6Dkr+Mq9FGHMxDGeiEmwkBqCvDP5KckpPh/GYSgbag66O6JsCw==
"@oxc-resolver/binding-android-arm-eabi@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz#ef832062e3a2f0c7604a8312c34eacf23a7bd7e3"
integrity sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==
"@oxc-resolver/binding-android-arm64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.23.0.tgz#744d8c82d37189ddb76f911b6fbd9915d8090a9b"
integrity sha512-pprVojnNhHxupwTT2gdeUlkxll6XEvWWBk3oVicOSNVWQC99OBnDhMQDoirqnzrE1bScQSMS2JgPpqdlrhz/Fg==
"@oxc-resolver/binding-android-arm64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz#be6093b76621981c57833172a7fa9b5b0753daef"
integrity sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==
"@oxc-resolver/binding-darwin-arm64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.23.0.tgz#a57f71c22d3d39da3ce08fbd672a25673ad38364"
integrity sha512-mbIrWIMAJeytyee36OyUP5XH92TP7FaKaQ2m5AjokKy7STgjrhRt7SMXqpqLjhGm6Xn721Xmsg6H3Rtd9YQETw==
"@oxc-resolver/binding-darwin-arm64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz#b5ec0b6774ef9c60bb3e9237b39b603aac39f75f"
integrity sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==
"@oxc-resolver/binding-darwin-x64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.23.0.tgz#ca28168b5aca8003c2a114731ffc556f04ac40e4"
integrity sha512-UnIphmZ1LazUCr9DXWaKYWtKDefPMbgLsywaoYxRqVCNHhq4MM6d2q1Nz1i9Vzxt5i+cE2nRUYpAUHr/lijNYA==
"@oxc-resolver/binding-darwin-x64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz#79dbeeca048fda1de9c71169b08924535369a14d"
integrity sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==
"@oxc-resolver/binding-freebsd-x64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.23.0.tgz#b956c5b3eda65e1302e291c185d7fca10fbfd7fb"
integrity sha512-aaZ/cSEYFkSxgS2hOrobT6RQcsWNviOX8dW6CEkVx2/UYkAf9MeHbjl3W0usWV53rVV//ndBdn2nb1y7jsu4lw==
"@oxc-resolver/binding-freebsd-x64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz#8d49e6cb2269597b6ef0bc90c5b23e68d946f8c3"
integrity sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==
"@oxc-resolver/binding-linux-arm-gnueabihf@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.23.0.tgz#f59878badc556b023a12980f8fd4ca363f8065e2"
integrity sha512-IoJLvO5SjLSVMaq83BNTrPCb1FppvoJc1IhZ5CoUVl3PykUBku7D+LK1j0GSurhJcIc6zfjghsvaZNpq5ev6Mg==
"@oxc-resolver/binding-linux-arm-gnueabihf@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz#84413d558915ecf81da5914336b9fe9a70a84acd"
integrity sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==
"@oxc-resolver/binding-linux-arm-musleabihf@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.23.0.tgz#19e4cd124f6fb0f17c2693525422be493edabf39"
integrity sha512-vskFpwg44T/LFsfjSCnVZ5ygcuqzPC1yUzVEiKa8BgHAQz0+QLQQW3EGWLPVi8EXFghzjR4EtgPBtOhCjU4jdw==
"@oxc-resolver/binding-linux-arm-musleabihf@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz#af2554d8984592926ec92fd838bd01e3e14f2f53"
integrity sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==
"@oxc-resolver/binding-linux-arm64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.23.0.tgz#770c64d6f976c6374de3314942616b6c8f2afb05"
integrity sha512-//TcHVhrChyw5RYtgts6WO7KcWq9387c1Z5Zvhqpk/ktAbyaRYgBZrpSY1GDCFq50ASt6B6jhh+JxB1rB45IAg==
"@oxc-resolver/binding-linux-arm64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz#453a2e5d1d858d7e5bff0ddde584ec46560a37b6"
integrity sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==
"@oxc-resolver/binding-linux-arm64-musl@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.23.0.tgz#88640f695e6ca9f71641b59f2996d5bb3eaaea42"
integrity sha512-ZFqlwiTf7CXLLSGyAR9tYiO33LiaeIEXW+xm42d8mnUGpDgPltyrCGYtQezyMMEXvjhOgCz1X+i7sbDTJEx+bg==
"@oxc-resolver/binding-linux-arm64-musl@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz#4e04fb8d43aa3baa037f88a292b0eee386dfd742"
integrity sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==
"@oxc-resolver/binding-linux-ppc64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.23.0.tgz#7c2a0c45ab65354b89f8777c806fcfebe09f12e8"
integrity sha512-oZ5LeN5+H1R19dRjTAxKrxQguH+AsemHcnthEfFxf4OjmBSty2doHLeSmMunKy3zpTHJQ3lh3Af+dNS+W6dYeA==
"@oxc-resolver/binding-linux-ppc64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz#662de504b88a9d7c745808514bfdb2a4e9f53760"
integrity sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==
"@oxc-resolver/binding-linux-riscv64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.23.0.tgz#fb502eb40ca7576d826672ba9f6c6ba9bb87094d"
integrity sha512-O4ciFDyX5ebQd0qkb1bjAIg8IEfiLT03GbSeylwlwlUMK9KwBWaALwrxSbc0Msaz4U6iPj+T9eRXpD5mxBfmvA==
"@oxc-resolver/binding-linux-riscv64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz#72e7d1edb906d42a2ad339a3a6be5b097c00a0a0"
integrity sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==
"@oxc-resolver/binding-linux-riscv64-musl@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.23.0.tgz#2159fe2807e1356d9149a151d06cd9a0c7253224"
integrity sha512-P3o8Y9kISYjcxadmbO+94ThRwLhwGuDAbA7dcdd4+YLpfeF+mmobz8fXf4NmSdfSqjyRSkceJDBRZha9NVYkiQ==
"@oxc-resolver/binding-linux-riscv64-musl@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz#cf5788f4dd858ae6a27d1252b0b662b39a4bbf2d"
integrity sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==
"@oxc-resolver/binding-linux-s390x-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.23.0.tgz#1fb7c19af499a5701d53f2aa38e841f153d7cf86"
integrity sha512-oj03m1E3RmTFczKhcKJDzHaEDKJnPIsDcQFVxBJsSdXGSuIPdt5TvcM332FfMQgzI6yDJqyl4InrnFfXrmUTKQ==
"@oxc-resolver/binding-linux-s390x-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz#ec3933ccb443b68eb9b879b7f25489fcde11cc53"
integrity sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==
"@oxc-resolver/binding-linux-x64-gnu@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.23.0.tgz#580644712d78ebe1c1d4b7077db992ce149d46e8"
integrity sha512-BqJxbSC8FdP7mSuSpRePTGHm0hXWV+dfz//f7SjsteZncLaBgWTBmi/OZNv7sX6CyG/Pt/eJkPorP+DkMOhMwQ==
"@oxc-resolver/binding-linux-x64-gnu@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz#9cb87e203ef40631d7ff2cd1a8a6c145a5595054"
integrity sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==
"@oxc-resolver/binding-linux-x64-musl@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.23.0.tgz#709b86ff4c0a43bfeb4d7e5cc7334ee77780c5ae"
integrity sha512-utmw+VmUrW4K8LI5/6jhg4aGYKJHOIjQ9syYOOA6pF3w7haKu4r4enTe2U0C04/HbUvkq/Zif43xFsKW1Pnq9w==
"@oxc-resolver/binding-linux-x64-musl@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz#a1ebb07307eccb2b150dc6bcac5a575cf3b647bc"
integrity sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==
"@oxc-resolver/binding-openharmony-arm64@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.23.0.tgz#580331f88fbdbc7b518a93e2ab43a7692e24f7f4"
integrity sha512-V6lbRrthHa4TbvsLjPtg+EkXT1tRY+s4I8rYLXUfiHlZzGx3sLv1EH9CEOOevjvUYHLsbe/gqCIc73XnQfPb9A==
"@oxc-resolver/binding-openharmony-arm64@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz#aba2e14c823e3356acfb8bce06be97d5aa5fcab8"
integrity sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==
"@oxc-resolver/binding-wasm32-wasi@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.23.0.tgz#4d5f1300d1a4ef18b933329ad7c6b395bac28580"
integrity sha512-gRoOxQPdnAmIAjxcuQNBxfihvx+wjTaQM/9/eP12xwnGNawOG/+Zz9RHN4WNSxT45b5CrscK4NB8aPh+oZQXAQ==
"@oxc-resolver/binding-wasm32-wasi@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz#8840346c1b8a3028b1b3575abb2e45fed529d3d6"
integrity sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==
dependencies:
"@emnapi/core" "1.11.1"
"@emnapi/runtime" "1.11.1"
"@napi-rs/wasm-runtime" "^1.1.6"
"@emnapi/core" "1.11.0"
"@emnapi/runtime" "1.11.0"
"@napi-rs/wasm-runtime" "^1.1.5"
"@oxc-resolver/binding-win32-arm64-msvc@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.23.0.tgz#87145652fdf60741d07b893150d61fa06e2a1d15"
integrity sha512-CgTGMYsJVe1eUiCdJTpGw21svXw79ITsemN1h0hcNkiswasDbN5MoibSLY+gRMWP5syfEz5iffrjZnwEP8xeUA==
"@oxc-resolver/binding-win32-arm64-msvc@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz#c987ab635ffe553e6230255a07c0576686f5ab45"
integrity sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==
"@oxc-resolver/binding-win32-x64-msvc@11.23.0":
version "11.23.0"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.23.0.tgz#8b66dbfa7b796139e719063fc0e44084e80a1c15"
integrity sha512-gUGJpr+Rn6zMxm5juApV0K3U845i8t47o8k+rbO0BHbi4PoJIfSPeQmrE2dgohQm2g5k6iviNFyXCGqvmaYUpw==
"@oxc-resolver/binding-win32-x64-msvc@11.21.2":
version "11.21.2"
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz#dbdf12721396ef4c31899d0963b002df32f57050"
integrity sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==
"@oxfmt/binding-android-arm-eabi@0.64.0":
version "0.64.0"
@@ -4122,23 +4129,23 @@
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
"@storybook/addon-docs@^10.5.9":
version "10.5.9"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.9.tgz#6d871977f7ad833dc142d12ee43490105dec4d07"
integrity sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw==
"@storybook/addon-docs@^10.5.10":
version "10.5.10"
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.10.tgz#c65d1d4a6d1e2de50f0decaf82ea57583f220b30"
integrity sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ==
dependencies:
"@mdx-js/react" "^3.0.0"
"@storybook/csf-plugin" "10.5.9"
"@storybook/csf-plugin" "10.5.10"
"@storybook/icons" "^2.0.2"
"@storybook/react-dom-shim" "10.5.9"
"@storybook/react-dom-shim" "10.5.10"
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
ts-dedent "^2.0.0"
"@storybook/csf-plugin@10.5.9":
version "10.5.9"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.9.tgz#805e4c93a1704b220351d62bb0c74ce3b78c5e10"
integrity sha512-4H5QIHQVtQYCuL43GCRLGjNQhZpQg9gL03ja0DV80kO2Dn9LEt6ol87bSnSjn4VDgcAXtgTzXFvRLknfVgAAqg==
"@storybook/csf-plugin@10.5.10":
version "10.5.10"
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.10.tgz#c65da1cdfe0a11795a14c518e9f8f828b8d54b3b"
integrity sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==
dependencies:
unplugin "^2.3.5"
@@ -4152,10 +4159,10 @@
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
"@storybook/react-dom-shim@10.5.9":
version "10.5.9"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.9.tgz#549793845bb8b966acd36002d33d7b54cc5c92d4"
integrity sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==
"@storybook/react-dom-shim@10.5.10":
version "10.5.10"
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.10.tgz#8ddcad879d8804a9bbc09a5d0ea10f8356f01ed6"
integrity sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==
"@superset-ui/core@^0.20.4":
version "0.20.4"
@@ -6522,10 +6529,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.15, baseline-browser-mapping@^2.9.19:
version "2.11.15"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz#9c0cac93d7d304f3d61bb41088a102cd62e68676"
integrity sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.16, baseline-browser-mapping@^2.9.19:
version "2.11.16"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz#0fa19a4ece2e34439ecaa3fdca8a59acfbd287fb"
integrity sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ==
batch@0.6.1:
version "0.6.1"
@@ -12237,30 +12244,30 @@ oxc-parser@^0.127.0:
"@oxc-parser/binding-win32-ia32-msvc" "0.127.0"
"@oxc-parser/binding-win32-x64-msvc" "0.127.0"
oxc-resolver@^11.19.1:
version "11.23.0"
resolved "https://registry.yarnpkg.com/oxc-resolver/-/oxc-resolver-11.23.0.tgz#bb9e32fa028dbde0584b5964dbc59de2878ac171"
integrity sha512-f0+l598CJMOLnYPXsXxttJALH0ljtivdRMKtvHhxRuWa5FYmw5+qODARl8oYjMC/brpzKcrpdORsOBrTqhBZ9A==
oxc-resolver@11.21.2:
version "11.21.2"
resolved "https://registry.yarnpkg.com/oxc-resolver/-/oxc-resolver-11.21.2.tgz#06f49557c98adb97133d85362797c460ef028057"
integrity sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==
optionalDependencies:
"@oxc-resolver/binding-android-arm-eabi" "11.23.0"
"@oxc-resolver/binding-android-arm64" "11.23.0"
"@oxc-resolver/binding-darwin-arm64" "11.23.0"
"@oxc-resolver/binding-darwin-x64" "11.23.0"
"@oxc-resolver/binding-freebsd-x64" "11.23.0"
"@oxc-resolver/binding-linux-arm-gnueabihf" "11.23.0"
"@oxc-resolver/binding-linux-arm-musleabihf" "11.23.0"
"@oxc-resolver/binding-linux-arm64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-arm64-musl" "11.23.0"
"@oxc-resolver/binding-linux-ppc64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-riscv64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-riscv64-musl" "11.23.0"
"@oxc-resolver/binding-linux-s390x-gnu" "11.23.0"
"@oxc-resolver/binding-linux-x64-gnu" "11.23.0"
"@oxc-resolver/binding-linux-x64-musl" "11.23.0"
"@oxc-resolver/binding-openharmony-arm64" "11.23.0"
"@oxc-resolver/binding-wasm32-wasi" "11.23.0"
"@oxc-resolver/binding-win32-arm64-msvc" "11.23.0"
"@oxc-resolver/binding-win32-x64-msvc" "11.23.0"
"@oxc-resolver/binding-android-arm-eabi" "11.21.2"
"@oxc-resolver/binding-android-arm64" "11.21.2"
"@oxc-resolver/binding-darwin-arm64" "11.21.2"
"@oxc-resolver/binding-darwin-x64" "11.21.2"
"@oxc-resolver/binding-freebsd-x64" "11.21.2"
"@oxc-resolver/binding-linux-arm-gnueabihf" "11.21.2"
"@oxc-resolver/binding-linux-arm-musleabihf" "11.21.2"
"@oxc-resolver/binding-linux-arm64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-arm64-musl" "11.21.2"
"@oxc-resolver/binding-linux-ppc64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-riscv64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-riscv64-musl" "11.21.2"
"@oxc-resolver/binding-linux-s390x-gnu" "11.21.2"
"@oxc-resolver/binding-linux-x64-gnu" "11.21.2"
"@oxc-resolver/binding-linux-x64-musl" "11.21.2"
"@oxc-resolver/binding-openharmony-arm64" "11.21.2"
"@oxc-resolver/binding-wasm32-wasi" "11.21.2"
"@oxc-resolver/binding-win32-arm64-msvc" "11.21.2"
"@oxc-resolver/binding-win32-x64-msvc" "11.21.2"
oxfmt@^0.64.0:
version "0.64.0"
@@ -14783,10 +14790,10 @@ stop-iteration-iterator@^1.1.0:
es-errors "^1.3.0"
internal-slot "^1.1.0"
storybook@^10.5.9:
version "10.5.9"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.9.tgz#61f476fd73785dcf09e9198ddf404b9b8c06964a"
integrity sha512-UfdMKSjEhIKr8LbqYyIE5r7vT/drL/PxN75YaouJ+UG0FssEy6cf49OdTF3kstAqVMHskc+zEqyRoiQHZXHwgA==
storybook@^10.5.10:
version "10.5.10"
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.10.tgz#97e9b4a71b4df7732e82d64edffe07f9e8d70083"
integrity sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==
dependencies:
"@storybook/global" "^5.0.0"
"@storybook/icons" "^2.0.2"
@@ -14800,7 +14807,7 @@ storybook@^10.5.9:
jsonc-parser "^3.3.1"
open "^10.2.0"
oxc-parser "^0.127.0"
oxc-resolver "^11.19.1"
oxc-resolver "11.21.2"
recast "^0.23.5"
semver "^7.7.3"
use-sync-external-store "^1.5.0"
+17 -7
View File
@@ -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
@@ -190,7 +200,7 @@ fastmcp = [
# 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
@@ -236,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
@@ -250,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",
@@ -260,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"]
@@ -294,7 +304,7 @@ development = [
"ruff",
"sqloxide",
"statsd",
"syntaqlite>=0.7.0,<0.8.0",
"syntaqlite>=0.9.0,<0.10.0",
]
[project.urls]
+2 -3
View File
@@ -168,11 +168,11 @@ greenlet==3.5.4
# 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
+1 -1
View File
@@ -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]
+8 -6
View File
@@ -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
@@ -624,7 +624,6 @@ packaging==25.0
# duckdb-engine
# fastmcp-slim
# google-cloud-bigquery
# gunicorn
# kombu
# limits
# matplotlib
@@ -962,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
@@ -995,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
@@ -1011,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
+1 -1
View File
@@ -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",
+210 -173
View File
@@ -108,7 +108,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",
@@ -180,9 +180,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",
@@ -218,7 +218,7 @@
"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.16",
"cheerio": "1.2.0",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
@@ -235,7 +235,7 @@
"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-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",
@@ -266,7 +266,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",
@@ -8117,9 +8117,9 @@
}
},
"node_modules/@oxc-resolver/binding-android-arm-eabi": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.20.0.tgz",
"integrity": "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz",
"integrity": "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==",
"cpu": [
"arm"
],
@@ -8131,9 +8131,9 @@
]
},
"node_modules/@oxc-resolver/binding-android-arm64": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.20.0.tgz",
"integrity": "sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz",
"integrity": "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==",
"cpu": [
"arm64"
],
@@ -8145,9 +8145,9 @@
]
},
"node_modules/@oxc-resolver/binding-darwin-arm64": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.20.0.tgz",
"integrity": "sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz",
"integrity": "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==",
"cpu": [
"arm64"
],
@@ -8159,9 +8159,9 @@
]
},
"node_modules/@oxc-resolver/binding-darwin-x64": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.20.0.tgz",
"integrity": "sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz",
"integrity": "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==",
"cpu": [
"x64"
],
@@ -8173,9 +8173,9 @@
]
},
"node_modules/@oxc-resolver/binding-freebsd-x64": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.20.0.tgz",
"integrity": "sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz",
"integrity": "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==",
"cpu": [
"x64"
],
@@ -8187,9 +8187,9 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.20.0.tgz",
"integrity": "sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz",
"integrity": "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==",
"cpu": [
"arm"
],
@@ -8201,9 +8201,9 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm-musleabihf": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.20.0.tgz",
"integrity": "sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz",
"integrity": "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==",
"cpu": [
"arm"
],
@@ -8215,13 +8215,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm64-gnu": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.20.0.tgz",
"integrity": "sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz",
"integrity": "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8229,13 +8232,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-arm64-musl": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.20.0.tgz",
"integrity": "sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz",
"integrity": "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8243,13 +8249,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-ppc64-gnu": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.20.0.tgz",
"integrity": "sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz",
"integrity": "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8257,13 +8266,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-riscv64-gnu": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.20.0.tgz",
"integrity": "sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz",
"integrity": "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8271,13 +8283,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-riscv64-musl": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.20.0.tgz",
"integrity": "sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz",
"integrity": "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==",
"cpu": [
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8285,13 +8300,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-s390x-gnu": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.20.0.tgz",
"integrity": "sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz",
"integrity": "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8299,13 +8317,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-x64-gnu": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.20.0.tgz",
"integrity": "sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz",
"integrity": "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8313,13 +8334,16 @@
]
},
"node_modules/@oxc-resolver/binding-linux-x64-musl": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.20.0.tgz",
"integrity": "sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz",
"integrity": "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -8327,9 +8351,9 @@
]
},
"node_modules/@oxc-resolver/binding-openharmony-arm64": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.20.0.tgz",
"integrity": "sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz",
"integrity": "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==",
"cpu": [
"arm64"
],
@@ -8341,9 +8365,9 @@
]
},
"node_modules/@oxc-resolver/binding-wasm32-wasi": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.20.0.tgz",
"integrity": "sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz",
"integrity": "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==",
"cpu": [
"wasm32"
],
@@ -8351,30 +8375,41 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "^1.1.4"
"@emnapi/core": "1.11.0",
"@emnapi/runtime": "1.11.0",
"@napi-rs/wasm-runtime": "^1.1.5"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz",
"integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
"integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -8383,28 +8418,31 @@
}
},
"node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz",
"integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.3"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
"@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4",
"@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4"
}
},
"node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -8413,9 +8451,9 @@
}
},
"node_modules/@oxc-resolver/binding-win32-arm64-msvc": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.20.0.tgz",
"integrity": "sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz",
"integrity": "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==",
"cpu": [
"arm64"
],
@@ -8427,9 +8465,9 @@
]
},
"node_modules/@oxc-resolver/binding-win32-x64-msvc": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.20.0.tgz",
"integrity": "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz",
"integrity": "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==",
"cpu": [
"x64"
],
@@ -10765,16 +10803,16 @@
"license": "MIT"
},
"node_modules/@storybook/addon-docs": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.9.tgz",
"integrity": "sha512-8sFsMkZYrrdqCLdV+hnwTwDF7RaBsBPRwl4wfc8ve9Q/7Yhi5REe/Xjvd8x1yn6fBPPw9tnID9dx6Agdnr81fw==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.10.tgz",
"integrity": "sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "10.5.9",
"@storybook/csf-plugin": "10.5.10",
"@storybook/icons": "^2.0.2",
"@storybook/react-dom-shim": "10.5.9",
"@storybook/react-dom-shim": "10.5.10",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -10785,7 +10823,7 @@
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.9"
"storybook": "10.5.10"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10794,9 +10832,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.9.tgz",
"integrity": "sha512-4H5QIHQVtQYCuL43GCRLGjNQhZpQg9gL03ja0DV80kO2Dn9LEt6ol87bSnSjn4VDgcAXtgTzXFvRLknfVgAAqg==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.10.tgz",
"integrity": "sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10809,7 +10847,7 @@
"peerDependencies": {
"esbuild": "*",
"rollup": "*",
"storybook": "10.5.9",
"storybook": "10.5.10",
"vite": "*",
"webpack": "*"
},
@@ -10829,9 +10867,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.9.tgz",
"integrity": "sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.10.tgz",
"integrity": "sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==",
"dev": true,
"license": "MIT",
"funding": {
@@ -10843,7 +10881,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.9"
"storybook": "10.5.10"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10855,9 +10893,9 @@
}
},
"node_modules/@storybook/addon-links": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.9.tgz",
"integrity": "sha512-ZDbPl6ia6hqjoV+CpQU3DjkXpc0TxUq6+y/rFD8w21dJMdqNWzY8zajHC8r4CfTWANjM1pGPUYisWtTKi1MxZw==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.10.tgz",
"integrity": "sha512-wZQX26vSKEBuQB+EKHuAoeDh0XiUDrN2GMX2lhhj2KpJ/di4OyXFaHXspXOXWkjNtyMTRd0QkCHQqKx4LY3E6w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10870,7 +10908,7 @@
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.9"
"storybook": "10.5.10"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -10964,15 +11002,15 @@
}
},
"node_modules/@storybook/react-webpack5": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.9.tgz",
"integrity": "sha512-mrCJub/WAt6RAU1P+bpBvcdTHMHePXLuNM+TuJl0Sl3r9Ta0YjDvtdODweud9sTEpVN3ao/fk0LpiaDMfLd84w==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.10.tgz",
"integrity": "sha512-PpC2CYbeqKGDYpG64PR/wkjo+Sm9EKWvdE0ZaKHBQb1PfC0p/TUxHy8XfXQp/4atjhET0VqSUDc0TP/ii1bGxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/builder-webpack5": "10.5.9",
"@storybook/preset-react-webpack": "10.5.9",
"@storybook/react": "10.5.9"
"@storybook/builder-webpack5": "10.5.10",
"@storybook/preset-react-webpack": "10.5.10",
"@storybook/react": "10.5.10"
},
"funding": {
"type": "opencollective",
@@ -10981,7 +11019,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.9",
"storybook": "10.5.10",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -10991,13 +11029,13 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.9.tgz",
"integrity": "sha512-XTLC95jP75V9NfhoUzDNKDCn9r0ZqXz4ZhrNzQXVDz4vNWkjU3/uDL6Ywh9WFu6Xrb+onQqSR9hlHjS/DOZj7Q==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.10.tgz",
"integrity": "sha512-9ZBRusBwZStoCj3Wm4wVpSpKV8OlLMpadEQ+vvOVrtDQdVJWM5xdLQpqrG21r/cJC4ZX53ZpbrDL+NMsIp1Rkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.5.9",
"@storybook/core-webpack": "10.5.10",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"cjs-module-lexer": "^1.2.3",
"css-loader": "^7.1.2",
@@ -11019,7 +11057,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.9"
"storybook": "10.5.10"
},
"peerDependenciesMeta": {
"typescript": {
@@ -11028,9 +11066,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5/node_modules/@storybook/core-webpack": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.9.tgz",
"integrity": "sha512-YmXR9RJdQpH8EtWEIjLTr5LMGiCXSmZp/A9UkATY5xHM4qG2QwJB++jU2wv4nGDyaPiUcbzT2PQSXN6RXhiUZA==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.10.tgz",
"integrity": "sha512-CiUo4AZRKm3nVb6KWw+yfV6g6VSTA97msiVN9bJlBOFS1dax2ulrRNJnHJxR/U1AT51hONfU4dZOpCXRwaobTA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11041,17 +11079,17 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.9"
"storybook": "10.5.10"
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.9.tgz",
"integrity": "sha512-LOr8SoM2CejVCHeLyxbdRMiKuQb2q95CE5D75oK3+513mMZ8LxVPVApxy8B6FvFYNe9CTqVK+95jETefOqabTw==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.10.tgz",
"integrity": "sha512-erAyPdNKqzOQOQVHm/kMSO4Nrr/g279KoA5LyzMOTJAQJHjDdsqmYdGXRDVfcnDL+H+Qm9pWaP+re2S8YG6JNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/core-webpack": "10.5.9",
"@storybook/core-webpack": "10.5.10",
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
"@types/semver": "^7.7.1",
"magic-string": "^0.30.5",
@@ -11068,7 +11106,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.9"
"storybook": "10.5.10"
},
"peerDependenciesMeta": {
"typescript": {
@@ -11077,9 +11115,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack/node_modules/@storybook/core-webpack": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.9.tgz",
"integrity": "sha512-YmXR9RJdQpH8EtWEIjLTr5LMGiCXSmZp/A9UkATY5xHM4qG2QwJB++jU2wv4nGDyaPiUcbzT2PQSXN6RXhiUZA==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.10.tgz",
"integrity": "sha512-CiUo4AZRKm3nVb6KWw+yfV6g6VSTA97msiVN9bJlBOFS1dax2ulrRNJnHJxR/U1AT51hONfU4dZOpCXRwaobTA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -11090,18 +11128,18 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "10.5.9"
"storybook": "10.5.10"
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.9.tgz",
"integrity": "sha512-kApGOuNT26NkpioTsr1iT/Q2c44tA7OIsNUSyFqtT7W8k3fRn/jQWfrDegYxty0WG0wxdNMZq2ndRfOOF8HaHw==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.10.tgz",
"integrity": "sha512-4MBV5e1SXIMfPynLHzr+Mp0dwGv/FW1bklWAsS4ynBOAbC98W9p/I9vqBnUctsvE3BJkhzHQQyPwMHL5tTcHVA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/react-dom-shim": "10.5.9",
"@storybook/react-dom-shim": "10.5.10",
"react-docgen": "^8.0.2",
"react-docgen-typescript": "^2.2.2"
},
@@ -11114,7 +11152,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.9",
"storybook": "10.5.10",
"typescript": ">= 4.9.x"
},
"peerDependenciesMeta": {
@@ -11130,9 +11168,9 @@
}
},
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.9.tgz",
"integrity": "sha512-7qZD6CSa64p1m/zX9tG4ALcEHlEk0Bx+5++4RL3MFlRCgCFfAomXsCHTzF0RyF8cI4vl+hS7Y0ryjQchVs6UQA==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.10.tgz",
"integrity": "sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==",
"dev": true,
"license": "MIT",
"funding": {
@@ -11144,7 +11182,7 @@
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"storybook": "10.5.9"
"storybook": "10.5.10"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -15715,9 +15753,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.11.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz",
"integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==",
"version": "2.11.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.16.tgz",
"integrity": "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -20202,9 +20240,9 @@
}
},
"node_modules/eslint-plugin-storybook": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.9.tgz",
"integrity": "sha512-4Hrqccy/zttV0S/32TSUbqtizzj4sbkgYvv7UzHveH58v96PhrO+fSpOFAqYvzNDzdOU6smKwObFtzX+RZqj4w==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.10.tgz",
"integrity": "sha512-NeOu3axmhNZfRuHRciFH0a/OVgvtAJLRHAwiJjcGkPAcElNmwEPp+pqoiwxytx7kHInz7mVVoBfPwVc8kPmOLw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -20212,8 +20250,7 @@
"@typescript-eslint/utils": "^8.60.0"
},
"peerDependencies": {
"eslint": ">=8",
"storybook": "10.5.9"
"eslint": ">=8"
}
},
"node_modules/eslint-plugin-testing-library": {
@@ -28451,9 +28488,9 @@
}
},
"node_modules/mapbox-gl": {
"version": "3.28.1",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.28.1.tgz",
"integrity": "sha512-f8bCHFzZ51bKig7rnD7e08aoFLOV3MNFZduspZ4lgOgiaNVp9sw4NSWcgo3IWTyekYKKXjiICD2BP2o4DiYfxw==",
"version": "3.29.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.29.0.tgz",
"integrity": "sha512-Fnh1WLsZMfihwRZY5scp456iQuZo9G97tTpb26bf/Ejsi/L7O+4dE9+I03VoOor2ul3DEOp6F2P3273omyVsNw==",
"license": "SEE LICENSE IN LICENSE.txt",
"workspaces": [
"src/style-spec",
@@ -31822,34 +31859,34 @@
}
},
"node_modules/oxc-resolver": {
"version": "11.20.0",
"resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.20.0.tgz",
"integrity": "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g==",
"version": "11.21.2",
"resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.2.tgz",
"integrity": "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxc-resolver/binding-android-arm-eabi": "11.20.0",
"@oxc-resolver/binding-android-arm64": "11.20.0",
"@oxc-resolver/binding-darwin-arm64": "11.20.0",
"@oxc-resolver/binding-darwin-x64": "11.20.0",
"@oxc-resolver/binding-freebsd-x64": "11.20.0",
"@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0",
"@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0",
"@oxc-resolver/binding-linux-arm64-gnu": "11.20.0",
"@oxc-resolver/binding-linux-arm64-musl": "11.20.0",
"@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0",
"@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0",
"@oxc-resolver/binding-linux-riscv64-musl": "11.20.0",
"@oxc-resolver/binding-linux-s390x-gnu": "11.20.0",
"@oxc-resolver/binding-linux-x64-gnu": "11.20.0",
"@oxc-resolver/binding-linux-x64-musl": "11.20.0",
"@oxc-resolver/binding-openharmony-arm64": "11.20.0",
"@oxc-resolver/binding-wasm32-wasi": "11.20.0",
"@oxc-resolver/binding-win32-arm64-msvc": "11.20.0",
"@oxc-resolver/binding-win32-x64-msvc": "11.20.0"
"@oxc-resolver/binding-android-arm-eabi": "11.21.2",
"@oxc-resolver/binding-android-arm64": "11.21.2",
"@oxc-resolver/binding-darwin-arm64": "11.21.2",
"@oxc-resolver/binding-darwin-x64": "11.21.2",
"@oxc-resolver/binding-freebsd-x64": "11.21.2",
"@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2",
"@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2",
"@oxc-resolver/binding-linux-arm64-gnu": "11.21.2",
"@oxc-resolver/binding-linux-arm64-musl": "11.21.2",
"@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2",
"@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2",
"@oxc-resolver/binding-linux-riscv64-musl": "11.21.2",
"@oxc-resolver/binding-linux-s390x-gnu": "11.21.2",
"@oxc-resolver/binding-linux-x64-gnu": "11.21.2",
"@oxc-resolver/binding-linux-x64-musl": "11.21.2",
"@oxc-resolver/binding-openharmony-arm64": "11.21.2",
"@oxc-resolver/binding-wasm32-wasi": "11.21.2",
"@oxc-resolver/binding-win32-arm64-msvc": "11.21.2",
"@oxc-resolver/binding-win32-x64-msvc": "11.21.2"
}
},
"node_modules/oxfmt": {
@@ -37700,9 +37737,9 @@
}
},
"node_modules/storybook": {
"version": "10.5.9",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.9.tgz",
"integrity": "sha512-UfdMKSjEhIKr8LbqYyIE5r7vT/drL/PxN75YaouJ+UG0FssEy6cf49OdTF3kstAqVMHskc+zEqyRoiQHZXHwgA==",
"version": "10.5.10",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.10.tgz",
"integrity": "sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -37718,7 +37755,7 @@
"jsonc-parser": "^3.3.1",
"open": "^10.2.0",
"oxc-parser": "^0.127.0",
"oxc-resolver": "^11.19.1",
"oxc-resolver": "11.21.2",
"recast": "^0.23.5",
"semver": "^7.7.3",
"use-sync-external-store": "^1.5.0",
@@ -43453,7 +43490,7 @@
"license": "Apache-2.0",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.28.1",
"mapbox-gl": "^3.29.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.2",
"supercluster": "^9.0.0"
+7 -7
View File
@@ -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,7 +295,7 @@
"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.16",
"cheerio": "1.2.0",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
@@ -312,7 +312,7 @@
"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-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",
@@ -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",
@@ -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,
@@ -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,
@@ -201,6 +201,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}`,
@@ -189,6 +189,8 @@ export default function transformProps(
showLegend,
showValue,
showValueB,
labelPosition,
labelPositionB,
onlyTotal,
onlyTotalB,
stack,
@@ -533,6 +535,7 @@ export default function transformProps(
thresholdValues,
timeShiftColor,
theme,
labelPosition,
},
);
@@ -621,6 +624,7 @@ export default function transformProps(
thresholdValues: thresholdValuesB,
timeShiftColor,
theme,
labelPosition: labelPositionB,
},
);
@@ -32,6 +32,7 @@ import {
ContextMenuTransformedProps,
CrossFilterTransformedProps,
EchartsTimeseriesSeriesType,
LabelPositionEnum,
LegendFormData,
StackType,
TitleFormData,
@@ -86,6 +87,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;
@@ -130,6 +143,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,
@@ -86,6 +86,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
xAxisLabelInterval: defaultXAxis.xAxisLabelInterval,
groupby: [],
showValue: false,
labelPosition: 'auto',
onlyTotal: false,
percentageThreshold: 0,
orientation: OrientationType.Vertical,
@@ -292,6 +292,7 @@ export default function transformProps(
showLegend,
showValue,
size,
labelPosition,
colorByPrimaryAxis,
sliceId,
sortSeriesType,
@@ -752,6 +753,7 @@ export default function transformProps(
theme,
hasDimensions: (groupBy?.length ?? 0) > 0,
colorByPrimaryAxis,
labelPosition,
},
);
if (transformedSeries) {
@@ -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,
@@ -99,6 +100,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;
@@ -140,6 +140,28 @@ export const showValueControl: ControlSetItem = {
},
};
export const labelPositionControl: ControlSetItem = {
name: 'label_position',
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?.value),
},
};
export const colorByPrimaryAxisControl: ControlSetItem = {
name: 'color_by_primary_axis',
config: {
@@ -219,6 +241,7 @@ export const percentageThresholdControl: ControlSetItem = {
export const showValueSection: ControlSetRow[] = [
[showValueControl],
[labelPositionControl],
[stackControl],
[onlyTotalControl],
[percentageThresholdControl],
@@ -230,11 +253,13 @@ export const colorByPrimaryAxisSection: ControlSetRow[] = [
export const showValueSectionWithoutStack: ControlSetRow[] = [
[showValueControl],
[labelPositionControl],
[onlyTotalControl],
];
export const showValueSectionWithoutStream: ControlSetRow[] = [
[showValueControl],
[labelPositionControl],
[stackControlWithoutStream],
[onlyTotalControl],
[percentageThresholdControl],
@@ -0,0 +1,109 @@
/**
* 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 { ControlPanelsContainerProps } from '@superset-ui/chart-controls/types';
import controlPanel from '../../src/MixedTimeseries/controlPanel';
// Narrow shape of the controls under test: enough to exercise `visibility`
// without reaching for `any`.
type VisibilityControl = {
name: string;
config: { visibility: (props: ControlPanelsContainerProps) => boolean };
};
const config = controlPanel;
const getControl = (controlName: string) => {
for (const section of config.controlPanelSections) {
if (section && section.controlSetRows) {
for (const row of section.controlSetRows) {
for (const control of row) {
if (
typeof control === 'object' &&
control !== null &&
'name' in control &&
control.name === controlName
) {
return control;
}
}
}
}
}
return null;
};
// Mock getStandardizedControls
jest.mock('@superset-ui/chart-controls', () => {
const actual = jest.requireActual('@superset-ui/chart-controls');
return {
...actual,
getStandardizedControls: jest.fn(() => ({
popAllMetrics: jest.fn(() => []),
popAllColumns: jest.fn(() => []),
})),
};
});
test('should have correct visibility for label_position', () => {
const labelPositionCtrl = getControl(
'label_position',
) as unknown as VisibilityControl;
expect(labelPositionCtrl).toBeDefined();
expect(labelPositionCtrl.config.visibility).toBeDefined();
expect(
labelPositionCtrl.config.visibility({
controls: {
show_value: { value: true },
},
} as unknown as ControlPanelsContainerProps),
).toBe(true);
expect(
labelPositionCtrl.config.visibility({
controls: {
show_value: { value: false },
},
} as unknown as ControlPanelsContainerProps),
).toBe(false);
});
test('should have correct visibility for label_positionB', () => {
const labelPositionBCtrl = getControl(
'label_positionB',
) as unknown as VisibilityControl;
expect(labelPositionBCtrl).toBeDefined();
expect(labelPositionBCtrl.config.visibility).toBeDefined();
expect(
labelPositionBCtrl.config.visibility({
controls: {
show_valueB: { value: true },
},
} as unknown as ControlPanelsContainerProps),
).toBe(true);
expect(
labelPositionBCtrl.config.visibility({
controls: {
show_valueB: { value: false },
},
} as unknown as ControlPanelsContainerProps),
).toBe(false);
});
@@ -34,6 +34,7 @@ import {
LegendOrientation,
LegendType,
EchartsTimeseriesSeriesType,
LabelPositionEnum,
} from '../../src';
import transformProps from '../../src/MixedTimeseries/transformProps';
import {
@@ -42,7 +43,7 @@ import {
EchartsMixedTimeseriesProps,
} from '../../src/MixedTimeseries/types';
import { createEchartsTimeseriesTestChartProps } from '../helpers';
import type { SeriesOption } from 'echarts';
import type { BarSeriesOption, LineSeriesOption, SeriesOption } from 'echarts';
type LabelFormatterParams = {
value: [number, number];
@@ -433,6 +434,58 @@ test('keeps bar value label clipping aligned with the assigned y-axis', () => {
expect(formatSeriesLabel(barSeries, [timestamp, 0.5])).toBe('');
});
test('threads labelPosition and labelPositionB to series A and B', () => {
const timestamp = 1704067200000;
const queryAData = createTestQueryData(
[{ __timestamp: timestamp, lineMetric: 0.25 }],
{
colnames: ['__timestamp', 'lineMetric'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { lineMetric: ['lineMetric'] },
},
);
const queryBData = createTestQueryData(
[{ __timestamp: timestamp, barMetric: 0.5 }],
{
colnames: ['__timestamp', 'barMetric'],
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
label_map: { 'barMetric (1)': ['barMetric'] },
},
);
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: [queryAData, queryBData],
formData: {
...formData,
groupby: [],
groupbyB: [],
metrics: ['lineMetric'],
metricsB: ['barMetric'],
showValue: true,
showValueB: true,
labelPosition: LabelPositionEnum.Inside,
labelPositionB: LabelPositionEnum.Bottom,
stack: null,
stackB: null,
x_axis: '__timestamp',
},
queriesData: [queryAData, queryBData],
});
const { echartOptions } = transformProps(chartProps);
// `SeriesOption` is a union across every echarts series type and does not
// carry `label`; the two series under test are a line and a bar.
const series = echartOptions.series as (LineSeriesOption | BarSeriesOption)[];
const seriesA = series.find(s => s.name === 'lineMetric');
const seriesB = series.find(s => s.name === 'barMetric');
expect(seriesA?.label?.position).toBe(LabelPositionEnum.Inside);
expect(seriesB?.label?.position).toBe(LabelPositionEnum.Bottom);
});
describe('legend sorting', () => {
const getChartProps = (overrides = {}) =>
createEchartsTimeseriesTestChartProps<
@@ -25,6 +25,13 @@ import {
} from '../../../src/constants';
import { OrientationType } from '../../../src/Timeseries/types';
// Narrow shape of the control under test: enough to exercise `visibility`
// without reaching for `any`.
type VisibilityControl = {
name: string;
config: { visibility: (props: ControlPanelsContainerProps) => boolean };
};
const config = controlPanel;
const getControl = (controlName: string) => {
@@ -292,3 +299,42 @@ test('x_axis_time_format should be hidden for numeric columns', () => {
false,
);
});
test('should have visibility function for label_position', () => {
const labelPositionCtrl = getControl(
'label_position',
) as unknown as VisibilityControl;
expect(labelPositionCtrl).toBeDefined();
expect(labelPositionCtrl.config.visibility).toBeDefined();
expect(typeof labelPositionCtrl.config.visibility).toBe('function');
expect(
labelPositionCtrl.config.visibility({
controls: {
show_value: { value: true },
show_valueB: { value: false },
},
} as unknown as ControlPanelsContainerProps),
).toBe(true);
// Visibility follows `show_value` alone. No Timeseries panel defines
// `show_valueB` — Mixed declares its own suffixed controls — so it must not
// reveal the control on its own.
expect(
labelPositionCtrl.config.visibility({
controls: {
show_value: { value: false },
show_valueB: { value: true },
},
} as unknown as ControlPanelsContainerProps),
).toBe(false);
expect(
labelPositionCtrl.config.visibility({
controls: {
show_value: { value: false },
show_valueB: { value: false },
},
} as unknown as ControlPanelsContainerProps),
).toBe(false);
});
@@ -48,6 +48,7 @@ import {
LegendOrientation,
LegendType,
EchartsTimeseriesChartProps,
LabelPositionEnum,
} from '../../src/types';
import { DEFAULT_FORM_DATA } from '../../src/Timeseries/constants';
import { createEchartsTimeseriesTestChartProps } from '../helpers';
@@ -747,6 +748,146 @@ describe('Does transformProps transform series correctly', () => {
});
});
test('should respect labelPosition configuration', () => {
const chartProps = createTestChartProps({
formData: {
...formData,
labelPosition: LabelPositionEnum.InsideBottom,
},
queriesData,
});
const transformedSeries = transformProps(chartProps).echartOptions
.series as any[];
transformedSeries.forEach(series => {
expect(series.label.position).toBe('insideBottom');
expect(series.label.overflow).toBeUndefined();
});
});
test('should default to top when labelPosition is auto and orientation is vertical', () => {
const chartProps = createTestChartProps({
formData: {
...formData,
labelPosition: 'auto',
orientation: OrientationType.Vertical,
},
queriesData,
});
const transformedSeries = transformProps(chartProps).echartOptions
.series as any[];
transformedSeries.forEach(series => {
expect(series.label.position).toBe('top');
expect(series.label.overflow).toBeUndefined();
});
});
test('should default to right when labelPosition is auto and orientation is horizontal', () => {
const chartProps = createTestChartProps({
formData: {
...formData,
labelPosition: 'auto',
orientation: OrientationType.Horizontal,
},
queriesData,
});
const transformedSeries = transformProps(chartProps).echartOptions
.series as any[];
transformedSeries.forEach(series => {
expect(series.label.position).toBe('right');
expect(series.label.overflow).toBeUndefined();
});
});
test('should default to right when labelPosition is unset and orientation is horizontal', () => {
const chartProps = createTestChartProps({
formData: {
...formData,
labelPosition: undefined,
orientation: OrientationType.Horizontal,
},
queriesData,
});
const transformedSeries = transformProps(chartProps).echartOptions
.series as any[];
transformedSeries.forEach(series => {
expect(series.label.position).toBe('right');
expect(series.label.overflow).toBeUndefined();
});
});
test('should set overflow: truncate only for bar series', () => {
const barChartProps = createTestChartProps({
formData: {
...formData,
seriesType: EchartsTimeseriesSeriesType.Bar,
},
queriesData,
});
const lineChartProps = createTestChartProps({
formData: {
...formData,
seriesType: EchartsTimeseriesSeriesType.Line,
},
queriesData,
});
const barSeries = transformProps(barChartProps).echartOptions
.series as any[];
const lineSeries = transformProps(lineChartProps).echartOptions
.series as any[];
barSeries.forEach(series => {
expect(series.label.overflow).toBe('truncate');
});
lineSeries.forEach(series => {
expect(series.label.overflow).toBeUndefined();
});
});
test('should respect labelPosition for negative values in unstacked bar charts', () => {
const negativeQueriesData = [
createTestQueryData(
createTestData(
[
{
'San Francisco': -1,
'New York': 2,
},
],
{ intervalMs: 300000000 },
),
),
];
const chartProps = createTestChartProps({
formData: {
...formData,
seriesType: EchartsTimeseriesSeriesType.Bar,
stack: false,
labelPosition: LabelPositionEnum.Inside,
},
queriesData: negativeQueriesData,
});
const transformedSeries = transformProps(chartProps).echartOptions
.series as any[];
expect(transformedSeries[0].data[0]).toEqual({
value: [expect.any(Number), -1],
label: {
position: 'inside',
},
});
});
test('should show only totals when onlyTotal is true', () => {
const chartProps = createTestChartProps({
formData: { ...formData, onlyTotal: true },
@@ -30,7 +30,7 @@
},
"dependencies": {
"@math.gl/web-mercator": "^4.1.0",
"mapbox-gl": "^3.28.1",
"mapbox-gl": "^3.29.0",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.2",
"supercluster": "^9.0.0"
@@ -0,0 +1,64 @@
/**
* 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.
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import { render, screen } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import '@testing-library/jest-dom';
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
import type { ReactElement } from 'react';
import Tooltip from './Tooltip';
const renderWithTheme = (component: ReactElement) =>
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
const tooltip = {
x: 10,
y: 20,
content: <span data-test="tooltip-content">Fremont: 42</span>,
};
test('renders an opaque, themed surface for the default variant', () => {
renderWithTheme(<Tooltip tooltip={tooltip} />);
const container = screen.getByTestId('tooltip-content').parentElement;
expect(container).toHaveStyle({
background: supersetTheme.colorBgElevated,
color: supersetTheme.colorText,
});
});
test('renders an opaque, themed surface for the custom (handlebars) variant, see #41154', () => {
renderWithTheme(<Tooltip tooltip={tooltip} variant="custom" />);
// Custom tooltip content comes from a user-supplied handlebars template, so
// the container must supply its own background instead of letting the map
// show through. See https://github.com/apache/superset/issues/41154
const container = screen.getByTestId('tooltip-content').parentElement;
expect(container).toHaveStyle({
background: supersetTheme.colorBgElevated,
color: supersetTheme.colorText,
'border-radius': `${supersetTheme.borderRadius}px`,
padding: `${supersetTheme.sizeUnit * 2}px`,
});
});
test('renders nothing without a tooltip', () => {
const { container } = renderWithTheme(<Tooltip tooltip={null} />);
expect(container).toBeEmptyDOMElement();
});
@@ -44,23 +44,15 @@ const StyledDiv = styled.div<{
left: ${left}px;
z-index: 9;
pointer-events: none;
${
variant === 'default'
? `
padding: ${theme.sizeUnit * 2}px;
margin: ${theme.sizeUnit * 2}px;
background: ${theme.colorBgElevated};
color: ${theme.colorText};
max-width: 300px;
font-size: ${theme.fontSizeSM}px;
border: 1px solid ${theme.colorBorder};
border-radius: ${theme.borderRadius}px;
box-shadow: ${theme.boxShadowSecondary};
`
: `
margin: ${theme.sizeUnit * 3}px;
`
}
padding: ${theme.sizeUnit * 2}px;
margin: ${variant === 'default' ? theme.sizeUnit * 2 : theme.sizeUnit * 3}px;
background: ${theme.colorBgElevated};
color: ${theme.colorText};
max-width: 300px;
font-size: ${theme.fontSizeSM}px;
border: 1px solid ${theme.colorBorder};
border-radius: ${theme.borderRadius}px;
box-shadow: ${theme.boxShadowSecondary};
`}
`;
@@ -22,7 +22,7 @@ import { t } from '@apache-superset/core/translation';
import { isDefined, NativeFilterScope } from '@superset-ui/core';
import { Alert } from '@apache-superset/core/components';
import { css, styled, useTheme } from '@apache-superset/core/theme';
import { Select, Tooltip } from '@superset-ui/core/components';
import { Select } from '@superset-ui/core/components';
import { noOp } from 'src/utils/common';
import ScopingTree from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/ScopingTree';
import {
@@ -33,7 +33,6 @@ import {
} from 'src/dashboard/types';
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
import type { SelectOptionsType } from '@superset-ui/core/components';
import { Icons } from '@superset-ui/core/components/Icons';
import { NEW_CHART_SCOPING_ID } from './constants';
interface ScopingTreePanelProps {
@@ -109,16 +108,6 @@ const ChartSelect = ({
margin-bottom: 0;
`}
>{`${t('Chart')} *`}</InfoText>
<Tooltip title={t('Tooltip')} placement="top">
<Icons.InfoCircleOutlined
iconSize="xs"
css={css`
& > span {
line-height: 0;
}
`}
/>
</Tooltip>
</div>
<Select
data-test="select-chart"
@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import {
render,
screen,
waitFor,
userEvent,
fireEvent,
} from 'spec/helpers/testing-library';
import Control, { ControlProps } from 'src/explore/components/Control';
const defaultProps: ControlProps = {
@@ -77,3 +83,72 @@ test('call setControlValue if isVisible is false', async () => {
expect(defaultProps.actions.setControlValue).toHaveBeenCalled(),
);
});
test('shows the description icon while the control is hovered', async () => {
render(
setup({
label: 'My checkbox',
description: 'Help text',
}),
);
expect(
screen.queryByRole('button', { name: 'Show info tooltip' }),
).not.toBeInTheDocument();
await userEvent.hover(screen.getByTestId('checkbox'));
expect(
screen.getByRole('button', { name: 'Show info tooltip' }),
).toBeInTheDocument();
await userEvent.unhover(screen.getByTestId('checkbox'));
expect(
screen.queryByRole('button', { name: 'Show info tooltip' }),
).not.toBeInTheDocument();
});
test('shows the description icon while the control has keyboard focus', () => {
render(
setup({
label: 'My checkbox',
description: 'Help text',
}),
);
expect(
screen.queryByRole('button', { name: 'Show info tooltip' }),
).not.toBeInTheDocument();
fireEvent.focus(screen.getByRole('checkbox'));
const infoIcon = screen.getByRole('button', { name: 'Show info tooltip' });
expect(infoIcon).toBeInTheDocument();
fireEvent.blur(screen.getByRole('checkbox'), { relatedTarget: infoIcon });
expect(
screen.getByRole('button', { name: 'Show info tooltip' }),
).toBeInTheDocument();
fireEvent.blur(infoIcon, { relatedTarget: document.body });
expect(
screen.queryByRole('button', { name: 'Show info tooltip' }),
).not.toBeInTheDocument();
});
test('keeps the description icon visible when the pointer leaves a focused control', () => {
render(
setup({
label: 'My checkbox',
description: 'Help text',
}),
);
fireEvent.focus(screen.getByRole('checkbox'));
expect(
screen.getByRole('button', { name: 'Show info tooltip' }),
).toBeInTheDocument();
fireEvent.mouseLeave(screen.getByTestId('checkbox'));
expect(
screen.getByRole('button', { name: 'Show info tooltip' }),
).toBeInTheDocument();
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, useCallback, useState, useEffect } from 'react';
import { ReactNode, useCallback, useState, useEffect, FocusEvent } from 'react';
import { isEqual } from 'lodash-es';
import {
ControlType,
@@ -70,7 +70,18 @@ export default function Control(props: ControlProps) {
} = props;
const [hovered, setHovered] = useState(false);
const [focused, setFocused] = useState(false);
const wasVisible = usePrevious(isVisible);
const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
if (
!(event.relatedTarget instanceof Node) ||
!event.currentTarget.contains(event.relatedTarget)
) {
setFocused(false);
}
};
const onChange = useCallback(
(value: any, errors: any[]) => setControlValue(name, value, errors),
[name, setControlValue],
@@ -119,9 +130,15 @@ export default function Control(props: ControlProps) {
style={hidden ? { display: 'none' } : undefined}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onFocus={() => setFocused(true)}
onBlur={handleBlur}
>
<ErrorBoundary>
<ControlComponent onChange={onChange} hovered={hovered} {...props} />
<ControlComponent
onChange={onChange}
hovered={hovered || focused}
{...props}
/>
</ErrorBoundary>
</StyledControl>
);
@@ -0,0 +1,112 @@
/**
* 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 {
render,
screen,
userEvent,
fireEvent,
} from 'spec/helpers/testing-library';
import ControlHeader from './ControlHeader';
const description = 'This control filters the whole chart.';
test('does not render the description icon until the control is hovered', () => {
const { rerender } = render(
<ControlHeader
name="time_range"
label="Date Range"
description={description}
/>,
);
expect(
screen.queryByRole('button', { name: 'Show info tooltip' }),
).not.toBeInTheDocument();
rerender(
<ControlHeader
name="time_range"
label="Date Range"
description={description}
hovered
/>,
);
expect(
screen.getByRole('button', { name: 'Show info tooltip' }),
).toBeInTheDocument();
});
test('notifies onDescriptionHoverChange when the info icon is hovered', async () => {
const onDescriptionHoverChange = jest.fn();
render(
<ControlHeader
name="time_range"
label="Date Range"
description={description}
hovered
onDescriptionHoverChange={onDescriptionHoverChange}
/>,
);
const infoIcon = screen.getByRole('button', { name: 'Show info tooltip' });
await userEvent.hover(infoIcon);
expect(onDescriptionHoverChange).toHaveBeenCalledWith(true);
await userEvent.unhover(infoIcon);
expect(onDescriptionHoverChange).toHaveBeenCalledWith(false);
});
test('notifies onDescriptionHoverChange when the info icon is focused', () => {
const onDescriptionHoverChange = jest.fn();
render(
<ControlHeader
name="time_range"
label="Date Range"
description={description}
hovered
onDescriptionHoverChange={onDescriptionHoverChange}
/>,
);
const infoIcon = screen.getByRole('button', { name: 'Show info tooltip' });
fireEvent.focus(infoIcon);
expect(onDescriptionHoverChange).toHaveBeenCalledWith(true);
fireEvent.blur(infoIcon);
expect(onDescriptionHoverChange).toHaveBeenCalledWith(false);
});
test('activates tooltipOnClick from the keyboard', () => {
const tooltipOnClick = jest.fn();
render(
<ControlHeader
name="time_range"
label="Date Range"
description={description}
hovered
tooltipOnClick={tooltipOnClick}
/>,
);
fireEvent.keyDown(screen.getByRole('button', { name: 'Show info tooltip' }), {
key: 'Enter',
});
expect(tooltipOnClick).toHaveBeenCalledTimes(1);
});
@@ -38,6 +38,7 @@ export type ControlHeaderProps = {
tooltipOnClick?: () => void;
warning?: string;
danger?: string;
onDescriptionHoverChange?: (hovered: boolean) => void;
// Allow extra props from control spread patterns (e.g. {...this.props})
[key: string]: unknown;
};
@@ -71,6 +72,7 @@ const ControlHeader: FC<ControlHeaderProps> = ({
tooltipOnClick = () => {},
warning,
danger,
onDescriptionHoverChange,
}) => {
const theme = useTheme();
@@ -89,24 +91,44 @@ const ControlHeader: FC<ControlHeaderProps> = ({
position: absolute;
top: 50%;
right: 0;
z-index: 1;
padding-left: ${theme.sizeUnit}px;
transform: translate(100%, -50%);
white-space: nowrap;
pointer-events: auto;
`}
>
{description && (
<span>
<>
<Tooltip
id="description-tooltip"
title={description}
placement="top"
mouseLeaveDelay={0}
trigger={['hover', 'focus']}
>
<Icons.InfoCircleOutlined
css={iconStyles}
{/* Same role="button" pattern as the label text: a real <button>
is not valid inside FormLabel's <label>. */}
<span
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
tabIndex={0}
data-test={`${name}-description-icon`}
aria-label={t('Show info tooltip')}
onMouseEnter={() => onDescriptionHoverChange?.(true)}
onMouseLeave={() => onDescriptionHoverChange?.(false)}
onFocus={() => onDescriptionHoverChange?.(true)}
onBlur={() => onDescriptionHoverChange?.(false)}
onClick={tooltipOnClick}
/>
onKeyDown={handleKeyboardActivation(tooltipOnClick)}
css={css`
cursor: pointer;
`}
>
<Icons.InfoCircleOutlined css={iconStyles} />
</span>
</Tooltip>{' '}
</span>
</>
)}
{renderTrigger && (
<span>
@@ -147,6 +147,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
onOpenPopover = noOp,
onClosePopover = noOp,
isOverflowingFilterBar = false,
hovered: isControlHovered = false,
} = props;
const defaultTimeFilter = useDefaultTimeFilter();
@@ -161,9 +162,16 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
const [validTimeRange, setValidTimeRange] = useState<boolean>(false);
const [evalResponse, setEvalResponse] = useState<string>(value);
const [tooltipTitle, setTooltipTitle] = useState<ReactNode | null>(t(value));
const [isDescriptionHovered, setIsDescriptionHovered] = useState(false);
const theme = useTheme();
const [labelRef, labelIsTruncated] = useCSSTextTruncation<HTMLSpanElement>();
useEffect(() => {
if (!isControlHovered) {
setIsDescriptionHovered(false);
}
}, [isControlHovered]);
useEffect(() => {
if (value === NO_TIME_RANGE) {
setActualTimeRange(NO_TIME_RANGE);
@@ -368,7 +376,12 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
}
overlayClassName="time-range-popover"
>
<Tooltip placement="top" title={tooltipTitle}>
<Tooltip
placement="top"
title={isDescriptionHovered ? null : tooltipTitle}
mouseLeaveDelay={0}
overlayStyle={{ pointerEvents: 'none' }}
>
{/* Wrap in a span so the Popover gets a stable DOM ref target;
DateLabel forwards its ref to an inner span used for measuring
text truncation, which would otherwise become the popover's
@@ -390,7 +403,10 @@ export default function DateFilterLabel(props: DateFilterControlProps) {
return (
<>
<ControlHeader {...props} />
<ControlHeader
{...props}
onDescriptionHoverChange={setIsDescriptionHovered}
/>
{popoverContent}
</>
);
@@ -18,16 +18,35 @@
*/
import thunk from 'redux-thunk';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import configureMockStore from 'redux-mock-store';
import { render, screen, userEvent } from 'spec/helpers/testing-library';
import {
render,
screen,
userEvent,
waitFor,
fireEvent,
} from 'spec/helpers/testing-library';
import { NO_TIME_RANGE } from '@superset-ui/core';
import { NO_TIME_RANGE, fetchTimeRange } from '@superset-ui/core';
import DateFilterLabel from '..';
import { DateFilterControlProps } from '../types';
import { DateFilterTestKey } from '../utils';
const mockStore = configureStore([thunk]);
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
fetchTimeRange: jest.fn(),
}));
const mockedFetchTimeRange = fetchTimeRange as jest.MockedFunction<
typeof fetchTimeRange
>;
const FIELD_TOOLTIP = '2024-01-01 ≤ col < 2024-01-08';
const DESCRIPTION_TOOLTIP =
'This control filters the whole chart based on the selected time range.';
const mockStore = configureMockStore([thunk]);
const defaultProps = {
onChange: jest.fn(),
@@ -35,6 +54,11 @@ const defaultProps = {
onOpenPopover: jest.fn(),
};
beforeEach(() => {
mockedFetchTimeRange.mockReset();
mockedFetchTimeRange.mockResolvedValue({ value: FIELD_TOOLTIP });
});
function setup(
props: Omit<DateFilterControlProps, 'name'> = defaultProps,
store: any = mockStore({}),
@@ -136,3 +160,61 @@ test('DateFilter should properly handle isOverflowingFilterBar prop changes', ()
expect(popoverAfterRerender?.parentElement).toBe(trigger.parentElement);
expect(popoverAfterRerender?.parentElement).not.toBe(document.body);
});
test('hovering the description icon does not show the date range tooltip', async () => {
const tooltipOnClick = jest.fn();
render(
setup({
...defaultProps,
value: 'Last week',
label: 'Date Range',
description: DESCRIPTION_TOOLTIP,
hovered: true,
tooltipOnClick,
}),
);
await waitFor(() => {
expect(screen.getByText('Last week')).toBeInTheDocument();
});
await userEvent.hover(screen.getByText('Last week'));
expect(await screen.findByRole('tooltip')).toHaveTextContent(FIELD_TOOLTIP);
const descriptionIcon = screen.getByRole('button', {
name: 'Show info tooltip',
});
fireEvent.focus(descriptionIcon);
await waitFor(() => {
expect(screen.getByRole('tooltip')).toHaveTextContent(DESCRIPTION_TOOLTIP);
expect(screen.getByRole('tooltip')).not.toHaveTextContent(FIELD_TOOLTIP);
expect(screen.getAllByRole('tooltip')).toHaveLength(1);
});
fireEvent.blur(descriptionIcon);
await waitFor(() => {
expect(screen.getByRole('tooltip')).toHaveTextContent(FIELD_TOOLTIP);
expect(screen.getAllByRole('tooltip')).toHaveLength(1);
});
await userEvent.unhover(screen.getByText('Last week'));
await waitFor(() => {
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
await userEvent.hover(descriptionIcon);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent(DESCRIPTION_TOOLTIP);
expect(tooltip).not.toHaveTextContent(FIELD_TOOLTIP);
expect(screen.getAllByRole('tooltip')).toHaveLength(1);
await userEvent.unhover(descriptionIcon);
await waitFor(() => {
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});
fireEvent.keyDown(descriptionIcon, { key: 'Enter' });
expect(tooltipOnClick).toHaveBeenCalled();
});
@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode } from 'react';
export type SelectOptionType = {
value: string;
label: string;
@@ -113,4 +115,8 @@ export interface DateFilterControlProps {
onOpenPopover?: () => void;
onClosePopover?: () => void;
isOverflowingFilterBar?: boolean;
hovered?: boolean;
description?: ReactNode;
label?: ReactNode;
tooltipOnClick?: () => void;
}
@@ -207,6 +207,38 @@ describe('AdhocFilter', () => {
expect(adhocFilter10.isValid()).toBe(true);
});
test('is invalid when a comparator-taking operator has no comparator', () => {
// A comparator that was never set, or that was cleared through the value
// Select's clear affordance, is `undefined` rather than `null` or `[]`.
const adhocFilter1 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'is_intro',
operator: 'IN',
comparator: undefined,
clause: Clauses.Where,
});
expect(adhocFilter1.isValid()).toBe(false);
const adhocFilter2 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'is_intro',
operator: '==',
comparator: undefined,
clause: Clauses.Where,
});
expect(adhocFilter2.isValid()).toBe(false);
// `false` is a legitimate boolean comparator, not a missing value
const adhocFilter3 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'is_intro',
operator: '==',
comparator: false,
clause: Clauses.Where,
});
expect(adhocFilter3.isValid()).toBe(true);
});
test('can translate from simple expressions to sql expressions', () => {
const adhocFilter1 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
@@ -182,8 +182,10 @@ export default class AdhocFilter {
// A non-empty array of values ('IN' or 'NOT IN' clauses)
return this.comparator.length > 0;
}
// A value has been selected or typed
return this.comparator !== null;
// A value has been selected or typed. An unset comparator is
// `undefined` rather than `null`: picking a new subject resets it, and
// the value Select's clear affordance emits `undefined` too.
return this.comparator != null;
}
}
@@ -181,6 +181,29 @@ describe('AdhocFilterEditPopover', () => {
expect(saveButton).toBeDisabled();
});
test('disables save button when a boolean column has no value selected', async () => {
const booleanColumn = { type: 'BOOL', column_name: 'is_intro' };
renderPopover({
adhocFilter: new AdhocFilter({
expressionType: ExpressionTypes.Simple,
clause: Clauses.Where,
}),
options: [booleanColumn],
datasource: { columns: [booleanColumn], filter_select: false },
});
// Picking the subject resets the comparator to `undefined`; the value
// control is then left untouched, mirroring the reported repro.
await userEvent.click(screen.getByTestId('select-element'));
await userEvent.click(
await screen.findByRole('option', { name: /is_intro/ }),
);
expect(
screen.getByTestId('adhoc-filter-edit-popover-save-button'),
).toBeDisabled();
});
test('initiates resize when resize handle is dragged', async () => {
const onResize = jest.fn();
renderPopover({ onResize });
@@ -0,0 +1,116 @@
/**
* 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 type { WorkBook } from 'xlsx';
import { getNumberFormatterRegistry } from '@superset-ui/core';
import exportPivotExcel from './downloadAsPivotExcel';
const mockWriteFile = jest.fn();
jest.mock('xlsx', () => {
const actual = jest.requireActual('xlsx');
return {
...actual,
writeFile: (...args: unknown[]) => mockWriteFile(...args),
};
});
// Renders a single-row pivot table with the given cell values, runs the
// export, and returns the resulting sheet so each test only has to state
// its input cells and assertions.
function exportRowAndGetSheet(cells: string[]): WorkBook['Sheets'][string] {
document.body.innerHTML = `
<table id="pivot-table">
<tbody>
<tr>
${cells.map(cell => `<td>${cell}</td>`).join('\n ')}
</tr>
</tbody>
</table>
`;
exportPivotExcel('#pivot-table', 'export');
const workbook = mockWriteFile.mock.calls.at(-1)?.[0] as WorkBook;
return workbook.Sheets[workbook.SheetNames[0]];
}
test('preserves locale-formatted numbers exactly as rendered, without SheetJS reinterpreting them', () => {
const sheet = exportRowAndGetSheet(['1.234,56', '12,50%', '3.500']);
expect(mockWriteFile).toHaveBeenCalledTimes(1);
// These are Spanish-locale D3_FORMAT strings ("." as thousands separator,
// "," as decimal separator). Each must survive the export untouched, as a
// text cell, rather than being silently reparsed as a different number
// (SheetJS's default HTML table parsing would otherwise turn "1.234,56"
// into the number 1.23456, "3.500" into 3.5, and "12,50%" into 12.5).
expect(sheet.A1).toMatchObject({ t: 's', v: '1.234,56' });
expect(sheet.B1).toMatchObject({ t: 's', v: '12,50%' });
expect(sheet.C1).toMatchObject({ t: 's', v: '3.500' });
});
test('restores unambiguous plain numbers to native Excel numeric cells', () => {
const sheet = exportRowAndGetSheet(['42', '-3.5', '3.500', '1,234']);
// "42" and "-3.5" round-trip exactly through Number(), so they're
// unambiguous under any locale and are restored to real numbers.
expect(sheet.A1).toMatchObject({ t: 'n', v: 42 });
expect(sheet.B1).toMatchObject({ t: 'n', v: -3.5 });
// "3.500" (trailing zero padding) and "1,234" (grouped thousands) don't
// round-trip, so they stay as text rather than risk misparsing them.
expect(sheet.C1).toMatchObject({ t: 's', v: '3.500' });
expect(sheet.D1).toMatchObject({ t: 's', v: '1,234' });
});
test('does not restore grouped-thousands numbers under a "." thousands-separator locale', () => {
const registry = getNumberFormatterRegistry();
const original = registry.d3Format;
registry.setD3Format({ decimal: ',', thousands: '.', grouping: [3] });
try {
// Under a Spanish-style D3_FORMAT, "1.234" is the plain integer 1234
// rendered with a "." group separator, not the decimal 1.234. It also
// round-trips cleanly through Number(), so without the locale check it
// would be misrestored to the number 1.234, silently corrupting the
// value this PR exists to preserve. "42" has no "." and still round
// trips safely, so it's still restored.
const sheet = exportRowAndGetSheet(['1.234', '42']);
expect(sheet.A1).toMatchObject({ t: 's', v: '1.234' });
expect(sheet.B1).toMatchObject({ t: 'n', v: 42 });
} finally {
registry.setD3Format(original);
}
});
test('leaves date-shaped strings as text rather than reinterpreting them as dates', () => {
const sheet = exportRowAndGetSheet([
'2024-01-01',
'2024-01-01 13:45:30',
'not-a-date',
]);
// A rendered string can't be reliably classified as a genuine date rather
// than a coincidentally date-shaped formatted number (e.g. a custom
// D3_FORMAT grouping/thousands locale can render a plain metric like
// 20240101 as "2024-01-01"), so date-shaped cells are left exactly as
// rendered instead of being reinterpreted as native Excel dates.
expect(sheet.A1).toMatchObject({ t: 's', v: '2024-01-01' });
expect(sheet.B1).toMatchObject({ t: 's', v: '2024-01-01 13:45:30' });
expect(sheet.C1).toMatchObject({ t: 's', v: 'not-a-date' });
});
@@ -16,13 +16,63 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getNumberFormatterRegistry } from '@superset-ui/core';
import { utils, writeFile } from 'xlsx';
import type { WorkSheet } from 'xlsx';
// `raw: true` (used below) keeps every table cell as text, so ordinary
// numbers lose their native Excel type along with the locale-formatted
// values. A cell's text is only restored to a real number when it is
// unambiguous under the active D3_FORMAT locale: a plain number that
// round-trips losslessly through Number() (e.g. "42" or "-3.5"). Restoring
// those can't reintroduce the misparsing raw: true guards against. Anything
// else (grouped thousands, percent suffixes, trailing zero padding,
// date-shaped text, other D3_FORMAT output, etc.) stays as text, exactly as
// rendered: a rendered string can't be reliably classified as a genuine date
// rather than a coincidentally date-shaped formatted number (e.g. a custom
// D3_FORMAT grouping/thousands locale can render a plain metric like
// 20240101 as "2024-01-01"), so cells are never reinterpreted as dates.
//
// Number()'s round-trip check assumes "." is a decimal point, which isn't
// true under every locale: a Spanish D3_FORMAT (thousands: '.') renders the
// plain integer 1234 as "1.234", which also round-trips through Number() as
// the decimal 1.234. When the active locale uses "." as its thousands
// separator, a cell containing "." can't be trusted as an unambiguous
// decimal, so it's left as text instead.
function restoreUnambiguousNumbers(sheet: WorkSheet): void {
const { thousands } = getNumberFormatterRegistry().d3Format;
Object.keys(sheet).forEach(cellRef => {
if (cellRef.startsWith('!')) {
return;
}
const cell = sheet[cellRef];
if (!cell || cell.t !== 's' || typeof cell.v !== 'string') {
return;
}
if (thousands === '.' && cell.v.includes('.')) {
return;
}
const value = Number(cell.v);
if (cell.v !== '' && Number.isFinite(value) && String(value) === cell.v) {
cell.t = 'n';
cell.v = value;
}
});
}
export default function exportPivotExcel(
tableSelector: string,
fileName: string,
) {
const table = document.querySelector(tableSelector);
const workbook = utils.table_to_book(table);
// `raw: true` keeps every cell as the literal text rendered in the DOM.
// Without it, SheetJS tries to infer numbers/dates from the displayed
// string, which mangles values that were formatted using a non-US
// D3_FORMAT (e.g. "1.234,56" gets misread as a date or truncated number).
const workbook = utils.table_to_book(table, { raw: true });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
if (sheet) {
restoreUnambiguousNumbers(sheet);
}
writeFile(workbook, `${fileName}.xlsx`);
}
+3 -11
View File
@@ -21,7 +21,7 @@ from io import BytesIO
from typing import Any, cast, Optional
from zipfile import is_zipfile, ZipFile
from flask import current_app, redirect, request, Response, send_file, url_for
from flask import current_app, redirect, request, Response, url_for
from flask_appbuilder.api import expose, protect, rison as parse_rison, safe
from flask_appbuilder.hooks import before_request
from flask_appbuilder.models.sqla.interface import SQLAInterface
@@ -103,7 +103,7 @@ from superset.subjects.filters import (
from superset.tasks.thumbnails import cache_chart_thumbnail
from superset.tasks.utils import get_current_user
from superset.utils import json
from superset.utils.core import sanitize_cookie_token
from superset.utils.core import send_export_zip
from superset.utils.screenshots import (
ChartScreenshot,
DEFAULT_CHART_WINDOW_SIZE,
@@ -1293,15 +1293,7 @@ class ChartRestApi(SoftDeleteApiMixin, BaseSupersetModelRestApi):
return self.response_404()
buf.seek(0)
response = send_file(
buf,
mimetype="application/zip",
as_attachment=True,
download_name=filename,
)
if token := sanitize_cookie_token(request.args.get("token")):
response.set_cookie(token, "done", max_age=600)
return response
return send_export_zip(buf, filename)
@expose("/favorite_status/", methods=("GET",))
@protect()
+11 -4
View File
@@ -91,8 +91,10 @@ class ExportChartsCommand(ExportModelsCommand):
def enable_tag_export(cls) -> None:
cls._include_tags = True
def run(self) -> Iterator[tuple[str, Callable[[], str]]]:
yield from super().run()
def run(
self, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
yield from super().run(seen=seen)
# Tags are exported once for all requested charts (rather than per
# chart in `_export`) so a multi-chart export doesn't lose tags to
@@ -108,12 +110,17 @@ class ExportChartsCommand(ExportModelsCommand):
@staticmethod
def _export(
model: Slice, export_related: bool = True
model: Slice, export_related: bool = True, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
# Initialize seen set if not provided
if seen is None:
seen = set()
yield (
ExportChartsCommand._file_name(model),
lambda: ExportChartsCommand._file_content(model),
)
if model.table and export_related:
yield from ExportDatasetsCommand([model.table.id]).run()
# Pass the shared seen set to the dataset export command
yield from ExportDatasetsCommand([model.table.id]).run(seen=seen)
+20 -6
View File
@@ -383,8 +383,12 @@ class ExportDashboardsCommand(ExportModelsCommand):
@staticmethod
# ruff: noqa: C901
def _export(
model: Dashboard, export_related: bool = True
model: Dashboard, export_related: bool = True, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
# Initialize seen set if not provided
if seen is None:
seen = set()
yield (
ExportDashboardsCommand._file_name(model),
lambda: ExportDashboardsCommand._file_content(model),
@@ -395,8 +399,11 @@ class ExportDashboardsCommand(ExportModelsCommand):
dashboard_ids = model.id
command = ExportChartsCommand(chart_ids)
command.disable_tag_export()
yield from command.run()
command.enable_tag_export()
try:
# Pass the shared seen set to the chart export command
yield from command.run(seen=seen)
finally:
command.enable_tag_export()
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
yield from ExportTagsCommand(
dashboard_ids=dashboard_ids, chart_ids=chart_ids
@@ -406,7 +413,8 @@ class ExportDashboardsCommand(ExportModelsCommand):
if model.theme:
from superset.commands.theme.export import ExportThemesCommand
yield from ExportThemesCommand([model.theme.id]).run()
# Pass the shared seen set to the theme export command
yield from ExportThemesCommand([model.theme.id]).run(seen=seen)
payload = model.export_to_dict(
recursive=False,
@@ -435,7 +443,10 @@ class ExportDashboardsCommand(ExportModelsCommand):
if dataset_id is not None:
dataset = DatasetDAO.find_by_id(dataset_id)
if dataset:
yield from ExportDatasetsCommand([dataset_id]).run()
# Pass the shared seen set to the dataset export command
yield from ExportDatasetsCommand([dataset_id]).run(
seen=seen
)
# Export datasets referenced by display controls
for customization in (
@@ -446,4 +457,7 @@ class ExportDashboardsCommand(ExportModelsCommand):
if dataset_id is not None:
dataset = DatasetDAO.find_by_id(dataset_id)
if dataset:
yield from ExportDatasetsCommand([dataset_id]).run()
# Pass the shared seen set to the dataset export command
yield from ExportDatasetsCommand([dataset_id]).run(
seen=seen
)
+5 -1
View File
@@ -113,8 +113,12 @@ class ExportDatabasesCommand(ExportModelsCommand):
@staticmethod
def _export(
model: Database, export_related: bool = True
model: Database, export_related: bool = True, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
# Initialize seen set if not provided
if seen is None:
seen = set()
yield (
ExportDatabasesCommand._file_name(model),
lambda: ExportDatabasesCommand._file_content(model),
+37 -24
View File
@@ -89,8 +89,12 @@ class ExportDatasetsCommand(ExportModelsCommand):
@staticmethod
def _export(
model: SqlaTable, export_related: bool = True
model: SqlaTable, export_related: bool = True, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
# Initialize seen set if not provided
if seen is None:
seen = set()
yield (
ExportDatasetsCommand._file_name(model),
lambda: ExportDatasetsCommand._file_content(model),
@@ -103,32 +107,41 @@ class ExportDatasetsCommand(ExportModelsCommand):
)
file_path = f"databases/{db_file_name}.yaml"
payload = model.database.export_to_dict(
recursive=False,
include_parent_ref=False,
include_defaults=True,
export_uuids=True,
)
# TODO (betodealmeida): move this logic to export_to_dict once this
# becomes the default export endpoint
if payload.get("extra"):
try:
payload["extra"] = json.loads(payload["extra"])
except json.JSONDecodeError:
logger.info("Unable to decode `extra` field: %s", payload["extra"])
if ssh_tunnel := model.database.ssh_tunnel:
ssh_tunnel_payload = ssh_tunnel.export_to_dict(
# Only yield the database file if not already seen. This is
# critical to fix the issue where databases were being
# duplicated and potentially overwritten when charts from
# different databases were exported.
if file_path not in seen:
payload = model.database.export_to_dict(
recursive=False,
include_parent_ref=False,
include_defaults=True,
export_uuids=False,
export_uuids=True,
)
payload["ssh_tunnel"] = mask_password_info(ssh_tunnel_payload)
# TODO (betodealmeida): move this logic to export_to_dict once this
# becomes the default export endpoint
if payload.get("extra"):
try:
payload["extra"] = json.loads(payload["extra"])
except json.JSONDecodeError:
logger.info(
"Unable to decode `extra` field: %s", payload["extra"]
)
payload["version"] = EXPORT_VERSION
if ssh_tunnel := model.database.ssh_tunnel:
ssh_tunnel_payload = ssh_tunnel.export_to_dict(
recursive=False,
include_parent_ref=False,
include_defaults=True,
export_uuids=False,
)
payload["ssh_tunnel"] = mask_password_info(ssh_tunnel_payload)
yield (
file_path,
lambda: yaml.safe_dump(payload, sort_keys=False, allow_unicode=True),
)
payload["version"] = EXPORT_VERSION
yield (
file_path,
lambda: yaml.safe_dump(
payload, sort_keys=False, allow_unicode=True
),
)
@@ -18,11 +18,13 @@ import logging
from functools import partial
from typing import Any, Optional
from jinja2.exceptions import TemplateError
from sqlalchemy.exc import SQLAlchemyError
from superset import db
from superset.commands.explore.permalink.base import BaseExplorePermalinkCommand
from superset.daos.key_value import KeyValueDAO
from superset.exceptions import SupersetTemplateException
from superset.explore.permalink.exceptions import ExplorePermalinkCreateFailedError
from superset.explore.utils import check_access as check_chart_access
from superset.key_value.exceptions import (
@@ -58,7 +60,10 @@ class CreateExplorePermalinkCommand(BaseExplorePermalinkCommand):
d_id, d_type = self.datasource.split("__")
datasource_id = int(d_id)
datasource_type = DatasourceType(d_type)
check_chart_access(datasource_id, self.chart_id, datasource_type)
try:
check_chart_access(datasource_id, self.chart_id, datasource_type)
except TemplateError as ex:
raise SupersetTemplateException(str(ex)) from ex
value = {
"chartId": self.chart_id,
"datasourceId": datasource_id,
+6 -1
View File
@@ -17,11 +17,13 @@
import logging
from typing import Optional
from jinja2.exceptions import TemplateError
from sqlalchemy.exc import SQLAlchemyError
from superset.commands.dataset.exceptions import DatasetNotFoundError
from superset.commands.explore.permalink.base import BaseExplorePermalinkCommand
from superset.daos.key_value import KeyValueDAO
from superset.exceptions import SupersetTemplateException
from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError
from superset.explore.permalink.types import ExplorePermalinkValue
from superset.explore.utils import check_access as check_chart_access
@@ -54,7 +56,10 @@ class GetExplorePermalinkCommand(BaseExplorePermalinkCommand):
datasource_type = DatasourceType(
value.get("datasourceType", DatasourceType.TABLE)
)
check_chart_access(datasource_id, chart_id, datasource_type)
try:
check_chart_access(datasource_id, chart_id, datasource_type)
except TemplateError as ex:
raise SupersetTemplateException(str(ex)) from ex
return value
return None
except (
+29 -11
View File
@@ -47,27 +47,45 @@ class ExportModelsCommand(BaseCommand):
@staticmethod
def _file_content(model: Model) -> str:
raise NotImplementedError("Subclasses MUST implement _export")
raise NotImplementedError("Subclasses MUST implement _file_content")
@staticmethod
def _export(
model: Model, export_related: bool = True
model: Model, export_related: bool = True, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
raise NotImplementedError("Subclasses MUST implement _export")
def run(self) -> Iterator[tuple[str, Callable[[], str]]]:
def run(
self, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
self.validate()
metadata = {
"version": EXPORT_VERSION,
"type": self.dao.model_cls.__name__, # type: ignore
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
}
yield METADATA_FILE_NAME, lambda: yaml.safe_dump(metadata, sort_keys=False)
# Use provided seen set or create new one
if seen is None:
seen = set()
should_add_metadata = True
else:
# If seen set is provided, we're being called from another command
should_add_metadata = False
# Only add metadata if this is the root command
if should_add_metadata:
metadata = {
"version": EXPORT_VERSION,
"type": self.dao.model_cls.__name__, # type: ignore
"timestamp": datetime.now(tz=timezone.utc).isoformat(),
}
if METADATA_FILE_NAME not in seen:
yield (
METADATA_FILE_NAME,
lambda: yaml.safe_dump(metadata, sort_keys=False),
)
seen.add(METADATA_FILE_NAME)
seen = {METADATA_FILE_NAME}
for model in self._models:
for file_name, file_content in self._export(model, self.export_related):
for file_name, file_content in self._export(
model, self.export_related, seen
):
if file_name not in seen:
yield file_name, file_content
seen.add(file_name)
+25 -17
View File
@@ -67,8 +67,12 @@ class ExportSavedQueriesCommand(ExportModelsCommand):
@staticmethod
def _export(
model: SavedQuery, export_related: bool = True
model: SavedQuery, export_related: bool = True, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
# Initialize seen set if not provided
if seen is None:
seen = set()
yield (
ExportSavedQueriesCommand._file_name(model),
lambda: ExportSavedQueriesCommand._file_content(model),
@@ -79,21 +83,25 @@ class ExportSavedQueriesCommand(ExportModelsCommand):
database_slug = secure_filename(model.database.database_name)
file_name = f"databases/{database_slug}.yaml"
payload = model.database.export_to_dict(
recursive=False,
include_parent_ref=False,
include_defaults=True,
export_uuids=True,
)
# TODO (betodealmeida): move this logic to export_to_dict once this
# becomes the default export endpoint
if "extra" in payload:
try:
payload["extra"] = json.loads(payload["extra"])
except json.JSONDecodeError:
logger.info("Unable to decode `extra` field: %s", payload["extra"])
# Only yield if not already seen (similar to dataset export)
if file_name not in seen:
payload = model.database.export_to_dict(
recursive=False,
include_parent_ref=False,
include_defaults=True,
export_uuids=True,
)
# TODO (betodealmeida): move this logic to export_to_dict once this
# becomes the default export endpoint
if "extra" in payload:
try:
payload["extra"] = json.loads(payload["extra"])
except (TypeError, json.JSONDecodeError):
logger.info(
"Unable to decode `extra` field: %s", payload["extra"]
)
payload["version"] = EXPORT_VERSION
payload["version"] = EXPORT_VERSION
file_content = yaml.safe_dump(payload, sort_keys=False)
yield file_name, lambda: file_content
file_content = yaml.safe_dump(payload, sort_keys=False)
yield file_name, lambda: file_content
+26 -2
View File
@@ -18,13 +18,15 @@ import logging
from functools import partial
from typing import Any
from jinja2.exceptions import TemplateError
from superset import security_manager
from superset.commands.base import BaseCommand, CreateMixin
from superset.commands.tag.exceptions import TagCreateFailedError, TagInvalidError
from superset.commands.tag.utils import to_object_model, to_object_type
from superset.commands.utils import current_user_can_modify_object
from superset.daos.tag import TagDAO
from superset.exceptions import SupersetSecurityException
from superset.exceptions import SupersetParseError, SupersetSecurityException
from superset.tags.models import ObjectType, TagType
from superset.utils.decorators import on_error, transaction
@@ -95,8 +97,30 @@ class CreateCustomTagCommand(CreateMixin, BaseCommand):
)
)
except SupersetSecurityException:
# A routine, expected authorization denial; swallowed silently by
# design (no logging) and surfaced to the caller as a validation
# failure rather than an unhandled 500.
exceptions.append(
TagCreateFailedError(f"Access denied for {object_type} {object_id}")
TagCreateFailedError(
f"Could not validate access for {object_type} {object_id}"
)
)
except (TemplateError, SupersetParseError) as ex:
# Authorizing a saved query parses its Jinja-templated SQL to resolve
# table references. Malformed Jinja (TemplateError) or an
# unresolvable partition macro (SupersetParseError) is a validation
# failure, not an unhandled 500 -- but unlike an access denial it is
# genuinely unexpected, so log it for server-side visibility and
# preserve the underlying error text instead of discarding it.
logger.warning(
"Could not parse query %s while validating tag access: %s",
object_id,
str(ex),
)
exceptions.append(
TagCreateFailedError(
f"Could not validate access for {object_type} {object_id}: {ex}"
)
)
+3 -1
View File
@@ -46,7 +46,9 @@ class ExportTagsCommand(ExportModelsCommand):
self.dashboard_ids = dashboard_ids
self.chart_ids = chart_ids
def run(self) -> Iterator[tuple[str, Callable[[], str]]]:
def run(
self, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
if not feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
return
+5 -1
View File
@@ -67,8 +67,12 @@ class ExportThemesCommand(ExportModelsCommand):
@staticmethod
def _export(
model: Theme, export_related: bool = True
model: Theme, export_related: bool = True, seen: set[str] | None = None
) -> Iterator[tuple[str, Callable[[], str]]]:
# Initialize seen set if not provided (for consistency)
if seen is None:
seen = set()
yield (
ExportThemesCommand._file_name(model),
lambda: ExportThemesCommand._file_content(model),
+16 -1
View File
@@ -38,7 +38,7 @@ from flask_appbuilder.models.filters import BaseFilter
from flask_appbuilder.models.sqla.interface import SQLAInterface
from pydantic import BaseModel, Field
from sqlalchemy import asc, cast, desc, false, or_, Text
from sqlalchemy.exc import SQLAlchemyError, StatementError
from sqlalchemy.exc import OperationalError, SQLAlchemyError, StatementError
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.inspection import inspect
from sqlalchemy.orm import ColumnProperty, joinedload, Query, RelationshipProperty
@@ -255,6 +255,11 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
filter = uuid_column == model_id_or_uuid
try:
return query.filter(filter).one_or_none()
except OperationalError:
# A transient connection-level failure (e.g. the server dropping the
# connection mid-query) surfaces as OperationalError, a StatementError
# subclass. Let it propagate instead of masking it as a "not found".
raise
except StatementError:
# can happen if neither uuid nor int is passed
return None
@@ -341,6 +346,11 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
try:
return query.filter(column == converted_value).one_or_none()
except OperationalError:
# A transient connection-level failure (e.g. the server dropping the
# connection mid-query) surfaces as OperationalError, a StatementError
# subclass. Let it propagate instead of masking it as a "not found".
raise
except StatementError:
# can happen if int is passed instead of a string or similar
return None
@@ -433,6 +443,11 @@ class BaseDAO(CoreBaseDAO[T], Generic[T]):
try:
results = query.all()
except OperationalError:
# A transient connection-level failure (e.g. the server dropping the
# connection mid-query) surfaces as OperationalError. Let it propagate
# as a 5xx instead of masking it as a 400 "record doesn't exist".
raise
except SQLAlchemyError as ex:
model_name = cls.model_cls.__name__ if cls.model_cls else "Unknown"
raise DAOFindFailedError(
+4 -20
View File
@@ -24,7 +24,7 @@ from typing import Any, Callable, cast
from zipfile import is_zipfile, ZipFile
import rison
from flask import current_app, g, redirect, request, Response, send_file, url_for
from flask import current_app, g, redirect, request, Response, url_for
from flask_appbuilder import permission_name
from flask_appbuilder.api import (
expose,
@@ -162,7 +162,7 @@ from superset.tasks.thumbnails import (
)
from superset.tasks.utils import get_current_user
from superset.utils import json
from superset.utils.core import parse_boolean_string, sanitize_cookie_token
from superset.utils.core import parse_boolean_string, send_export_zip
from superset.utils.file import get_filename
from superset.utils.pdf import build_pdf_from_screenshots
from superset.utils.screenshots import (
@@ -1629,15 +1629,7 @@ class DashboardRestApi(
return self.response_404()
buf.seek(0)
response = send_file(
buf,
mimetype="application/zip",
as_attachment=True,
download_name=filename,
)
if token := sanitize_cookie_token(request.args.get("token")):
response.set_cookie(token, "done", max_age=600)
return response
return send_export_zip(buf, filename)
@expose("/<pk>/export_as_example/", methods=("GET",))
@protect()
@@ -1720,15 +1712,7 @@ class DashboardRestApi(
filename = f"{safe_name}_example.zip"
response = send_file(
buf,
mimetype="application/zip",
as_attachment=True,
download_name=filename,
)
if token := sanitize_cookie_token(request.args.get("token")):
response.set_cookie(token, "done", max_age=600)
return response
return send_export_zip(buf, filename)
@expose("/<pk>/export_xlsx/", methods=("POST",))
@protect()
+2 -11
View File
@@ -31,7 +31,6 @@ from flask import (
render_template,
request,
Response,
send_file,
)
from flask_appbuilder.api import expose, protect, rison as parse_rison, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface
@@ -131,7 +130,7 @@ from superset.utils.core import (
error_msg_from_exception,
get_username,
parse_js_uri_path_item,
sanitize_cookie_token,
send_export_zip,
)
from superset.utils.decorators import transaction
from superset.utils.oauth2 import decode_oauth2_state
@@ -1572,15 +1571,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
return self.response_404()
buf.seek(0)
response = send_file(
buf,
mimetype="application/zip",
as_attachment=True,
download_name=filename,
)
if token := sanitize_cookie_token(request.args.get("token")):
response.set_cookie(token, "done", max_age=600)
return response
return send_export_zip(buf, filename)
@expose("/import/", methods=("POST",))
@protect()
+3 -11
View File
@@ -23,7 +23,7 @@ from io import BytesIO
from typing import Any, Callable
from zipfile import is_zipfile, ZipFile
from flask import request, Response, send_file
from flask import request, Response
from flask_appbuilder import permission_name
from flask_appbuilder.api import expose, protect, rison as parse_rison, safe
from flask_appbuilder.api.schemas import get_item_schema
@@ -93,7 +93,7 @@ from superset.exceptions import (
from superset.jinja_context import BaseTemplateProcessor, get_template_processor
from superset.subjects.filters import FilterRelatedSubjects, subject_type_filter
from superset.utils import json
from superset.utils.core import parse_boolean_string, sanitize_cookie_token
from superset.utils.core import parse_boolean_string, send_export_zip
from superset.versioning.api_helpers import (
current_entity_etag_uuid,
current_entity_version_info,
@@ -811,15 +811,7 @@ class DatasetRestApi(SoftDeleteApiMixin, BaseSupersetModelRestApi):
return self.response_404()
buf.seek(0)
response = send_file(
buf,
mimetype="application/zip",
as_attachment=True,
download_name=filename,
)
if token := sanitize_cookie_token(request.args.get("token")):
response.set_cookie(token, "done", max_age=600)
return response
return send_export_zip(buf, filename)
@expose("/duplicate", methods=("POST",))
@protect()
+11 -11
View File
@@ -746,18 +746,18 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
@classmethod
def encrypted_extra_sensitive_field_paths(cls) -> set[str]:
"""
Returns a set of paths for fields that should be masked in the
``masked_encrypted_extra`` JSON.
Returns a set of JSONPath expressions for fields that should be masked
in the ``masked_encrypted_extra`` JSON.
:param cls: Description
:return: Description
:rtype: set[str]
The OAuth2 client secret is always included, since
``Database.get_oauth2_config`` reads ``oauth2_client_info`` from the
``encrypted_extra`` of any database regardless of its engine, so engine
specs that override ``encrypted_extra_sensitive_fields`` cannot
accidentally expose it.
"""
return (
set(cls.encrypted_extra_sensitive_fields)
if isinstance(cls.encrypted_extra_sensitive_fields, dict)
else cls.encrypted_extra_sensitive_fields
)
return set(cls.encrypted_extra_sensitive_fields) | {
"$.oauth2_client_info.secret"
}
@classmethod
def get_rls_method(cls) -> RLSMethod:
@@ -2870,7 +2870,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
corresponding entry is updated, otherwise the old value is used (see
`unmask_encrypted_extra` below).
"""
if encrypted_extra is None or not cls.encrypted_extra_sensitive_fields:
if encrypted_extra is None:
return encrypted_extra
try:
+1 -1
View File
@@ -44,7 +44,7 @@ class CockroachDbEngineSpec(PostgresEngineSpec):
DatabaseCategory.TRADITIONAL_RDBMS,
DatabaseCategory.OPEN_SOURCE,
],
"pypi_packages": ["cockroachdb"],
"pypi_packages": ["sqlalchemy-cockroachdb", "psycopg2"],
"connection_string": "cockroachdb://root@{hostname}:{port}/{database}?sslmode=disable",
"default_port": 26257,
"docs_url": "https://github.com/cockroachdb/sqlalchemy-cockroachdb",
+7 -7
View File
@@ -176,7 +176,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
"compare_lag": "10",
"compare_suffix": "o10Y",
"limit": "25",
"granularity_sqla": "ds",
"granularity": "ds",
"groupby": [],
"row_limit": current_app.config["ROW_LIMIT"],
"time_range": "100 years ago : now",
@@ -213,7 +213,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
params=get_slice_json(
defaults,
viz_type="big_number",
granularity_sqla="ds",
granularity="ds",
compare_lag="5",
compare_suffix="over 5Y",
metric=metric,
@@ -237,7 +237,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
defaults,
viz_type="echarts_timeseries_line",
groupby=["name"],
granularity_sqla="ds",
granularity="ds",
rich_tooltip=True,
show_legend=True,
metrics=metrics,
@@ -420,7 +420,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
}
],
metrics_b=["sum__num"],
granularity_sqla="ds",
granularity="ds",
yAxisIndex=0,
yAxisIndexB=1,
),
@@ -474,7 +474,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
"label": "SUM(num_california)",
},
viz_type="big_number_total",
granularity_sqla="ds",
granularity="ds",
),
editors=[],
),
@@ -496,7 +496,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
}
],
viz_type="echarts_timeseries_line",
granularity_sqla="ds",
granularity="ds",
groupby=["name"],
series_limit_metric={
"expressionType": "SIMPLE",
@@ -542,7 +542,7 @@ def create_slices(tbl: SqlaTable) -> tuple[list[Slice], list[Slice]]:
defaults,
metric=metric,
viz_type="big_number_total",
granularity_sqla="ds",
granularity="ds",
adhoc_filters=[gen_filter("gender", "girl")],
subheader="total female participants",
),
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby: []
limit: '100'
markup_type: markdown
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: '25'
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- gender
limit: '25'
@@ -30,7 +30,7 @@ params:
subject: state
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- state
limit: '25'
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby: []
limit: '100'
markup_type: markdown
@@ -28,7 +28,7 @@ params:
subject: gender
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: '25'
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '5'
compare_suffix: over 5Y
granularity_sqla: ds
granularity: ds
groupby: []
limit: '25'
markup_type: markdown
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby: []
groupbyColumns:
- state
@@ -29,7 +29,7 @@ params:
compare_lag: '10'
compare_suffix: o10Y
comparison_type: values
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: 10
@@ -29,7 +29,7 @@ params:
compare_lag: '10'
compare_suffix: o10Y
comparison_type: values
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: 10
@@ -22,7 +22,7 @@ description: null
params:
compare_lag: '10'
compare_suffix: o10Y
granularity_sqla: ds
granularity: ds
groupby:
- name
limit: '25'
+1 -1
View File
@@ -154,7 +154,7 @@ def create_slices(tbl: BaseDatasource) -> list[Slice]:
"compare_lag": "10",
"compare_suffix": "o10Y",
"limit": "25",
"granularity_sqla": "year",
"granularity": "year",
"groupby": [],
"row_limit": current_app.config["ROW_LIMIT"],
"since": "2014-01-01",
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- region
limit: '25'
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- country_name
limit: '25'
@@ -43,7 +43,7 @@ params:
subject: country_code
color_scheme: supersetColors
entity: country_name
granularity_sqla: year
granularity: year
legendOrientation: top
legendType: scroll
max_bubble_size: '50'
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- country_name
limit: '25'
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby: []
limit: '25'
markup_type: markdown
@@ -27,7 +27,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby: []
limit: '25'
markup_type: markdown
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- region
- country_code
@@ -24,7 +24,7 @@ params:
compare_suffix: o10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby:
- region
limit: '25'
@@ -24,7 +24,7 @@ params:
compare_suffix: over 10Y
country_fieldtype: cca3
entity: country_code
granularity_sqla: year
granularity: year
groupby: []
limit: '25'
markup_type: markdown
+5
View File
@@ -31,6 +31,7 @@ from superset.commands.dataset.exceptions import (
from superset.commands.explore.permalink.create import CreateExplorePermalinkCommand
from superset.commands.explore.permalink.get import GetExplorePermalinkCommand
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.exceptions import SupersetTemplateException
from superset.explore.permalink.exceptions import ExplorePermalinkInvalidStateError
from superset.explore.permalink.schemas import ExplorePermalinkStateSchema
from superset.extensions import event_logger
@@ -107,6 +108,8 @@ class ExplorePermalinkRestApi(BaseSupersetApi):
return self.response(403, message=str(ex))
except (ChartNotFoundError, DatasetNotFoundError) as ex:
return self.response(404, message=str(ex))
except SupersetTemplateException as ex:
return self.response(ex.status, message=str(ex))
@expose("/permalink/<string:key>", methods=("GET",))
@protect()
@@ -162,3 +165,5 @@ class ExplorePermalinkRestApi(BaseSupersetApi):
return self.response(403, message=str(ex))
except (ChartNotFoundError, DatasetNotFoundError) as ex:
return self.response(404, message=str(ex))
except SupersetTemplateException as ex:
return self.response(ex.status, message=str(ex))
+3 -9
View File
@@ -18,7 +18,7 @@ from datetime import datetime
from io import BytesIO
from zipfile import is_zipfile, ZipFile
from flask import request, Response, send_file
from flask import request, Response
from flask_appbuilder.api import expose, protect
from superset.commands.export.assets import ExportAssetsCommand
@@ -30,7 +30,7 @@ from superset.commands.importers.v1.assets import ImportAssetsCommand
from superset.commands.importers.v1.utils import get_contents_from_bundle
from superset.extensions import event_logger
from superset.utils import json
from superset.utils.core import parse_boolean_string
from superset.utils.core import parse_boolean_string, send_export_zip
from superset.views.base_api import BaseSupersetApi, requires_form_data, statsd_metrics
@@ -84,13 +84,7 @@ class ImportExportRestApi(BaseSupersetApi):
fp.write(file_content().encode())
buf.seek(0)
response = send_file(
buf,
mimetype="application/zip",
as_attachment=True,
download_name=filename,
)
return response
return send_export_zip(buf, filename)
@expose("/import/", methods=("POST",))
@protect()
+10 -3
View File
@@ -119,6 +119,7 @@ from superset.extensions import event_logger
title="My new tool",
readOnlyHint=True,
destructiveHint=False,
openWorldHint=False,
),
)
async def my_new_tool(request: MyRequest, ctx: Context) -> MyResponse:
@@ -139,6 +140,8 @@ async def my_new_tool(request: MyRequest, ctx: Context) -> MyResponse:
title="Create something",
readOnlyHint=False,
destructiveHint=False,
idempotentHint=False,
openWorldHint=False,
),
)
async def create_something(request: CreateRequest, ctx: Context) -> CreateResponse:
@@ -210,8 +213,10 @@ The `@tool` decorator from `superset_core.mcp.decorators` accepts:
```python
annotations=ToolAnnotations(
title="Human-readable title",
readOnlyHint=True, # Whether tool only reads data
destructiveHint=False, # Whether tool has destructive side effects
readOnlyHint=False, # Whether tool only reads data
destructiveHint=False, # Whether tool has destructive side effects
idempotentHint=False, # Whether repeated mutating calls have additional effects
openWorldHint=False, # Whether tool interacts with external entities
)
```
@@ -572,7 +577,9 @@ async def test_my_tool_success(mcp_server):
### 4. Missing ToolAnnotations
**Problem**: Tool lacks MCP directory compliance metadata.
**Solution**: Always include `annotations=ToolAnnotations(title=..., readOnlyHint=..., destructiveHint=...)`.
**Solution**: Always include `title`, `readOnlyHint`, `destructiveHint`, and
`openWorldHint`. Mutating tools (`readOnlyHint=False`) must also include
`idempotentHint`.
### 5. Using `Optional` Instead of Union Syntax
**Problem**: Old-style `Optional[T]` is not Python 3.10+ style.
@@ -42,6 +42,7 @@ logger = logging.getLogger(__name__)
title="Get annotation layer info",
readOnlyHint=True,
destructiveHint=False,
openWorldHint=False,
),
)
async def get_annotation_layer_info(
@@ -41,6 +41,7 @@ logger = logging.getLogger(__name__)
title="Get annotation info",
readOnlyHint=True,
destructiveHint=False,
openWorldHint=False,
),
)
async def get_layer_annotation_info(
@@ -49,6 +49,7 @@ _SORTABLE_LAYER_COLUMNS = ["id", "name", "changed_on", "created_on"]
title="List annotation layers",
readOnlyHint=True,
destructiveHint=False,
openWorldHint=False,
),
)
async def list_annotation_layers(
@@ -56,6 +56,7 @@ _SORTABLE_ANNOTATION_COLUMNS = ["id", "short_descr", "start_dttm", "end_dttm"]
title="List annotations in a layer",
readOnlyHint=True,
destructiveHint=False,
openWorldHint=False,
),
)
async def list_layer_annotations(

Some files were not shown because too many files have changed in this diff Show More