mirror of
https://github.com/apache/superset.git
synced 2026-08-30 03:51:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
390be50f37 | ||
|
|
7aa6fcaf1e | ||
|
|
8dbdfb4fc6 | ||
|
|
e518b21994 | ||
|
|
e18f27e1ce | ||
|
|
2b1df8d462 | ||
|
|
9bf457dea6 | ||
|
|
cb7b790733 | ||
|
|
8bec85158c | ||
|
|
a30e4a4350 | ||
|
|
933dbbc2a2 | ||
|
|
feee3dea2f | ||
|
|
192ddf9a6d | ||
|
|
a59b96c4f5 | ||
|
|
12cd259c55 | ||
|
|
3ddc3b1d56 | ||
|
|
98ec6018df | ||
|
|
2ebd415b8a | ||
|
|
81b3e85522 | ||
|
|
fd64efd72d | ||
|
|
e39bfb255b | ||
|
|
b7301ac88a | ||
|
|
fa59b44cfe | ||
|
|
8c5be889d9 | ||
|
|
b733b57e9e | ||
|
|
13e97ba913 | ||
|
|
f9e43a37d8 | ||
|
|
5ff44c5ed6 | ||
|
|
7b145a520c | ||
|
|
9f62aace7e | ||
|
|
20be48c085 | ||
|
|
e1c7a51674 | ||
|
|
ecbd6578b0 | ||
|
|
ea3206b076 | ||
|
|
478f612f25 | ||
|
|
a16bc0d94f | ||
|
|
bcc6af6c5f | ||
|
|
94b59420b7 | ||
|
|
6814a89230 | ||
|
|
ebbe2e8714 | ||
|
|
b8acf4b207 | ||
|
|
ba5d5091b9 | ||
|
|
8c88c0d3ac | ||
|
|
e9e652d22d | ||
|
|
8f82d851cf | ||
|
|
83934c038e | ||
|
|
35fc36a97e | ||
|
|
88c4e689e3 | ||
|
|
29f4a05eb7 | ||
|
|
323741b043 | ||
|
|
5c3c6362ae | ||
|
|
c1b6ff340b | ||
|
|
05830ebc77 | ||
|
|
74dd690102 | ||
|
|
22027ec24c | ||
|
|
a3072c0bf6 | ||
|
|
6d8cbbfe8d | ||
|
|
926e0e6a92 | ||
|
|
d013bc7a21 | ||
|
|
87c48cc473 | ||
|
|
c2266f860c | ||
|
|
e8aea6b6df | ||
|
|
bde1c483b0 | ||
|
|
ff1d9f9e09 | ||
|
|
76151beade | ||
|
|
905a35d05f | ||
|
|
fde0fbd315 | ||
|
|
5879994e68 | ||
|
|
d1b7a4fdb4 | ||
|
|
5ea7d05d98 | ||
|
|
0ab64e305c | ||
|
|
7d355a254e | ||
|
|
ca63bb532d | ||
|
|
b71293acde | ||
|
|
0e172a6ff3 | ||
|
|
1c66b71e23 | ||
|
|
6b0aa8714d | ||
|
|
371e5e25e8 | ||
|
|
a140e74f5f | ||
|
|
b8308a44a3 | ||
|
|
13927f27e2 | ||
|
|
434511cb37 | ||
|
|
70c9203aa9 | ||
|
|
455e6603c1 | ||
|
|
53b88da3b9 | ||
|
|
3f4fdf5f07 | ||
|
|
5a6c1b977b | ||
|
|
e8577368d3 | ||
|
|
540f8cb2d0 | ||
|
|
aae997e546 |
@@ -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:
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -78,6 +78,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -141,7 +141,7 @@ jobs:
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics (if triggered by integration tests)
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
path: docs/src/data/
|
||||
- name: Try to download latest diagnostics (for push/dispatch triggers)
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics from integration tests
|
||||
uses: dawidd6/action-download-artifact@b6e2e70617bc3265edd6dab6c906732b2f1ae151 # v21
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -76,6 +76,27 @@ value defined in `DATA_CACHE_CONFIG`.
|
||||
Note, that by setting the cache timeout to `-1`, caching for charting data can be disabled, either
|
||||
per chart, dataset or database, or by default if set in `DATA_CACHE_CONFIG`.
|
||||
|
||||
Native filter option queries (the dropdown values for native filters) go through this same
|
||||
chart-data cache, but their freshness needs often differ from regular chart queries, especially for
|
||||
datasets whose visible values change frequently, including RLS-constrained datasets. Set
|
||||
`NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT` in `superset_config.py` to give these queries a dedicated
|
||||
timeout, checked before the chart/dataset/database chain and the `DATA_CACHE_CONFIG` default above:
|
||||
|
||||
```python
|
||||
NATIVE_FILTER_OPTIONS_CACHE_TIMEOUT = 60 # seconds
|
||||
```
|
||||
|
||||
- `None` (default): native filter option queries fall through to the normal
|
||||
chart/dataset/database/`DATA_CACHE_CONFIG` resolution chain.
|
||||
- `-1`: disables caching for native filter option queries entirely.
|
||||
- `0`: passed directly to the cache backend; behavior is backend-specific, so use `-1` if the intent
|
||||
is to disable caching.
|
||||
- A positive integer: cache native filter option queries for that many seconds.
|
||||
|
||||
This setting only applies to requests detected as native filter option queries. It takes precedence
|
||||
over the per-chart/dataset/database timeouts, but not over an explicit per-request
|
||||
`custom_cache_timeout` override (e.g. "Force refresh").
|
||||
|
||||
## Limiting Cached Result Size
|
||||
|
||||
Very large chart or SQL query results can flood the cache backend (Redis/Memcached), evicting many
|
||||
|
||||
@@ -83,6 +83,26 @@ The optional username flag **-u** sets the user used for the datasource import.
|
||||
superset import_datasources -p <path / filename> -u 'admin'
|
||||
```
|
||||
|
||||
## Importing a Directory of Assets
|
||||
|
||||
The `import_directory` command imports a directory of exported assets (databases, datasets,
|
||||
dashboards, charts) in the same layout produced by the ZIP-based export. Saved queries and tags
|
||||
included in a full export are not imported by this command:
|
||||
|
||||
```bash
|
||||
superset import_directory <path / directory>
|
||||
```
|
||||
|
||||
As with `import_datasources`, the optional username flag **-u** sets the user assigned as the
|
||||
owner of the imported assets. The default is 'admin'. Example:
|
||||
|
||||
```bash
|
||||
superset import_directory <path / directory> -u 'admin'
|
||||
```
|
||||
|
||||
If the specified user does not exist, the command fails immediately with an error rather than
|
||||
importing the assets without an owner.
|
||||
|
||||
## Dashboard Import Overwrite Behavior
|
||||
|
||||
When importing a dashboard ZIP with the **overwrite** option enabled, any existing charts that are part of the dashboard are **replaced** rather than duplicated. This applies to:
|
||||
|
||||
@@ -505,6 +505,8 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
|
||||
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
|
||||
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
|
||||
| `MCP_DISABLED_TOOLS` | `set()` | Set of tool names to remove from the MCP server at startup. Disabled tools are never advertised to AI clients during tool discovery. Useful when a custom extension tool should replace a built-in Superset tool. See [Disabling built-in tools](#disabling-built-in-tools). |
|
||||
| `MCP_DISABLED_CHART_PLUGINS` | `frozenset()` | Set of chart type plugin names (e.g. `"handlebars"`) to hide from `generate_chart`. Does not affect `get_chart_type_schema`. See [Disabling chart type plugins](#disabling-chart-type-plugins). |
|
||||
| `MCP_CHART_PLUGIN_ENABLED_FUNC` | `None` | Callable `(chart_type: str) -> bool` evaluated per registry lookup for dynamic enable/disable decisions. Takes precedence over `MCP_DISABLED_CHART_PLUGINS` when set. See [Disabling chart type plugins](#disabling-chart-type-plugins). |
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -893,6 +895,39 @@ MCP_DISABLED_TOOLS = {"extensions.myorg.myextension.some_tool"}
|
||||
Specifying a tool name that does not exist logs a warning at startup and is otherwise ignored — it will not prevent the server from starting.
|
||||
:::
|
||||
|
||||
## Disabling chart type plugins
|
||||
|
||||
The `generate_chart` tool dispatches per chart type (`xy`, `table`, `pie`, `pivot_table`, `mixed_timeseries`, `handlebars`, `big_number`, `histogram`, `box_plot`, `waterfall`) to a registered chart type plugin. Two settings let operators enable or disable individual chart type plugins at runtime, without a code deploy.
|
||||
|
||||
### Static deny-list
|
||||
|
||||
Set `MCP_DISABLED_CHART_PLUGINS` in your `superset_config.py` to a set of chart type names:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
|
||||
# Emergency kill switch: hide "handlebars" from all callers
|
||||
MCP_DISABLED_CHART_PLUGINS = {"handlebars"}
|
||||
```
|
||||
|
||||
Disabled chart types stay registered but are filtered out at lookup time: they're never listed in `generate_chart`'s supported chart types, and `generate_chart` calls for them are rejected. `get_chart_type_schema` consults its own static schema/example map rather than the registry filter, so a disabled chart type's schema remains queryable through that tool even though `generate_chart` will reject it.
|
||||
|
||||
### Dynamic predicate
|
||||
|
||||
For per-request control (A/B tests, gradual rollout, entitlement checks), set `MCP_CHART_PLUGIN_ENABLED_FUNC` to a callable. It's evaluated as `enabled_func(chart_type: str) -> bool` on every registry lookup, and it takes precedence over `MCP_DISABLED_CHART_PLUGINS` when set:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
from flask import g
|
||||
|
||||
|
||||
def MCP_CHART_PLUGIN_ENABLED_FUNC(chart_type: str) -> bool:
|
||||
flags = getattr(g, "feature_flags", {})
|
||||
return flags.get(f"mcp_chart_{chart_type}", True)
|
||||
```
|
||||
|
||||
The callable must be cheap and in-process (consult already-loaded feature flags or request-local context) -- do not perform network I/O per call. If it raises, the registry fails closed (the plugin is hidden) and logs a warning.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
- **Use TLS** for all production MCP endpoints -- place the server behind a reverse proxy with HTTPS
|
||||
|
||||
@@ -88,6 +88,27 @@ THEME_DARK = {
|
||||
# - OS preference detection is automatically enabled
|
||||
```
|
||||
|
||||
### Default Theme Mode
|
||||
|
||||
By default, Superset mimics the visitor's OS/browser preference (light or dark) for
|
||||
sessions that don't have a saved user preference. Use `THEME_DEFAULT_MODE` to override
|
||||
that starting point instance-wide for the standard application:
|
||||
|
||||
```python
|
||||
# Default theme mode for sessions without a saved user preference.
|
||||
# One of "default" (always light), "dark" (always dark), or "system" (mimic OS preference).
|
||||
THEME_DEFAULT_MODE = "dark"
|
||||
```
|
||||
|
||||
- `"system"` (the default) preserves the existing behavior of following the OS/browser
|
||||
preference, provided a dark theme is configured (`THEME_DARK` is not `None`). If no dark
|
||||
theme is available, Superset always starts in light mode regardless of this setting.
|
||||
- `"default"` or `"dark"` forces that starting mode for first-time visitors; users can still switch themes manually afterward if both `THEME_DEFAULT` and `THEME_DARK` are available.
|
||||
- A user's own saved preference, once they toggle the theme switcher, always takes precedence over `THEME_DEFAULT_MODE`.
|
||||
- `THEME_DEFAULT_MODE` has no effect on embedded dashboards: the embed SDK sets the
|
||||
starting mode via its own `themeMode` URL parameter, which takes precedence and falls
|
||||
back to light mode when the parameter is absent.
|
||||
|
||||
### App Branding
|
||||
|
||||
The application name shown in the browser title bar and navigation can be
|
||||
|
||||
@@ -243,6 +243,42 @@ Each subject in the response includes flat scalar ids (`user_id`, `role_id`, `gr
|
||||
than a nested object, so callers can match directly on whichever id they already have — only the
|
||||
id field matching the subject's `type` is populated; the others are `null`.
|
||||
|
||||
#### Extending Editorship with EXTRA_EDITORS_RESOLVER
|
||||
|
||||
Deployments that grant edit access to a dashboard or chart through a mechanism outside
|
||||
Superset's own Subject-based `editors` list — for example, a folder-permission system or an
|
||||
internal directory service — can plug that logic in with `EXTRA_EDITORS_RESOLVER`:
|
||||
|
||||
```python
|
||||
def extra_editors_resolver(resource):
|
||||
# `resource` is the Dashboard or Slice instance being checked.
|
||||
# Return Subject instances, raw subject ids, or dicts with an `id` key.
|
||||
return [...]
|
||||
|
||||
|
||||
EXTRA_EDITORS_RESOLVER = extra_editors_resolver
|
||||
```
|
||||
|
||||
The resolver's result is unioned with the resource's own `editors` for editorship checks: it
|
||||
feeds `is_editor`, `raise_for_editorship`, save-as, and soft-delete restore. When
|
||||
`EXTRA_EDITORS_RESOLVER` is configured, the usual lockout-prevention behavior — automatically
|
||||
re-adding a non-admin who removes themselves from a resource's editors — is skipped, since the
|
||||
deployment has its own way of keeping the resource editable. This skip is global to the setting,
|
||||
not per-resource: it still applies on a resource where the resolver currently returns no
|
||||
subjects, so a resolver that can't guarantee an alternate editor for every resource can let the
|
||||
last editor remove themselves and leave it uneditable by non-admins.
|
||||
|
||||
Resolved subject ids are also surfaced as `extra_editors` in the chart and dashboard `GET`
|
||||
responses, so API clients can distinguish resolver-granted editorship from the resource's own
|
||||
`editors` list. This field is attached after serialization and isn't part of the OpenAPI response
|
||||
schema, so generated API clients won't see it as a typed field.
|
||||
|
||||
Because the resolver is arbitrary per-deployment Python rather than a SQL-expressible condition,
|
||||
editorship it grants cannot be reflected in list-view filtering (for example, the soft-deleted
|
||||
archive is scoped to editors via a SQL query). It does still run once per row on chart and
|
||||
dashboard list responses to populate `extra_editors`, so a slow or unavailable external resolver
|
||||
affects ordinary list requests, not just direct per-object checks.
|
||||
|
||||
### Dashboard Access Control
|
||||
|
||||
Access to dashboards is managed via editors (subjects that have edit permissions to the dashboard).
|
||||
@@ -583,6 +619,28 @@ SELECT * FROM (
|
||||
queries run against tables that have associated datasets with RLS filters will then have
|
||||
the appropriate predicates injected automatically.
|
||||
|
||||
#### RLS Indicator in the Dataset List and Explore
|
||||
|
||||
When a dataset has one or more RLS filters that apply to it, Superset shows a lock
|
||||
icon badge next to the dataset name in the **Datasets** list and next to the dataset
|
||||
selector in **Explore**. Hovering over the badge shows a tooltip listing each
|
||||
applicable filter's name, filter type (Regular or Base), group key (if any), assigned
|
||||
subjects (labeled "Roles" in the tooltip, but may include users and groups too), and
|
||||
clause.
|
||||
|
||||
This badge also surfaces filters that are inherited from the physical tables
|
||||
referenced by a virtual (SQL-based) dataset, as described above. Inherited filters
|
||||
are marked "from underlying table" in the tooltip, and a summary note is shown
|
||||
whenever any of the listed filters are inherited rather than assigned directly to
|
||||
the dataset. Inherited-filter detection depends on Superset's SQL parser being able
|
||||
to identify the referenced tables and match them to a physical dataset by name,
|
||||
schema, and database, so it's best-effort: unparseable or unmatched references won't
|
||||
surface a filter on the badge even if one would apply at query time.
|
||||
|
||||
The badge is a visibility aid only — it does not change which filters are applied to
|
||||
a query. Use the RLS REST API described below if you need to confirm exactly which
|
||||
filters affect a dataset.
|
||||
|
||||
#### Checking RLS Filters via the API
|
||||
|
||||
You can use the RLS REST API to audit which filters are configured and which datasets
|
||||
|
||||
@@ -406,6 +406,18 @@ ECharts option overrides bypass Superset's validation layer. Invalid option keys
|
||||
|
||||
When the **Search Box** is visible in a Table chart, the **Download** action exports only the rows currently visible after the search filter is applied — not the full underlying dataset. This matches the visual output and is intentional. To export the full dataset regardless of search state, use the **Download as CSV** option from the chart's three-dot menu in the dashboard or from the Explore chart toolbar before applying a search filter.
|
||||
|
||||
### Exporting a Chart as an Image or PDF
|
||||
|
||||
Alongside the raw-data export options (CSV, JSON, Excel), a chart's three-dot menu — in a dashboard or from the Explore chart toolbar — offers a few ways to export a visual snapshot of the chart:
|
||||
|
||||
- **Export screenshot (jpeg)** — a single-click JPEG screenshot of the chart.
|
||||
- **Export screenshot (png)** — opens a submenu with **Transparent background** and **Solid background** options. The solid option uses the current theme's background color. PNG produces a higher-quality image than the JPEG export.
|
||||
- **Export as PDF** — downloads the chart as a PDF file.
|
||||
|
||||
The dropdown menu is briefly hidden while the screenshot or PDF is being captured so it doesn't appear in the exported file. In Explore, these image and PDF options are available from the **Export All Data** submenu, and also from the **Export current view** submenu when the chart type supports current-view export.
|
||||
|
||||
These menu items respect your permissions: the dashboard export menu only appears if you can download, and the image/PDF options are disabled if you lack image-export permission.
|
||||
|
||||
### Sharing a Specific Tab
|
||||
|
||||
When a dashboard has tabs, each tab gets its own shareable URL. Navigate to the tab you want to share and copy the URL from your browser's address bar — the tab anchor is encoded in the URL so that anyone opening the link lands directly on that tab.
|
||||
|
||||
@@ -88,6 +88,21 @@ embedDashboard({
|
||||
|
||||
If the callback returns `null` or is not provided, Superset uses its own permalink URL as a fallback.
|
||||
|
||||
### Permalink origin rewriting
|
||||
|
||||
This rewrite only applies to the non-embedded permalink path — it has no effect on embedded dashboards. When Superset is not embedded, it rewrites the origin of any permalink URL it generates to `window.location.origin` before showing it to the user, which keeps a proxied or subdirectory-deployed Superset from handing out a permalink that points at an internal hostname the user's browser can't reach.
|
||||
|
||||
When Superset **is** embedded, this rewrite is skipped entirely regardless of the flag below: a `resolvePermalinkUrl` callback's return value is used as-is, and if no callback is provided (or it fails), the backend-supplied URL is also returned as-is.
|
||||
|
||||
If your reverse proxy correctly forwards `X-Forwarded-Host` and you'd rather non-embedded permalinks carry the backend's literal origin, opt out of the rewrite with `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE`:
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True
|
||||
```
|
||||
|
||||
This defaults to `False` (rewrite enabled) and only affects non-embedded permalinks. Flipping the default would regress the common proxied/subdirectory deployment by exposing an unreachable internal host in copied permalinks.
|
||||
|
||||
---
|
||||
|
||||
## Feature Flags for Embedded Mode
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Vendored
+6
@@ -120,6 +120,12 @@
|
||||
"lifecycle": "testing",
|
||||
"description": "Enables filter functionality in Alerts and Reports"
|
||||
},
|
||||
{
|
||||
"name": "ALERT_REPORTS_RETRY",
|
||||
"default": false,
|
||||
"lifecycle": "testing",
|
||||
"description": "Enables automatic retry functionality for failed report executions"
|
||||
},
|
||||
{
|
||||
"name": "ALERT_REPORT_SLACK_V2",
|
||||
"default": true,
|
||||
|
||||
+141
-134
@@ -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"
|
||||
|
||||
+21
-10
@@ -94,7 +94,7 @@ dependencies = [
|
||||
"parsedatetime",
|
||||
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
|
||||
"pgsanity",
|
||||
"Pillow>=11.0.0, <13",
|
||||
"Pillow>=12.3.0, <13", # raise floor to match resolved pin; closes SCA false-positive on 11.x-range CVEs already fixed in 12.3.0
|
||||
"polyline>=2.0.4, <3.0",
|
||||
"pydantic>=2.8.0",
|
||||
"pyparsing>=3.3.2, <4",
|
||||
@@ -103,7 +103,7 @@ dependencies = [
|
||||
"pygeohash",
|
||||
"pyarrow>=25.0.1, <26", # before upgrading pyarrow, check that all db dependencies support this, see e.g. https://github.com/apache/superset/pull/34693
|
||||
"pyyaml>=6.0.3, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"PyJWT>=2.13.0, <3.0", # raise floor to match resolved pin; closes SCA false-positive on 2.4.x-range CVEs already fixed in 2.13.0
|
||||
"redis>=5.0.0, <9.0",
|
||||
"rison>=2.0.1, <3.0",
|
||||
|
||||
@@ -141,8 +141,18 @@ bigquery = [
|
||||
"sqlalchemy-bigquery>=1.17.2",
|
||||
"google-cloud-bigquery>=3.42.3",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.7.1, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
clickhouse = ["clickhouse-connect>=1.7.2, <2.0"]
|
||||
# The `cockroachdb` PyPI package (last released 2021) is abandoned and its
|
||||
# SQLAlchemy dialect cannot even import under SQLAlchemy 2.0 (it references
|
||||
# sqlalchemy.dialects.postgresql.psycopg2.PGCompiler_psycopg2, removed in
|
||||
# 2.0). sqlalchemy-cockroachdb is the actively maintained replacement,
|
||||
# already linked from CockroachDbEngineSpec.metadata's docs_url, and
|
||||
# registers the same `cockroachdb` SQLAlchemy dialect entry point.
|
||||
# sqlalchemy-cockroachdb depends only on SQLAlchemy itself, not on a DBAPI
|
||||
# driver, so psycopg2-binary is pinned alongside it (matching the `postgres`
|
||||
# extra) to keep this extra self-contained -- CockroachDB speaks the
|
||||
# PostgreSQL wire protocol, so psycopg2 is what actually opens connections.
|
||||
cockroachdb = ["sqlalchemy-cockroachdb>=2.0.0, <3", "psycopg2-binary==2.9.12"]
|
||||
crate = ["sqlalchemy-cratedb>=0.43.1, <1"]
|
||||
# sqlalchemy-d1's only release (0.1.0, Nov 2025) pins sqlalchemy<2,>=1.4,
|
||||
# explicitly excluding SQLAlchemy 2.0. See superset/db_engine_specs/d1.py's
|
||||
@@ -185,11 +195,12 @@ excel = ["xlrd>=2.0.2, <2.1"]
|
||||
# installing this extra is only required to actually run exports.
|
||||
excel-export = ["boto3"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.4.6,<4.0",
|
||||
"fastmcp>=3.4.7,<4.0",
|
||||
"mcp>=1.29.1,<3.0",
|
||||
# tiktoken backs the response-size-guard token estimator. Without
|
||||
# it, the middleware falls back to a coarser character-based
|
||||
# heuristic that under-counts JSON-heavy MCP responses.
|
||||
"tiktoken>=0.13.0,<1.0",
|
||||
"tiktoken>=0.14.0,<1.0",
|
||||
]
|
||||
# sqlalchemy-firebird >=2.0.0 unconditionally requires SQLAlchemy 2.0 on
|
||||
# Python >=3.8 (which covers Superset's >=3.11 floor), with no dual-compat
|
||||
@@ -235,7 +246,7 @@ pinot = ["pinotdb>=5.0.0, <10.0.0"]
|
||||
playwright = ["playwright>=1.62.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.12"]
|
||||
presto = ["pyhive[presto]>=0.6.5"]
|
||||
trino = ["trino>=0.338.0"]
|
||||
trino = ["trino>=0.339.0"]
|
||||
prophet = ["prophet>=1.4.0, <2"]
|
||||
# sqlalchemy-redshift cuts hard from SQLAlchemy 1.4-only (0.8.x) to 2.0-only
|
||||
# (>=1.0.0) with no dual-compat release. Bumped now that Superset's own
|
||||
@@ -249,7 +260,7 @@ risingwave = ["sqlalchemy-risingwave>=2.0.0"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.5, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"]
|
||||
sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
|
||||
sqlite = ["syntaqlite>=0.9.0,<0.10.0"]
|
||||
spark = [
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
"tableschema",
|
||||
@@ -259,7 +270,7 @@ tdengine = [
|
||||
"taospy>=2.8.10",
|
||||
"taos-ws-py>=0.7.0"
|
||||
]
|
||||
teradata = ["teradatasql>=20.0.0.65"]
|
||||
teradata = ["teradatasql>=20.0.0.66"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
|
||||
netezza = ["nzalchemy>= 11.1.2, < 11.2"]
|
||||
@@ -293,7 +304,7 @@ development = [
|
||||
"ruff",
|
||||
"sqloxide",
|
||||
"statsd",
|
||||
"syntaqlite>=0.7.0,<0.8.0",
|
||||
"syntaqlite>=0.9.0,<0.10.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -391,7 +391,7 @@ grpcio==1.83.0
|
||||
# grpcio-status
|
||||
grpcio-status==1.60.1
|
||||
# via google-api-core
|
||||
gunicorn==26.0.0
|
||||
gunicorn==26.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -403,7 +403,7 @@ hashids==1.3.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
holidays==0.102
|
||||
holidays==0.103
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -547,8 +547,10 @@ matplotlib==3.9.0
|
||||
# via prophet
|
||||
mccabe==0.7.0
|
||||
# via pylint
|
||||
mcp==1.24.0
|
||||
# via fastmcp-slim
|
||||
mcp==1.29.1
|
||||
# via
|
||||
# apache-superset
|
||||
# fastmcp-slim
|
||||
mdurl==0.1.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -622,7 +624,6 @@ packaging==25.0
|
||||
# duckdb-engine
|
||||
# fastmcp-slim
|
||||
# google-cloud-bigquery
|
||||
# gunicorn
|
||||
# kombu
|
||||
# limits
|
||||
# matplotlib
|
||||
@@ -960,10 +961,13 @@ sqlalchemy==2.0.52
|
||||
# marshmallow-sqlalchemy
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
# sqlalchemy-cockroachdb
|
||||
# sqlalchemy-continuum
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.17.2
|
||||
# via apache-superset
|
||||
sqlalchemy-cockroachdb==2.0.4
|
||||
# via apache-superset
|
||||
sqlalchemy-continuum==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -993,13 +997,13 @@ starlette==1.3.1
|
||||
# mcp
|
||||
statsd==4.0.1
|
||||
# via apache-superset
|
||||
syntaqlite==0.7.1
|
||||
syntaqlite==0.9.0
|
||||
# via apache-superset
|
||||
tabulate==0.10.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
tiktoken==0.13.0
|
||||
tiktoken==0.14.0
|
||||
# via apache-superset
|
||||
tomli-w==1.2.0
|
||||
# via apache-superset-extensions-cli
|
||||
@@ -1009,7 +1013,7 @@ tqdm==4.67.1
|
||||
# via
|
||||
# cmdstanpy
|
||||
# prophet
|
||||
trino==0.338.0
|
||||
trino==0.339.0
|
||||
# via apache-superset
|
||||
typing-extensions==4.16.0
|
||||
# via
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
// === Import plugin rules ===
|
||||
"import/named": "error",
|
||||
"import/export": "error",
|
||||
"import/no-named-as-default": "error",
|
||||
"import/no-named-as-default": "warn",
|
||||
"import/no-named-as-default-member": "error",
|
||||
"import/no-mutable-exports": "error",
|
||||
"import/no-amd": "error",
|
||||
|
||||
Generated
+231
-228
@@ -100,7 +100,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^11.0.2",
|
||||
"immer": "^11.1.17",
|
||||
"immer": "^11.1.18",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -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",
|
||||
@@ -234,8 +234,8 @@
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.9",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.2",
|
||||
"eslint-plugin-storybook": "10.5.10",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -249,7 +249,7 @@
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdom": "^30.0.1",
|
||||
"lerna": "^10.0.0",
|
||||
"lerna": "^10.0.1",
|
||||
"lightningcss": "^1.33.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.6.1",
|
||||
@@ -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": {
|
||||
@@ -17528,23 +17566,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/conventional-changelog/node_modules/conventional-commits-parser": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz",
|
||||
"integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@simple-libs/stream-utils": "^2.0.0",
|
||||
"argue-cli": "^3.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"conventional-commits-parser": "dist/cli/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/conventional-commits-filter": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-6.0.1.tgz",
|
||||
@@ -17556,9 +17577,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/conventional-commits-parser": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-DPp6hkUjvwIivxbkrTiLXeRswNv1A/4GFA2X6scXma0AMa9632V3TwxmrlkUIEtUktiM3Ln+RrSH2xlP3/jUTw==",
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz",
|
||||
"integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -17619,23 +17640,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/conventional-recommended-bump/node_modules/conventional-commits-parser": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-7.1.2.tgz",
|
||||
"integrity": "sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@simple-libs/stream-utils": "^2.0.0",
|
||||
"argue-cli": "^3.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"conventional-commits-parser": "dist/cli/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -19140,9 +19144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||
"version": "3.4.14",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz",
|
||||
"integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
@@ -20204,9 +20208,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-react-you-might-not-need-an-effect": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-1.0.1.tgz",
|
||||
"integrity": "sha512-oOhQTYhor88Xp8RVytq25tvBfiAjU0r9SCDC51Qop+3Wg5BR1xGMAkM+/dV4MZbcMhdaU1L9bkv6LC95JmiTig==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-1.0.2.tgz",
|
||||
"integrity": "sha512-HgYol2zhH3KbnW9Q4FY/FcIINbsYVL6rwQESChqBC2s9SLWtw1wg05+J7SCrr3BfQpNY2ReYhf8xjpd2JhKJOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -20236,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": {
|
||||
@@ -20246,8 +20250,7 @@
|
||||
"@typescript-eslint/utils": "^8.60.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8",
|
||||
"storybook": "10.5.9"
|
||||
"eslint": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-testing-library": {
|
||||
@@ -23924,9 +23927,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "11.1.17",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.17.tgz",
|
||||
"integrity": "sha512-8Vu44Y0MuMBlTQz/jQ8HEMYNq/bBqk87MnBwYR5mC8AthfhEXidZ5aT/oA/CUqboa8THKltnD9L3xyqhU/Sy1Q==",
|
||||
"version": "11.1.18",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz",
|
||||
"integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -27276,9 +27279,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lerna": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lerna/-/lerna-10.0.0.tgz",
|
||||
"integrity": "sha512-U1Rkz2lMZEGstg7h6vw2LfuGNTomXANZ+mX80+3pR0L2RWWcXI/gZJya9EDq4wTY+1AoUzhNNUnlAEWdFugisw==",
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lerna/-/lerna-10.0.1.tgz",
|
||||
"integrity": "sha512-ibmMaBmH/sR2HpZzgvpEI5/6bCgMp0AISIFZ/cQcCzRVH2EgPapBYHXS6A6NgI6vRJNE4pgnxQbNiSq00HVSjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -27294,7 +27297,7 @@
|
||||
"conventional-changelog": "8.1.0",
|
||||
"conventional-changelog-angular": "9.2.1",
|
||||
"conventional-commits-filter": "6.0.1",
|
||||
"conventional-commits-parser": "7.1.0",
|
||||
"conventional-commits-parser": "7.1.2",
|
||||
"conventional-recommended-bump": "12.1.0",
|
||||
"cosmiconfig": "9.0.0",
|
||||
"dedent": "1.5.3",
|
||||
@@ -27325,7 +27328,7 @@
|
||||
"signal-exit": "3.0.7",
|
||||
"ssri": "12.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"tar": "7.5.20",
|
||||
"tar": "7.5.22",
|
||||
"tinyglobby": "0.2.12",
|
||||
"validate-npm-package-license": "3.0.4",
|
||||
"validate-npm-package-name": "6.0.2",
|
||||
@@ -28485,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",
|
||||
@@ -31856,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": {
|
||||
@@ -37734,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": {
|
||||
@@ -37752,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",
|
||||
@@ -42986,7 +42989,7 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.23",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.14",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
@@ -43487,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"
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^11.0.2",
|
||||
"immer": "^11.1.17",
|
||||
"immer": "^11.1.18",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -185,7 +185,7 @@
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.28.1",
|
||||
"mapbox-gl": "^3.29.0",
|
||||
"markdown-to-jsx": "^9.10.2",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
@@ -257,9 +257,9 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.9",
|
||||
"@storybook/addon-links": "10.5.9",
|
||||
"@storybook/react-webpack5": "10.5.9",
|
||||
"@storybook/addon-docs": "10.5.10",
|
||||
"@storybook/addon-links": "10.5.10",
|
||||
"@storybook/react-webpack5": "10.5.10",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.16.1",
|
||||
@@ -295,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",
|
||||
@@ -311,8 +311,8 @@
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.9",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.2",
|
||||
"eslint-plugin-storybook": "10.5.10",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -326,7 +326,7 @@
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"jsdom": "^30.0.1",
|
||||
"lerna": "^10.0.0",
|
||||
"lerna": "^10.0.1",
|
||||
"lightningcss": "^1.33.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.6.1",
|
||||
@@ -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",
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-time-format": "^4.1.0",
|
||||
"dayjs": "^1.11.23",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.14",
|
||||
"fetch-retry": "^6.0.0",
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { fireEvent, render, screen } from '@superset-ui/core/spec';
|
||||
import { Input } from '../Input';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
const drag = (
|
||||
target: Element,
|
||||
from: [number, number],
|
||||
to: [number, number],
|
||||
) => {
|
||||
fireEvent.mouseDown(target, { clientX: from[0], clientY: from[1] });
|
||||
fireEvent.mouseMove(document, { clientX: to[0], clientY: to[1] });
|
||||
fireEvent.mouseUp(document);
|
||||
};
|
||||
|
||||
const isDragged = () => !!document.querySelector('.react-draggable-dragged');
|
||||
|
||||
describe('Modal draggable', () => {
|
||||
test('dragging from the title bar moves the modal', () => {
|
||||
render(
|
||||
<Modal show onHide={() => {}} title="Edit Dataset" draggable name="test">
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
|
||||
drag(trigger, [100, 50], [150, 90]);
|
||||
|
||||
expect(isDragged()).toBe(true);
|
||||
});
|
||||
|
||||
test('dragging inside modal content does not move the modal', () => {
|
||||
render(
|
||||
<Modal show onHide={() => {}} title="Edit Dataset" draggable name="test">
|
||||
<Input data-test="field" defaultValue="first_view_event" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const input = screen.getByTestId('field');
|
||||
drag(input, [200, 400], [260, 430]);
|
||||
|
||||
expect(isDragged()).toBe(false);
|
||||
});
|
||||
|
||||
test('dragging inside modal content does not move the modal, even after an unrelated re-render while the title was hovered', () => {
|
||||
// Regression test: the title bar used to gate dragging with a
|
||||
// hover-tracked boolean (mouseover/mouseout on `.draggable-trigger`)
|
||||
// instead of react-draggable's own `handle` prop. Because the title
|
||||
// element was defined as an inline component recreated on every
|
||||
// render, any unrelated state change while the cursor was over the
|
||||
// title (e.g. typing in any field) force-remounted it without a real
|
||||
// mouseout ever firing, leaving dragging permanently enabled -- so
|
||||
// selecting text anywhere in the modal dragged the whole modal
|
||||
// instead.
|
||||
function Harness() {
|
||||
const [tick, setTick] = useState(0);
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
onHide={() => {}}
|
||||
title="Edit Dataset"
|
||||
draggable
|
||||
name="test"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-test="rerender"
|
||||
onClick={() => setTick(tick + 1)}
|
||||
>
|
||||
rerender
|
||||
</button>
|
||||
<Input data-test="field" defaultValue="first_view_event" />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Harness />);
|
||||
|
||||
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
|
||||
fireEvent.mouseOver(trigger);
|
||||
fireEvent.click(screen.getByTestId('rerender'));
|
||||
|
||||
const input = screen.getByTestId('field');
|
||||
drag(input, [200, 400], [260, 430]);
|
||||
|
||||
expect(isDragged()).toBe(false);
|
||||
});
|
||||
|
||||
test('dragging is disabled entirely when draggable is not set', () => {
|
||||
render(
|
||||
<Modal show onHide={() => {}} title="Edit Dataset" name="test">
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
expect(document.querySelector('.draggable-trigger')).toBeNull();
|
||||
});
|
||||
|
||||
test('draggableConfig cannot re-enable dragging on a non-draggable modal', () => {
|
||||
render(
|
||||
<Modal
|
||||
show
|
||||
onHide={() => {}}
|
||||
title="Edit Dataset"
|
||||
name="test"
|
||||
draggableConfig={{ disabled: false }}
|
||||
>
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
expect(document.querySelector('.draggable-trigger')).toBeNull();
|
||||
});
|
||||
|
||||
test('draggableConfig can still opt a draggable modal out of dragging', () => {
|
||||
render(
|
||||
<Modal
|
||||
show
|
||||
onHide={() => {}}
|
||||
title="Edit Dataset"
|
||||
draggable
|
||||
name="test"
|
||||
draggableConfig={{ disabled: true }}
|
||||
>
|
||||
<Input data-test="field" defaultValue="value" />
|
||||
</Modal>,
|
||||
);
|
||||
|
||||
const trigger = document.querySelector('.draggable-trigger') as HTMLElement;
|
||||
drag(trigger, [100, 50], [150, 90]);
|
||||
|
||||
expect(isDragged()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -269,7 +269,6 @@ const CustomModal = ({
|
||||
);
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
const [bounds, setBounds] = useState<DraggableBounds>({});
|
||||
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleOnHide = () => {
|
||||
@@ -339,19 +338,7 @@ const CustomModal = ({
|
||||
}, [hideFooter, resizableConfig]);
|
||||
|
||||
const ModalTitle = () =>
|
||||
draggable ? (
|
||||
<div
|
||||
className="draggable-trigger"
|
||||
onMouseOver={() => dragDisabled && setDragDisabled(false)}
|
||||
onMouseOut={() => !dragDisabled && setDragDisabled(true)}
|
||||
onFocus={() => dragDisabled && setDragDisabled(false)}
|
||||
onBlur={() => !dragDisabled && setDragDisabled(true)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<>{title}</>
|
||||
);
|
||||
draggable ? <div className="draggable-trigger">{title}</div> : <>{title}</>;
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
@@ -378,13 +365,19 @@ const CustomModal = ({
|
||||
modalRender={modal =>
|
||||
resizable || draggable ? (
|
||||
<Draggable
|
||||
disabled={!draggable || dragDisabled}
|
||||
bounds={bounds ?? false}
|
||||
onStart={(event, uiData) => onDragStart(event, uiData)}
|
||||
{...draggableConfig}
|
||||
// `disabled` and `handle` are applied after the spread so callers
|
||||
// can't use `draggableConfig` to re-enable dragging on a
|
||||
// non-draggable modal or move the drag handle off the title bar.
|
||||
// A caller opting a draggable modal out via
|
||||
// `draggableConfig.disabled` is still honored.
|
||||
disabled={!draggable || !!draggableConfig?.disabled}
|
||||
handle={draggable ? '.draggable-trigger' : undefined}
|
||||
// Pass nodeRef so react-draggable does not fall back to
|
||||
// ReactDOM.findDOMNode (deprecated in React 18+ Strict Mode).
|
||||
nodeRef={draggableRef}
|
||||
{...draggableConfig}
|
||||
>
|
||||
{resizable ? (
|
||||
<Resizable className="resizable" {...getResizableConfig}>
|
||||
|
||||
@@ -65,6 +65,7 @@ export type AntdExposedProps = Pick<
|
||||
| 'onOpenChange'
|
||||
| 'optionRender'
|
||||
| 'placeholder'
|
||||
| 'prefix'
|
||||
| 'showArrow'
|
||||
| 'showSearch'
|
||||
| 'tokenSeparators'
|
||||
|
||||
+28
@@ -246,6 +246,34 @@ test('wraps component with proper container div', () => {
|
||||
expect(wrapper).toHaveAttribute('data-themed-ag-grid', 'true');
|
||||
});
|
||||
|
||||
test('applies non-transparent backgrounds to native menus, tooltips and overlays', () => {
|
||||
const customTheme = {
|
||||
...supersetTheme,
|
||||
colorBgElevated: '#f2f2f2',
|
||||
};
|
||||
|
||||
render(
|
||||
<ThemeProvider theme={customTheme}>
|
||||
<ThemedAgGridReact rowData={mockRowData} columnDefs={mockColumnDefs} />
|
||||
</ThemeProvider>,
|
||||
);
|
||||
|
||||
const agGrid = screen.getByTestId('ag-grid-react');
|
||||
const theme = JSON.parse(agGrid.getAttribute('data-theme') || '{}');
|
||||
|
||||
// ag-grid's own context/column menus, side bar, tooltips and overlays are
|
||||
// rendered against these params rather than `backgroundColor` (which is
|
||||
// intentionally 'transparent' so the surrounding app shows through the
|
||||
// grid body). Without explicit values they inherit transparency too,
|
||||
// making native menus/popups unreadable.
|
||||
expect(theme.chromeBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.menuBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.menuBorder).toBe(true);
|
||||
expect(theme.sideBarBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.tooltipBackgroundColor).toBe('#f2f2f2');
|
||||
expect(theme.modalOverlayBackgroundColor).toBe('#f2f2f2');
|
||||
});
|
||||
|
||||
test('handles missing theme gracefully', () => {
|
||||
const incompleteTheme = {
|
||||
...supersetTheme,
|
||||
|
||||
+11
@@ -104,6 +104,17 @@ export const ThemedAgGridReact = forwardRef<
|
||||
foregroundColor: theme.colorText,
|
||||
browserColorScheme: isDarkMode ? 'dark' : 'light',
|
||||
|
||||
// Native menus, popups, side bar, tooltips and loading/no-rows overlays
|
||||
// are rendered against these params rather than `backgroundColor`
|
||||
// (which is intentionally transparent). Without explicit values they
|
||||
// inherit transparency too, making them unreadable.
|
||||
chromeBackgroundColor: theme.colorBgElevated,
|
||||
menuBackgroundColor: theme.colorBgElevated,
|
||||
menuBorder: true,
|
||||
sideBarBackgroundColor: theme.colorBgElevated,
|
||||
tooltipBackgroundColor: theme.colorBgElevated,
|
||||
modalOverlayBackgroundColor: theme.colorBgElevated,
|
||||
|
||||
// Header styling
|
||||
headerBackgroundColor: theme.colorFillTertiary,
|
||||
headerTextColor: theme.colorTextHeading,
|
||||
|
||||
@@ -119,6 +119,20 @@ export function retrieveErrorMessage(
|
||||
return statusError || parseStringResponse(str);
|
||||
}
|
||||
|
||||
function getFirstValidationError(message: JsonObject): string | undefined {
|
||||
const [firstError] = Object.values(message);
|
||||
|
||||
if (typeof firstError === 'string') {
|
||||
return firstError;
|
||||
}
|
||||
|
||||
if (Array.isArray(firstError)) {
|
||||
return firstError.find((item): item is string => typeof item === 'string');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
|
||||
let error = { ...responseJson };
|
||||
// Backwards compatibility for old error renderers with the new error object
|
||||
@@ -126,13 +140,12 @@ export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
|
||||
error.error = error.description = error.errors[0].message;
|
||||
error.link = error.errors[0]?.extra?.link;
|
||||
}
|
||||
// Marshmallow field validation returns the error message in the format
|
||||
// of { message: { field1: [msg1, msg2], field2: [msg], } }
|
||||
// Marshmallow field validation returns arrays for string messages, but
|
||||
// serializes lazy translation messages as strings instead.
|
||||
if (!error.error && error.message) {
|
||||
if (typeof error.message === 'object') {
|
||||
error.error =
|
||||
Object.values(error.message as Record<string, string[]>)[0]?.[0] ||
|
||||
t('Invalid input');
|
||||
getFirstValidationError(error.message) || t('Invalid input');
|
||||
}
|
||||
if (typeof error.message === 'string') {
|
||||
if (checkForHtml(error.message)) {
|
||||
|
||||
@@ -28,6 +28,7 @@ export enum FeatureFlag {
|
||||
AlertReportSlackV2 = 'ALERT_REPORT_SLACK_V2',
|
||||
AlertReportWebhook = 'ALERT_REPORT_WEBHOOK',
|
||||
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
|
||||
AlertReportsRetry = 'ALERT_REPORTS_RETRY',
|
||||
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
|
||||
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
|
||||
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
|
||||
|
||||
@@ -244,6 +244,24 @@ test('parseErrorJson with message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('parseErrorJson preserves string-valued validation messages', () => {
|
||||
const calculatedColumnError =
|
||||
'Custom SQL fields cannot be parsed as a single SQL statement.';
|
||||
|
||||
expect(
|
||||
parseErrorJson({
|
||||
message: {
|
||||
'columns.0.expression': calculatedColumnError,
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
message: {
|
||||
'columns.0.expression': calculatedColumnError,
|
||||
},
|
||||
error: calculatedColumnError,
|
||||
});
|
||||
});
|
||||
|
||||
test('parseErrorJson with HTML message', () => {
|
||||
expect(
|
||||
parseErrorJson({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -175,6 +175,18 @@ const config: ControlPanelConfig = {
|
||||
label: t('X Axis'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'show_x_axis',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show X axis'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t('Show or hide the X axis line, ticks, and labels'),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'x_axis_label',
|
||||
@@ -183,6 +195,8 @@ const config: ControlPanelConfig = {
|
||||
label: t('X Axis Label'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
visibility: ({ controls }) =>
|
||||
controls?.show_x_axis?.value !== false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -193,6 +207,8 @@ const config: ControlPanelConfig = {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
description: `${D3_TIME_FORMAT_DOCS}.`,
|
||||
visibility: ({ controls }) =>
|
||||
controls?.show_x_axis?.value !== false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -213,6 +229,8 @@ const config: ControlPanelConfig = {
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
description: t('The way the ticks are laid out on the X-axis'),
|
||||
visibility: ({ controls }) =>
|
||||
controls?.show_x_axis?.value !== false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -222,6 +240,20 @@ const config: ControlPanelConfig = {
|
||||
label: t('Y Axis'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'show_y_axis',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Show Y axis'),
|
||||
renderTrigger: true,
|
||||
default: true,
|
||||
description: t(
|
||||
'Show or hide the Y axis line, ticks, gridlines, and labels',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'y_axis_label',
|
||||
@@ -230,6 +262,8 @@ const config: ControlPanelConfig = {
|
||||
label: t('Y Axis Label'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
visibility: ({ controls }) =>
|
||||
controls?.show_y_axis?.value !== false,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -239,6 +273,10 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
// Note: y_axis_format and currency_format are intentionally NOT gated on
|
||||
// show_y_axis. They drive `defaultFormatter`, which formats the bar labels
|
||||
// and tooltips as well as the axis, so they must stay configurable even
|
||||
// when the Y axis itself is hidden.
|
||||
groupby: {
|
||||
label: t('Breakdowns'),
|
||||
description:
|
||||
|
||||
@@ -185,6 +185,8 @@ export default function transformProps(
|
||||
xTicksLayout,
|
||||
xAxisTimeFormat,
|
||||
showLegend,
|
||||
showXAxis = true,
|
||||
showYAxis = true,
|
||||
yAxisLabel,
|
||||
xAxisLabel,
|
||||
yAxisFormat,
|
||||
@@ -433,8 +435,10 @@ export default function transformProps(
|
||||
grid: {
|
||||
...defaultGrid,
|
||||
top: theme.sizeUnit * 7,
|
||||
bottom: theme.sizeUnit * 7,
|
||||
left: theme.sizeUnit * 5,
|
||||
// Reclaim the axis-oriented padding when an axis is hidden so an
|
||||
// axis-free chart gets a clean, tight layout instead of empty margins.
|
||||
bottom: theme.sizeUnit * (showXAxis ? 7 : 3),
|
||||
left: theme.sizeUnit * (showYAxis ? 5 : 2),
|
||||
right: theme.sizeUnit * 7,
|
||||
},
|
||||
legend: {
|
||||
@@ -443,6 +447,7 @@ export default function transformProps(
|
||||
data: [legendNames.INCREASE, legendNames.DECREASE, legendNames.TOTAL],
|
||||
},
|
||||
xAxis: {
|
||||
show: showXAxis,
|
||||
data: xAxisData,
|
||||
type: 'category',
|
||||
name: xAxisLabel,
|
||||
@@ -454,6 +459,7 @@ export default function transformProps(
|
||||
},
|
||||
yAxis: {
|
||||
...defaultYAxis,
|
||||
show: showYAxis,
|
||||
type: 'value',
|
||||
nameTextStyle: {
|
||||
padding: [0, 0, theme.sizeUnit * 5, 0],
|
||||
|
||||
@@ -55,8 +55,10 @@ export type EchartsWaterfallFormData = QueryFormData &
|
||||
xAxisLabel: string;
|
||||
xAxisTimeFormat?: string;
|
||||
xTicksLayout?: WaterfallFormXTicksLayout;
|
||||
showXAxis: boolean;
|
||||
yAxisLabel: string;
|
||||
yAxisFormat: string;
|
||||
showYAxis: boolean;
|
||||
increaseLabel?: string;
|
||||
decreaseLabel?: string;
|
||||
totalLabel?: string;
|
||||
@@ -65,6 +67,8 @@ export type EchartsWaterfallFormData = QueryFormData &
|
||||
|
||||
export const DEFAULT_FORM_DATA: Partial<EchartsWaterfallFormData> = {
|
||||
showLegend: true,
|
||||
showXAxis: true,
|
||||
showYAxis: true,
|
||||
};
|
||||
|
||||
export interface EchartsWaterfallChartProps extends ChartProps {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -114,6 +114,23 @@ export const textStyleSchema = z.object({
|
||||
// Style Schemas
|
||||
// =============================================================================
|
||||
|
||||
/** Repeating tile pattern painted over a fill, e.g. hatching */
|
||||
export const decalSchema = z.object({
|
||||
symbol: z.union([symbolTypeSchema, z.array(symbolTypeSchema)]).optional(),
|
||||
symbolSize: z.number().optional(),
|
||||
symbolKeepAspect: z.boolean().optional(),
|
||||
color: colorSchema.optional(),
|
||||
backgroundColor: colorSchema.optional(),
|
||||
dashArrayX: z
|
||||
.union([z.number(), z.array(z.union([z.number(), z.array(z.number())]))])
|
||||
.optional(),
|
||||
dashArrayY: z.union([z.number(), z.array(z.number())]).optional(),
|
||||
/** Radians, not degrees. */
|
||||
rotation: z.number().optional(),
|
||||
maxTileWidth: z.number().optional(),
|
||||
maxTileHeight: z.number().optional(),
|
||||
});
|
||||
|
||||
export const lineStyleSchema = z.object({
|
||||
color: colorSchema.optional(),
|
||||
width: z.number().optional(),
|
||||
@@ -150,6 +167,7 @@ export const itemStyleSchema = z.object({
|
||||
shadowOffsetX: z.number().optional(),
|
||||
shadowOffsetY: z.number().optional(),
|
||||
opacity: z.number().min(0).max(1).optional(),
|
||||
decal: decalSchema.optional(),
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
@@ -818,6 +836,7 @@ export type TextStyleOption = z.infer<typeof textStyleSchema>;
|
||||
export type LineStyleOption = z.infer<typeof lineStyleSchema>;
|
||||
export type AreaStyleOption = z.infer<typeof areaStyleSchema>;
|
||||
export type ItemStyleOption = z.infer<typeof itemStyleSchema>;
|
||||
export type DecalOption = z.infer<typeof decalSchema>;
|
||||
export type LabelOption = z.infer<typeof labelSchema>;
|
||||
export type TitleOption = z.infer<typeof titleSchema>;
|
||||
export type LegendOption = z.infer<typeof legendSchema>;
|
||||
|
||||
@@ -47,6 +47,9 @@ const getCrossFilterDataMask =
|
||||
) =>
|
||||
(value: string) => {
|
||||
const selected = Object.values(selectedValues);
|
||||
if (!labelMap[value] && !selected.includes(value)) {
|
||||
return undefined;
|
||||
}
|
||||
let values: string[];
|
||||
if (selected.includes(value)) {
|
||||
values = selected.filter(v => v !== value);
|
||||
|
||||
+91
@@ -593,3 +593,94 @@ test('strips tooltip extraCssText instead of passing raw CSS through', () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({ tooltip: { show: true } });
|
||||
});
|
||||
|
||||
test('accepts a decal pattern on itemStyle', () => {
|
||||
const result = parseEChartOptions(
|
||||
`{ series: { itemStyle: { decal: {
|
||||
symbol: 'rect',
|
||||
dashArrayX: [1, 0],
|
||||
dashArrayY: [2, 4],
|
||||
rotation: -0.7853981633974483,
|
||||
color: 'rgba(0, 0, 0, 0.2)',
|
||||
} } } }`,
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
series: {
|
||||
itemStyle: {
|
||||
decal: {
|
||||
symbol: 'rect',
|
||||
dashArrayX: [1, 0],
|
||||
dashArrayY: [2, 4],
|
||||
rotation: -0.7853981633974483,
|
||||
color: 'rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('accepts the full decal shape, including per-row dash arrays', () => {
|
||||
// `dashArrayX` nests one level to offset rows from each other; `dashArrayY`
|
||||
// has no equivalent.
|
||||
const result = parseEChartOptions(
|
||||
`{ series: { itemStyle: { decal: {
|
||||
symbol: ['rect', 'circle'],
|
||||
symbolSize: 0.8,
|
||||
symbolKeepAspect: false,
|
||||
color: '#383838',
|
||||
backgroundColor: 'transparent',
|
||||
dashArrayX: [[1, 0], [0, 1]],
|
||||
dashArrayY: 5,
|
||||
rotation: 0,
|
||||
maxTileWidth: 512,
|
||||
maxTileHeight: 512,
|
||||
} } } }`,
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(
|
||||
(result.data as { series: { itemStyle: { decal: unknown } } }).series
|
||||
.itemStyle.decal,
|
||||
).toEqual({
|
||||
symbol: ['rect', 'circle'],
|
||||
symbolSize: 0.8,
|
||||
symbolKeepAspect: false,
|
||||
color: '#383838',
|
||||
backgroundColor: 'transparent',
|
||||
dashArrayX: [
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
],
|
||||
dashArrayY: 5,
|
||||
rotation: 0,
|
||||
maxTileWidth: 512,
|
||||
maxTileHeight: 512,
|
||||
});
|
||||
});
|
||||
|
||||
test('strips unknown keys from a decal rather than passing them through', () => {
|
||||
const result = parseEChartOptions(
|
||||
`{ series: { itemStyle: { decal: { symbol: 'rect', onclick: 'alert(1)' } } } }`,
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual({
|
||||
series: { itemStyle: { decal: { symbol: 'rect' } } },
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects a decal whose values are of the wrong type', () => {
|
||||
// Unknown keys are stripped; a known key with a bad type is an error.
|
||||
const input = `{ series: { itemStyle: { decal: { rotation: 'sideways' } } } }`;
|
||||
|
||||
expect(() => parseEChartOptions(input)).toThrow(EChartOptionsParseError);
|
||||
try {
|
||||
parseEChartOptions(input);
|
||||
} catch (error) {
|
||||
expect((error as EChartOptionsParseError).errorType).toBe(
|
||||
'validation_error',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+109
@@ -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);
|
||||
});
|
||||
+54
-1
@@ -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<
|
||||
|
||||
+46
@@ -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);
|
||||
});
|
||||
|
||||
+141
@@ -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 },
|
||||
|
||||
@@ -166,3 +166,56 @@ test('hide totals', () => {
|
||||
['-', '-'],
|
||||
]);
|
||||
});
|
||||
|
||||
const buildAxes = (extraFormData: Record<string, unknown>) => {
|
||||
const chartProps = new ChartProps({
|
||||
formData: { ...formData, ...extraFormData },
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [{ data }],
|
||||
theme: supersetTheme,
|
||||
});
|
||||
const transformedProps = transformProps(
|
||||
chartProps as unknown as EchartsWaterfallChartProps,
|
||||
);
|
||||
return {
|
||||
xAxis: transformedProps.echartOptions.xAxis as any,
|
||||
yAxis: transformedProps.echartOptions.yAxis as any,
|
||||
grid: transformedProps.echartOptions.grid as any,
|
||||
};
|
||||
};
|
||||
|
||||
test('shows both axes by default', () => {
|
||||
const { xAxis, yAxis } = buildAxes({});
|
||||
expect(xAxis.show).not.toBe(false);
|
||||
expect(yAxis.show).not.toBe(false);
|
||||
});
|
||||
|
||||
test('hides the whole X axis when showXAxis is false', () => {
|
||||
const { xAxis } = buildAxes({ showXAxis: false });
|
||||
// echarts hides the axis line, ticks, labels, name, and gridlines when
|
||||
// `show` is false — a single flag rather than a set of sub-flags.
|
||||
expect(xAxis.show).toBe(false);
|
||||
});
|
||||
|
||||
test('hides the whole Y axis when showYAxis is false', () => {
|
||||
const { yAxis } = buildAxes({ showYAxis: false });
|
||||
expect(yAxis.show).toBe(false);
|
||||
});
|
||||
|
||||
test('reclaims the bottom grid margin when the X axis is hidden', () => {
|
||||
const { grid: shown } = buildAxes({ showXAxis: true });
|
||||
const { grid: hidden } = buildAxes({ showXAxis: false });
|
||||
// The bottom margin reserves room for the X-axis labels and name; with the
|
||||
// axis hidden that space should be reclaimed for a clean, axis-free layout.
|
||||
expect(hidden.bottom).toBeLessThan(shown.bottom);
|
||||
// Hiding the X axis must not shrink the Y-axis (left) margin.
|
||||
expect(hidden.left).toBe(shown.left);
|
||||
});
|
||||
|
||||
test('reclaims the left grid margin when the Y axis is hidden', () => {
|
||||
const { grid: shown } = buildAxes({ showYAxis: true });
|
||||
const { grid: hidden } = buildAxes({ showYAxis: false });
|
||||
expect(hidden.left).toBeLessThan(shown.left);
|
||||
expect(hidden.bottom).toBe(shown.bottom);
|
||||
});
|
||||
|
||||
@@ -180,3 +180,46 @@ test('cross-filter does nothing when emitCrossFilters is false', () => {
|
||||
|
||||
expect(setDataMask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('cross-filter does nothing when name is missing from labelMap', () => {
|
||||
const setDataMask = jest.fn();
|
||||
const props = buildProps({
|
||||
groupby: ['topics'],
|
||||
labelMap: {
|
||||
cancellations: ['cancellations'],
|
||||
},
|
||||
selectedValues: {},
|
||||
setDataMask,
|
||||
});
|
||||
|
||||
const handlers = allEventHandlers(props);
|
||||
// e.g. Pie "Other" category is not present in labelMap
|
||||
handlers.click({ name: 'Other' });
|
||||
|
||||
expect(setDataMask).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('cross-filter still deselects a previously selected value that is missing from labelMap', () => {
|
||||
const setDataMask = jest.fn();
|
||||
const props = buildProps({
|
||||
groupby: ['topics'],
|
||||
labelMap: {
|
||||
cancellations: ['cancellations'],
|
||||
},
|
||||
// "Other" was selected before it dropped out of labelMap (e.g. a stale
|
||||
// cross-filter from an earlier render or dashboard state).
|
||||
selectedValues: { 0: 'Other' },
|
||||
setDataMask,
|
||||
});
|
||||
|
||||
const handlers = allEventHandlers(props);
|
||||
handlers.click({ name: 'Other' });
|
||||
|
||||
expect(setDataMask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
extraFormData: {
|
||||
filters: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -107,6 +107,11 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
|
||||
);
|
||||
const metricsArr = ensureIsArray(formData.metrics);
|
||||
let data: QueryData[];
|
||||
// The rows that conditional formatting derives its color scale from. Only the
|
||||
// leaf (detail) cells belong in that domain: the totals are aggregates of the
|
||||
// very cells being shaded, so letting them in makes the grand total the max
|
||||
// and leaves every detail cell nearly unshaded.
|
||||
let colorScaleRows: DataRecord[];
|
||||
if (allMetricsAdditive(metricsArr)) {
|
||||
// Additive fast-path: a single full-detail query was issued; synthesize
|
||||
// each rollup level by reducing the leaf rows on the client (see SIP.md).
|
||||
@@ -128,6 +133,11 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
|
||||
data: synthesized[i] as DataRecord[],
|
||||
groupby: combination,
|
||||
}));
|
||||
// The query returned the leaf rows and nothing else, so no totals can leak
|
||||
// into the domain. Use those raw rows rather than the synthesized leaf
|
||||
// level, whose reduction coerces values through `Number` and drops
|
||||
// non-numeric ones -- that would shift the domain for additive metrics.
|
||||
colorScaleRows = leafRows;
|
||||
} else {
|
||||
// Non-additive: a single GROUPING SETS query returned all rollup levels
|
||||
// tagged with GROUPING() markers; split the combined result back into one
|
||||
@@ -145,6 +155,13 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
|
||||
data: splitRows[i] as DataRecord[],
|
||||
groupby: combination,
|
||||
}));
|
||||
// This result *does* carry the rollup levels, so pick out the leaf level --
|
||||
// the one grouping every dimension, same definition the splitter uses.
|
||||
const leafIndex = levelLabels.findIndex(labels => {
|
||||
const grouped = new Set(labels);
|
||||
return allGroupbyLabels.every(label => grouped.has(label));
|
||||
});
|
||||
colorScaleRows = (splitRows[leafIndex] as DataRecord[]) ?? [];
|
||||
}
|
||||
// The full-granularity query has the most colnames -- use it for column/type
|
||||
// metadata and formatters.
|
||||
@@ -221,7 +238,7 @@ export default function transformProps(chartProps: ChartProps<QueryFormData>) {
|
||||
);
|
||||
const metricColorFormatters = getColorFormatters(
|
||||
pivotConditionalFormatting,
|
||||
mainQuery.data,
|
||||
colorScaleRows,
|
||||
theme,
|
||||
);
|
||||
|
||||
|
||||
+149
@@ -452,3 +452,152 @@ test('additive metrics: synthesizes rollup levels from a single leaf query', ()
|
||||
{ region: 'EU', v: 5 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('conditional formatting scales over leaf cells only, not rollup totals', () => {
|
||||
const gm = (col: string) => `${col}__superset_grouping`;
|
||||
// `metrics` below is a plain string, i.e. a saved-metric reference.
|
||||
// `isAdditiveMetric` treats every string as non-additive no matter what it is
|
||||
// named, because form data does not reveal the aggregate behind a saved
|
||||
// metric -- so a saved metric labelled "SUM(sales)" (as in the report this
|
||||
// regression comes from) takes the non-additive path despite the name. That
|
||||
// path issues a single GROUPING SETS query whose result carries the rollup
|
||||
// levels alongside the leaf rows, so with both totals toggles on the grand
|
||||
// total (100) is part of that result.
|
||||
const row = (
|
||||
productLine: string | null,
|
||||
dealSize: string | null,
|
||||
sales: number,
|
||||
) => ({
|
||||
product_line: productLine,
|
||||
deal_size: dealSize,
|
||||
'SUM(sales)': sales,
|
||||
[gm('product_line')]: productLine === null ? 1 : 0,
|
||||
[gm('deal_size')]: dealSize === null ? 1 : 0,
|
||||
});
|
||||
const totalsChartProps = new ChartProps<QueryFormData>({
|
||||
formData: {
|
||||
...formData,
|
||||
combineMetric: false,
|
||||
transposePivot: false,
|
||||
metricsLayout: MetricsLayoutEnum.ROWS,
|
||||
groupbyRows: ['product_line'],
|
||||
groupbyColumns: ['deal_size'],
|
||||
metrics: ['SUM(sales)'],
|
||||
colTotals: true,
|
||||
rowTotals: true,
|
||||
conditionalFormatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'SUM(sales)',
|
||||
operator: '>',
|
||||
targetValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
// leaf cells
|
||||
row('Classic Cars', 'Small', 10),
|
||||
row('Classic Cars', 'Large', 20),
|
||||
row('Motorcycles', 'Small', 30),
|
||||
row('Motorcycles', 'Large', 40),
|
||||
// row totals
|
||||
row('Classic Cars', null, 30),
|
||||
row('Motorcycles', null, 70),
|
||||
// column totals
|
||||
row(null, 'Small', 40),
|
||||
row(null, 'Large', 60),
|
||||
// grand total
|
||||
row(null, null, 100),
|
||||
],
|
||||
colnames: [
|
||||
'product_line',
|
||||
'deal_size',
|
||||
'SUM(sales)',
|
||||
gm('product_line'),
|
||||
gm('deal_size'),
|
||||
],
|
||||
coltypes: [1, 1, 0, 0, 0],
|
||||
},
|
||||
],
|
||||
hooks: { setDataMask },
|
||||
filterState: { selectedFilters: {} },
|
||||
datasource: { verboseMap: {}, columnFormats: {} },
|
||||
theme: supersetTheme,
|
||||
});
|
||||
|
||||
const { getColorFromValue } =
|
||||
transformProps(totalsChartProps).metricColorFormatters[0];
|
||||
// The largest leaf cell must be fully saturated. Including the grand total
|
||||
// in the domain would stretch it to 100 and leave this cell washed out.
|
||||
expect(getColorFromValue(40)).toEqual('#ACE1C4FF');
|
||||
});
|
||||
|
||||
test('conditional formatting on the additive path uses the raw leaf query rows', () => {
|
||||
// Counterpart to the test above for the additive fast path. Its query returns
|
||||
// the leaf rows only, so the domain is those rows verbatim -- deliberately not
|
||||
// the synthesized leaf level, whose reduction would coerce values through
|
||||
// `Number` and drop non-numeric ones.
|
||||
const additiveChartProps = new ChartProps<QueryFormData>({
|
||||
formData: {
|
||||
...formData,
|
||||
combineMetric: false,
|
||||
transposePivot: false,
|
||||
metricsLayout: MetricsLayoutEnum.ROWS,
|
||||
groupbyRows: ['product_line'],
|
||||
groupbyColumns: ['deal_size'],
|
||||
metrics: [
|
||||
{
|
||||
expressionType: 'SIMPLE',
|
||||
aggregate: 'SUM',
|
||||
column: { column_name: 'sales' },
|
||||
label: 'SUM(sales)',
|
||||
},
|
||||
],
|
||||
colTotals: true,
|
||||
rowTotals: true,
|
||||
conditionalFormatting: [
|
||||
{
|
||||
colorScheme: '#ACE1C4',
|
||||
column: 'SUM(sales)',
|
||||
operator: '>',
|
||||
targetValue: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
product_line: 'Classic Cars',
|
||||
deal_size: 'Small',
|
||||
'SUM(sales)': 10,
|
||||
},
|
||||
{
|
||||
product_line: 'Classic Cars',
|
||||
deal_size: 'Large',
|
||||
'SUM(sales)': 20,
|
||||
},
|
||||
{ product_line: 'Motorcycles', deal_size: 'Small', 'SUM(sales)': 30 },
|
||||
{ product_line: 'Motorcycles', deal_size: 'Large', 'SUM(sales)': 40 },
|
||||
],
|
||||
colnames: ['product_line', 'deal_size', 'SUM(sales)'],
|
||||
coltypes: [1, 1, 0],
|
||||
},
|
||||
],
|
||||
hooks: { setDataMask },
|
||||
filterState: { selectedFilters: {} },
|
||||
datasource: { verboseMap: {}, columnFormats: {} },
|
||||
theme: supersetTheme,
|
||||
});
|
||||
|
||||
const { getColorFromValue } =
|
||||
transformProps(additiveChartProps).metricColorFormatters[0];
|
||||
// Scale spans the leaf cells (max 40), never the client-side grand total 100.
|
||||
expect(getColorFromValue(40)).toEqual('#ACE1C4FF');
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1138,7 +1138,8 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
?.backgroundColor || backgroundColor;
|
||||
arrow =
|
||||
column.label === comparisonLabels[0]
|
||||
? basicColorColumnFormatters[row.index]?.[column.key]?.mainArrow
|
||||
? (basicColorColumnFormatters[row.index]?.[column.key]
|
||||
?.mainArrow ?? arrow)
|
||||
: '';
|
||||
}
|
||||
const rowSurfaceColor =
|
||||
@@ -1194,30 +1195,36 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
}
|
||||
`;
|
||||
|
||||
let arrowStyles = css`
|
||||
color: ${
|
||||
// Plain inline style (rather than the `css` prop) so the arrow's
|
||||
// color is guaranteed to apply regardless of whether the consuming
|
||||
// app's build wires up the emotion JSX pragma for the `css` prop --
|
||||
// notably, this codebase's own Jest/Babel config does not, which
|
||||
// silently no-ops any `css` prop on a plain DOM element.
|
||||
let arrowStyles: CSSProperties = {
|
||||
color:
|
||||
basicColorFormatters &&
|
||||
basicColorFormatters[row.index]?.[originKey]?.arrowColor ===
|
||||
ColorSchemeEnum.Green
|
||||
? theme.colorSuccess
|
||||
: theme.colorError
|
||||
};
|
||||
margin-right: ${theme.sizeUnit}px;
|
||||
`;
|
||||
: theme.colorError,
|
||||
marginRight: theme.sizeUnit,
|
||||
};
|
||||
|
||||
if (
|
||||
basicColorColumnFormatters &&
|
||||
basicColorColumnFormatters?.length > 0
|
||||
) {
|
||||
arrowStyles = css`
|
||||
color: ${
|
||||
basicColorColumnFormatters[row.index]?.[column.key]
|
||||
?.arrowColor === ColorSchemeEnum.Green
|
||||
? theme.colorSuccess
|
||||
: theme.colorError
|
||||
const columnArrowColor =
|
||||
basicColorColumnFormatters[row.index]?.[column.key]?.arrowColor;
|
||||
if (columnArrowColor) {
|
||||
arrowStyles = {
|
||||
color:
|
||||
columnArrowColor === ColorSchemeEnum.Green
|
||||
? theme.colorSuccess
|
||||
: theme.colorError,
|
||||
marginRight: theme.sizeUnit,
|
||||
};
|
||||
margin-right: ${theme.sizeUnit}px;
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const cellProps = {
|
||||
@@ -1302,12 +1309,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
className="dt-truncate-cell"
|
||||
style={columnWidth ? { width: columnWidth } : undefined}
|
||||
>
|
||||
{arrow && <span css={arrowStyles}>{arrow}</span>}
|
||||
{arrow && <span style={arrowStyles}>{arrow}</span>}
|
||||
{text}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{arrow && <span css={arrowStyles}>{arrow}</span>}
|
||||
{arrow && <span style={arrowStyles}>{arrow}</span>}
|
||||
{text}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2111,7 +2111,14 @@ describe('plugin-chart-table', () => {
|
||||
|
||||
expect(() =>
|
||||
render(
|
||||
<TableChart {...propsWithMissingFormatterEntry} sticky={false} />,
|
||||
ProviderWrapper({
|
||||
children: (
|
||||
<TableChart
|
||||
{...propsWithMissingFormatterEntry}
|
||||
sticky={false}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
),
|
||||
).not.toThrow();
|
||||
|
||||
@@ -2125,8 +2132,24 @@ describe('plugin-chart-table', () => {
|
||||
'rgba(0, 150, 0, 0.2)',
|
||||
);
|
||||
|
||||
// the row missing a formatter entry still renders its raw value
|
||||
expect(screen.getAllByTitle('110').length).toBeGreaterThan(0);
|
||||
// the row missing a formatter entry falls back to the row-level
|
||||
// comparison arrow instead of losing it: before the fix, this row's
|
||||
// arrow was silently cleared (and its color, computed the same way,
|
||||
// would have flipped to the "decrease" color) whenever the
|
||||
// column-specific lookup for this row was undefined.
|
||||
const arrowCell = screen
|
||||
.getAllByTitle('110')
|
||||
.find(cell => cell.querySelector('span'));
|
||||
expect(arrowCell).toHaveTextContent('↑110');
|
||||
expect(getComputedStyle(arrowCell!).background).toContain(
|
||||
'rgba(0, 150, 0, 0.2)',
|
||||
);
|
||||
// the fallback arrow itself must also keep the "increase" color --
|
||||
// asserting only the cell background would still pass if the arrow's
|
||||
// own color had regressed to the "decrease" color.
|
||||
expect(arrowCell!.querySelector('span')).toHaveStyle({
|
||||
color: supersetTheme.colorSuccess,
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves client-side search text across temporal table rerenders', async () => {
|
||||
|
||||
@@ -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};
|
||||
`}
|
||||
`;
|
||||
|
||||
|
||||
+17
@@ -43,6 +43,23 @@ describe('SaveDatasetActionButton', () => {
|
||||
expect(saveDatasetBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('disables only the dataset button when canSaveDataset is false', () => {
|
||||
const onSaveAsExplore = jest.fn();
|
||||
render(
|
||||
<SaveDatasetActionButton
|
||||
setShowSave={() => true}
|
||||
onSaveAsExplore={onSaveAsExplore}
|
||||
canSaveDataset={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Saving the query needs no results.
|
||||
expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save dataset/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('disables the save dataset button when the query did not run successfully', async () => {
|
||||
render(
|
||||
<SaveDatasetActionButton
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
import { act, type ComponentProps } from 'react';
|
||||
import {
|
||||
cleanup,
|
||||
createStore,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import reducerIndex from 'spec/helpers/reducerIndex';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
|
||||
@@ -63,6 +65,12 @@ beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// In-body restores are skipped when an assertion throws, leaking a
|
||||
// configured spy into later tests.
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// Mock createDatasource to return a thunk that resolves with the dataset's
|
||||
// new id. The test's mock store includes redux-thunk middleware (from RTK's
|
||||
// getDefaultMiddleware), so dispatch(createDatasource(...)) properly unwraps
|
||||
@@ -518,6 +526,39 @@ describe('SaveDatasetModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('surfaces the error and keeps the modal open when saving fails', async () => {
|
||||
// The chart-payload step's toast was built but never dispatched, so a
|
||||
// failure there was silent.
|
||||
const postFormData = jest.spyOn(
|
||||
require('src/explore/exploreUtils/formData'),
|
||||
'postFormData',
|
||||
);
|
||||
postFormData.mockRejectedValue(new Error('Boom'));
|
||||
const onHide = jest.fn();
|
||||
const store = createStore({ user }, reducerIndex);
|
||||
|
||||
render(<SaveDatasetModal {...mockedProps} onHide={onHide} />, { store });
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue(/unimportant/i), {
|
||||
target: { value: 'my dataset' },
|
||||
});
|
||||
userEvent.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
// `createStore` builds its reducer map at runtime, so state isn't typed.
|
||||
const toasts = () =>
|
||||
(
|
||||
store.getState() as unknown as {
|
||||
messageToasts: { toastType: string }[];
|
||||
}
|
||||
).messageToasts;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toasts()).toHaveLength(1);
|
||||
});
|
||||
expect(toasts()[0].toastType).toBe('DANGER_TOAST');
|
||||
expect(onHide).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('clearDatasetCache is imported and available', () => {
|
||||
const { clearDatasetCache } = require('src/utils/cachedSupersetGet');
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ import type Subject from 'src/types/Subject';
|
||||
import { openInNewTab, redirect } from 'src/utils/navigationUtils';
|
||||
import { mapSubjectValuesToIds } from 'src/features/subjects/SubjectPicker';
|
||||
|
||||
// Derived so it can't drift from what `getClientErrorObject` accepts.
|
||||
type SaveErrorSource = Parameters<typeof getClientErrorObject>[0];
|
||||
|
||||
interface QueryDatabase {
|
||||
id?: number;
|
||||
}
|
||||
@@ -391,9 +394,18 @@ export const SaveDatasetModal = ({
|
||||
setDatasetName(getDefaultDatasetName());
|
||||
onHide();
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error?: SaveErrorSource) => {
|
||||
setLoading(false);
|
||||
addDangerToast(t('An error occurred saving dataset'));
|
||||
// `createDatasource` already toasted the server's message and rejects
|
||||
// with nothing; only the chart-payload step needs its own.
|
||||
if (!error) {
|
||||
return;
|
||||
}
|
||||
getClientErrorObject(error).then(e =>
|
||||
dispatch(
|
||||
addDangerToast(e.error || t('An error occurred saving dataset')),
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
import SaveQuery from 'src/SqlLab/components/SaveQuery';
|
||||
import { initialState, databases } from 'src/SqlLab/fixtures';
|
||||
|
||||
const RESULT_COLUMNS = [{ column_name: 'col', type: 'STRING' }];
|
||||
|
||||
const mockedProps = {
|
||||
queryEditorId: '123',
|
||||
animation: false,
|
||||
@@ -35,7 +37,6 @@ const mockedProps = {
|
||||
onSave: () => {},
|
||||
saveQueryWarning: null,
|
||||
columns: [],
|
||||
canSaveDataset: true,
|
||||
};
|
||||
|
||||
const mockState = {
|
||||
@@ -60,8 +61,31 @@ const splitSaveBtnProps = {
|
||||
...mockedProps.database,
|
||||
allows_virtual_table_explore: true,
|
||||
},
|
||||
columns: RESULT_COLUMNS,
|
||||
};
|
||||
|
||||
const EDITOR_SQL = 'SELECT * FROM t';
|
||||
|
||||
const stateWithLatestQuery = ({
|
||||
id,
|
||||
state,
|
||||
sql = EDITOR_SQL,
|
||||
}: {
|
||||
id: string;
|
||||
state: string;
|
||||
sql?: string;
|
||||
}) => ({
|
||||
...mockState,
|
||||
sqlLab: {
|
||||
...mockState.sqlLab,
|
||||
queryEditors: mockState.sqlLab.queryEditors.map(qe => ({
|
||||
...qe,
|
||||
latestQueryId: id,
|
||||
})),
|
||||
queries: { [id]: { id, state, sql } },
|
||||
},
|
||||
});
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
@@ -97,6 +121,71 @@ describe('SavedQuery', () => {
|
||||
expect(saveBtn).toBeVisible();
|
||||
});
|
||||
|
||||
test('blocks "Save dataset" until the query has run successfully', () => {
|
||||
// Without a successful run the save can only fail server-side.
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'failed' })),
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save dataset/i }),
|
||||
).toBeDisabled();
|
||||
// Saving the query itself is unaffected.
|
||||
expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled();
|
||||
});
|
||||
|
||||
test('blocks "Save dataset" when no query has been run at all', () => {
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(mockState),
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save dataset/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('blocks "Save dataset" when the SQL changed after a successful run', () => {
|
||||
// The run succeeded, but not for what is in the editor now -- and it is
|
||||
// the editor's SQL that gets saved.
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(
|
||||
stateWithLatestQuery({
|
||||
id: 'qid-1',
|
||||
state: 'success',
|
||||
sql: 'SELECT 1 AS ran_earlier',
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save dataset/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('blocks "Save dataset" when the successful query returned no columns', () => {
|
||||
// e.g. a DDL/DML statement -- there is nothing to introspect into a dataset.
|
||||
render(<SaveQuery {...splitSaveBtnProps} columns={[]} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save dataset/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
test('enables "Save dataset" once the query has succeeded', () => {
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
|
||||
});
|
||||
|
||||
expect(screen.getByRole('button', { name: /save dataset/i })).toBeEnabled();
|
||||
});
|
||||
|
||||
test('renders a save query modal when user clicks save button', () => {
|
||||
render(<SaveQuery {...mockedProps} />, {
|
||||
useRedux: true,
|
||||
@@ -234,7 +323,7 @@ describe('SavedQuery', () => {
|
||||
test('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(mockState),
|
||||
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
|
||||
});
|
||||
|
||||
const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i);
|
||||
@@ -248,7 +337,7 @@ describe('SavedQuery', () => {
|
||||
test('renders the save dataset modal UI', async () => {
|
||||
render(<SaveQuery {...splitSaveBtnProps} />, {
|
||||
useRedux: true,
|
||||
store: mockStore(mockState),
|
||||
store: mockStore(stateWithLatestQuery({ id: 'qid-1', state: 'success' })),
|
||||
});
|
||||
const saveDatasetMenuItem = await screen.findByLabelText(/save dataset/i);
|
||||
userEvent.click(saveDatasetMenuItem);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useState, useEffect, useMemo, ChangeEvent } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Query, QueryState } from '@superset-ui/core';
|
||||
import type { DatabaseObject } from 'src/features/databases/types';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
@@ -37,7 +39,7 @@ import {
|
||||
} from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
import { QueryEditor } from 'src/SqlLab/types';
|
||||
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
|
||||
import useLogAction from 'src/logger/useLogAction';
|
||||
import {
|
||||
LOG_ACTIONS_SQLLAB_CREATE_CHART,
|
||||
@@ -52,7 +54,6 @@ interface SaveQueryProps {
|
||||
onUpdate: (arg0: QueryPayload, id: string) => void;
|
||||
saveQueryWarning: string | null;
|
||||
database: Partial<DatabaseObject> | undefined;
|
||||
canSaveDataset: boolean;
|
||||
}
|
||||
|
||||
export type QueryPayload = {
|
||||
@@ -82,7 +83,6 @@ const SaveQuery = ({
|
||||
saveQueryWarning,
|
||||
database,
|
||||
columns,
|
||||
canSaveDataset,
|
||||
}: SaveQueryProps) => {
|
||||
const queryEditor = useQueryEditor(queryEditorId, [
|
||||
'autorun',
|
||||
@@ -113,6 +113,17 @@ const SaveQuery = ({
|
||||
const [label, setLabel] = useState<string>(defaultLabel);
|
||||
const [showSave, setShowSave] = useState<boolean>(false);
|
||||
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
|
||||
// Saving a dataset runs the SQL to introspect columns, so it needs a
|
||||
// successful run of the SQL being saved that produced at least one column
|
||||
// -- editing after a run invalidates it, and running a selection only
|
||||
// validates that selection.
|
||||
const latestQuery = useSelector<SqlLabRootState, Query | undefined>(
|
||||
({ sqlLab }) => sqlLab.queries[queryEditor.latestQueryId || ''],
|
||||
);
|
||||
const canSaveDataset =
|
||||
latestQuery?.state === QueryState.Success &&
|
||||
latestQuery.sql === queryEditor.sql &&
|
||||
columns.length > 0;
|
||||
const isSaved = !!query.remoteId;
|
||||
const isLabelEmpty = label.trim().length === 0;
|
||||
const canExploreDatabase = !!database?.allows_virtual_table_explore;
|
||||
|
||||
@@ -355,25 +355,32 @@ describe('SqlEditor', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// findByRole('button', { name }) walks every stylesheet rule via nwsapi to
|
||||
// compute the accessible name, which can crash on an unrelated antd Tabs
|
||||
// "more" button style; findByLabelText matches the same aria-label without
|
||||
// that traversal.
|
||||
test('enables the save dataset button when the latest query succeeded', async () => {
|
||||
const { findByRole } = setupWithLatestQuery({ state: QueryState.Success });
|
||||
expect(await findByRole('button', { name: 'Save dataset' })).toBeEnabled();
|
||||
const { findByLabelText } = setupWithLatestQuery({
|
||||
state: QueryState.Success,
|
||||
sql: mockedProps.queryEditor.sql,
|
||||
});
|
||||
expect(await findByLabelText('Save dataset')).toBeEnabled();
|
||||
});
|
||||
|
||||
test('disables the save dataset button when the latest query failed', async () => {
|
||||
const { findByRole } = setupWithLatestQuery({
|
||||
const { findByLabelText } = setupWithLatestQuery({
|
||||
state: QueryState.Failed,
|
||||
results: undefined,
|
||||
});
|
||||
expect(await findByRole('button', { name: 'Save dataset' })).toBeDisabled();
|
||||
expect(await findByLabelText('Save dataset')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('disables the save dataset button when the results are not loaded', async () => {
|
||||
const { findByRole } = setupWithLatestQuery({
|
||||
const { findByLabelText } = setupWithLatestQuery({
|
||||
state: QueryState.Success,
|
||||
results: undefined,
|
||||
});
|
||||
expect(await findByRole('button', { name: 'Save dataset' })).toBeDisabled();
|
||||
expect(await findByLabelText('Save dataset')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('renders an Extension if provided', async () => {
|
||||
|
||||
@@ -868,7 +868,6 @@ const SqlEditor: FC<Props> = ({
|
||||
}
|
||||
saveQueryWarning={saveQueryWarning}
|
||||
database={database}
|
||||
canSaveDataset={successful && resultColumns.length > 0}
|
||||
/>
|
||||
<ShareSqlLabQuery queryEditorId={queryEditor.id} />
|
||||
</>
|
||||
|
||||
@@ -61,6 +61,53 @@ describe('sqlLabReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should default extra_json to an empty object when extra is unset', () => {
|
||||
// `extra` is nullable in the metadata database, and JSON.parse(extra || '')
|
||||
// is guaranteed to throw because '' is never valid JSON, so one such row
|
||||
// took down the whole reducer.
|
||||
const incomingDb = {
|
||||
...databases.result[0],
|
||||
extra: null,
|
||||
};
|
||||
const incomingDbId = Number(incomingDb.id);
|
||||
|
||||
const action = actions.setDatabases([incomingDb] as any);
|
||||
|
||||
const newState = sqlLabReducer(initialState, action);
|
||||
|
||||
expect(newState.databases[incomingDbId]).toEqual({
|
||||
...incomingDb,
|
||||
extra_json: {},
|
||||
});
|
||||
});
|
||||
|
||||
test('defaults extra_json when a database has malformed extra', () => {
|
||||
const incomingDb = { ...databases.result[0], extra: '{not json' };
|
||||
|
||||
const newState = sqlLabReducer(
|
||||
initialState,
|
||||
actions.setDatabases([incomingDb] as any),
|
||||
);
|
||||
|
||||
expect(newState.databases[Number(incomingDb.id)].extra_json).toEqual({});
|
||||
});
|
||||
|
||||
test('keeps a valid extra payload', () => {
|
||||
const incomingDb = {
|
||||
...databases.result[0],
|
||||
extra: '{"engine_params": {"pool_size": 5}}',
|
||||
};
|
||||
|
||||
const newState = sqlLabReducer(
|
||||
initialState,
|
||||
actions.setDatabases([incomingDb] as any),
|
||||
);
|
||||
|
||||
expect(newState.databases[Number(incomingDb.id)].extra_json).toEqual({
|
||||
engine_params: { pool_size: 5 },
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('Query editors actions', () => {
|
||||
let newState: SqlLabState;
|
||||
|
||||
@@ -36,6 +36,27 @@ import {
|
||||
|
||||
type SqlLabState = SqlLabRootState['sqlLab'];
|
||||
|
||||
/**
|
||||
* A database's `extra` column is free-form and frequently empty: it is nullable
|
||||
* in the metadata database and the API returns it verbatim. `JSON.parse` cannot
|
||||
* represent that, and `JSON.parse(extra || '')` is guaranteed to throw, since
|
||||
* the empty string is never valid JSON — so a single database row with no
|
||||
* `extra` took down the whole SET_DATABASES reducer and with it SQL Lab.
|
||||
* Malformed JSON is treated the same way: one bad row must not cost the user
|
||||
* every other database.
|
||||
*/
|
||||
function parseDatabaseExtra(extra: unknown): Record<string, unknown> {
|
||||
if (typeof extra !== 'string' || extra.trim() === '') {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(extra);
|
||||
return parsed && typeof parsed === 'object' ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function alterUnsavedQueryEditorState(
|
||||
state: SqlLabState,
|
||||
updatedState: Partial<QueryEditor>,
|
||||
@@ -727,7 +748,7 @@ export default function sqlLabReducer(
|
||||
(action.databases as any[])!.forEach((db: any) => {
|
||||
databases[db.id] = {
|
||||
...db,
|
||||
extra_json: JSON.parse(db.extra || ''),
|
||||
extra_json: parseDatabaseExtra(db.extra),
|
||||
};
|
||||
});
|
||||
return {
|
||||
|
||||
@@ -53,6 +53,22 @@ test('RowCountLabel renders limit with danger and tooltip', async () => {
|
||||
expect(tooltip).toHaveTextContent('The row limit');
|
||||
});
|
||||
|
||||
test('RowCountLabel uses a caller-provided limitReachedMessage instead of the default', async () => {
|
||||
render(
|
||||
<RowCountLabel
|
||||
rowcount={100}
|
||||
limit={100}
|
||||
limitReachedMessage="Custom limit message"
|
||||
/>,
|
||||
);
|
||||
const expectedText = '100 rows';
|
||||
expect(screen.getByText(expectedText)).toBeInTheDocument();
|
||||
userEvent.hover(screen.getByText(expectedText));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('Custom limit message');
|
||||
expect(tooltip).not.toHaveTextContent('The row limit set for the chart');
|
||||
});
|
||||
|
||||
test('RowCountLabel renders loading', () => {
|
||||
render(<RowCountLabel loading />);
|
||||
const expectedText = 'Loading...';
|
||||
|
||||
@@ -26,6 +26,9 @@ type RowCountLabelProps = {
|
||||
limit?: number;
|
||||
loading?: boolean;
|
||||
label?: JSX.Element;
|
||||
// Overrides the default "chart" wording for panes (e.g. samples) where the
|
||||
// limit reached isn't the chart's own row_limit.
|
||||
limitReachedMessage?: React.ReactNode;
|
||||
};
|
||||
|
||||
const limitReachedMsg = t(
|
||||
@@ -33,7 +36,13 @@ const limitReachedMsg = t(
|
||||
);
|
||||
|
||||
export default function RowCountLabel(props: RowCountLabelProps) {
|
||||
const { rowcount = 0, limit = null, loading, label } = props;
|
||||
const {
|
||||
rowcount = 0,
|
||||
limit = null,
|
||||
loading,
|
||||
label,
|
||||
limitReachedMessage,
|
||||
} = props;
|
||||
const limitReached = limit && rowcount >= limit;
|
||||
const type =
|
||||
limitReached || (rowcount === 0 && !loading) ? 'error' : 'default';
|
||||
@@ -50,7 +59,10 @@ export default function RowCountLabel(props: RowCountLabelProps) {
|
||||
</Label>
|
||||
);
|
||||
return limitReached ? (
|
||||
<Tooltip id="tt-rowcount-tooltip" title={<span>{limitReachedMsg}</span>}>
|
||||
<Tooltip
|
||||
id="tt-rowcount-tooltip"
|
||||
title={<span>{limitReachedMessage ?? limitReachedMsg}</span>}
|
||||
>
|
||||
{label || labelText}
|
||||
</Tooltip>
|
||||
) : (
|
||||
|
||||
@@ -117,7 +117,7 @@ const StyledDiv = styled.div`
|
||||
${
|
||||
isMobileConsumptionEnabled()
|
||||
? `@media (max-width: ${theme.screenSMMax}px) {
|
||||
[data-test='slice-header'] .header-title {
|
||||
.slice-header .header-title {
|
||||
-webkit-line-clamp: unset;
|
||||
display: block;
|
||||
white-space: normal;
|
||||
|
||||
@@ -79,6 +79,15 @@ type PropertiesModalProps = {
|
||||
addSuccessToast: (message: string) => void;
|
||||
addDangerToast: (message: string) => void;
|
||||
onlyApply?: boolean;
|
||||
renderExtraFields?: (context: {
|
||||
assetId: number;
|
||||
assetType: 'dashboard';
|
||||
accessorCount: number;
|
||||
}) => {
|
||||
content: React.ReactNode;
|
||||
saveDisabled?: boolean;
|
||||
saveTooltip?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type DashboardInfo = {
|
||||
@@ -107,6 +116,7 @@ const PropertiesModal = ({
|
||||
onlyApply = false,
|
||||
onSubmit = () => {},
|
||||
show = false,
|
||||
renderExtraFields,
|
||||
}: PropertiesModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const [form] = Form.useForm();
|
||||
@@ -123,6 +133,17 @@ const PropertiesModal = ({
|
||||
});
|
||||
const [editors, setEditors] = useState<Subject[]>([]);
|
||||
const [viewers, setViewers] = useState<Subject[]>([]);
|
||||
|
||||
const extraFields = useMemo(
|
||||
() =>
|
||||
renderExtraFields?.({
|
||||
assetId: dashboardId,
|
||||
assetType: 'dashboard',
|
||||
accessorCount: editors.length + viewers.length,
|
||||
}),
|
||||
[renderExtraFields, dashboardId, editors.length, viewers.length],
|
||||
);
|
||||
|
||||
const saveLabel = onlyApply ? t('Apply') : t('Save');
|
||||
const [tags, setTags] = useState<TagType[]>([]);
|
||||
const [customCss, setCustomCss] = useState('');
|
||||
@@ -698,15 +719,21 @@ const PropertiesModal = ({
|
||||
}}
|
||||
title={t('Dashboard properties')}
|
||||
isEditMode
|
||||
saveDisabled={dashboardInfo?.isManagedExternally || hasErrors}
|
||||
saveDisabled={
|
||||
dashboardInfo?.isManagedExternally ||
|
||||
hasErrors ||
|
||||
extraFields?.saveDisabled
|
||||
}
|
||||
saveLoading={isApplying}
|
||||
contentLoading={isLoading}
|
||||
errorTooltip={
|
||||
dashboardInfo?.isManagedExternally
|
||||
? t(
|
||||
"This dashboard is managed externally, and can't be edited in Superset",
|
||||
)
|
||||
: errorTooltip
|
||||
extraFields?.saveDisabled && extraFields?.saveTooltip
|
||||
? extraFields.saveTooltip
|
||||
: dashboardInfo?.isManagedExternally
|
||||
? t(
|
||||
"This dashboard is managed externally, and can't be edited in Superset",
|
||||
)
|
||||
: errorTooltip
|
||||
}
|
||||
saveText={saveLabel}
|
||||
wrapProps={{ 'data-test': 'properties-edit-modal' }}
|
||||
@@ -769,6 +796,7 @@ const PropertiesModal = ({
|
||||
onChangeViewers={handleOnChangeViewers}
|
||||
onChangeTags={handleChangeTags}
|
||||
onClearTags={handleClearTags}
|
||||
renderExtraFields={extraFields}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -38,6 +38,11 @@ interface AccessSectionProps {
|
||||
onChangeViewers: (viewers: SubjectPickerValue[]) => void;
|
||||
onChangeTags: (tags: { label: string; value: number }[]) => void;
|
||||
onClearTags: () => void;
|
||||
renderExtraFields?: {
|
||||
content: React.ReactNode;
|
||||
saveDisabled?: boolean;
|
||||
saveTooltip?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const AccessSection = ({
|
||||
@@ -49,6 +54,7 @@ const AccessSection = ({
|
||||
onChangeViewers,
|
||||
onChangeTags,
|
||||
onClearTags,
|
||||
renderExtraFields,
|
||||
}: AccessSectionProps) => {
|
||||
const enableViewers = isFeatureEnabled(FeatureFlag.EnableViewers);
|
||||
|
||||
@@ -134,6 +140,7 @@ const AccessSection = ({
|
||||
/>
|
||||
</ModalFormField>
|
||||
)}
|
||||
{renderExtraFields?.content}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -208,6 +208,18 @@ test('Should render', () => {
|
||||
expect(screen.getByTestId('slice-header')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should expose a class hook, not just data-test, for fullscreen styling', () => {
|
||||
const props = createProps();
|
||||
render(<SliceHeader {...props} />, {
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState,
|
||||
});
|
||||
// The production build strips data-test attributes, so CSS that targets the
|
||||
// header must hang off a class instead.
|
||||
expect(screen.getByTestId('slice-header')).toHaveClass('slice-header');
|
||||
});
|
||||
|
||||
test('Should render - default props', () => {
|
||||
const props = createProps();
|
||||
|
||||
|
||||
@@ -270,7 +270,11 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
<ChartHeaderStyles data-test="slice-header" ref={ref}>
|
||||
<ChartHeaderStyles
|
||||
className="slice-header"
|
||||
data-test="slice-header"
|
||||
ref={ref}
|
||||
>
|
||||
<div className="header-title" ref={headerRef}>
|
||||
<Tooltip title={headerTooltip}>
|
||||
{/* this div ensures the hover event triggers correctly and prevents flickering */}
|
||||
|
||||
@@ -19,20 +19,13 @@
|
||||
import { css, SupersetTheme } from '@apache-superset/core/theme';
|
||||
|
||||
export const fullscreenStyles = (theme: SupersetTheme) => css`
|
||||
[data-test='dashboard-component-chart-holder']:fullscreen {
|
||||
.dashboard-component-chart-holder:fullscreen {
|
||||
background-color: ${theme.colorBgBase};
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
pointer-events: auto;
|
||||
z-index: ${theme.zIndexPopupBase};
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
||||
/* Ensure children take up available space */
|
||||
.dashboard-chart,
|
||||
@@ -58,13 +51,8 @@ export const fullscreenStyles = (theme: SupersetTheme) => css`
|
||||
}
|
||||
}
|
||||
|
||||
/* Interaction and Header fixes */
|
||||
[data-test='dashboard-component-chart-holder']:fullscreen * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
[data-test='dashboard-component-chart-holder']:fullscreen
|
||||
[data-test='slice-header'] {
|
||||
/* Keep the header above the chart it shares the fullscreen layer with */
|
||||
.dashboard-component-chart-holder:fullscreen .slice-header {
|
||||
z-index: ${theme.zIndexPopupBase};
|
||||
position: relative;
|
||||
}
|
||||
|
||||
+11
-7
@@ -337,13 +337,17 @@ const ChartHolder = ({
|
||||
)}
|
||||
>
|
||||
<AntdThemeProvider
|
||||
getPopupContainer={(triggerNode: HTMLElement) =>
|
||||
document.fullscreenElement
|
||||
? (triggerNode?.closest?.(
|
||||
'[data-test="dashboard-component-chart-holder"]',
|
||||
) as HTMLElement) || document.body
|
||||
: document.body
|
||||
}
|
||||
getPopupContainer={(triggerNode?: HTMLElement) => {
|
||||
// Only the fullscreen element's subtree is painted, so popups
|
||||
// have to be portaled into it rather than to document.body.
|
||||
// Resolve it directly instead of matching a selector: the
|
||||
// production build strips data-test attributes.
|
||||
const fullscreenElement =
|
||||
document.fullscreenElement as HTMLElement | null;
|
||||
return triggerNode && fullscreenElement?.contains(triggerNode)
|
||||
? fullscreenElement
|
||||
: document.body;
|
||||
}}
|
||||
>
|
||||
{!editMode && (
|
||||
<AnchorLink
|
||||
|
||||
+1
-12
@@ -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"
|
||||
|
||||
+26
@@ -97,3 +97,29 @@ test('does not render DeckglLayerVisibilityTooltip for standard filter type', ()
|
||||
screen.queryByTestId('deckgl-layer-visibility-tooltip-icon'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not mark a defaultToFirstItem-only filter as required', () => {
|
||||
render(
|
||||
<FilterControl
|
||||
filter={{
|
||||
...nativeFilter,
|
||||
controlValues: { defaultToFirstItem: true },
|
||||
}}
|
||||
onFilterSelectionChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText('*')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('marks an enableEmptyFilter filter as required', () => {
|
||||
render(
|
||||
<FilterControl
|
||||
filter={{
|
||||
...nativeFilter,
|
||||
controlValues: { enableEmptyFilter: true },
|
||||
}}
|
||||
onFilterSelectionChange={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('*')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
+1
-3
@@ -65,9 +65,7 @@ const FilterControl = ({
|
||||
isFilterInScope(filter) &&
|
||||
checkIsMissingRequiredValue(filter, filter.dataMask?.filterState);
|
||||
const validateStatus = isMissingRequiredValue ? 'error' : undefined;
|
||||
const isRequired =
|
||||
!!filter.controlValues?.enableEmptyFilter ||
|
||||
!!filter.controlValues?.defaultToFirstItem;
|
||||
const isRequired = !!filter.controlValues?.enableEmptyFilter;
|
||||
const inverseSelection = !!filter.controlValues?.inverseSelection;
|
||||
|
||||
const {
|
||||
|
||||
@@ -211,6 +211,16 @@ test('checkIsMissingRequiredValue returns false for non-required filter with und
|
||||
expect(checkIsMissingRequiredValue(filter, filterState)).toBe(false);
|
||||
});
|
||||
|
||||
test('checkIsMissingRequiredValue returns false when only defaultToFirstItem is set', () => {
|
||||
const filter = createFilter('test-filter', {
|
||||
enableEmptyFilter: false,
|
||||
controlValues: { defaultToFirstItem: true },
|
||||
});
|
||||
|
||||
expect(checkIsMissingRequiredValue(filter, { value: null })).toBe(false);
|
||||
expect(checkIsMissingRequiredValue(filter, { value: undefined })).toBe(false);
|
||||
});
|
||||
|
||||
test('checkIsMissingRequiredValue returns falsy for filter without controlValues', () => {
|
||||
const filter = { id: 'test-filter' } as Filter;
|
||||
const filterState: FilterState = { value: undefined };
|
||||
@@ -299,6 +309,48 @@ test('checkIsApplyDisabled returns true when required filter is missing value in
|
||||
);
|
||||
});
|
||||
|
||||
test('checkIsApplyDisabled enables Apply after clearing a cascading defaultToFirstItem child', () => {
|
||||
// Regression: a child filter that is dependent on a parent and configured with
|
||||
// "Select first filter value by default" but NOT "Filter value is required"
|
||||
// must stay clearable — clearing it may not disable Apply.
|
||||
const parent = createFilter('parent', {
|
||||
enableEmptyFilter: true,
|
||||
controlValues: { defaultToFirstItem: true },
|
||||
});
|
||||
const child = createFilter('child', {
|
||||
enableEmptyFilter: false,
|
||||
controlValues: { defaultToFirstItem: true },
|
||||
});
|
||||
const dataMaskSelected: DataMaskStateWithId = {
|
||||
parent: {
|
||||
id: 'parent',
|
||||
filterState: { value: ['USA'] },
|
||||
extraFormData: createExtraFormDataWithFilter('country', ['USA']),
|
||||
},
|
||||
child: {
|
||||
id: 'child',
|
||||
filterState: { value: null },
|
||||
extraFormData: {},
|
||||
},
|
||||
};
|
||||
const dataMaskApplied: DataMaskStateWithId = {
|
||||
parent: {
|
||||
id: 'parent',
|
||||
filterState: { value: ['USA'] },
|
||||
extraFormData: createExtraFormDataWithFilter('country', ['USA']),
|
||||
},
|
||||
child: {
|
||||
id: 'child',
|
||||
filterState: { value: ['CA'] },
|
||||
extraFormData: createExtraFormDataWithFilter('state', ['CA']),
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
checkIsApplyDisabled(dataMaskSelected, dataMaskApplied, [parent, child]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('checkIsApplyDisabled enables Apply when Selected has a filter value not yet in Applied', () => {
|
||||
// Regression: when a required filter's default isn't applied (Applied missing
|
||||
// the entry) and the user types a value, Selected gains an entry Applied
|
||||
|
||||
@@ -48,9 +48,10 @@ export const checkIsMissingRequiredValue = (
|
||||
filter: FilterElement,
|
||||
filterState?: FilterState,
|
||||
) => {
|
||||
const isRequired =
|
||||
!!filter.controlValues?.enableEmptyFilter ||
|
||||
!!filter.controlValues?.defaultToFirstItem;
|
||||
// Only `enableEmptyFilter` ("Filter value is required") makes a value
|
||||
// mandatory. `defaultToFirstItem` merely seeds an initial selection, so a
|
||||
// filter cleared by the user must stay clearable, with Apply enabled.
|
||||
const isRequired = !!filter.controlValues?.enableEmptyFilter;
|
||||
|
||||
if (!isRequired) return false;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
+11
-4
@@ -68,6 +68,8 @@ export const TableControls = ({
|
||||
canDownload,
|
||||
rowLimit,
|
||||
rowLimitOptions,
|
||||
effectiveRowLimit,
|
||||
limitReachedMessage,
|
||||
onRowLimitChange,
|
||||
onDownloadCSV,
|
||||
onDownloadXLSX,
|
||||
@@ -111,14 +113,19 @@ export const TableControls = ({
|
||||
value={rowLimit}
|
||||
onChange={onRowLimitChange}
|
||||
options={rowLimitOptions ?? []}
|
||||
// Labelled as the applied limit to avoid a second row count next to RowCountLabel.
|
||||
prefix={t('Limit')}
|
||||
css={css`
|
||||
min-width: 110px;
|
||||
min-width: 160px;
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
{(!onRowLimitChange || rowcount < (rowLimit ?? Infinity)) && (
|
||||
<RowCountLabel rowcount={rowcount} loading={isLoading} />
|
||||
)}
|
||||
<RowCountLabel
|
||||
rowcount={rowcount}
|
||||
limit={effectiveRowLimit ?? rowLimit}
|
||||
limitReachedMessage={limitReachedMessage}
|
||||
loading={isLoading}
|
||||
/>
|
||||
{canDownload && onDownloadCSV && onDownloadXLSX && (
|
||||
<DownloadDropdown
|
||||
onDownloadCSV={onDownloadCSV}
|
||||
|
||||
@@ -136,6 +136,12 @@ export const SamplesPane = ({
|
||||
|
||||
const columns = useGridColumns(colnames, coltypes, data);
|
||||
const keywordFilter = useKeywordFilter(filterText);
|
||||
// Samples aren't capped by a chart's row_limit, just this pane's own
|
||||
// page-size selector, so RowCountLabel's default "chart" wording is wrong here.
|
||||
const limitReachedMessage = t(
|
||||
'The sample row limit was reached. This %s may contain more rows.',
|
||||
datasetLabelLower(),
|
||||
);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(input: string) => setFilterText(input),
|
||||
@@ -161,6 +167,7 @@ export const SamplesPane = ({
|
||||
canDownload={canDownload}
|
||||
rowLimit={rowLimit}
|
||||
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||
limitReachedMessage={limitReachedMessage}
|
||||
onRowLimitChange={handleRowLimitChange}
|
||||
/>
|
||||
<ErrorAlertWrapper>
|
||||
@@ -197,6 +204,7 @@ export const SamplesPane = ({
|
||||
canDownload={canDownload}
|
||||
rowLimit={rowLimit}
|
||||
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||
limitReachedMessage={limitReachedMessage}
|
||||
onRowLimitChange={handleRowLimitChange}
|
||||
/>
|
||||
<GridContainer>
|
||||
|
||||
+4
@@ -56,6 +56,8 @@ export const SingleQueryResultPane = ({
|
||||
columnDisplayNames,
|
||||
rowLimit,
|
||||
rowLimitOptions,
|
||||
effectiveRowLimit,
|
||||
limitReachedMessage,
|
||||
onRowLimitChange,
|
||||
onDownloadCSV,
|
||||
onDownloadXLSX,
|
||||
@@ -86,6 +88,8 @@ export const SingleQueryResultPane = ({
|
||||
canDownload={canDownload}
|
||||
rowLimit={rowLimit}
|
||||
rowLimitOptions={rowLimitOptions}
|
||||
effectiveRowLimit={effectiveRowLimit}
|
||||
limitReachedMessage={limitReachedMessage}
|
||||
onRowLimitChange={onRowLimitChange}
|
||||
onDownloadCSV={onDownloadCSV}
|
||||
onDownloadXLSX={onDownloadXLSX}
|
||||
|
||||
@@ -84,6 +84,17 @@ export const useResultsPane = ({
|
||||
// Never exceed the chart's own row_limit
|
||||
const effectiveRowLimit = Math.min(rowLimit, chartRowLimit);
|
||||
|
||||
// When this pane's own row-limit selector is stricter than the chart's
|
||||
// row_limit, it - not the chart - is what caps the result, so
|
||||
// RowCountLabel's default "chart" wording would be misleading (the chart's
|
||||
// configured row_limit was never actually reached).
|
||||
const limitReachedMessage =
|
||||
rowLimit < chartRowLimit
|
||||
? t(
|
||||
'The row limit selected for this pane was reached. There may be more matching rows.',
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const cappedFormData = useMemo(
|
||||
() => ({ ...queryFormData, row_limit: effectiveRowLimit }),
|
||||
[queryFormData, effectiveRowLimit],
|
||||
@@ -236,6 +247,8 @@ export const useResultsPane = ({
|
||||
columnDisplayNames={columnDisplayNames}
|
||||
rowLimit={rowLimit}
|
||||
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||
effectiveRowLimit={effectiveRowLimit}
|
||||
limitReachedMessage={limitReachedMessage}
|
||||
onRowLimitChange={handleRowLimitChange}
|
||||
/>
|
||||
</StyledDiv>
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* 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 {
|
||||
act,
|
||||
render,
|
||||
screen,
|
||||
sleep,
|
||||
userEvent,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import {
|
||||
TableControls,
|
||||
ROW_LIMIT_OPTIONS,
|
||||
} from '../components/DataTableControls';
|
||||
import { TableControlsProps } from '../types';
|
||||
|
||||
const setup = (overrides: Partial<TableControlsProps> = {}) =>
|
||||
render(
|
||||
<TableControls
|
||||
data={[]}
|
||||
columnNames={['name']}
|
||||
columnTypes={[GenericDataType.String]}
|
||||
rowcount={0}
|
||||
onInputChange={jest.fn()}
|
||||
isLoading={false}
|
||||
canDownload
|
||||
rowLimit={100}
|
||||
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||
onRowLimitChange={jest.fn()}
|
||||
{...overrides}
|
||||
/>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
test('shows the row count when the result fills the selected row limit', () => {
|
||||
setup({ rowcount: 100, rowLimit: 100 });
|
||||
|
||||
expect(screen.getByTestId('row-count-label')).toHaveTextContent('100 rows');
|
||||
});
|
||||
|
||||
test('warns that the row limit was reached when the result fills it', async () => {
|
||||
setup({ rowcount: 100, rowLimit: 100 });
|
||||
|
||||
userEvent.hover(screen.getByTestId('row-count-label'));
|
||||
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
'The row limit set for the chart was reached',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not warn when the result is smaller than the selected row limit', async () => {
|
||||
setup({ rowcount: 42, rowLimit: 100 });
|
||||
|
||||
expect(screen.getByTestId('row-count-label')).toHaveTextContent('42 rows');
|
||||
userEvent.hover(screen.getByTestId('row-count-label'));
|
||||
|
||||
// Wait past antd's 0.1s mouseEnterDelay so a regression that made the
|
||||
// tooltip appear would be caught here instead of racing the delay.
|
||||
await act(() => sleep(150));
|
||||
|
||||
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("warns when the chart's own row limit truncates below the selected row limit", async () => {
|
||||
setup({ rowcount: 250, rowLimit: 1000, effectiveRowLimit: 250 });
|
||||
|
||||
userEvent.hover(screen.getByTestId('row-count-label'));
|
||||
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
'The row limit set for the chart was reached',
|
||||
);
|
||||
});
|
||||
|
||||
test('labels the row limit selector so it is not read as a second row count', () => {
|
||||
setup({ rowcount: 100, rowLimit: 100 });
|
||||
|
||||
expect(screen.getByText('Limit')).toBeInTheDocument();
|
||||
});
|
||||
+42
-1
@@ -16,7 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { screen, render, waitFor } from 'spec/helpers/testing-library';
|
||||
import {
|
||||
screen,
|
||||
render,
|
||||
waitFor,
|
||||
userEvent,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import { ResultsPaneOnDashboard } from '../components';
|
||||
@@ -157,6 +162,42 @@ describe('useResultsPane query data reuse', () => {
|
||||
expect(screen.queryByText('Sci-Fi')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('2 rows')).toBeVisible();
|
||||
expect(mockedGetChartDataRequest).not.toHaveBeenCalled();
|
||||
|
||||
userEvent.hover(screen.getByText('2 rows'));
|
||||
expect(await screen.findByRole('tooltip')).toHaveTextContent(
|
||||
'The row limit set for the chart was reached',
|
||||
);
|
||||
});
|
||||
|
||||
test("warns about this pane's own row limit, not the chart's, when the pane's selector is what caps the result", async () => {
|
||||
// chart row_limit (2000) is well above this pane's default 1000-row
|
||||
// selector, so the selector - not the chart - is what truncates here.
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 208,
|
||||
rowLimit: 2000,
|
||||
queriesResponse: [
|
||||
{
|
||||
colnames: ['genre'],
|
||||
coltypes: [1],
|
||||
data: Array.from({ length: 1500 }, (_, i) => ({
|
||||
genre: `genre-${i}`,
|
||||
})),
|
||||
rowcount: 1500,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<ResultsPaneOnDashboard {...props} />, { useRedux: true });
|
||||
|
||||
const rowCountLabel = await screen.findByTestId('row-count-label');
|
||||
expect(rowCountLabel).toHaveTextContent('1k rows');
|
||||
|
||||
userEvent.hover(rowCountLabel);
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent(
|
||||
'The row limit selected for this pane was reached',
|
||||
);
|
||||
expect(tooltip).not.toHaveTextContent('for the chart');
|
||||
});
|
||||
|
||||
test('renders an empty (0 rows) result from reused data without an API call', async () => {
|
||||
|
||||
@@ -84,6 +84,12 @@ export interface TableControlsProps extends DrillControlsProps {
|
||||
canDownload: boolean;
|
||||
rowLimit?: number;
|
||||
rowLimitOptions?: { value: number; label: string }[];
|
||||
// Effective result limit, capped by the chart's row limit.
|
||||
// Defaults to `rowLimit` and controls the "row limit reached" warning.
|
||||
effectiveRowLimit?: number;
|
||||
// Overrides RowCountLabel's default "chart" wording for panes (e.g.
|
||||
// samples) where the limit reached isn't the chart's own row_limit.
|
||||
limitReachedMessage?: React.ReactNode;
|
||||
onRowLimitChange?: (limit: number) => void;
|
||||
}
|
||||
|
||||
@@ -104,5 +110,9 @@ export interface SingleQueryResultPaneProp
|
||||
columnDisplayNames?: Record<string, string>;
|
||||
rowLimit?: number;
|
||||
rowLimitOptions?: { value: number; label: string }[];
|
||||
effectiveRowLimit?: number;
|
||||
// Overrides RowCountLabel's default "chart" wording when the pane's own
|
||||
// row-limit selector, not the chart's row_limit, is what capped the result.
|
||||
limitReachedMessage?: React.ReactNode;
|
||||
onRowLimitChange?: (limit: number) => void;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ChangeEvent, useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
type ReactNode,
|
||||
ChangeEvent,
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
Input,
|
||||
@@ -56,6 +63,12 @@ export type PropertiesModalProps = {
|
||||
permissionsError?: string;
|
||||
addSuccessToast: (msg: string) => void;
|
||||
addDangerToast: (msg: string) => void;
|
||||
/** Optional render prop for injecting extra fields (e.g. folder selector). */
|
||||
renderExtraFields?: (context: {
|
||||
assetId: number;
|
||||
assetType: 'chart';
|
||||
accessorCount: number;
|
||||
}) => { content: ReactNode; saveDisabled?: boolean; saveTooltip?: string };
|
||||
};
|
||||
|
||||
function PropertiesModal({
|
||||
@@ -65,6 +78,7 @@ function PropertiesModal({
|
||||
show,
|
||||
addSuccessToast,
|
||||
addDangerToast,
|
||||
renderExtraFields,
|
||||
}: PropertiesModalProps) {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
// values of form inputs
|
||||
@@ -87,6 +101,25 @@ function PropertiesModal({
|
||||
>(null);
|
||||
const [tags, setTags] = useState<TagType[]>([]);
|
||||
|
||||
const chartId = slice.slice_id;
|
||||
const extraFields = useMemo(
|
||||
() =>
|
||||
chartId
|
||||
? renderExtraFields?.({
|
||||
assetId: chartId,
|
||||
assetType: 'chart',
|
||||
accessorCount:
|
||||
(selectedEditors?.length ?? 0) + (selectedViewers?.length ?? 0),
|
||||
})
|
||||
: undefined,
|
||||
[
|
||||
chartId,
|
||||
renderExtraFields,
|
||||
selectedEditors?.length,
|
||||
selectedViewers?.length,
|
||||
],
|
||||
);
|
||||
|
||||
// Validation setup
|
||||
const modalSections = useMemo(
|
||||
() => [
|
||||
@@ -281,14 +314,20 @@ function PropertiesModal({
|
||||
title={t('Chart properties')}
|
||||
isEditMode
|
||||
saveDisabled={
|
||||
submitting || !name || slice.is_managed_externally || hasErrors
|
||||
submitting ||
|
||||
!name ||
|
||||
slice.is_managed_externally ||
|
||||
hasErrors ||
|
||||
extraFields?.saveDisabled
|
||||
}
|
||||
errorTooltip={
|
||||
slice.is_managed_externally
|
||||
? t(
|
||||
"This chart is managed externally, and can't be edited in Superset",
|
||||
)
|
||||
: errorTooltip
|
||||
extraFields?.saveDisabled && extraFields?.saveTooltip
|
||||
? extraFields.saveTooltip
|
||||
: slice.is_managed_externally
|
||||
? t(
|
||||
"This chart is managed externally, and can't be edited in Superset",
|
||||
)
|
||||
: errorTooltip
|
||||
}
|
||||
wrapProps={{ 'data-test': 'properties-edit-modal' }}
|
||||
>
|
||||
@@ -395,6 +434,7 @@ function PropertiesModal({
|
||||
/>
|
||||
</ModalFormField>
|
||||
)}
|
||||
{extraFields?.content}
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
+18
-2
@@ -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}
|
||||
</>
|
||||
);
|
||||
|
||||
+86
-4
@@ -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;
|
||||
}
|
||||
|
||||
+32
@@ -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,
|
||||
|
||||
+4
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user