Compare commits

...

68 Commits

Author SHA1 Message Date
Beto Dealmeida
3bb4b5f3a6 fix: SSH tunnel and test connection error handling
- Use sshtunnel.open_tunnel() instead of SSHTunnelForwarder directly
  to properly handle debug_level parameter
- Fix keepalive parameter name (set_keepalive, not keepalive)
- Fix test assertions that were inside pytest.raises blocks and never
  executed - now check error_type instead of string messages
- Update SSH tunnel test mocks to patch open_tunnel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 10:58:39 -05:00
Beto Dealmeida
c00fae53a5 Rebase 2026-02-04 09:58:15 -05:00
Beto Dealmeida
99935fc035 Fix tests 2026-02-04 09:44:54 -05:00
Beto Dealmeida
d06ccf5152 Fix poolclass check 2026-02-04 09:44:54 -05:00
Beto Dealmeida
62d2d82ed8 Fix more tests 2026-02-04 09:44:54 -05:00
Beto Dealmeida
688224c4c0 Simplify key generation 2026-02-04 09:44:54 -05:00
Beto Dealmeida
de8c250f86 Update existing tests 2026-02-04 09:44:54 -05:00
Beto Dealmeida
be31abeb7e Hash key 2026-02-04 09:42:43 -05:00
Beto Dealmeida
5f61bb8d76 Small improvements 2026-02-04 09:42:43 -05:00
Beto Dealmeida
bb5a15dc5a Cleanup 2026-02-04 09:42:42 -05:00
Beto Dealmeida
929b0337f4 Connecting 2026-02-04 09:42:42 -05:00
Beto Dealmeida
baf6e03d16 Add extension 2026-02-04 09:42:42 -05:00
Beto Dealmeida
e82e06891b Cleanup locks 2026-02-04 09:42:41 -05:00
Beto Dealmeida
5753dfbb6e feat: engine manager 2026-02-04 09:42:41 -05:00
dependabot[bot]
45f883c9cd chore(deps-dev): bump webpack from 5.104.1 to 5.105.0 in /docs (#37656)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-04 21:35:51 +07:00
Nancy Chauhan
8fd3401077 fix(security): update jspdf to 4.0.0 to address CVE-2025-68428 (#37553) 2026-02-04 21:29:57 +07:00
Richard Fogaca Nienkotter
89a98ab9a4 fix(dataset-editor): include calculated columns in currency code dropdown (#37621) 2026-02-04 11:17:07 -03:00
Jamile Celento
2dfc770b0f fix(native-filters): update TEMPORAL_RANGE filter subject when Time Column filter is applied (#36985) 2026-02-04 12:37:17 +03:00
Vanessa Giannoni
6b7b23ed78 fix(timeseries): restore ECharts tooltip after closing drill menu (#37284) 2026-02-04 12:32:23 +03:00
dependabot[bot]
5ac5480f35 chore(deps): bump caniuse-lite from 1.0.30001766 to 1.0.30001767 in /docs (#37601)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 12:35:32 -08:00
Evan Rusackas
76889c1a69 feat(db_engine_specs): add Apache Phoenix and Apache IoTDB engine specs (#37590)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:29:19 -05:00
Evan Rusackas
569606635b docs(databases): add Supabase, AlloyDB, and Neon as PostgreSQL-compatible databases (#37589)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 12:22:49 -08:00
dependabot[bot]
66264856a7 chore(deps): bump googleapis from 171.0.0 to 171.1.0 in /superset-frontend (#37630)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 22:28:07 +07:00
dependabot[bot]
3eb860a663 chore(deps): bump hot-shots from 13.1.0 to 13.2.0 in /superset-websocket (#37596)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 22:19:07 +07:00
dependabot[bot]
a44980da65 chore(deps-dev): bump globals from 17.2.0 to 17.3.0 in /superset-websocket (#37595)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 22:18:33 +07:00
dependabot[bot]
7112bce961 chore(deps-dev): bump @types/node from 25.1.0 to 25.2.0 in /superset-websocket (#37597)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 22:18:14 +07:00
dependabot[bot]
568486a304 chore(deps): bump @babel/core from 7.28.6 to 7.29.0 in /docs (#37598)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 22:17:50 +07:00
dependabot[bot]
fea135b46c chore(deps-dev): bump @playwright/test from 1.58.0 to 1.58.1 in /superset-frontend (#37633)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 22:17:13 +07:00
dependabot[bot]
601fcb3382 chore(deps-dev): bump @babel/preset-env from 7.28.6 to 7.29.0 in /superset-frontend (#37635)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 22:02:46 +07:00
dependabot[bot]
0d7cc88b2b chore(deps): bump antd from 6.2.2 to 6.2.3 in /docs (#37629)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-03 21:50:13 +07:00
Vitor Avila
32ee160c75 chore: Properly untrack WebSocket config file for docker (#37624) 2026-02-03 11:48:08 -03:00
Amin Ghadersohi
5914e83436 chore(mcp): remove unused MCP_SERVICE feature flag (#37618) 2026-02-03 15:23:08 +01:00
Amin Ghadersohi
0b5e4dd5de feat(mcp): add config toggle to disable parse_request decorator (#37617)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 15:22:44 +01:00
Joe Li
3a565a6c16 fix(tests): update DatasetList tests to new fetch-mock API (#37623)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 14:15:58 +03:00
Ramiro Aquino Romero
f60c82e4a6 fix: charts row limit warning is missing for server (#37112) 2026-02-02 15:49:31 -08:00
Luis Sánchez
91131d5996 chore(charts): echarts left padding too big and automation of title (#36993) 2026-02-02 15:48:03 -08:00
Joe Li
4b0d497513 test: add new RTL and integration tests for DatasetList (#36681)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 12:08:38 -08:00
Joe Li
86f690d17f fix(dashboard): fix Export as Example with app prefix and enable Dashboard Export E2E tests (#37529)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 12:07:22 -08:00
Elizabeth Thompson
e9b494163b refactor(db): use Dialect instead of Engine in select_star to avoid SSH tunnels (#35540)
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 10:26:35 -08:00
JUST.in DO IT
be404f9b84 fix(dashboard): Avoid calling loadData for invisible charts on virtual rendering (#37452) 2026-02-02 10:07:25 -08:00
Daniel Vaz Gaspar
11257c0536 fix(examples): skip URI safety check for system imports (#37577)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 09:24:16 -08:00
Beto Dealmeida
f2b6c395cd feat: Add PWA file handler for CSV/XLS/Parquet uploads (#36191)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 11:24:01 -05:00
dependabot[bot]
2d35ed2391 chore(deps-dev): bump @babel/runtime-corejs3 from 7.28.6 to 7.29.0 in /superset-frontend (#37605)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 23:03:59 +07:00
dependabot[bot]
bd65469091 chore(deps-dev): bump globals from 17.2.0 to 17.3.0 in /docs (#37599)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 21:48:25 +07:00
Kamil Gabryjelski
a6a66ca483 feat: Dataset folders editor (#36239) 2026-02-02 14:54:33 +01:00
Jonathan Alberth Quispe Fuentes
4a7cdccdad fix: Heatmap does not render correctly on normalization (#37208) 2026-02-02 12:34:46 +03:00
dependabot[bot]
61bd8f0cf2 chore(deps): bump use-query-params from 1.2.3 to 2.2.2 in /superset-frontend (#36997)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-02-01 23:55:39 -08:00
Evan Rusackas
ae10e105c2 fix(chart): enable cross-filter on bar charts without dimensions (#37407)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 17:14:24 -08:00
dependabot[bot]
901dca58f7 chore(deps): bump JustinBeckwith/linkinator-action from 2.3 to 2.4 (#37562)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 03:06:30 -08:00
dependabot[bot]
d95a3d8426 chore(deps-dev): bump @applitools/eyes-storybook from 3.63.9 to 3.63.10 in /superset-frontend (#37566)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 03:06:09 -08:00
alok kumar priyadarshi
70b95ca1b9 fix(build): eliminate PostgreSQL extra installation on Python 3.12-based Superset Docker images (#37587) 2026-01-31 15:54:19 +07:00
Michael S. Molina
004f02746f fix(build): Increase ForkTsCheckerWebpackPlugin memory limit to fix OOM error (#37583) 2026-01-31 14:22:17 +07:00
Beto Dealmeida
5d20dc57d7 feat(oauth2): add PKCE support for database OAuth2 authentication (#37067) 2026-01-30 23:28:10 -05:00
Beto Dealmeida
05c2354997 feat: AWS Cross-Account IAM Authentication for Aurora (#37585) 2026-01-30 19:18:34 -05:00
Vitor Avila
6043e7e7e3 fix: more DB OAuth2 fixes (#37398) 2026-01-30 21:11:26 -03:00
Amin Ghadersohi
1ee14c5993 fix(mcp): improve prompts, resources, and instructions clarity (#37389) 2026-01-30 12:25:38 -08:00
Felipe López
9764a84402 fix(charts): Table chart shows an error on row limit (#37218) 2026-01-30 11:45:50 -08:00
JUST.in DO IT
570cc3e5f8 feat(sqllab): treeview table selection ui (#37298) 2026-01-30 11:07:56 -08:00
dependabot[bot]
66519c3a85 chore(deps-dev): bump fetch-mock from 11.1.5 to 12.6.0 in /superset-frontend/packages/superset-ui-core (#36662)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
2026-01-30 21:27:35 +07:00
dependabot[bot]
1f43138888 chore(deps): bump babel-loader from 9.2.1 to 10.0.0 in /docs (#37541)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 21:06:23 +07:00
dependabot[bot]
652d029a2d chore(deps-dev): bump @types/node from 25.0.10 to 25.1.0 in /superset-frontend (#37563)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 21:03:26 +07:00
dependabot[bot]
e67b1f5326 chore(deps-dev): bump baseline-browser-mapping from 2.9.18 to 2.9.19 in /superset-frontend (#37565)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 20:56:46 +07:00
dependabot[bot]
fa79a467e4 chore(deps): bump googleapis from 170.1.0 to 171.0.0 in /superset-frontend (#37564)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 16:57:04 +07:00
Pedro Rodrigues
2cce0308d4 fix: big number drill to details column data (#37068) 2026-01-30 12:32:49 +03:00
dependabot[bot]
c7fd1a2f65 chore(deps-dev): bump @types/node from 25.0.10 to 25.1.0 in /superset-websocket (#37539)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 10:22:41 +07:00
dependabot[bot]
ab4f646ef6 chore(deps): bump @babel/core from 7.28.5 to 7.28.6 in /docs (#37540)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 10:22:15 +07:00
Alejandro Solares
d6029f5c8a chore(deps): bump dependencies to address security vulnerabilities (#37552) 2026-01-30 10:19:43 +07:00
dependabot[bot]
c16e8f747c chore(deps-dev): bump css-loader from 7.1.2 to 7.1.3 in /superset-frontend (#37544)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-30 10:18:20 +07:00
339 changed files with 23573 additions and 3584 deletions

View File

@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v6
# Do not bump this linkinator-action version without opening
# an ASF Infra ticket to allow the new version first!
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
- uses: JustinBeckwith/linkinator-action@f62ba0c110a76effb2ee6022cc6ce4ab161085e3 # v2.4
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
with:
paths: "**/*.md, **/*.mdx"

View File

@@ -27,6 +27,7 @@ repos:
args: [--check-untyped-defs]
exclude: ^superset-extensions-cli/
additional_dependencies: [
types-cachetools,
types-simplejson,
types-python-dateutil,
types-requests,

View File

@@ -24,6 +24,56 @@ assists people when migrating to a new version.
## Next
### Engine Manager for Connection Pooling
A new `EngineManager` class has been introduced to centralize SQLAlchemy engine creation and management. This enables connection pooling for analytics databases and provides a more flexible architecture for engine configuration.
#### Breaking Changes
1. **Removed `SSH_TUNNEL_MANAGER_CLASS` config**: SSH tunnel handling is now integrated into the EngineManager. If you have custom SSH tunnel managers, you'll need to migrate to the new architecture.
2. **Removed `nullpool` parameter**: The `get_sqla_engine()` and `get_raw_connection()` methods on the `Database` model no longer accept a `nullpool` parameter. Pool configuration is now controlled through the engine manager.
3. **Removed `_get_sqla_engine()` method**: The private `_get_sqla_engine()` method has been removed from the `Database` model. All engine creation now goes through the `EngineManager`.
#### New Configuration Options
```python
# Engine manager mode:
# - EngineModes.NEW: Creates a new engine for every connection (default, original behavior)
# - EngineModes.SINGLETON: Reuses engines with connection pooling
from superset.engines.manager import EngineModes
ENGINE_MANAGER_MODE = EngineModes.NEW
# Cleanup interval for abandoned locks (default: 5 minutes)
from datetime import timedelta
ENGINE_MANAGER_CLEANUP_INTERVAL = timedelta(minutes=5)
# Automatically start cleanup thread for SINGLETON mode (default: True)
ENGINE_MANAGER_AUTO_START_CLEANUP = True
```
#### Migration Guide
- If you were using the `nullpool` parameter, remove it from your calls
- If you had a custom `SSH_TUNNEL_MANAGER_CLASS`, refactor to use the new EngineManager architecture
- If you need connection pooling, set `ENGINE_MANAGER_MODE = EngineModes.SINGLETON` and configure the pool in your database's `extra` JSON field
### WebSocket config for GAQ with Docker
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
- Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward)
- Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example:
``` yaml
services:
superset-websocket:
volumes:
- ./superset-websocket:/home/superset-websocket
- /home/superset-websocket/node_modules
- /home/superset-websocket/dist
- ./docker/superset-websocket/config.json:/home/superset-websocket/config.json:ro
```
### Example Data Loading Improvements
#### New Directory Structure

View File

@@ -105,7 +105,7 @@ class CeleryConfig:
CELERY_CONFIG = CeleryConfig
FEATURE_FLAGS = {"ALERT_REPORTS": True}
FEATURE_FLAGS = {"ALERT_REPORTS": True, "DATASET_FOLDERS": True}
ALERT_REPORTS_NOTIFICATION_DRY_RUN = True
WEBDRIVER_BASEURL = f"http://superset_app{os.environ.get('SUPERSET_APP_ROOT', '/')}/" # When using docker compose baseurl should be http://superset_nginx{ENV{BASEPATH}}/ # noqa: E501
# The base URL for the email report hyperlinks.

View File

@@ -1,22 +0,0 @@
{
"port": 8080,
"logLevel": "info",
"logToFile": false,
"logFilename": "app.log",
"statsd": {
"host": "127.0.0.1",
"port": 8125,
"globalTags": []
},
"redis": {
"port": 6379,
"host": "127.0.0.1",
"password": "",
"db": 0,
"ssl": false
},
"redisStreamPrefix": "async-events-",
"jwtAlgorithms": ["HS256"],
"jwtSecret": "CHANGE-ME-IN-PRODUCTION-GOTTA-BE-LONG-AND-SECRET",
"jwtCookieName": "async-token"
}

View File

@@ -38,7 +38,7 @@
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"@babel/core": "^7.26.0",
"@babel/core": "^7.29.0",
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@docusaurus/core": "3.9.2",
@@ -66,9 +66,9 @@
"@storybook/preview-api": "^8.6.11",
"@storybook/theming": "^8.6.11",
"@superset-ui/core": "^0.20.4",
"antd": "^6.2.2",
"babel-loader": "^9.2.1",
"caniuse-lite": "^1.0.30001766",
"antd": "^6.2.3",
"babel-loader": "^10.0.0",
"caniuse-lite": "^1.0.30001767",
"docusaurus-plugin-less": "^2.0.2",
"docusaurus-plugin-openapi-docs": "^4.6.0",
"docusaurus-theme-openapi-docs": "^4.6.0",
@@ -104,11 +104,11 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.2.0",
"globals": "^17.3.0",
"prettier": "^3.8.1",
"typescript": "~5.9.3",
"typescript-eslint": "^8.54.0",
"webpack": "^5.104.1"
"webpack": "^5.105.0"
},
"browserslist": {
"production": [

View File

@@ -114,6 +114,12 @@
"lifecycle": "testing",
"description": "Allow users to export full CSV of table viz type. Warning: Could cause server memory/compute issues with large datasets."
},
{
"name": "AWS_DATABASE_IAM_AUTH",
"default": false,
"lifecycle": "testing",
"description": "Enable AWS IAM authentication for database connections (Aurora, Redshift). Allows cross-account role assumption via STS AssumeRole. Security note: When enabled, ensure Superset's IAM role has restricted sts:AssumeRole permissions to prevent unauthorized access."
},
{
"name": "CACHE_IMPERSONATION",
"default": false,
@@ -241,6 +247,13 @@
"description": "Enables dashboard virtualization for improved performance",
"category": "path_to_deprecation"
},
{
"name": "DASHBOARD_VIRTUALIZATION_DEFER_DATA",
"default": false,
"lifecycle": "stable",
"description": "Supports simultaneous data and dashboard virtualization for backend performance",
"category": "runtime_config"
},
{
"name": "DATAPANEL_CLOSED_BY_DEFAULT",
"default": false,

BIN
docs/static/img/databases/alloydb.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Õ_xBA__x2264__x201E__1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 107.7 107.7"
style="enable-background:new 0 0 107.7 107.7;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#9E2878;}
</style>
<g>
<g id="g1133" transform="translate(-244.51235,-228.78793)">
<path id="path1119" class="st0" d="M340.8,253.8c2.6-1,5.5,0.4,6.2,3c0.7,2.6-1.1,5.7-3.7,6.4c-3.2,0.6-6.2-1.4-9.3,0.2
c-3.1,1.7-4,5.2-4.7,8.3c-1.4,5-8.5,7.3-12.1,4.2c-3.3-2.4-3.4-7.8-0.2-11c2.2-2.5,5.9-3.3,8.8-2c2.7,1.2,6,1.7,8.6-0.4
C337.5,260.1,336.7,255.1,340.8,253.8L340.8,253.8z"/>
<path id="path1121" class="st0" d="M280.5,244.7c4.2-2.2,9.5,1.5,8.2,6.1c-1.4,5.4-0.7,11.5,2.9,15.5c3.4,4,9.8,4.8,14.6,1.9
c3.7-2.1,6-5.8,7.4-9.6c1-3.1,0.6-6.2,1.1-9.3c1-3.8,5.8-6,9.1-4.2c3.2,1.4,4,5.9,1.7,8.8c-1.4,2.2-4.2,2.7-6.3,3.8
c-3.6,1.9-6.5,4.9-8.4,8.6c-1.2,2.1-1.1,4.5-1.7,6.7c-0.9,1.9-3,2.7-4.8,2.9c-4.8,0.6-9.5,3-13,6.7c-1.7,1.7-3.1,4.2-5.6,4.2
c-2.7-0.2-4.7-2.6-7.4-2.7c-4.9-0.7-10.2,0.7-14.4,4c-3.7,3.1-9.4,0.8-9.6-3.7c-0.9-5.1,6.2-9.5,10.1-6.2c4.3,4,10.8,5.6,16.9,3.7
c5.6-1.9,9.7-8.3,8.6-14c-0.9-4.9-4.2-9.3-8.6-11.4c-1.7-0.8-3.6-1.6-4.2-3.5C275.6,250.2,277.3,246.2,280.5,244.7L280.5,244.7z"
/>
<path id="path1123" class="st0" d="M277.8,235.9c2.2-0.8,4.2,1.3,3.3,3.4c-0.6,2.1-3.7,2.7-4.8,1.1
C275,238.9,276,236.4,277.8,235.9z"/>
<path id="path1125" class="st0" d="M246.9,278.8c2.2-0.8,4.2,1.2,3.3,3.4c-0.6,2.1-3.7,2.7-4.8,1C244,281.9,245,279.3,246.9,278.8
L246.9,278.8z"/>
<path id="path1127" class="st0" d="M328.2,236.2c2.2-0.7,4.2,1.3,3.3,3.5c-0.6,2-3.7,2.7-4.8,1
C325.4,239.2,326.3,236.8,328.2,236.2z"/>
<path id="path1129" class="st0" d="M253.6,257.7c0.4-3.7,5.5-5.9,8.1-3.6c1.9,1.1,1.6,3.6,2.2,5.4c0.4,2.4,2.7,4.3,5.2,4.3
c3.2,0.3,6.4-2.3,9.5-1.2c4.8,1.2,6.5,7.5,3.2,11.5c-2.9,4.1-9.3,4.5-12,0.7c-2.4-2.8-0.5-7.1-2.7-10.1c-1.7-2.9-5.4-2.7-8.3-1.9
C255.8,263.8,253,260.7,253.6,257.7L253.6,257.7z"/>
<path id="path1131" class="st0" d="M300.8,230c3.3-1.9,7.5,0.9,6.7,4.6c0,2.3-2.2,3.6-3.5,5.2c-1.9,1.9-2.3,4.9-1.2,7
c1.3,2.9,4.9,4,5.5,7.2c1.2,4.8-3.3,10.1-8.2,9.8c-4.8,0.1-8.3-5-6.3-9.5c1.2-3.8,5.8-4.9,7.2-8.5c1.7-3.2-0.4-6.1-2.4-8.1
C296.7,235.6,297.9,231.4,300.8,230z"/>
</g>
<g id="g1149" transform="translate(-244.51235,-228.78793)">
<path id="path1135" class="st0" d="M256,311.5c-2.6,1-5.5-0.4-6.2-3c-0.7-2.6,1.1-5.7,3.7-6.4c3.2-0.6,6.2,1.4,9.3-0.2
c3.1-1.6,4-5.1,4.7-8.2c1.4-5,8.5-7.3,12.1-4.2c3.3,2.4,3.4,7.8,0.2,10.9c-2.2,2.5-5.9,3.3-8.8,2c-2.7-1.2-6-1.7-8.6,0.4
C259.1,305,259.9,310.2,256,311.5L256,311.5z"/>
<path id="path1137" class="st0" d="M316.1,320.5c-4.2,2.2-9.5-1.5-8.2-6c1.4-5.4,0.7-11.6-2.9-15.6c-3.4-4-9.8-4.7-14.6-1.9
c-3.7,2-6,5.7-7.4,9.5c-1,3.1-0.6,6.2-1.1,9.3c-1,3.8-5.8,6-9.1,4.3c-3.2-1.4-4-5.9-1.7-8.9c1.4-2.2,4.2-2.7,6.3-3.8
c3.6-1.9,6.5-4.9,8.4-8.6c1.2-2,1.1-4.5,1.7-6.6c0.9-1.9,3-2.7,4.8-2.9c4.8-0.6,9.5-3,13-6.7c1.7-1.6,3.1-4.2,5.6-4.1
c2.7,0.1,4.7,2.5,7.4,2.7c4.9,0.6,10.2-0.8,14.4-4c3.7-3.2,9.4-0.9,9.6,3.6c0.9,5.1-6.2,9.5-10.1,6.2c-4.3-4-10.8-5.6-16.9-3.7
c-5.6,1.9-9.7,8.3-8.6,14c0.9,4.8,4.2,9.3,8.6,11.3c1.7,0.8,3.6,1.6,4.2,3.5C321.2,314.9,319.4,319.1,316.1,320.5L316.1,320.5z"/>
<path id="path1139" class="st0" d="M318.9,329.4c-2.2,0.8-4.2-1.3-3.3-3.4c0.6-2.1,3.7-2.7,4.8-1.1
C321.7,326.3,320.7,328.8,318.9,329.4z"/>
<path id="path1141" class="st0" d="M349.9,286.5c-2.2,0.7-4.2-1.3-3.3-3.5c0.6-2.1,3.7-2.7,4.8-1
C352.8,283.5,351.8,285.9,349.9,286.5z"/>
<path id="path1143" class="st0" d="M268.5,329c-2.2,0.7-4.2-1.3-3.3-3.5c0.6-2,3.7-2.7,4.8-1C271.3,325.9,270.3,328.5,268.5,329z"
/>
<path id="path1145" class="st0" d="M343.1,307.4c-0.4,3.7-5.5,5.9-8.1,3.6c-1.9-1.1-1.6-3.6-2.2-5.4c-0.4-2.4-2.7-4.3-5.2-4.3
c-3.2-0.3-6.4,2.3-9.5,1.2c-4.8-1.2-6.5-7.5-3.2-11.5c2.9-4.1,9.3-4.5,12-0.7c2.4,2.8,0.5,7,2.7,10c1.7,2.9,5.4,2.7,8.3,1.9
C341,301.4,343.8,304.4,343.1,307.4L343.1,307.4z"/>
<path id="path1147" class="st0" d="M295.8,335.2c-3.3,2-7.5-0.9-6.7-4.6c0-2.3,2.2-3.6,3.5-5.2c1.9-1.9,2.3-4.9,1.2-7
c-1.3-2.9-4.9-4-5.5-7.2c-1.2-4.8,3.3-10.1,8.2-9.8c4.8-0.1,8.3,5,6.3,9.6c-1.2,3.7-5.8,4.8-7.2,8.4c-1.7,3.2,0.4,6.1,2.4,8.2
C300,329.6,298.8,333.8,295.8,335.2L295.8,335.2z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/static/img/databases/neon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -188,14 +188,6 @@
dependencies:
"@algolia/client-common" "5.40.0"
"@ampproject/remapping@^2.2.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4"
integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@ant-design/colors@^8.0.0", "@ant-design/colors@^8.0.1":
version "8.0.1"
resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-8.0.1.tgz#6b5444f2ab4061c7b1aa4bc776adb023b0253161"
@@ -277,55 +269,39 @@
"@types/json-schema" "^7.0.15"
js-yaml "^4.1.0"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c"
integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==
dependencies:
"@babel/helper-validator-identifier" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0":
"@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790"
integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==
"@babel/core@^7.21.3", "@babel/core@^7.25.9":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.3.tgz#aceddde69c5d1def69b839d09efa3e3ff59c97cb"
integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.3"
"@babel/helper-compilation-targets" "^7.27.2"
"@babel/helper-module-transforms" "^7.28.3"
"@babel/helpers" "^7.28.3"
"@babel/parser" "^7.28.3"
"@babel/template" "^7.27.2"
"@babel/traverse" "^7.28.3"
"@babel/types" "^7.28.2"
convert-source-map "^2.0.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.3"
semver "^6.3.1"
"@babel/compat-data@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.6.tgz#103f466803fa0f059e82ccac271475470570d74c"
integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==
"@babel/core@^7.26.0":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e"
integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==
"@babel/core@^7.21.3", "@babel/core@^7.25.9", "@babel/core@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322"
integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.5"
"@babel/helper-compilation-targets" "^7.27.2"
"@babel/helper-module-transforms" "^7.28.3"
"@babel/helpers" "^7.28.4"
"@babel/parser" "^7.28.5"
"@babel/template" "^7.27.2"
"@babel/traverse" "^7.28.5"
"@babel/types" "^7.28.5"
"@babel/code-frame" "^7.29.0"
"@babel/generator" "^7.29.0"
"@babel/helper-compilation-targets" "^7.28.6"
"@babel/helper-module-transforms" "^7.28.6"
"@babel/helpers" "^7.28.6"
"@babel/parser" "^7.29.0"
"@babel/template" "^7.28.6"
"@babel/traverse" "^7.29.0"
"@babel/types" "^7.29.0"
"@jridgewell/remapping" "^2.3.5"
convert-source-map "^2.0.0"
debug "^4.1.0"
@@ -333,24 +309,13 @@
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.25.9", "@babel/generator@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.3.tgz#9626c1741c650cbac39121694a0f2d7451b8ef3e"
integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==
"@babel/generator@^7.25.9", "@babel/generator@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.0.tgz#4cba5a76b3c71d8be31761b03329d5dc7768447f"
integrity sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==
dependencies:
"@babel/parser" "^7.28.3"
"@babel/types" "^7.28.2"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/generator@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298"
integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==
dependencies:
"@babel/parser" "^7.28.5"
"@babel/types" "^7.28.5"
"@babel/parser" "^7.29.0"
"@babel/types" "^7.29.0"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
@@ -362,12 +327,12 @@
dependencies:
"@babel/types" "^7.27.3"
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz#46a0f6efab808d51d29ce96858dd10ce8732733d"
integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==
"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2", "@babel/helper-compilation-targets@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25"
integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==
dependencies:
"@babel/compat-data" "^7.27.2"
"@babel/compat-data" "^7.28.6"
"@babel/helper-validator-option" "^7.27.1"
browserslist "^4.24.0"
lru-cache "^5.1.1"
@@ -448,14 +413,22 @@
"@babel/traverse" "^7.27.1"
"@babel/types" "^7.27.1"
"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz#a2b37d3da3b2344fe085dab234426f2b9a2fa5f6"
integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==
"@babel/helper-module-imports@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c"
integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==
dependencies:
"@babel/helper-module-imports" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@babel/traverse" "^7.28.3"
"@babel/traverse" "^7.28.6"
"@babel/types" "^7.28.6"
"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e"
integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==
dependencies:
"@babel/helper-module-imports" "^7.28.6"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/traverse" "^7.28.6"
"@babel/helper-optimise-call-expression@^7.27.1":
version "7.27.1"
@@ -524,35 +497,20 @@
"@babel/traverse" "^7.28.3"
"@babel/types" "^7.28.2"
"@babel/helpers@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.3.tgz#b83156c0a2232c133d1b535dd5d3452119c7e441"
integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==
"@babel/helpers@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7"
integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==
dependencies:
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.2"
"@babel/template" "^7.28.6"
"@babel/types" "^7.28.6"
"@babel/helpers@^7.28.4":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.4.tgz#fe07274742e95bdf7cf1443593eeb8926ab63827"
integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==
"@babel/parser@^7.28.6", "@babel/parser@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6"
integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==
dependencies:
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.4"
"@babel/parser@^7.27.2", "@babel/parser@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.3.tgz#d2d25b814621bca5fe9d172bc93792547e7a2a71"
integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==
dependencies:
"@babel/types" "^7.28.2"
"@babel/parser@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08"
integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==
dependencies:
"@babel/types" "^7.28.5"
"@babel/types" "^7.29.0"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1":
version "7.27.1"
@@ -1255,53 +1213,32 @@
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/template@^7.27.1", "@babel/template@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
"@babel/template@^7.27.1", "@babel/template@^7.27.2", "@babel/template@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57"
integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/parser" "^7.27.2"
"@babel/types" "^7.27.1"
"@babel/code-frame" "^7.28.6"
"@babel/parser" "^7.28.6"
"@babel/types" "^7.28.6"
"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3":
version "7.28.3"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.3.tgz#6911a10795d2cce43ec6a28cffc440cca2593434"
integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==
"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a"
integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.3"
"@babel/code-frame" "^7.29.0"
"@babel/generator" "^7.29.0"
"@babel/helper-globals" "^7.28.0"
"@babel/parser" "^7.28.3"
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.2"
"@babel/parser" "^7.29.0"
"@babel/template" "^7.28.6"
"@babel/types" "^7.29.0"
debug "^4.3.1"
"@babel/traverse@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.5.tgz#450cab9135d21a7a2ca9d2d35aa05c20e68c360b"
integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.5"
"@babel/helper-globals" "^7.28.0"
"@babel/parser" "^7.28.5"
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.5"
debug "^4.3.1"
"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.4.4":
version "7.28.2"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.2.tgz#da9db0856a9a88e0a13b019881d7513588cf712b"
integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@babel/types@^7.28.4", "@babel/types@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b"
integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==
"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.4.4":
version "7.29.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
@@ -3004,24 +2941,24 @@
dependencies:
"@rc-component/util" "^1.3.0"
"@rc-component/dialog@~1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.0.tgz#b1c05c0a8df6292f00a46b3025b490c54fb4da13"
integrity sha512-zGksezfULKixYCIWctIhUC2M3zUJrc81JKWbi9dJrQdPaM7J+8vSOrhLoOHHkZFpBpb2Ri6JqnSuGYb2N+FrRA==
"@rc-component/dialog@~1.8.2":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@rc-component/dialog/-/dialog-1.8.2.tgz#d2bb66ba60c9f632e65c5e471cae38e357397d4e"
integrity sha512-CwDSjpjZ1FcgsdKFPuSoYfi9Vbt2bp+ak4Pzkwq4APQC8DopJKWetRu1V+HE9vI1CNAeqvT5WAvAxE6RiDhl7A==
dependencies:
"@rc-component/motion" "^1.1.3"
"@rc-component/portal" "^2.1.0"
"@rc-component/util" "^1.5.0"
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/drawer@~1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/drawer/-/drawer-1.4.0.tgz#aad9002307899b3b2a31e7c2160c44f38421e026"
integrity sha512-Zr1j1LRLDauz4a5JXHEmeYQfvEzfh4CddNa7tszyJnfd5GySYdZ5qLO63Tt2tgG4k+qi6tkFDKmcT46ikZfzbQ==
"@rc-component/drawer@~1.4.1":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@rc-component/drawer/-/drawer-1.4.1.tgz#df1173ce8e387fd558f73dcc18500f3c778a5ff7"
integrity sha512-kNJQie/QjJO5wGeWrZQwSGeuo8staxXx1nYN+dpK2UY7i8teo5PQdZ6ukKSnnW9vmPXsLn3F5nKYRbf43e8+5g==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/portal" "^2.1.3"
"@rc-component/util" "^1.2.1"
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/dropdown@~1.0.0", "@rc-component/dropdown@~1.0.2":
@@ -3200,10 +3137,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.5.0", "@rc-component/select@~1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.5.1.tgz#315e4f8dce55facae4d948cd5182cf7166a60e27"
integrity sha512-ARXtwfCVnpDJj1bQjh1cimUlNQkZiN72hvtL2G4mKXIYfkokYdA2Vyu2deAfY7kuHSWpmZygVuohQt6TxOYjnA==
"@rc-component/select@~1.5.0", "@rc-component/select@~1.5.2":
version "1.5.2"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.5.2.tgz#43fc247336d6b8ed6ae1656628c56d9a09959c11"
integrity sha512-7wqD5D4I2+fc5XoB4nzDDK656QPlDnFAUaxLljkU1wwSpi4+MZxndv9vgg7NQfveuuf0/ilUdOjuPg7NPl7Mmg==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/trigger" "^3.0.0"
@@ -3326,10 +3263,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.5.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.7.0.tgz#c6eb178e0b1c48c5ae6325b21c60aeaf4f3d8d04"
integrity sha512-tIvIGj4Vl6fsZFvWSkYw9sAfiCKUXMyhVz6kpKyZbwyZyRPqv2vxYZROdaO1VB4gqTNvUZFXh6i3APUiterw5g==
"@rc-component/util@^1.1.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.6.2", "@rc-component/util@^1.7.0", "@rc-component/util@^1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.8.1.tgz#a3515ca34b983d6098b25a19a325346213e648b0"
integrity sha512-Ku6BzF0Ov5L9U3ewFJZDQ//iWCR2nIkLBBiYSrhxIVl3PqeUqiYP2W8gNI8qoKAFQKZdYcS0+B6+SQTDtv/erw==
dependencies:
is-mobile "^5.0.0"
react-is "^18.2.0"
@@ -5205,10 +5142,10 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^6.2.2:
version "6.2.2"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.2.2.tgz#d7b31a5998396ab40750b80e9b2fe6da837a1982"
integrity sha512-f5RvWnhjt2gZTpBMW3msHwA3IeaCJBHDwVyEsskYGp0EXcRhhklWrltkybDki0ysBNywkjLPp3wuuWhIKfplcQ==
antd@^6.2.3:
version "6.2.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.2.3.tgz#b4e7a73d2300f5c23eff5570b84be56ebe9a1d23"
integrity sha512-q92r7/hcQAR2iv6CCysdz7c2Pdl/3nhslc3azF9e6AEl4knO6v+nlaeor1oF2jBanZ/tiw2m3NprOVUgPDvyhg==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.0.3"
@@ -5221,8 +5158,8 @@ antd@^6.2.2:
"@rc-component/checkbox" "~1.0.1"
"@rc-component/collapse" "~1.2.0"
"@rc-component/color-picker" "~3.0.3"
"@rc-component/dialog" "~1.8.0"
"@rc-component/drawer" "~1.4.0"
"@rc-component/dialog" "~1.8.2"
"@rc-component/drawer" "~1.4.1"
"@rc-component/dropdown" "~1.0.2"
"@rc-component/form" "~1.6.2"
"@rc-component/image" "~1.6.0"
@@ -5240,7 +5177,7 @@ antd@^6.2.2:
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.1.1"
"@rc-component/segmented" "~1.3.0"
"@rc-component/select" "~1.5.1"
"@rc-component/select" "~1.5.2"
"@rc-component/slider" "~1.0.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
@@ -5253,7 +5190,7 @@ antd@^6.2.2:
"@rc-component/tree-select" "~1.6.0"
"@rc-component/trigger" "^3.9.0"
"@rc-component/upload" "~1.1.0"
"@rc-component/util" "^1.7.0"
"@rc-component/util" "^1.8.1"
clsx "^2.1.1"
dayjs "^1.11.11"
scroll-into-view-if-needed "^3.1.0"
@@ -5449,6 +5386,13 @@ axios@^1.12.2:
form-data "^4.0.4"
proxy-from-env "^1.1.0"
babel-loader@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-10.0.0.tgz#b9743714c0e1e084b3e4adef3cd5faee33089977"
integrity sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==
dependencies:
find-up "^5.0.0"
babel-loader@^9.2.1:
version "9.2.1"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b"
@@ -5753,10 +5697,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001766:
version "1.0.30001766"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz#b6f6b55cb25a2d888d9393104d14751c6a7d6f7a"
integrity sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001767:
version "1.0.30001767"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz#0279c498e862efb067938bba0a0aabafe8d0b730"
integrity sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==
ccount@^2.0.0:
version "2.0.1"
@@ -7237,13 +7181,13 @@ encodeurl@~2.0.0:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.17.4:
version "5.18.4"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz#c22d33055f3952035ce6a144ce092447c525f828"
integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.19.0:
version "5.19.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz#6687446a15e969eaa63c2fa2694510e17ae6d97c"
integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
tapable "^2.3.0"
entities@^2.0.0:
version "2.2.0"
@@ -8221,10 +8165,10 @@ globals@^15.14.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8"
integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
globals@^17.2.0:
version "17.2.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.2.0.tgz#41d29408d6f5408457d2ef965d29215e3026779f"
integrity sha512-tovnCz/fEq+Ripoq+p/gN1u7l6A7wwkoBT9pRCzTHzsD/LvADIzXZdjmRymh5Ztf0DYC3Rwg5cZRYjxzBmzbWg==
globals@^17.3.0:
version "17.3.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9"
integrity sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==
globalthis@^1.0.4:
version "1.0.4"
@@ -13401,12 +13345,7 @@ semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4, semver@^7.6.2:
version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
semver@^7.7.3:
semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.4, semver@^7.6.2, semver@^7.7.3:
version "7.7.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
@@ -14194,7 +14133,7 @@ synckit@^0.11.12:
dependencies:
"@pkgr/core" "^0.2.9"
tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0:
tapable@^2.0.0, tapable@^2.2.1, tapable@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
@@ -14993,10 +14932,10 @@ warning@^4.0.3:
dependencies:
loose-envify "^1.0.0"
watchpack@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947"
integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==
watchpack@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.1.tgz#dd38b601f669e0cbf567cb802e75cead82cde102"
integrity sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
@@ -15120,10 +15059,10 @@ webpack-virtual-modules@^0.6.2:
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.104.1, webpack@^5.88.1, webpack@^5.95.0:
version "5.104.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.104.1.tgz#94bd41eb5dbf06e93be165ba8be41b8260d4fb1a"
integrity sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==
webpack@^5.105.0, webpack@^5.88.1, webpack@^5.95.0:
version "5.105.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.0.tgz#38b5e6c5db8cbe81debbd16e089335ada05ea23a"
integrity sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==
dependencies:
"@types/eslint-scope" "^3.7.7"
"@types/estree" "^1.0.8"
@@ -15135,7 +15074,7 @@ webpack@^5.104.1, webpack@^5.88.1, webpack@^5.95.0:
acorn-import-phases "^1.0.3"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.17.4"
enhanced-resolve "^5.19.0"
es-module-lexer "^2.0.0"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -15148,7 +15087,7 @@ webpack@^5.104.1, webpack@^5.88.1, webpack@^5.95.0:
schema-utils "^4.3.3"
tapable "^2.3.0"
terser-webpack-plugin "^5.3.16"
watchpack "^2.4.4"
watchpack "^2.5.1"
webpack-sources "^3.3.3"
webpackbar@^6.0.1:

View File

@@ -174,7 +174,7 @@ oracle = ["cx-Oracle>8.0.0, <8.1"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <6.0.0"]
playwright = ["playwright>=1.37.0, <2"]
postgres = ["psycopg2-binary==2.9.6"]
postgres = ["psycopg2-binary==2.9.9"]
presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.328.0"]
prophet = ["prophet>=1.1.6, <2"]
@@ -204,6 +204,7 @@ ydb = ["ydb-sqlalchemy>=0.1.2"]
development = [
# no bounds for apache-superset-extensions-cli until a stable version
"apache-superset-extensions-cli",
"boto3",
"docker",
"flask-testing",
"freezegun",
@@ -437,6 +438,7 @@ authorized_licenses = [
"apache software",
"apache software, bsd",
"bsd",
"bsd-2-clause",
"bsd-3-clause",
"isc license (iscl)",
"isc license",

View File

@@ -16,8 +16,14 @@
# specific language governing permissions and limitations
# under the License.
#
urllib3>=2.6.0,<3.0.0
werkzeug>=3.0.1
# Security: CVE-2026-21441 - decompression bomb bypass on redirects
urllib3>=2.6.3,<3.0.0
# Security: GHSA-87hc-h4r5-73f7 - Windows path traversal fix
werkzeug>=3.1.5,<4.0.0
# Security: CVE-2025-68146 - TOCTOU symlink vulnerability
filelock>=3.20.3,<4.0.0
# Security: decompression bomb fix (required by aiohttp 3.13.3)
brotli>=1.2.0,<2.0.0
numexpr>=2.9.0
# 5.0.0 has a sensitive deprecation used in other libs

View File

@@ -36,8 +36,10 @@ blinker==1.9.0
# via flask
bottleneck==1.5.0
# via apache-superset (pyproject.toml)
brotli==1.1.0
# via flask-compress
brotli==1.2.0
# via
# -r requirements/base.in
# flask-compress
cachelib==0.13.0
# via
# flask-caching
@@ -101,6 +103,8 @@ email-validator==2.2.0
# via flask-appbuilder
et-xmlfile==2.0.0
# via openpyxl
filelock==3.20.3
# via -r requirements/base.in
flask==2.3.3
# via
# apache-superset (pyproject.toml)
@@ -289,7 +293,7 @@ prompt-toolkit==3.0.51
# via click-repl
pyarrow==16.1.0
# via apache-superset (pyproject.toml)
pyasn1==0.6.1
pyasn1==0.6.2
# via
# pyasn1-modules
# rsa
@@ -436,7 +440,7 @@ tzdata==2025.2
# pandas
url-normalize==2.2.1
# via requests-cache
urllib3==2.6.0
urllib3==2.6.3
# via
# -r requirements/base.in
# requests
@@ -453,7 +457,7 @@ wcwidth==0.2.13
# via prompt-toolkit
websocket-client==1.8.0
# via selenium
werkzeug==3.1.3
werkzeug==3.1.5
# via
# -r requirements/base.in
# flask

View File

@@ -76,11 +76,17 @@ blinker==1.9.0
# via
# -c requirements/base-constraint.txt
# flask
boto3==1.42.39
# via apache-superset
botocore==1.42.39
# via
# boto3
# s3transfer
bottleneck==1.5.0
# via
# -c requirements/base-constraint.txt
# apache-superset
brotli==1.1.0
brotli==1.2.0
# via
# -c requirements/base-constraint.txt
# flask-compress
@@ -235,8 +241,10 @@ fakeredis==2.32.1
# via pydocket
fastmcp==2.14.3
# via apache-superset
filelock==3.12.2
# via virtualenv
filelock==3.20.3
# via
# -c requirements/base-constraint.txt
# virtualenv
flask==2.3.3
# via
# -c requirements/base-constraint.txt
@@ -458,6 +466,10 @@ jinja2==3.1.6
# apache-superset-extensions-cli
# flask
# flask-babel
jmespath==1.1.0
# via
# boto3
# botocore
jsonpath-ng==1.7.0
# via
# -c requirements/base-constraint.txt
@@ -700,7 +712,7 @@ protobuf==4.25.5
# proto-plus
psutil==6.1.0
# via apache-superset
psycopg2-binary==2.9.6
psycopg2-binary==2.9.9
# via apache-superset
py-key-value-aio==0.3.0
# via
@@ -714,7 +726,7 @@ pyarrow==16.1.0
# apache-superset
# db-dtypes
# pandas-gbq
pyasn1==0.6.1
pyasn1==0.6.2
# via
# -c requirements/base-constraint.txt
# pyasn1-modules
@@ -810,6 +822,7 @@ python-dateutil==2.9.0.post0
# via
# -c requirements/base-constraint.txt
# apache-superset
# botocore
# celery
# croniter
# flask-appbuilder
@@ -913,6 +926,8 @@ rsa==4.9.1
# google-auth
ruff==0.9.7
# via apache-superset
s3transfer==0.16.0
# via boto3
secretstorage==3.5.0
# via keyring
selenium==4.32.0
@@ -1061,9 +1076,10 @@ url-normalize==2.2.1
# via
# -c requirements/base-constraint.txt
# requests-cache
urllib3==2.6.0
urllib3==2.6.3
# via
# -c requirements/base-constraint.txt
# botocore
# docker
# requests
# requests-cache
@@ -1095,7 +1111,7 @@ websocket-client==1.8.0
# selenium
websockets==15.0.1
# via fastmcp
werkzeug==3.1.3
werkzeug==3.1.5
# via
# -c requirements/base-constraint.txt
# flask

View File

@@ -59,7 +59,7 @@ module.exports = {
],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
],
preset: 'ts-jest',
transform: {

View File

@@ -79,7 +79,7 @@
"geostyler-openlayers-parser": "^4.3.0",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^170.1.0",
"googleapis": "^171.1.0",
"immer": "^11.1.3",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -100,6 +100,7 @@
"query-string": "6.14.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react-arborist": "^3.4.3",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0",
"react-dnd": "^11.1.3",
@@ -134,12 +135,13 @@
"urijs": "^1.19.8",
"use-event-callback": "^0.1.0",
"use-immer": "^0.11.0",
"use-query-params": "^1.1.9",
"use-query-params": "^2.2.2",
"uuid": "^13.0.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yargs": "^17.7.2"
},
"devDependencies": {
"@applitools/eyes-storybook": "^3.63.9",
"@applitools/eyes-storybook": "^3.63.10",
"@babel/cli": "^7.28.6",
"@babel/compat-data": "^7.28.4",
"@babel/core": "^7.28.6",
@@ -149,12 +151,12 @@
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/preset-env": "^7.28.6",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.28.6",
"@babel/runtime-corejs3": "^7.28.6",
"@babel/runtime-corejs3": "^7.29.0",
"@babel/types": "^7.28.6",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
@@ -162,7 +164,7 @@
"@hot-loader/react-dom": "^17.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.58.0",
"@playwright/test": "^1.58.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-actions": "8.6.14",
"@storybook/addon-controls": "8.6.14",
@@ -190,7 +192,7 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.0.10",
"@types/node": "^25.1.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-loadable": "^5.5.11",
@@ -212,12 +214,12 @@
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
"baseline-browser-mapping": "^2.9.18",
"baseline-browser-mapping": "^2.9.19",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^13.0.1",
"cross-env": "^10.1.0",
"css-loader": "^7.1.2",
"css-loader": "^7.1.3",
"css-minimizer-webpack-plugin": "^7.0.4",
"eslint": "^8.56.0",
"eslint-config-prettier": "^7.2.0",
@@ -240,7 +242,7 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.15.4",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^11.1.5",
"fetch-mock": "^12.6.0",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.6",
@@ -460,9 +462,9 @@
"link": true
},
"node_modules/@applitools/core": {
"version": "4.56.0",
"resolved": "https://registry.npmjs.org/@applitools/core/-/core-4.56.0.tgz",
"integrity": "sha512-f0KnddAJpCLKahzecZ880RgkIFdu3T5k3jj2Q+b/XHIE81xyE7xBnEweEzPDqTDkO52C6nwVRfI2FlMbLcxPcQ==",
"version": "4.56.1",
"resolved": "https://registry.npmjs.org/@applitools/core/-/core-4.56.1.tgz",
"integrity": "sha512-EOIc/BkgjuX2qWvrrIOmSE+hZn/tJN6qoj+zP6cNGfvh7LKmqUuq+TMKzRmU1xjexyrpvQCQDXdtHex1UCZpKw==",
"dev": true,
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
@@ -684,14 +686,28 @@
"node": ">=14.0.0"
}
},
"node_modules/@applitools/execution-grid-tunnel/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@applitools/eyes": {
"version": "1.38.1",
"resolved": "https://registry.npmjs.org/@applitools/eyes/-/eyes-1.38.1.tgz",
"integrity": "sha512-1iDNGkvX/fM4DM/ddMg9h2pxT9Jmb6wfJnEY5eDdha3clyuN2CwuRdhIBZc0MXEEK7fJcfjylZKMqahxbTxjCw==",
"version": "1.38.2",
"resolved": "https://registry.npmjs.org/@applitools/eyes/-/eyes-1.38.2.tgz",
"integrity": "sha512-LD1ynXpyc7h9KmV8ZAF55s0zqtH80N2G0BRWsumjphfRp/D0m5uc73oRQdmQ2TYvcaUxllpQhgQd0TxLJvvHwA==",
"dev": true,
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@applitools/core": "4.56.0",
"@applitools/core": "4.56.1",
"@applitools/logger": "2.2.7",
"@applitools/utils": "1.14.1",
"chalk": "4.1.2",
@@ -705,15 +721,15 @@
}
},
"node_modules/@applitools/eyes-storybook": {
"version": "3.63.9",
"resolved": "https://registry.npmjs.org/@applitools/eyes-storybook/-/eyes-storybook-3.63.9.tgz",
"integrity": "sha512-Rn0NKw+E6aH6zTMMUGtwHR4yh/of663SZAmt8VHJEWPwfFjT2bijly07U+UMuJs7+1OIq/UFptqVM+KX7+jPeQ==",
"version": "3.63.10",
"resolved": "https://registry.npmjs.org/@applitools/eyes-storybook/-/eyes-storybook-3.63.10.tgz",
"integrity": "sha512-oJ8MnkvYS3BgG40DJYL8/WYafH8lDs7oBa88PE8OKsnaHyzZbAg6vTIaHsSKN8N1jjt3wk1HwLXlSAwYoKR0hA==",
"dev": true,
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@applitools/core": "4.56.0",
"@applitools/core": "4.56.1",
"@applitools/driver": "1.25.0",
"@applitools/eyes": "1.38.1",
"@applitools/eyes": "1.38.2",
"@applitools/functional-commons": "1.6.0",
"@applitools/logger": "2.2.7",
"@applitools/monitoring-commons": "1.0.19",
@@ -1142,9 +1158,9 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
@@ -1156,9 +1172,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
"integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1236,13 +1252,13 @@
}
},
"node_modules/@babel/generator": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
"integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz",
"integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -1352,17 +1368,17 @@
}
},
"node_modules/@babel/helper-define-polyfill-provider": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz",
"integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==",
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz",
"integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"debug": "^4.4.1",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
"debug": "^4.4.3",
"lodash.debounce": "^4.0.8",
"resolve": "^1.22.10"
"resolve": "^1.22.11"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -1602,12 +1618,12 @@
}
},
"node_modules/@babel/parser": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.6"
"@babel/types": "^7.29.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2015,15 +2031,15 @@
}
},
"node_modules/@babel/plugin-transform-async-generator-functions": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.6.tgz",
"integrity": "sha512-9knsChgsMzBV5Yh3kkhrZNxH3oCYAfMBkNNaVN4cP2RVlFPe8wYdwwcnOsAbkdDoV9UjFtOXWrWB52M8W4jNeA==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz",
"integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-remap-async-to-generator": "^7.27.1",
"@babel/traverse": "^7.28.6"
"@babel/traverse": "^7.29.0"
},
"engines": {
"node": ">=6.9.0"
@@ -2205,9 +2221,9 @@
}
},
"node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz",
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz",
"integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2420,16 +2436,16 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
"integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.5"
"@babel/traverse": "^7.29.0"
},
"engines": {
"node": ">=6.9.0"
@@ -2456,14 +2472,14 @@
}
},
"node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz",
"integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz",
"integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1"
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -2743,9 +2759,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.6.tgz",
"integrity": "sha512-eZhoEZHYQLL5uc1gS5e9/oTknS0sSSAtd5TkKMUp3J+S/CaUjagc0kOUPsEbDmMeva0nC3WWl4SxVY6+OBuxfw==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz",
"integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3008,13 +3024,13 @@
"license": "MIT"
},
"node_modules/@babel/preset-env": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz",
"integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz",
"integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/compat-data": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/helper-validator-option": "^7.27.1",
@@ -3028,7 +3044,7 @@
"@babel/plugin-syntax-import-attributes": "^7.28.6",
"@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
"@babel/plugin-transform-arrow-functions": "^7.27.1",
"@babel/plugin-transform-async-generator-functions": "^7.28.6",
"@babel/plugin-transform-async-generator-functions": "^7.29.0",
"@babel/plugin-transform-async-to-generator": "^7.28.6",
"@babel/plugin-transform-block-scoped-functions": "^7.27.1",
"@babel/plugin-transform-block-scoping": "^7.28.6",
@@ -3039,7 +3055,7 @@
"@babel/plugin-transform-destructuring": "^7.28.5",
"@babel/plugin-transform-dotall-regex": "^7.28.6",
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6",
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0",
"@babel/plugin-transform-dynamic-import": "^7.27.1",
"@babel/plugin-transform-explicit-resource-management": "^7.28.6",
"@babel/plugin-transform-exponentiation-operator": "^7.28.6",
@@ -3052,9 +3068,9 @@
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
"@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-modules-systemjs": "^7.28.5",
"@babel/plugin-transform-modules-systemjs": "^7.29.0",
"@babel/plugin-transform-modules-umd": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0",
"@babel/plugin-transform-new-target": "^7.27.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6",
"@babel/plugin-transform-numeric-separator": "^7.28.6",
@@ -3066,7 +3082,7 @@
"@babel/plugin-transform-private-methods": "^7.28.6",
"@babel/plugin-transform-private-property-in-object": "^7.28.6",
"@babel/plugin-transform-property-literals": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.28.6",
"@babel/plugin-transform-regenerator": "^7.29.0",
"@babel/plugin-transform-regexp-modifiers": "^7.28.6",
"@babel/plugin-transform-reserved-words": "^7.27.1",
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
@@ -3079,10 +3095,10 @@
"@babel/plugin-transform-unicode-regex": "^7.27.1",
"@babel/plugin-transform-unicode-sets-regex": "^7.28.6",
"@babel/preset-modules": "0.1.6-no-external-plugins",
"babel-plugin-polyfill-corejs2": "^0.4.14",
"babel-plugin-polyfill-corejs3": "^0.13.0",
"babel-plugin-polyfill-regenerator": "^0.6.5",
"core-js-compat": "^3.43.0",
"babel-plugin-polyfill-corejs2": "^0.4.15",
"babel-plugin-polyfill-corejs3": "^0.14.0",
"babel-plugin-polyfill-regenerator": "^0.6.6",
"core-js-compat": "^3.48.0",
"semver": "^6.3.1"
},
"engines": {
@@ -3092,6 +3108,20 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz",
"integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.6",
"core-js-compat": "^3.48.0"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
}
},
"node_modules/@babel/preset-env/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -3188,13 +3218,13 @@
}
},
"node_modules/@babel/runtime-corejs3": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.6.tgz",
"integrity": "sha512-kz2fAQ5UzjV7X7D3ySxmj3vRq89dTpqOZWv76Z6pNPztkwb/0Yj1Mtx1xFrYj6mbIHysxtBot8J4o0JLCblcFw==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz",
"integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-js-pure": "^3.43.0"
"core-js-pure": "^3.48.0"
},
"engines": {
"node": ">=6.9.0"
@@ -3215,17 +3245,17 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
"integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6",
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.28.6",
"@babel/types": "^7.29.0",
"debug": "^4.3.1"
},
"engines": {
@@ -3233,9 +3263,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -10601,13 +10631,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
"integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==",
"version": "1.58.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.1.tgz",
"integrity": "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.0"
"playwright": "1.58.1"
},
"bin": {
"playwright": "cli.js"
@@ -14820,6 +14850,20 @@
"storybook": "^8.6.14"
}
},
"node_modules/@storybook/addon-actions/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@storybook/addon-backgrounds": {
"version": "8.6.14",
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.6.14.tgz",
@@ -19453,9 +19497,9 @@
"license": "MIT"
},
"node_modules/@types/http-cache-semantics": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"dev": true,
"license": "MIT"
},
@@ -19903,9 +19947,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz",
"integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==",
"version": "25.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
"integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
@@ -23829,14 +23873,14 @@
}
},
"node_modules/babel-plugin-polyfill-corejs2": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz",
"integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==",
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz",
"integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.7",
"@babel/helper-define-polyfill-provider": "^0.6.5",
"@babel/compat-data": "^7.28.6",
"@babel/helper-define-polyfill-provider": "^0.6.6",
"semver": "^6.3.1"
},
"peerDependencies": {
@@ -23868,13 +23912,13 @@
}
},
"node_modules/babel-plugin-polyfill-regenerator": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz",
"integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==",
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz",
"integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.5"
"@babel/helper-define-polyfill-provider": "^0.6.6"
},
"peerDependencies": {
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
@@ -24080,9 +24124,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.18",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
"integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -26405,13 +26449,13 @@
}
},
"node_modules/core-js-compat": {
"version": "3.45.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz",
"integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==",
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz",
"integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.25.3"
"browserslist": "^4.28.1"
},
"funding": {
"type": "opencollective",
@@ -26419,9 +26463,9 @@
}
},
"node_modules/core-js-pure": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.0.tgz",
"integrity": "sha512-OtwjqcDpY2X/eIIg1ol/n0y/X8A9foliaNt1dSK0gV3J2/zw+89FcNG3mPK+N8YWts4ZFUPxnrAzsxs/lf8yDA==",
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz",
"integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -27325,20 +27369,20 @@
}
},
"node_modules/css-loader": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz",
"integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.3.tgz",
"integrity": "sha512-frbERmjT0UC5lMheWpJmMilnt9GEhbZJN/heUb7/zaJYeIzj5St9HvDcfshzzOqbsS+rYpMk++2SD3vGETDSyA==",
"dev": true,
"license": "MIT",
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.33",
"postcss": "^8.4.40",
"postcss-modules-extract-imports": "^3.1.0",
"postcss-modules-local-by-default": "^4.0.5",
"postcss-modules-scope": "^3.2.0",
"postcss-modules-values": "^4.0.0",
"postcss-value-parser": "^4.2.0",
"semver": "^7.5.4"
"semver": "^7.6.3"
},
"engines": {
"node": ">= 18.12.0"
@@ -27360,6 +27404,19 @@
}
}
},
"node_modules/css-loader/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/css-minimizer-webpack-plugin": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-7.0.4.tgz",
@@ -32105,25 +32162,19 @@
}
},
"node_modules/fetch-mock": {
"version": "11.1.5",
"resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-11.1.5.tgz",
"integrity": "sha512-KHmZDnZ1ry0pCTrX4YG5DtThHi0MH+GNI9caESnzX/nMJBrvppUHMvLx47M0WY9oAtKOMiPfZDRpxhlHg89BOA==",
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-12.6.0.tgz",
"integrity": "sha512-oAy0OqAvjAvduqCeWveBix7LLuDbARPqZZ8ERYtBcCURA3gy7EALA3XWq0tCNxsSg+RmmJqyaeeZlOCV9abv6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/glob-to-regexp": "^0.4.4",
"dequal": "^2.0.3",
"glob-to-regexp": "^0.4.1",
"is-subset": "^0.1.1",
"regexparam": "^3.0.0"
},
"engines": {
"node": ">=8.0.0"
},
"peerDependenciesMeta": {
"node-fetch": {
"optional": true
}
"node": ">=18.11.0"
}
},
"node_modules/fetch-retry": {
@@ -34526,9 +34577,9 @@
}
},
"node_modules/googleapis": {
"version": "170.1.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-170.1.0.tgz",
"integrity": "sha512-RLbc7yG6qzZqvAmGcgjvNIoZ7wpcCFxtc+HN+46etxDrlO4a8l5Cb7NxNQGhV91oRmL7mt56VoRoypAtEQEIKg==",
"version": "171.1.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-171.1.0.tgz",
"integrity": "sha512-2f3O75VjbKRnvwN5Pwi6ZZEcnOiO10ZfPSX19oE2ehxC8689NxWZLMPKsap7qgT48adu2NEA8tlUQZK2nt0EDA==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^10.2.0",
@@ -36901,7 +36952,9 @@
"resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
"integrity": "sha512-6Ybun0IkarhmEqxXCNw/C0bna6Zb/TkfUX9UbwJtK6ObwAVCxmAP308WWTHviM/zAqXk05cdhYsUsZeGQh99iw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/is-symbol": {
"version": "1.1.1",
@@ -41295,9 +41348,9 @@
}
},
"node_modules/jspdf": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz",
"integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
@@ -47332,13 +47385,13 @@
}
},
"node_modules/playwright": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
"integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
"version": "1.58.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.1.tgz",
"integrity": "sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.0"
"playwright-core": "1.58.1"
},
"bin": {
"playwright": "cli.js"
@@ -47351,9 +47404,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
"integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
"version": "1.58.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.1.tgz",
"integrity": "sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -49655,6 +49708,88 @@
"react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-arborist": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.4.3.tgz",
"integrity": "sha512-yFnq1nIQhT2uJY4TZVz2tgAiBb9lxSyvF4vC3S8POCK8xLzjGIxVv3/4dmYquQJ7AHxaZZArRGHiHKsEewKdTQ==",
"license": "MIT",
"dependencies": {
"react-dnd": "^14.0.3",
"react-dnd-html5-backend": "^14.0.3",
"react-window": "^1.8.11",
"redux": "^5.0.0",
"use-sync-external-store": "^1.2.0"
},
"peerDependencies": {
"react": ">= 16.14",
"react-dom": ">= 16.14"
}
},
"node_modules/react-arborist/node_modules/dnd-core": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.1.tgz",
"integrity": "sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==",
"license": "MIT",
"dependencies": {
"@react-dnd/asap": "^4.0.0",
"@react-dnd/invariant": "^2.0.0",
"redux": "^4.1.1"
}
},
"node_modules/react-arborist/node_modules/dnd-core/node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/react-arborist/node_modules/react-dnd": {
"version": "14.0.5",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.5.tgz",
"integrity": "sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==",
"license": "MIT",
"dependencies": {
"@react-dnd/invariant": "^2.0.0",
"@react-dnd/shallowequal": "^2.0.0",
"dnd-core": "14.0.1",
"fast-deep-equal": "^3.1.3",
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"@types/hoist-non-react-statics": ">= 3.3.1",
"@types/node": ">= 12",
"@types/react": ">= 16",
"react": ">= 16.14"
},
"peerDependenciesMeta": {
"@types/hoist-non-react-statics": {
"optional": true
},
"@types/node": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/react-arborist/node_modules/react-dnd-html5-backend": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.1.0.tgz",
"integrity": "sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==",
"license": "MIT",
"dependencies": {
"dnd-core": "14.0.1"
}
},
"node_modules/react-arborist/node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/react-async-script": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
@@ -52056,12 +52191,12 @@
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -53061,13 +53196,10 @@
}
},
"node_modules/serialize-query-params": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-1.3.6.tgz",
"integrity": "sha512-VlH7sfWNyPVZClPkRacopn6sn5uQMXBsjPVz1+pBHX895VpcYVznfJtZ49e6jymcrz+l/vowkepCZn/7xEAEdw==",
"license": "ISC",
"peerDependencies": {
"query-string": ">=5.1.1"
}
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-2.0.4.tgz",
"integrity": "sha512-y9WzzDj3BsGgKLCh0ugiinufS//YqOfao/yVJjkXA4VLuyNCfHOLU/cbulGPxs3aeCqhvROw7qPL04JSZnCo0w==",
"license": "ISC"
},
"node_modules/serve-index": {
"version": "1.9.1",
@@ -57863,17 +57995,35 @@
}
},
"node_modules/use-query-params": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-1.2.3.tgz",
"integrity": "sha512-cdG0tgbzK+FzsV6DAt2CN8Saa3WpRnze7uC4Rdh7l15epSFq7egmcB/zuREvPNwO5Yk80nUpDZpiyHsoq50d8w==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-2.2.2.tgz",
"integrity": "sha512-OwGab8u8/x2xZp9uSyBsx0kXlkR9IR436zbygsYVGikPYY3OJosvve6IJVGwIJPcfyb/YHwvPrUNu65/JR++Kw==",
"license": "ISC",
"dependencies": {
"serialize-query-params": "^1.3.5"
"serialize-query-params": "^2.0.3"
},
"peerDependencies": {
"query-string": ">=5.1.1",
"@reach/router": "^1.2.1",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
"react-dom": ">=16.8.0",
"react-router-dom": ">=5"
},
"peerDependenciesMeta": {
"@reach/router": {
"optional": true
},
"react-router-dom": {
"optional": true
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util": {
@@ -57923,17 +58073,16 @@
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"dev": true,
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/uvu": {
@@ -60863,7 +61012,7 @@
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.28.6",
"@babel/preset-env": "^7.28.6",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@emotion/styled": "^11.14.1",
@@ -63662,13 +63811,13 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^3.5.33",
"@types/lodash": "^4.17.23",
"@types/node": "^25.0.10",
"@types/node": "^25.1.0",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
"fetch-mock": "^11.1.4",
"fetch-mock": "^12.6.0",
"jest-mock-console": "^2.0.0",
"resize-observer-polyfill": "1.5.1",
"timezone-mock": "1.3.6"
@@ -64841,7 +64990,7 @@
},
"devDependencies": {
"@babel/core": "^7.28.6",
"@babel/preset-env": "^7.28.6",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@storybook/react-webpack5": "8.6.14",

View File

@@ -161,7 +161,7 @@
"geostyler-openlayers-parser": "^4.3.0",
"geostyler-style": "7.5.0",
"geostyler-wfs-parser": "^2.0.3",
"googleapis": "^170.1.0",
"googleapis": "^171.1.0",
"immer": "^11.1.3",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -182,6 +182,7 @@
"query-string": "6.14.1",
"re-resizable": "^6.11.2",
"react": "^17.0.2",
"react-arborist": "^3.4.3",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^3.4.0",
"react-dnd": "^11.1.3",
@@ -216,12 +217,13 @@
"urijs": "^1.19.8",
"use-event-callback": "^0.1.0",
"use-immer": "^0.11.0",
"use-query-params": "^1.1.9",
"use-query-params": "^2.2.2",
"uuid": "^13.0.0",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yargs": "^17.7.2"
},
"devDependencies": {
"@applitools/eyes-storybook": "^3.63.9",
"@applitools/eyes-storybook": "^3.63.10",
"@babel/cli": "^7.28.6",
"@babel/compat-data": "^7.28.4",
"@babel/core": "^7.28.6",
@@ -231,12 +233,12 @@
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.28.6",
"@babel/plugin-transform-runtime": "^7.28.5",
"@babel/preset-env": "^7.28.6",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/register": "^7.23.7",
"@babel/runtime": "^7.28.6",
"@babel/runtime-corejs3": "^7.28.6",
"@babel/runtime-corejs3": "^7.29.0",
"@babel/types": "^7.28.6",
"@cypress/react": "^8.0.2",
"@emotion/babel-plugin": "^11.13.5",
@@ -244,7 +246,7 @@
"@hot-loader/react-dom": "^17.0.2",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@mihkeleidast/storybook-addon-source": "^1.0.1",
"@playwright/test": "^1.58.0",
"@playwright/test": "^1.58.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-actions": "8.6.14",
"@storybook/addon-controls": "8.6.14",
@@ -272,7 +274,7 @@
"@types/js-levenshtein": "^1.1.3",
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.0.10",
"@types/node": "^25.1.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-loadable": "^5.5.11",
@@ -294,12 +296,12 @@
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
"baseline-browser-mapping": "^2.9.18",
"baseline-browser-mapping": "^2.9.19",
"cheerio": "1.2.0",
"concurrently": "^9.2.1",
"copy-webpack-plugin": "^13.0.1",
"cross-env": "^10.1.0",
"css-loader": "^7.1.2",
"css-loader": "^7.1.3",
"css-minimizer-webpack-plugin": "^7.0.4",
"eslint": "^8.56.0",
"eslint-config-prettier": "^7.2.0",
@@ -322,7 +324,7 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.15.4",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^11.1.5",
"fetch-mock": "^12.6.0",
"fork-ts-checker-webpack-plugin": "^9.1.0",
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.6",
@@ -386,7 +388,7 @@
"puppeteer": "^22.4.1",
"remark-gfm": "^3.0.1",
"underscore": "^1.13.7",
"jspdf": "^3.0.2",
"jspdf": "^4.0.0",
"nwsapi": "^2.2.13",
"@deck.gl/aggregation-layers": "~9.2.2",
"@deck.gl/core": "~9.2.2",

View File

@@ -13,7 +13,7 @@
"devDependencies": {
"@babel/cli": "^7.28.6",
"@babel/core": "^7.28.6",
"@babel/preset-env": "^7.28.6",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"install": "^0.13.0",

View File

@@ -29,8 +29,9 @@ import {
FieldStringOutlined,
NumberOutlined,
} from '@ant-design/icons';
import { Icons } from '@superset-ui/core/components';
export type ColumnLabelExtendedType = 'expression' | '';
export type ColumnLabelExtendedType = 'expression' | 'metric' | '';
export type ColumnTypeLabelProps = {
type?: ColumnLabelExtendedType | GenericDataType;
@@ -59,7 +60,9 @@ export function ColumnTypeLabel({ type }: ColumnTypeLabelProps) {
<QuestionOutlined aria-label={t('unknown type icon')} />
);
if (type === '' || type === 'expression') {
if (type === 'metric') {
typeIcon = <Icons.Sigma aria-label={t('metric type icon')} />;
} else if (type === '' || type === 'expression') {
typeIcon = <FunctionOutlined aria-label={t('function type icon')} />;
} else if (type === GenericDataType.String) {
typeIcon = <FieldStringOutlined aria-label={t('string type icon')} />;

View File

@@ -95,7 +95,7 @@ export function MetricOption({
return (
<FlexRowContainer className="metric-option">
{showType && <ColumnTypeLabel type="expression" />}
{showType && <ColumnTypeLabel type="metric" />}
{shouldShowTooltip ? (
<Tooltip id="metric-name-tooltip" title={tooltipText}>
{label}

View File

@@ -23,7 +23,7 @@ import { ControlPanelSectionConfig } from '../types';
import { formatSelectOptions } from '../utils';
export const TITLE_MARGIN_OPTIONS: number[] = [
15, 30, 50, 75, 100, 125, 150, 200,
0, 15, 30, 50, 75, 100, 125, 150, 200,
];
export const TITLE_POSITION_OPTIONS: [string, string][] = [
['Left', t('Left')],
@@ -82,7 +82,7 @@ export const titleControls: ControlPanelSectionConfig = {
clearable: true,
label: t('Y Axis Title Margin'),
renderTrigger: true,
default: TITLE_MARGIN_OPTIONS[1],
default: TITLE_MARGIN_OPTIONS[0],
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
},
},

View File

@@ -52,6 +52,10 @@ describe('ColumnOption', () => {
renderColumnTypeLabel({ type: 'expression' });
expect(screen.getByLabelText('function type icon')).toBeVisible();
});
it('metric type shows sigma icon', () => {
renderColumnTypeLabel({ type: 'metric' });
expect(screen.getByLabelText('metric type icon')).toBeVisible();
});
it('unknown type shows question mark', () => {
renderColumnTypeLabel({ type: undefined });
expect(screen.getByLabelText('unknown type icon')).toBeVisible();

View File

@@ -78,11 +78,11 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@types/jquery": "^3.5.33",
"@types/lodash": "^4.17.23",
"@types/node": "^25.0.10",
"@types/node": "^25.1.0",
"@types/prop-types": "^15.7.15",
"@types/rison": "0.1.0",
"@types/seedrandom": "^3.0.8",
"fetch-mock": "^11.1.4",
"fetch-mock": "^12.6.0",
"jest-mock-console": "^2.0.0",
"resize-observer-polyfill": "1.5.1",
"timezone-mock": "1.3.6"

View File

@@ -126,7 +126,7 @@ export function Button(props: ButtonProps) {
minWidth: cta ? theme.sizeUnit * 36 : undefined,
minHeight: cta ? theme.sizeUnit * 8 : undefined,
marginLeft: 0,
'& + .superset-button': {
'& + .superset-button:not(.ant-btn-compact-item)': {
marginLeft: theme.sizeUnit * 2,
},
'& > span > :first-of-type': {

View File

@@ -76,6 +76,10 @@ import {
FileOutlined,
FileTextOutlined,
FireOutlined,
FolderAddOutlined,
FolderOpenOutlined,
FolderOutlined,
FolderViewOutlined,
FormOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
@@ -94,15 +98,18 @@ import {
MenuFoldOutlined,
MenuUnfoldOutlined,
MinusCircleOutlined,
MinusSquareOutlined,
MoonOutlined,
LoadingOutlined,
LoginOutlined,
MonitorOutlined,
MoreOutlined,
OrderedListOutlined,
PartitionOutlined,
PieChartOutlined,
PicCenterOutlined,
PlusCircleOutlined,
PlusSquareOutlined,
PlusOutlined,
ProfileOutlined,
QuestionCircleOutlined,
@@ -217,6 +224,10 @@ const AntdIcons = {
FileOutlined,
FileTextOutlined,
FireOutlined,
FolderAddOutlined,
FolderOpenOutlined,
FolderOutlined,
FolderViewOutlined,
FormOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
@@ -240,13 +251,16 @@ const AntdIcons = {
MenuFoldOutlined,
MenuUnfoldOutlined,
MinusCircleOutlined,
MinusSquareOutlined,
MonitorOutlined,
MoonOutlined,
MoreOutlined,
OrderedListOutlined,
PartitionOutlined,
PieChartOutlined,
PicCenterOutlined,
PlusCircleOutlined,
PlusSquareOutlined,
PlusOutlined,
ProfileOutlined,
ReloadOutlined,

View File

@@ -42,10 +42,12 @@ const customIcons = [
'Error',
'Full',
'Layers',
'Move',
'Multiple',
'Queued',
'Redo',
'Running',
'Sigma',
'Slack',
'Square',
'SortAsc',

View File

@@ -24,12 +24,18 @@ import { ImageLoader, type BackgroundPosition } from './ImageLoader';
global.URL.createObjectURL = jest.fn(() => '/local_url');
const blob = new Blob([], { type: 'image/png' });
beforeAll(() => {
fetchMock.mockGlobal();
});
afterAll(() => {
fetchMock.hardReset();
});
fetchMock.get(
'/thumbnail',
'glob:*/thumbnail',
{ body: blob, headers: { 'Content-Type': 'image/png' } },
{
sendAsJson: false,
},
{ name: 'thumbnail' },
);
describe('ImageLoader', () => {
@@ -44,7 +50,7 @@ describe('ImageLoader', () => {
return render(<ImageLoader {...props} />);
};
afterEach(() => fetchMock.resetHistory());
afterEach(() => fetchMock.clearHistory());
it('is a valid element', async () => {
setup();
@@ -57,7 +63,7 @@ describe('ImageLoader', () => {
'src',
'/fallback',
);
expect(fetchMock.calls(/thumbnail/)).toHaveLength(1);
expect(fetchMock.callHistory.calls(/thumbnail/)).toHaveLength(1);
expect(global.URL.createObjectURL).toHaveBeenCalled();
expect(await screen.findByTestId('image-loader')).toHaveAttribute(
'src',
@@ -66,13 +72,14 @@ describe('ImageLoader', () => {
});
it('displays fallback image when response is not an image', async () => {
fetchMock.once('/thumbnail2', {});
setup({ src: '/thumbnail2' });
fetchMock.once('glob:*/thumbnail2', {}, { name: 'thumbnail2' });
setup({ src: 'glob:*/thumbnail2' });
expect(screen.getByTestId('image-loader')).toHaveAttribute(
'src',
'/fallback',
);
expect(fetchMock.calls(/thumbnail2/)).toHaveLength(1);
expect(fetchMock.callHistory.calls(/thumbnail2/)).toHaveLength(1);
expect(await screen.findByTestId('image-loader')).toHaveAttribute(
'src',
'/fallback',

View File

@@ -34,8 +34,10 @@ export enum FeatureFlag {
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
CssTemplates = 'CSS_TEMPLATES',
DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION',
DashboardVirtualizationDeferData = 'DASHBOARD_VIRTUALIZATION_DEFER_DATA',
DashboardRbac = 'DASHBOARD_RBAC',
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
DatasetFolders = 'DATASET_FOLDERS',
DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED',
/** @deprecated */
DrillToDetail = 'DRILL_TO_DETAIL',

View File

@@ -25,6 +25,7 @@ export { default as isEqualArray } from './isEqualArray';
export { default as makeSingleton } from './makeSingleton';
export { default as promiseTimeout } from './promiseTimeout';
export { default as removeDuplicates } from './removeDuplicates';
export { default as withLabel } from './withLabel';
export { lruCache } from './lruCache';
export { getSelectedText } from './getSelectedText';
export * from './featureFlags';

View File

@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { ValidatorFunction } from '../validator';
/**
* Wraps a validator function to prepend a label to its error message.
*
* @param validator - The validator function to wrap
* @param label - The label to prepend to error messages
* @returns A new validator function that includes the label in error messages
*
* @example
* validators: [
* withLabel(validateInteger, t('Row limit')),
* ]
* // Returns: "Row limit is expected to be an integer"
*/
export default function withLabel<V = unknown, S = unknown>(
validator: ValidatorFunction<V, S>,
label: string,
): ValidatorFunction<V, S> {
return (value: V, state?: S): string | false => {
const error = validator(value, state);
return error ? `${label} ${error}` : false;
};
}

View File

@@ -17,6 +17,7 @@
* under the License.
*/
export * from './types';
export { default as legacyValidateInteger } from './legacyValidateInteger';
export { default as legacyValidateNumber } from './legacyValidateNumber';
export { default as validateInteger } from './validateInteger';

View File

@@ -23,7 +23,7 @@ import { t } from '@apache-superset/core';
* formerly called integer()
* @param v
*/
export default function legacyValidateInteger(v: unknown) {
export default function legacyValidateInteger(v: unknown): string | false {
if (
v &&
(Number.isNaN(Number(v)) || parseInt(v as string, 10) !== Number(v))

View File

@@ -23,7 +23,7 @@ import { t } from '@apache-superset/core';
* formerly called numeric()
* @param v
*/
export default function numeric(v: unknown) {
export default function numeric(v: unknown): string | false {
if (v && Number.isNaN(Number(v))) {
return t('is expected to be a number');
}

View File

@@ -0,0 +1,27 @@
/*
* 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.
*/
/**
* Type definition for a validator function.
* Returns an error message string if validation fails, or false if validation passes.
*/
export type ValidatorFunction<V = unknown, S = unknown> = (
value: V,
state?: S,
) => string | false;

View File

@@ -19,7 +19,7 @@
import { t } from '@apache-superset/core';
export default function validateInteger(v: unknown) {
export default function validateInteger(v: unknown): string | false {
if (
(typeof v === 'string' &&
v.trim().length > 0 &&

View File

@@ -25,7 +25,7 @@ const VALIDE_OSM_URLS = ['https://tile.osm', 'https://tile.openstreetmap'];
* Validate a [Mapbox styles URL](https://docs.mapbox.com/help/glossary/style-url/)
* @param v
*/
export default function validateMapboxStylesUrl(v: unknown) {
export default function validateMapboxStylesUrl(v: unknown): string | false {
if (typeof v === 'string') {
const trimmed_v = v.trim();
if (

View File

@@ -18,7 +18,10 @@
*/
import { t } from '@apache-superset/core';
export default function validateMaxValue(v: unknown, max: number) {
export default function validateMaxValue(
v: unknown,
max: number,
): string | false {
if (Number(v) > +max) {
return t('Value cannot exceed %s', max);
}

View File

@@ -19,7 +19,7 @@
import { t } from '@apache-superset/core';
export default function validateNonEmpty(v: unknown) {
export default function validateNonEmpty(v: unknown): string | false {
if (
v === null ||
typeof v === 'undefined' ||

View File

@@ -19,7 +19,7 @@
import { t } from '@apache-superset/core';
export default function validateInteger(v: any) {
export default function validateNumber(v: unknown): string | false {
if (
(typeof v === 'string' &&
v.trim().length > 0 &&

View File

@@ -23,7 +23,7 @@ export default function validateServerPagination(
serverPagination: boolean,
maxValueWithoutServerPagination: number,
maxServer: number,
) {
): string | false {
if (
Number(v) > +maxValueWithoutServerPagination &&
Number(v) <= maxServer &&

View File

@@ -22,13 +22,13 @@ import { t } from '@apache-superset/core';
import { ensureIsArray } from '../utils';
export const validateTimeComparisonRangeValues = (
timeRangeValue?: any,
controlValue?: any,
) => {
timeRangeValue?: unknown,
controlValue?: unknown,
): string[] => {
const isCustomTimeRange = timeRangeValue === ComparisonTimeRangeType.Custom;
const isCustomControlEmpty = controlValue?.every(
(val: any) => ensureIsArray(val).length === 0,
);
const isCustomControlEmpty =
Array.isArray(controlValue) &&
controlValue.every((val: unknown) => ensureIsArray(val).length === 0);
return isCustomTimeRange && isCustomControlEmpty
? [t('Filters for comparison must have a value')]
: [];

View File

@@ -37,6 +37,9 @@ import { SliceIdAndOrFormData } from '../../../src/chart/clients/ChartClient';
configureTranslation();
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('ChartClient', () => {
let chartClient: ChartClient;
@@ -50,7 +53,7 @@ describe('ChartClient', () => {
chartClient = new ChartClient();
});
afterEach(() => fetchMock.restore());
afterEach(() => fetchMock.removeRoutes().clearHistory());
describe('new ChartClient(config)', () => {
it('creates a client without argument', () => {

View File

@@ -21,10 +21,13 @@ import fetchMock from 'fetch-mock';
import { SupersetClient, SupersetClientClass } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
describe('SupersetClient', () => {
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '' }));
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
afterAll(() => fetchMock.restore());
describe('SupersetClient', () => {
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
afterAll(() => fetchMock.removeRoutes().clearHistory());
afterEach(() => SupersetClient.reset());
@@ -108,9 +111,11 @@ describe('SupersetClient', () => {
mockDeleteUrl,
];
networkCalls.map((url: string) =>
expect(fetchMock.calls(url)[0][1]?.headers).toStrictEqual({
Accept: 'application/json',
'X-CSRFToken': '1234',
expect(
fetchMock.callHistory.calls(url)[0].options?.headers,
).toStrictEqual({
accept: 'application/json',
'x-csrftoken': '1234',
}),
);
@@ -137,6 +142,6 @@ describe('SupersetClient', () => {
authenticatedSpy.mockRestore();
csrfSpy.mockRestore();
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
});
});

View File

@@ -20,14 +20,15 @@ import fetchMock from 'fetch-mock';
import { SupersetClientClass, ClientConfig, CallApi } from '@superset-ui/core';
import { LOGIN_GLOB } from './fixtures/constants';
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('SupersetClientClass', () => {
beforeEach(() => {
fetchMock.reset();
fetchMock.get(LOGIN_GLOB, { result: '' });
fetchMock.clearHistory().removeRoutes();
fetchMock.get(LOGIN_GLOB, { result: '' }, { name: LOGIN_GLOB });
});
afterAll(() => fetchMock.restore());
describe('new SupersetClientClass()', () => {
it('fallback protocol to https when setting only host', () => {
const client = new SupersetClientClass({ host: 'TEST-HOST' });
@@ -89,21 +90,22 @@ describe('SupersetClientClass', () => {
});
describe('.init()', () => {
beforeEach(() =>
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true }),
);
afterEach(() => fetchMock.reset());
beforeEach(() => {
fetchMock.removeRoute(LOGIN_GLOB);
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { name: LOGIN_GLOB });
});
afterEach(() => fetchMock.clearHistory().removeRoutes());
it('calls api/v1/security/csrf_token/ when init() is called if no CSRF token is passed', async () => {
expect.assertions(1);
// expect.assertions(1);
await new SupersetClientClass().init();
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(1);
});
it('does NOT call api/v1/security/csrf_token/ when init() is called if a CSRF token is passed', async () => {
expect.assertions(1);
await new SupersetClientClass({ csrfToken: 'abc' }).init();
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0);
});
it('calls api/v1/security/csrf_token/ when init(force=true) is called even if a CSRF token is passed', async () => {
@@ -112,20 +114,19 @@ describe('SupersetClientClass', () => {
const client = new SupersetClientClass({ csrfToken: initialToken });
await client.init();
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(0);
expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0);
expect(client.csrfToken).toBe(initialToken);
await client.init(true);
expect(fetchMock.calls(LOGIN_GLOB)).toHaveLength(1);
expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(1);
expect(client.csrfToken).not.toBe(initialToken);
});
it('throws if api/v1/security/csrf_token/ returns an error', async () => {
expect.assertions(1);
const rejectError = { status: 403 };
fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectError), {
overwriteRoutes: true,
});
fetchMock.removeRoute(LOGIN_GLOB);
fetchMock.get(LOGIN_GLOB, { throws: rejectError }, { name: LOGIN_GLOB });
let error;
try {
@@ -141,7 +142,7 @@ describe('SupersetClientClass', () => {
it('throws if api/v1/security/csrf_token/ does not return a token', async () => {
expect.assertions(1);
fetchMock.get(LOGIN_GLOB, {}, { overwriteRoutes: true });
fetchMock.modifyRoute(LOGIN_GLOB, { response: {} });
let error;
try {
@@ -157,9 +158,8 @@ describe('SupersetClientClass', () => {
it('does not set csrfToken if response is not json', async () => {
expect.assertions(1);
fetchMock.get(LOGIN_GLOB, '123', {
overwriteRoutes: true,
});
fetchMock.removeRoute(LOGIN_GLOB);
fetchMock.get(LOGIN_GLOB, { response: '123' }, { name: LOGIN_GLOB });
let error;
try {
@@ -175,7 +175,7 @@ describe('SupersetClientClass', () => {
});
describe('.isAuthenticated()', () => {
afterEach(() => fetchMock.reset());
afterEach(() => fetchMock.clearHistory().removeRoutes());
it('returns true if there is a token and false if not', async () => {
expect.assertions(2);
@@ -227,9 +227,8 @@ describe('SupersetClientClass', () => {
expect.assertions(4);
const rejectValue = { status: 403 };
fetchMock.get(LOGIN_GLOB, () => Promise.reject(rejectValue), {
overwriteRoutes: true,
});
fetchMock.removeRoutes();
fetchMock.get(LOGIN_GLOB, { throws: rejectValue }, { name: LOGIN_GLOB });
const client = new SupersetClientClass({});
let error;
@@ -253,18 +252,19 @@ describe('SupersetClientClass', () => {
}
// reset
fetchMock.removeRoutes();
fetchMock.get(
LOGIN_GLOB,
{ result: 1234 },
{
overwriteRoutes: true,
name: LOGIN_GLOB,
},
);
});
});
describe('requests', () => {
afterEach(() => fetchMock.restore());
afterEach(() => fetchMock.clearHistory().removeRoutes());
const protocol = 'https:';
const host = 'host';
@@ -306,11 +306,11 @@ describe('SupersetClientClass', () => {
await client.delete({ url: mockDeleteUrl });
await client.request({ url: mockRequestUrl, method: 'DELETE' });
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
expect(fetchMock.calls(mockDeleteUrl)).toHaveLength(1);
expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
expect(fetchMock.calls(mockRequestUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockDeleteUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPutUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockRequestUrl)).toHaveLength(1);
expect(authSpy).toHaveBeenCalledTimes(5);
authSpy.mockRestore();
@@ -331,7 +331,8 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl });
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi;
const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0]
.options as CallApi;
expect(fetchRequest.mode).toBe(clientConfig.mode);
expect(fetchRequest.credentials).toBe(clientConfig.credentials);
expect(fetchRequest.headers).toEqual(
@@ -354,10 +355,11 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl });
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi;
const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0]
.options as CallApi;
expect(fetchRequest.headers).toEqual(
expect.objectContaining({
guestTokenHeader: 'abc123',
guesttokenheader: 'abc123',
}),
);
});
@@ -370,10 +372,10 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl });
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
await client.get({ endpoint: mockGetEndpoint });
expect(fetchMock.calls(mockGetUrl)).toHaveLength(2);
expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(2);
});
it('supports parsing a response as text', async () => {
@@ -384,7 +386,7 @@ describe('SupersetClientClass', () => {
url: mockTextUrl,
parseMethod: 'text',
});
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1);
expect(text).toBe(mockTextJsonResponse);
});
@@ -409,7 +411,8 @@ describe('SupersetClientClass', () => {
await client.init();
await client.get({ url: mockGetUrl, ...overrideConfig });
const fetchRequest = fetchMock.calls(mockGetUrl)[0][1] as CallApi;
const fetchRequest = fetchMock.callHistory.calls(mockGetUrl)[0]
.options as CallApi;
expect(fetchRequest.mode).toBe(overrideConfig.mode);
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
expect(fetchRequest.headers).toEqual(
@@ -428,10 +431,10 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl });
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
await client.post({ endpoint: mockPostEndpoint });
expect(fetchMock.calls(mockPostUrl)).toHaveLength(2);
expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(2);
});
it('allows overriding host, headers, mode, and credentials per-request', async () => {
@@ -454,7 +457,8 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl, ...overrideConfig });
const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi;
const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0]
.options as CallApi;
expect(fetchRequest.mode).toBe(overrideConfig.mode);
expect(fetchRequest.credentials).toBe(overrideConfig.credentials);
@@ -473,7 +477,7 @@ describe('SupersetClientClass', () => {
url: mockTextUrl,
parseMethod: 'text',
});
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1);
expect(text).toBe(mockTextJsonResponse);
});
@@ -485,10 +489,11 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl, postPayload });
const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi;
const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0]
.options as CallApi;
const formData = fetchRequest.body as FormData;
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
Object.entries(postPayload).forEach(([key, value]) => {
expect(formData.get(key)).toBe(JSON.stringify(value));
});
@@ -502,10 +507,11 @@ describe('SupersetClientClass', () => {
await client.init();
await client.post({ url: mockPostUrl, postPayload, stringify: false });
const fetchRequest = fetchMock.calls(mockPostUrl)[0][1] as CallApi;
const fetchRequest = fetchMock.callHistory.calls(mockPostUrl)[0]
.options as CallApi;
const formData = fetchRequest.body as FormData;
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
Object.entries(postPayload).forEach(([key, value]) => {
expect(formData.get(key)).toBe(String(value));
});
@@ -528,6 +534,7 @@ describe('SupersetClientClass', () => {
// @ts-ignore
window.location = {
pathname: mockRequestPath,
// @ts-ignore
search: mockRequestSearch,
href: mockHref,
};
@@ -535,9 +542,7 @@ describe('SupersetClientClass', () => {
.spyOn(SupersetClientClass.prototype, 'ensureAuth')
.mockImplementation();
const rejectValue = { status: 401 };
fetchMock.get(mockRequestUrl, () => Promise.reject(rejectValue), {
overwriteRoutes: true,
});
fetchMock.get(mockRequestUrl, () => Promise.reject(rejectValue));
});
afterEach(() => {
@@ -563,10 +568,11 @@ describe('SupersetClientClass', () => {
it('should not redirect again if already on login page', async () => {
const client = new SupersetClientClass({});
// @ts-expect-error
// @ts-ignore
window.location = {
href: '/login?next=something',
pathname: '/login',
// @ts-ignore
search: '?next=something',
};
@@ -636,7 +642,8 @@ describe('SupersetClientClass', () => {
let createElement: any;
beforeEach(async () => {
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { overwriteRoutes: true });
fetchMock.removeRoute(LOGIN_GLOB);
fetchMock.get(LOGIN_GLOB, { result: 1234 }, { name: LOGIN_GLOB });
client = new SupersetClientClass({ protocol, host });
authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth');

View File

@@ -29,14 +29,17 @@ const corruptObject = new BadObject();
/* @ts-expect-error */
BadObject.prototype.toString = undefined;
const mockGetUrl = '/mock/get/url';
const mockPostUrl = '/mock/post/url';
const mockPutUrl = '/mock/put/url';
const mockPatchUrl = '/mock/patch/url';
const mockCacheUrl = '/mock/cache/url';
const mockNotFound = '/mock/notfound';
const mockErrorUrl = '/mock/error/url';
const mock503 = '/mock/503';
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
const mockGetUrl = 'glob:*/mock/get/url';
const mockPostUrl = 'glob:*/mock/post/url';
const mockPutUrl = 'glob:*/mock/put/url';
const mockPatchUrl = 'glob:*/mock/patch/url';
const mockCacheUrl = 'glob:*/mock/cache/url';
const mockNotFound = 'glob:*/mock/notfound';
const mockErrorUrl = 'glob:*/mock/error/url';
const mock503 = 'glob:*/mock/503';
const mockGetPayload = { get: 'payload' };
const mockPostPayload = { post: 'payload' };
@@ -50,20 +53,23 @@ const mockCachePayload = {
const mockErrorPayload = { status: 500, statusText: 'Internal error' };
describe('callApi()', () => {
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
beforeAll(() => {
fetchMock.mockGlobal();
fetchMock.get(LOGIN_GLOB, { result: '1234' });
});
beforeEach(() => {
fetchMock.get(mockGetUrl, mockGetPayload);
fetchMock.post(mockPostUrl, mockPostPayload);
fetchMock.put(mockPutUrl, mockPutPayload);
fetchMock.patch(mockPatchUrl, mockPatchPayload);
fetchMock.get(mockCacheUrl, mockCachePayload);
fetchMock.get(mockCacheUrl, mockCachePayload, { name: mockCacheUrl });
fetchMock.get(mockNotFound, { status: 404 });
fetchMock.get(mock503, { status: 503 });
fetchMock.get(mockErrorUrl, () => Promise.reject(mockErrorPayload));
});
afterEach(() => fetchMock.reset());
afterEach(() => fetchMock.clearHistory().removeRoutes());
describe('request config', () => {
it('calls the right url with the specified method', async () => {
@@ -74,10 +80,10 @@ describe('callApi()', () => {
callApi({ url: mockPutUrl, method: 'PUT' }),
callApi({ url: mockPatchUrl, method: 'PATCH' }),
]);
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
expect(fetchMock.calls(mockPostUrl)).toHaveLength(1);
expect(fetchMock.calls(mockPutUrl)).toHaveLength(1);
expect(fetchMock.calls(mockPatchUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPostUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPutUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockPatchUrl)).toHaveLength(1);
});
it('passes along mode, cache, credentials, headers, body, signal, and redirect parameters in the request', async () => {
@@ -92,12 +98,11 @@ describe('callApi()', () => {
},
redirect: 'follow',
signal: undefined,
body: 'BODY',
};
await callApi(mockRequest);
const calls = fetchMock.calls(mockGetUrl);
const fetchParams = calls[0][1] as RequestInit;
const calls = fetchMock.callHistory.calls(mockGetUrl);
const fetchParams = calls[0].options as RequestInit;
expect(calls).toHaveLength(1);
expect(fetchParams.mode).toBe(mockRequest.mode);
expect(fetchParams.cache).toBe(mockRequest.cache);
@@ -119,10 +124,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', anotherKey: 1237 };
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
const calls = fetchMock.calls(mockPostUrl);
const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(1);
const fetchParams = calls[0][1] as RequestInit;
const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -136,10 +141,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', noValue: undefined };
await callApi({ url: mockPostUrl, method: 'POST', postPayload });
const calls = fetchMock.calls(mockPostUrl);
const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(1);
const fetchParams = calls[0][1] as RequestInit;
const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
expect(body.get('noValue')).toBeNull();
@@ -167,13 +172,13 @@ describe('callApi()', () => {
}),
callApi({ url: mockPostUrl, method: 'POST', jsonPayload: postPayload }),
]);
const calls = fetchMock.calls(mockPostUrl);
const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(3);
const stringified = (calls[0][1] as RequestInit).body as FormData;
const unstringified = (calls[1][1] as RequestInit).body as FormData;
const stringified = (calls[0].options as RequestInit).body as FormData;
const unstringified = (calls[1].options as RequestInit).body as FormData;
const jsonRequestBody = JSON.parse(
(calls[2][1] as RequestInit).body as string,
(calls[2].options as RequestInit).body as string,
) as JsonObject;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -211,9 +216,9 @@ describe('callApi()', () => {
stringify: false,
});
const calls = fetchMock.calls(mockPostUrl);
const calls = fetchMock.callHistory.calls(mockPostUrl);
expect(calls).toHaveLength(1);
const unstringified = (calls[0][1] as RequestInit).body as FormData;
const unstringified = (calls[0].options as RequestInit).body as FormData;
const hasCorruptKey = unstringified.has('corrupt');
expect(hasCorruptKey).toBeFalsy();
// When a corrupt attribute is encountered, a console.error call is made with info about the corrupt attribute
@@ -228,10 +233,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', anotherKey: 1237 };
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
const calls = fetchMock.calls(mockPutUrl);
const calls = fetchMock.callHistory.calls(mockPutUrl);
expect(calls).toHaveLength(1);
const fetchParams = calls[0][1] as RequestInit;
const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -245,10 +250,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', noValue: undefined };
await callApi({ url: mockPutUrl, method: 'PUT', postPayload });
const calls = fetchMock.calls(mockPutUrl);
const calls = fetchMock.callHistory.calls(mockPutUrl);
expect(calls).toHaveLength(1);
const fetchParams = calls[0][1] as RequestInit;
const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
expect(body.get('noValue')).toBeNull();
@@ -275,11 +280,11 @@ describe('callApi()', () => {
stringify: false,
}),
]);
const calls = fetchMock.calls(mockPutUrl);
const calls = fetchMock.callHistory.calls(mockPutUrl);
expect(calls).toHaveLength(2);
const stringified = (calls[0][1] as RequestInit).body as FormData;
const unstringified = (calls[1][1] as RequestInit).body as FormData;
const stringified = (calls[0].options as RequestInit).body as FormData;
const unstringified = (calls[1].options as RequestInit).body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
expect(stringified.get(key)).toBe(JSON.stringify(value));
@@ -294,10 +299,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', anotherKey: 1237 };
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
const calls = fetchMock.calls(mockPatchUrl);
const calls = fetchMock.callHistory.calls(mockPatchUrl);
expect(calls).toHaveLength(1);
const fetchParams = calls[0][1] as RequestInit;
const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
@@ -311,10 +316,10 @@ describe('callApi()', () => {
const postPayload = { key: 'value', noValue: undefined };
await callApi({ url: mockPatchUrl, method: 'PATCH', postPayload });
const calls = fetchMock.calls(mockPatchUrl);
const calls = fetchMock.callHistory.calls(mockPatchUrl);
expect(calls).toHaveLength(1);
const fetchParams = calls[0][1] as RequestInit;
const fetchParams = calls[0].options as RequestInit;
const body = fetchParams.body as FormData;
expect(body.get('key')).toBe(JSON.stringify(postPayload.key));
expect(body.get('noValue')).toBeNull();
@@ -341,11 +346,11 @@ describe('callApi()', () => {
stringify: false,
}),
]);
const calls = fetchMock.calls(mockPatchUrl);
const calls = fetchMock.callHistory.calls(mockPatchUrl);
expect(calls).toHaveLength(2);
const stringified = (calls[0][1] as RequestInit).body as FormData;
const unstringified = (calls[1][1] as RequestInit).body as FormData;
const stringified = (calls[0].options as RequestInit).body as FormData;
const unstringified = (calls[1].options as RequestInit).body as FormData;
Object.entries(postPayload).forEach(([key, value]) => {
expect(stringified.get(key)).toBe(JSON.stringify(value));
@@ -373,7 +378,7 @@ describe('callApi()', () => {
it('caches requests with ETags', async () => {
expect.assertions(2);
await callApi({ url: mockCacheUrl, method: 'GET' });
const calls = fetchMock.calls(mockCacheUrl);
const calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
const supersetCache = await caches.open(constants.CACHE_KEY);
const cachedResponse = await supersetCache.match(mockCacheUrl);
@@ -385,7 +390,7 @@ describe('callApi()', () => {
window.location.protocol = 'http:';
await callApi({ url: mockCacheUrl, method: 'GET' });
const calls = fetchMock.calls(mockCacheUrl);
const calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
const supersetCache = await caches.open(constants.CACHE_KEY);
@@ -399,7 +404,7 @@ describe('callApi()', () => {
Object.defineProperty(constants, 'CACHE_AVAILABLE', { value: false });
const firstResponse = await callApi({ url: mockCacheUrl, method: 'GET' });
let calls = fetchMock.calls(mockCacheUrl);
let calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
const firstBody = await firstResponse.text();
expect(firstBody).toEqual('BODY');
@@ -408,8 +413,8 @@ describe('callApi()', () => {
url: mockCacheUrl,
method: 'GET',
});
calls = fetchMock.calls(mockCacheUrl);
const fetchParams = calls[1][1] as RequestInit;
calls = fetchMock.callHistory.calls(mockCacheUrl);
const fetchParams = calls[1].options as RequestInit;
expect(calls).toHaveLength(2);
// second call should not have If-None-Match header
expect(fetchParams.headers).toBeUndefined();
@@ -424,14 +429,14 @@ describe('callApi()', () => {
expect.assertions(3);
// first call sets the cache
await callApi({ url: mockCacheUrl, method: 'GET' });
let calls = fetchMock.calls(mockCacheUrl);
let calls = fetchMock.callHistory.calls(mockCacheUrl);
expect(calls).toHaveLength(1);
// second call sends the Etag in the If-None-Match header
await callApi({ url: mockCacheUrl, method: 'GET' });
calls = fetchMock.calls(mockCacheUrl);
const fetchParams = calls[1][1] as RequestInit;
const headers = { 'If-None-Match': 'etag' };
calls = fetchMock.callHistory.calls(mockCacheUrl);
const fetchParams = calls[1].options as RequestInit;
const headers = { 'if-none-match': 'etag' };
expect(calls).toHaveLength(2);
expect(fetchParams.headers).toEqual(
expect.objectContaining(headers) as typeof fetchParams.headers,
@@ -442,16 +447,16 @@ describe('callApi()', () => {
expect.assertions(3);
// first call sets the cache
await callApi({ url: mockCacheUrl, method: 'GET' });
expect(fetchMock.calls(mockCacheUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockCacheUrl)).toHaveLength(1);
// second call reuses the cached payload on a 304
const mockCachedPayload = { status: 304 };
fetchMock.get(mockCacheUrl, mockCachedPayload, { overwriteRoutes: true });
fetchMock.modifyRoute(mockCacheUrl, { response: mockCachedPayload });
const secondResponse = await callApi({
url: mockCacheUrl,
method: 'GET',
});
expect(fetchMock.calls(mockCacheUrl)).toHaveLength(2);
expect(fetchMock.callHistory.calls(mockCacheUrl)).toHaveLength(2);
const secondBody = await secondResponse.text();
expect(secondBody).toEqual('BODY');
});
@@ -461,7 +466,7 @@ describe('callApi()', () => {
// this should never happen, since a 304 is only returned if we have
// the cached response and sent the If-None-Match header
const mockUncachedUrl = '/mock/uncached/url';
const mockUncachedUrl = 'glob:*/mock/uncached/url';
const mockCachedPayload = { status: 304 };
let error;
fetchMock.get(mockUncachedUrl, mockCachedPayload);
@@ -471,7 +476,7 @@ describe('callApi()', () => {
} catch (err) {
error = err;
} finally {
const calls = fetchMock.calls(mockUncachedUrl);
const calls = fetchMock.callHistory.calls(mockUncachedUrl);
expect(calls).toHaveLength(1);
expect((error as { message: string }).message).toEqual(
'Received 304 but no content is cached!',
@@ -483,7 +488,7 @@ describe('callApi()', () => {
expect.assertions(3);
const url = mockGetUrl;
const response = await callApi({ url, method: 'GET' });
const calls = fetchMock.calls(url);
const calls = fetchMock.callHistory.calls(url);
expect(calls).toHaveLength(1);
expect(response.status).toEqual(200);
const body = await response.json();
@@ -494,7 +499,7 @@ describe('callApi()', () => {
expect.assertions(2);
const url = mockNotFound;
const response = await callApi({ url, method: 'GET' });
const calls = fetchMock.calls(url);
const calls = fetchMock.callHistory.calls(url);
expect(calls).toHaveLength(1);
expect(response.status).toEqual(404);
});
@@ -513,7 +518,7 @@ describe('callApi()', () => {
error = err;
} finally {
const err = error as { status: number; statusText: string };
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(4);
expect(fetchMock.callHistory.calls(mockErrorUrl)).toHaveLength(4);
expect(err.status).toBe(mockErrorPayload.status);
expect(err.statusText).toBe(mockErrorPayload.statusText);
}
@@ -531,7 +536,7 @@ describe('callApi()', () => {
} catch (err) {
error = err as { status: number; statusText: string };
} finally {
expect(fetchMock.calls(mockErrorUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockErrorUrl)).toHaveLength(1);
expect(error?.status).toBe(mockErrorPayload.status);
expect(error?.statusText).toBe(mockErrorPayload.statusText);
}
@@ -545,7 +550,7 @@ describe('callApi()', () => {
url,
method: 'GET',
});
const calls = fetchMock.calls(url);
const calls = fetchMock.callHistory.calls(url);
expect(calls).toHaveLength(4);
expect(response.status).toEqual(503);
});
@@ -581,7 +586,9 @@ describe('callApi()', () => {
const result = await response.json();
expect(response.status).toEqual(200);
expect(result).toEqual({ yes: 'ok' });
expect(fetchMock.lastUrl()).toEqual(`http://localhost/get-search?abc=1`);
expect(fetchMock.callHistory.lastCall()?.url).toEqual(
`http://localhost/get-search?abc=1`,
);
});
it('should accept URLSearchParams', async () => {
@@ -596,8 +603,10 @@ describe('callApi()', () => {
method: 'POST',
jsonPayload: { request: 'ok' },
});
expect(fetchMock.lastUrl()).toEqual(`http://localhost/post-search?abc=1`);
expect(fetchMock.lastOptions()).toEqual(
expect(fetchMock.callHistory.lastCall()?.url).toEqual(
`http://localhost/post-search?abc=1`,
);
expect(fetchMock.callHistory.lastCall()?.options).toEqual(
expect.objectContaining({
body: JSON.stringify({ request: 'ok' }),
}),
@@ -634,7 +643,7 @@ describe('callApi()', () => {
method: 'POST',
postPayload: payload,
});
expect(fetchMock.lastOptions()?.body).toBe(payload);
expect(fetchMock.callHistory.lastCall()?.options.body).toBe(payload);
});
it('should ignore "null" postPayload string', async () => {
@@ -646,6 +655,6 @@ describe('callApi()', () => {
method: 'POST',
postPayload: 'null',
});
expect(fetchMock.lastOptions()?.body).toBeUndefined();
expect(fetchMock.callHistory.lastCall()?.options.body).toBeUndefined();
});
});

View File

@@ -30,15 +30,16 @@ import { LOGIN_GLOB } from '../fixtures/constants';
const mockGetUrl = '/mock/get/url';
const mockGetPayload = { get: 'payload' };
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('callApiAndParseWithTimeout()', () => {
beforeAll(() => fetchMock.get(LOGIN_GLOB, { result: '1234' }));
beforeEach(() => fetchMock.get(mockGetUrl, mockGetPayload));
afterAll(() => fetchMock.restore());
afterEach(() => {
fetchMock.reset();
fetchMock.removeRoutes().clearHistory();
jest.useRealTimers();
});
@@ -108,7 +109,7 @@ describe('callApiAndParseWithTimeout()', () => {
} catch (err) {
error = err;
} finally {
expect(fetchMock.calls(mockTimeoutUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockTimeoutUrl)).toHaveLength(1);
expect(error).toEqual({
error: 'Request timed out',
statusText: 'timeout',

View File

@@ -22,12 +22,15 @@ import parseResponse from '../../../src/connection/callApi/parseResponse';
import { LOGIN_GLOB } from '../fixtures/constants';
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('parseResponse()', () => {
beforeAll(() => {
fetchMock.get(LOGIN_GLOB, { result: '1234' });
});
afterAll(() => fetchMock.restore());
afterAll(() => fetchMock.removeRoutes().clearHistory());
const mockGetUrl = '/mock/get/url';
const mockPostUrl = '/mock/post/url';
@@ -45,7 +48,7 @@ describe('parseResponse()', () => {
fetchMock.get(mockNoParseUrl, new Response('test response'));
});
afterEach(() => fetchMock.reset());
afterEach(() => fetchMock.removeRoutes().clearHistory());
it('returns a Promise', () => {
const apiPromise = callApi({ url: mockGetUrl, method: 'GET' });
@@ -58,7 +61,7 @@ describe('parseResponse()', () => {
const args = await parseResponse(
callApi({ url: mockGetUrl, method: 'GET' }),
);
expect(fetchMock.calls(mockGetUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockGetUrl)).toHaveLength(1);
const keys = Object.keys(args);
expect(keys).toContain('response');
expect(keys).toContain('json');
@@ -81,7 +84,7 @@ describe('parseResponse()', () => {
} catch (err) {
error = err as Error;
} finally {
expect(fetchMock.calls(mockTextUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockTextUrl)).toHaveLength(1);
expect(error?.stack).toBeDefined();
expect(error?.message).toContain('Unexpected token');
}
@@ -99,7 +102,7 @@ describe('parseResponse()', () => {
callApi({ url: mockTextParseUrl, method: 'GET' }),
'text',
);
expect(fetchMock.calls(mockTextParseUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockTextParseUrl)).toHaveLength(1);
const keys = Object.keys(args);
expect(keys).toContain('response');
expect(keys).toContain('text');
@@ -134,7 +137,7 @@ describe('parseResponse()', () => {
callApi({ url: mockNoParseUrl, method: 'GET' }),
'raw',
);
expect(fetchMock.calls(mockNoParseUrl)).toHaveLength(2);
expect(fetchMock.callHistory.calls(mockNoParseUrl)).toHaveLength(2);
expect(responseNull.bodyUsed).toBe(false);
expect(responseRaw.bodyUsed).toBe(false);
});
@@ -193,7 +196,7 @@ describe('parseResponse()', () => {
} catch (err) {
error = err as { ok: boolean; status: number };
} finally {
expect(fetchMock.calls(mockNotOkayUrl)).toHaveLength(1);
expect(fetchMock.callHistory.calls(mockNotOkayUrl)).toHaveLength(1);
expect(error?.ok).toBe(false);
expect(error?.status).toBe(404);
}

View File

@@ -21,10 +21,13 @@ import { getDatasourceMetadata } from '../../../../src/query/api/legacy';
import setupClientForTest from '../setupClientForTest';
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('getFormData()', () => {
beforeAll(() => setupClientForTest());
afterEach(() => fetchMock.restore());
afterEach(() => fetchMock.clearHistory().removeRoutes());
it('returns datasource metadata for given datasource key', () => {
const mockData = {

View File

@@ -22,10 +22,13 @@ import { getFormData } from '../../../../src/query/api/legacy';
import setupClientForTest from '../setupClientForTest';
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('getFormData()', () => {
beforeAll(() => setupClientForTest());
afterEach(() => fetchMock.restore());
afterEach(() => fetchMock.clearHistory().removeRoutes());
const mockData = {
datasource: '1__table',

View File

@@ -20,9 +20,13 @@ import fetchMock from 'fetch-mock';
import { buildQueryContext, ApiV1, VizType } from '@superset-ui/core';
import setupClientForTest from '../setupClientForTest';
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('API v1 > getChartData()', () => {
beforeAll(() => setupClientForTest());
afterEach(() => fetchMock.restore());
afterEach(() => fetchMock.clearHistory().removeRoutes());
it('returns a promise of ChartDataResponse', async () => {
const response = {

View File

@@ -21,9 +21,13 @@ import { JsonValue, SupersetClientClass } from '@superset-ui/core';
import { makeApi, SupersetApiError } from '../../../../src/query';
import setupClientForTest from '../setupClientForTest';
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
describe('makeApi()', () => {
beforeAll(() => setupClientForTest());
afterEach(() => fetchMock.restore());
afterEach(() => fetchMock.clearHistory().removeRoutes());
it('should expose method and endpoint', () => {
const api = makeApi({
@@ -95,7 +99,7 @@ describe('makeApi()', () => {
const expected = new FormData();
expected.append('request', JSON.stringify('test'));
const received = fetchMock.lastOptions()?.body as FormData;
const received = fetchMock.callHistory.lastCall()?.options.body as FormData;
expect(received).toBeInstanceOf(FormData);
expect(received.get('request')).toEqual(expected.get('request'));
@@ -109,7 +113,7 @@ describe('makeApi()', () => {
});
fetchMock.get('glob:*/test-get-search*', { search: 'get' });
await api({ p1: 1, p2: 2, p3: [1, 2] });
expect(fetchMock.lastUrl()).toContain(
expect(fetchMock.callHistory.lastCall()?.url).toContain(
'/test-get-search?p1=1&p2=2&p3=1%2C2',
);
});
@@ -123,7 +127,7 @@ describe('makeApi()', () => {
});
fetchMock.get('glob:*/test-post-search*', { rison: 'get' });
await api({ p1: 1, p3: [1, 2] });
expect(fetchMock.lastUrl()).toContain(
expect(fetchMock.callHistory.lastCall()?.url).toContain(
'/test-post-search?q=(p1:1,p3:!(1,2))',
);
});
@@ -137,7 +141,9 @@ describe('makeApi()', () => {
});
fetchMock.post('glob:*/test-post-search*', { search: 'post' });
await api({ p1: 1, p3: [1, 2] });
expect(fetchMock.lastUrl()).toContain('/test-post-search?p1=1&p3=1%2C2');
expect(fetchMock.callHistory.lastCall()?.url).toContain(
'/test-post-search?p1=1&p3=1%2C2',
);
});
it('should throw when requestType is invalid', () => {
@@ -215,6 +221,8 @@ describe('makeApi()', () => {
fetchMock.delete('glob:*/test-raw-response?*', 'ok');
const result = await api({ field1: 11 }, {});
expect(result).toEqual(200);
expect(fetchMock.lastUrl()).toContain('/test-raw-response?field1=11');
expect(fetchMock.callHistory.lastCall()?.url).toContain(
'/test-raw-response?field1=11',
);
});
});

View File

@@ -25,7 +25,10 @@ import {
formatTimeRangeComparison,
} from '../../src/time-comparison/fetchTimeRange';
afterEach(() => fetchMock.restore());
beforeAll(() => fetchMock.mockGlobal());
afterAll(() => fetchMock.hardReset());
afterEach(() => fetchMock.clearHistory().removeRoutes());
test('generates proper time range string', () => {
expect(
@@ -84,34 +87,41 @@ test('returns a formatted time range from empty response', async () => {
});
test('returns a formatted error message from response', async () => {
fetchMock.get('glob:*/api/v1/time_range/?q=%27Last+day%27', {
throws: new Response(JSON.stringify({ message: 'Network error' })),
});
const getTimeRangeUrl = 'glob:*/api/v1/time_range/?q=%27Last+day%27';
fetchMock.get(
getTimeRangeUrl,
{
throws: new Response(JSON.stringify({ message: 'Network error' })),
},
{ name: getTimeRangeUrl },
);
let timeRange = await fetchTimeRange('Last day');
expect(timeRange).toEqual({
error: 'Network error',
});
fetchMock.removeRoute(getTimeRangeUrl);
fetchMock.get(
'glob:*/api/v1/time_range/?q=%27Last+day%27',
getTimeRangeUrl,
{
throws: new Error('Internal Server Error'),
},
{ overwriteRoutes: true },
{ name: getTimeRangeUrl },
);
timeRange = await fetchTimeRange('Last day');
expect(timeRange).toEqual({
error: 'Internal Server Error',
});
fetchMock.removeRoute(getTimeRangeUrl);
fetchMock.get(
'glob:*/api/v1/time_range/?q=%27Last+day%27',
getTimeRangeUrl,
{
throws: new Response(JSON.stringify({ statusText: 'Network error' }), {
statusText: 'Network error',
}),
},
{ overwriteRoutes: true },
{ name: getTimeRangeUrl },
);
timeRange = await fetchTimeRange('Last day');
expect(timeRange).toEqual({

View File

@@ -20,13 +20,13 @@
import { validateMaxValue } from '@superset-ui/core';
import './setup';
test('validateInteger returns the warning message if invalid', () => {
test('validateMaxValue returns the warning message if invalid', () => {
expect(validateMaxValue(10.1, 10)).toBeTruthy();
expect(validateMaxValue(1, 0)).toBeTruthy();
expect(validateMaxValue('2', 1)).toBeTruthy();
});
test('validateInteger returns false if the input is valid', () => {
test('validateMaxValue returns false if the input is valid', () => {
expect(validateMaxValue(0, 1)).toBeFalsy();
expect(validateMaxValue(10, 10)).toBeFalsy();
expect(validateMaxValue(undefined, 1)).toBeFalsy();

View File

@@ -53,7 +53,7 @@
},
"devDependencies": {
"@babel/core": "^7.28.6",
"@babel/preset-env": "^7.28.6",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@storybook/react-webpack5": "8.6.14",

View File

@@ -0,0 +1,217 @@
/**
* 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 { Locator, Page } from '@playwright/test';
import { TIMEOUT } from '../../utils/constants';
/**
* Menu component for Ant Design dropdown menus.
* Uses hover as primary approach (most natural user interaction).
* Falls back to keyboard navigation, then dispatchEvent if hover fails.
*
* This component handles menu content only - not the trigger that opens the menu.
* The calling page object should open the menu first, then use this component.
*
* @example
* // In a page object
* async selectDownloadOption(optionText: string): Promise<void> {
* await this.openHeaderActionsMenu();
* const menu = new Menu(this.page, '[data-test="header-actions-menu"]');
* await menu.selectSubmenuItem('Download', optionText);
* }
*/
export class Menu {
private readonly page: Page;
private readonly locator: Locator;
private static readonly SELECTORS = {
SUBMENU: '.ant-dropdown-menu-submenu',
SUBMENU_POPUP: '.ant-dropdown-menu-submenu-popup',
SUBMENU_TITLE: '.ant-dropdown-menu-submenu-title',
} as const;
/**
* Ant Design animation delay - allows slide-in animation to complete.
* Without this, elements may be "not stable" and clicks can fail.
*/
private static readonly ANIMATION_DELAY = 150;
constructor(page: Page, selector: string);
constructor(page: Page, locator: Locator);
constructor(page: Page, selectorOrLocator: string | Locator) {
this.page = page;
if (typeof selectorOrLocator === 'string') {
this.locator = page.locator(selectorOrLocator);
} else {
this.locator = selectorOrLocator;
}
}
/**
* Opens a submenu and selects an item within it.
* Uses hover as primary approach, falls back to keyboard then dispatchEvent.
*
* @param submenuText - The text of the submenu to open (e.g., "Download")
* @param itemText - The text of the item to select (e.g., "Export YAML")
* @param options - Optional timeout settings
*/
async selectSubmenuItem(
submenuText: string,
itemText: string,
options?: { timeout?: number },
): Promise<void> {
const timeout = options?.timeout ?? TIMEOUT.FORM_LOAD;
// Try hover first (most natural user interaction)
let popup = await this.openSubmenuWithHover(submenuText, itemText, timeout);
// Fallback to keyboard navigation
if (!popup) {
popup = await this.openSubmenuWithKeyboard(
submenuText,
itemText,
timeout,
);
}
// Last resort: dispatchEvent
if (!popup) {
popup = await this.openSubmenuWithDispatchEvent(
submenuText,
itemText,
timeout,
);
}
if (!popup) {
throw new Error(
`Failed to open submenu "${submenuText}". Tried hover, keyboard, and dispatchEvent.`,
);
}
// Use dispatchEvent instead of click to bypass viewport and pointer interception
// issues. Ant Design renders submenu popups in a portal that can be positioned
// outside the viewport or behind chart content (e.g., large tables with z-index).
await popup.getByText(itemText, { exact: true }).dispatchEvent('click');
}
/**
* Opens a submenu using native Playwright hover.
* Returns the popup locator if successful, null otherwise.
*/
private async openSubmenuWithHover(
submenuText: string,
itemText: string,
timeout: number,
): Promise<Locator | null> {
try {
const submenuTitle = this.getSubmenuTitle(submenuText);
await submenuTitle.hover();
// Find the popup that contains the expected item (scopes to correct popup)
const popup = this.page
.locator(Menu.SELECTORS.SUBMENU_POPUP)
.filter({ hasText: itemText });
await popup.waitFor({ state: 'visible', timeout });
// Allow Ant Design's slide-in animation to complete before clicking.
// Without this, the element may be "not stable" and clicks can fail.
await this.page.waitForTimeout(Menu.ANIMATION_DELAY);
return popup;
} catch {
return null;
}
}
/**
* Opens a submenu using keyboard navigation.
* Returns the popup locator if successful, null otherwise.
*/
private async openSubmenuWithKeyboard(
submenuText: string,
itemText: string,
timeout: number,
): Promise<Locator | null> {
try {
const submenuTitle = this.getSubmenuTitle(submenuText);
await submenuTitle.focus();
await this.page.keyboard.press('ArrowRight');
const popup = this.page
.locator(Menu.SELECTORS.SUBMENU_POPUP)
.filter({ hasText: itemText });
await popup.waitFor({ state: 'visible', timeout });
return popup;
} catch {
return null;
}
}
/**
* Opens a submenu using dispatchEvent to trigger mouseover/mouseenter.
* Returns the popup locator if successful, null otherwise.
*/
private async openSubmenuWithDispatchEvent(
submenuText: string,
itemText: string,
timeout: number,
): Promise<Locator | null> {
try {
const submenuTitle = this.getSubmenuTitle(submenuText);
await submenuTitle.evaluate(el => {
el.dispatchEvent(
new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
view: window,
}),
);
el.dispatchEvent(
new MouseEvent('mouseenter', {
bubbles: true,
cancelable: true,
view: window,
}),
);
});
const popup = this.page
.locator(Menu.SELECTORS.SUBMENU_POPUP)
.filter({ hasText: itemText });
await popup.waitFor({ state: 'visible', timeout });
return popup;
} catch {
return null;
}
}
/**
* Gets the submenu title element for a submenu containing the given text.
*/
private getSubmenuTitle(submenuText: string): Locator {
return this.locator
.locator(Menu.SELECTORS.SUBMENU)
.filter({ hasText: submenuText })
.locator(Menu.SELECTORS.SUBMENU_TITLE);
}
}

View File

@@ -21,5 +21,7 @@
export { Button } from './Button';
export { Form } from './Form';
export { Input } from './Input';
export { Menu } from './Menu';
export { Modal } from './Modal';
export { Table } from './Table';
export { Toast } from './Toast';

View File

@@ -18,6 +18,7 @@
*/
import { Page, Download } from '@playwright/test';
import { Menu } from '../components/core';
import { TIMEOUT } from '../utils/constants';
/**
@@ -54,7 +55,7 @@ export class DashboardPage {
}
/**
* Wait for the dashboard to load
* Wait for the dashboard header to be visible.
*/
async waitForLoad(options?: { timeout?: number }): Promise<void> {
const timeout = options?.timeout ?? TIMEOUT.PAGE_LOAD;
@@ -63,6 +64,35 @@ export class DashboardPage {
});
}
/**
* Wait for all charts on the dashboard to finish loading.
* Waits until no loading indicators are visible on the page.
*/
async waitForChartsToLoad(options?: { timeout?: number }): Promise<void> {
const timeout = options?.timeout ?? TIMEOUT.API_RESPONSE;
// Use browser-context evaluation to check visibility directly.
// Loading indicators ([aria-label="Loading"]) may persist in the DOM as hidden
// elements after charts finish loading. This checks that none are currently visible,
// returning immediately when charts are already loaded (no timeout penalty).
await this.page.waitForFunction(
() => {
const loaders = document.querySelectorAll('[aria-label="Loading"]');
if (loaders.length === 0) return true;
return Array.from(loaders).every(el => {
const style = getComputedStyle(el);
return (
style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0'
);
});
},
undefined,
{ timeout },
);
}
/**
* Open the dashboard header actions menu (three-dot menu)
*/
@@ -78,33 +108,21 @@ export class DashboardPage {
}
/**
* Hover over the Download submenu to open it (Ant Design submenus open on hover)
* Selects an option from the Download submenu.
* Opens the header actions menu, navigates to Download submenu,
* and clicks the specified option.
*
* @param optionText - The download option to select (e.g., "Export YAML")
*/
async openDownloadMenu(): Promise<void> {
// Find the Download menu item within the header actions menu and hover
const menu = this.page.locator(DashboardPage.SELECTORS.HEADER_ACTIONS_MENU);
await menu.getByText('Download', { exact: true }).hover();
// Wait for Export YAML to become visible (indicates submenu opened)
await this.page.getByText('Export YAML').waitFor({ state: 'visible' });
}
async selectDownloadOption(optionText: string): Promise<Download> {
await this.openHeaderActionsMenu();
/**
* Click "Export YAML" in the download menu
* Returns a Promise that resolves when download starts
*/
async clickExportYaml(): Promise<Download> {
const menu = new Menu(
this.page,
DashboardPage.SELECTORS.HEADER_ACTIONS_MENU,
);
const downloadPromise = this.page.waitForEvent('download');
await this.page.getByText('Export YAML').click();
return downloadPromise;
}
/**
* Click "Export as Example" in the download menu
* Returns a Promise that resolves when download starts
*/
async clickExportAsExample(): Promise<Download> {
const downloadPromise = this.page.waitForEvent('download');
await this.page.getByText('Export as Example').click();
await menu.selectSubmenuItem('Download', optionText);
return downloadPromise;
}
}

View File

@@ -19,6 +19,7 @@
import { test, expect } from '@playwright/test';
import { DashboardPage } from '../../../pages/DashboardPage';
import { Toast } from '../../../components/core';
import { TIMEOUT } from '../../../utils/constants';
/**
@@ -31,74 +32,56 @@ import { TIMEOUT } from '../../../utils/constants';
* Prerequisites:
* - Superset running with example dashboards loaded
* - Admin user authenticated (via global-setup)
*
* SKIP REASON: Ant Design Menu submenu hover behavior is not reliably
* triggered by Playwright. The submenu popup doesn't appear consistently
* when hovering over the Download menu item. This functionality is
* covered by unit tests in DownloadMenuItems.test.tsx.
*
* TODO: Investigate Ant Design Menu triggerSubMenuAction or alternative
* approaches for E2E testing of nested menus.
*/
let dashboardPage: DashboardPage;
const downloads: { delete: () => Promise<void> }[] = [];
test.describe('Dashboard Export', () => {
// Dashboard with multiple charts needs extra time for cold-cache CI runs:
// waitForLoad (10s) + waitForChartsToLoad (15s) + menu + download + toast
test.setTimeout(60_000);
test.describe.skip('Dashboard Export', () => {
test.beforeEach(async ({ page }) => {
dashboardPage = new DashboardPage(page);
// Navigate to World Health dashboard (standard example)
await dashboardPage.gotoBySlug('world_health');
await dashboardPage.waitForLoad({ timeout: TIMEOUT.PAGE_LOAD });
// Wait for charts to finish loading - Download menu may be disabled while loading
await dashboardPage.waitForChartsToLoad();
});
test('should download ZIP when clicking Export YAML', async ({ page }) => {
// Open the header actions menu (three-dot menu)
await dashboardPage.openHeaderActionsMenu();
// Open the Download submenu
await dashboardPage.openDownloadMenu();
// Click Export YAML and wait for download
const download = await dashboardPage.clickExportYaml();
// Verify the download
const filename = download.suggestedFilename();
expect(filename).toMatch(/\.zip$/);
test.afterEach(async () => {
// Clean up downloaded files
await Promise.all(downloads.map(d => d.delete().catch(() => {})));
downloads.length = 0;
});
test('should download example bundle when clicking Export as Example', async ({
test('should download ZIP and show success toast when clicking Export YAML', async ({
page,
}) => {
// Open the header actions menu
await dashboardPage.openHeaderActionsMenu();
const toast = new Toast(page);
const download = await dashboardPage.selectDownloadOption('Export YAML');
downloads.push(download);
// Open the Download submenu
await dashboardPage.openDownloadMenu();
// Click Export as Example and wait for download
const download = await dashboardPage.clickExportAsExample();
// Verify the download
const filename = download.suggestedFilename();
expect(filename).toMatch(/_example\.zip$/);
expect(download.suggestedFilename()).toMatch(/\.zip$/);
await expect(toast.getSuccess()).toBeVisible({
timeout: TIMEOUT.API_RESPONSE,
});
});
test('should show success toast after Export as Example', async ({
test('should download example bundle and show success toast when clicking Export as Example', async ({
page,
}) => {
// Open the header actions menu
await dashboardPage.openHeaderActionsMenu();
const toast = new Toast(page);
const download =
await dashboardPage.selectDownloadOption('Export as Example');
downloads.push(download);
// Open the Download submenu
await dashboardPage.openDownloadMenu();
// Click Export as Example
await dashboardPage.clickExportAsExample();
// Verify success toast appears
await expect(
page.locator('.ant-message-success, [data-test="toast-success"]'),
).toBeVisible({ timeout: TIMEOUT.API_RESPONSE });
expect(download.suggestedFilename()).toMatch(/_example\.zip$/);
await expect(toast.getSuccess()).toBeVisible({
timeout: TIMEOUT.API_RESPONSE,
});
});
});

View File

@@ -51,6 +51,7 @@ import {
SMART_DATE_ID,
validateMaxValue,
validateServerPagination,
withLabel,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import { isEmpty, last } from 'lodash';
@@ -384,7 +385,7 @@ const config: ControlPanelConfig = {
description: t('Rows per page, 0 means no pagination'),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.server_pagination?.value),
validators: [validateInteger],
validators: [withLabel(validateInteger, t('Server Page Length'))],
},
},
],
@@ -403,7 +404,7 @@ const config: ControlPanelConfig = {
state?.common?.conf?.SQL_MAX_ROW,
}),
validators: [
validateInteger,
withLabel(validateInteger, t('Row limit')),
(v, state) =>
validateMaxValue(
v,

View File

@@ -25,6 +25,7 @@ import {
computeMaxFontSize,
BRAND_COLOR,
BinaryQueryObjectFilterClause,
DTTM_ALIAS,
} from '@superset-ui/core';
import { styled, useTheme } from '@apache-superset/core/ui';
import Echart from '../components/Echart';
@@ -357,7 +358,10 @@ function BigNumberVis({
const pointerEvent = eventParams.event.event;
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
drillToDetailFilters.push({
col: formData?.granularitySqla,
col:
formData?.xAxis === DTTM_ALIAS
? formData?.granularitySqla
: formData?.xAxis,
grain: formData?.timeGrainSqla,
op: '==',
val: data[0],

View File

@@ -106,6 +106,7 @@ describe('BigNumberWithTrendline transformProps', () => {
subtitleFontSize: 14,
forceTimestampFormatting: false,
timeFormat: 'YYYY-MM-DD',
xAxis: '__timestamp',
yAxisFormat: 'SMART_NUMBER',
compareLag: 1,
compareSuffix: 'WoW',

View File

@@ -47,6 +47,7 @@ export type BigNumberWithTrendlineFormData = BigNumberTotalFormData & {
b: number;
};
compareLag?: string | number;
xAxis: string;
showXAxis?: boolean;
showXAxisMinMaxLabels?: boolean;
showYAxis?: boolean;

View File

@@ -242,8 +242,10 @@ export default function transformProps(
// @ts-ignore
...outlierData,
];
const addYAxisTitleOffset = !!yAxisTitle;
const addXAxisTitleOffset = !!xAxisTitle;
const addYAxisTitleOffset =
!!yAxisTitle && convertInteger(yAxisTitleMargin) !== 0;
const addXAxisTitleOffset =
!!xAxisTitle && convertInteger(xAxisTitleMargin) !== 0;
const chartPadding = getPadding(
true,
legendOrientation,

View File

@@ -46,6 +46,12 @@ type EChartsOption = ComposeOption<HeatmapSeriesOption>;
const DEFAULT_ECHARTS_BOUNDS = [0, 200];
/**
* Column name for the rank values added by the backend's rank post-processing operation.
* This is used when the heatmap is in normalized mode to color cells by percentile rank.
*/
const RANK_COLUMN_NAME = 'rank';
/**
* Extract unique values for an axis from the data.
* Filters out null and undefined values.
@@ -212,7 +218,7 @@ export default function transformProps(
currencyFormats = {},
currencyCodeColumn,
} = datasource;
const colorColumn = normalized ? 'rank' : metricLabel;
const colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel;
const colors = getSequentialSchemeRegistry().get(linearColorScheme)?.colors;
const getAxisFormatter =
(colType: GenericDataType) => (value: number | string) => {
@@ -291,6 +297,7 @@ export default function transformProps(
const xValue = row[xAxisColumnName];
const yValue = row[yAxisColumnName];
const metricValue = row[metricLabel];
const rankValue = row[RANK_COLUMN_NAME];
// Convert to axis indices for ECharts when explicit axis data is provided
const xIndex = xAxisIndexMap.get(xValue);
@@ -304,8 +311,21 @@ export default function transformProps(
);
return [];
}
return [[xIndex, yIndex, metricValue] as [number, number, any]];
}),
if (normalized && rankValue === undefined) {
logging.error(
`Heatmap: Skipping row due to missing rank value. xValue: ${xValue}, yValue: ${yValue}, metricValue: ${metricValue}`,
row,
);
return [];
}
// Include rank as 4th dimension when normalized is enabled
// This allows visualMap to use dimension: 3 to color by rank percentile
if (normalized) {
return [[xIndex, yIndex, metricValue, rankValue]];
}
return [[xIndex, yIndex, metricValue]];
}) as any,
label: {
show: showValues,
formatter: (params: CallbackDataParams) => {
@@ -336,6 +356,9 @@ export default function transformProps(
bottom: bottomMargin,
left: leftMargin,
},
legend: {
show: false,
},
series,
tooltip: {
...getDefaultTooltip(refs),

View File

@@ -17,7 +17,11 @@
* under the License.
*/
import { t } from '@apache-superset/core';
import { validateInteger, validateNonEmpty } from '@superset-ui/core';
import {
validateInteger,
validateNonEmpty,
withLabel,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import {
ControlPanelConfig,
@@ -66,7 +70,7 @@ const config: ControlPanelConfig = {
default: 5,
choices: formatSelectOptionsForRange(5, 20, 5),
description: t('The number of bins for the histogram'),
validators: [validateInteger],
validators: [withLabel(validateInteger, t('Bins'))],
},
},
],

View File

@@ -576,8 +576,11 @@ export default function transformProps(
? getXAxisFormatter(xAxisTimeFormat)
: String;
const addYAxisTitleOffset = !!(yAxisTitle || yAxisTitleSecondary);
const addXAxisTitleOffset = !!xAxisTitle;
const addYAxisTitleOffset =
!!(yAxisTitle || yAxisTitleSecondary) &&
convertInteger(yAxisTitleMargin) !== 0;
const addXAxisTitleOffset =
!!xAxisTitle && convertInteger(xAxisTitleMargin) !== 0;
const chartPadding = getPadding(
showLegend,

View File

@@ -309,3 +309,93 @@ test('falls back to window resize listener when ResizeObserver is unavailable',
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
});
// Test for issue #25334: Bar chart cross-filter without dimensions
test('emits cross-filter on X-axis value when no dimensions and categorical X-axis', async () => {
const setDataMaskMock = jest.fn();
const propsWithCategoricalXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
setDataMask: setDataMaskMock,
groupby: [], // No dimensions
xAxis: {
label: 'category_column',
type: AxisType.Category, // Categorical X-axis
},
};
render(<EchartsTimeseries {...propsWithCategoricalXAxis} />);
// Get the click handler from the mock
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
expect(props.eventHandlers).toBeDefined();
expect(props.eventHandlers?.click).toBeDefined();
// Simulate a click event with X-axis data
const clickHandler = props.eventHandlers?.click;
if (clickHandler) {
clickHandler({
seriesName: 'Sales', // This is the metric name
data: ['Product A', 100], // X-axis value is 'Product A'
name: 'Product A',
dataIndex: 0,
});
// Wait for the timer (TIMER_DURATION = 300ms)
await waitFor(
() => {
expect(setDataMaskMock).toHaveBeenCalled();
},
{ timeout: 500 },
);
// Verify the cross-filter uses the X-axis column and value, not the metric
const dataMaskCall = setDataMaskMock.mock.calls[0][0];
expect(dataMaskCall.extraFormData.filters).toEqual([
{
col: 'category_column', // X-axis column
op: 'IN',
val: ['Product A'], // X-axis value, not 'Sales' (metric)
},
]);
}
});
test('does not emit cross-filter when no dimensions and time-based X-axis', async () => {
const setDataMaskMock = jest.fn();
const propsWithTimeXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
setDataMask: setDataMaskMock,
groupby: [], // No dimensions
xAxis: {
label: '__timestamp',
type: AxisType.Time, // Time-based X-axis (not categorical)
},
};
render(<EchartsTimeseries {...propsWithTimeXAxis} />);
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
// Simulate a click event
const clickHandler = props.eventHandlers?.click;
if (clickHandler) {
clickHandler({
seriesName: 'Sales',
data: [1609459200000, 100], // Timestamp
name: '2021-01-01',
dataIndex: 0,
});
// Wait a bit and verify setDataMask was NOT called
await new Promise(resolve => setTimeout(resolve, 400));
expect(setDataMaskMock).not.toHaveBeenCalled();
}
});

View File

@@ -154,6 +154,43 @@ export default function EchartsTimeseries({
[groupby, labelMap, selectedValues],
);
// Cross-filter using X-axis value when no dimensions are set (issue #25334)
const getXAxisCrossFilterDataMask = useCallback(
(xAxisValue: string | number) => {
const stringValue = String(xAxisValue);
const selected: string[] = Object.values(selectedValues);
let values: string[];
if (selected.includes(stringValue)) {
values = selected.filter(v => v !== stringValue);
} else {
values = [stringValue];
}
return {
dataMask: {
extraFormData: {
filters:
values.length === 0
? []
: [
{
col: xAxis.label,
op: 'IN' as const,
val: values,
},
],
},
filterState: {
label: values.length ? values : undefined,
value: values.length ? values : null,
selectedValues: values.length ? values : null,
},
},
isCurrentValueSelected: selected.includes(stringValue),
};
},
[selectedValues, xAxis.label],
);
const handleChange = useCallback(
(value: string) => {
if (!emitCrossFilters) {
@@ -164,9 +201,25 @@ export default function EchartsTimeseries({
[emitCrossFilters, setDataMask, getCrossFilterDataMask],
);
// Handle cross-filter using X-axis value when no dimensions (issue #25334)
const handleXAxisChange = useCallback(
(xAxisValue: string | number) => {
if (!emitCrossFilters) {
return;
}
setDataMask(getXAxisCrossFilterDataMask(xAxisValue).dataMask);
},
[emitCrossFilters, setDataMask, getXAxisCrossFilterDataMask],
);
// Determine if X-axis can be used for cross-filtering (categorical axis without dimensions)
const canCrossFilterByXAxis =
!hasDimensions && xAxis.type === AxisType.Category;
const eventHandlers: EventHandlers = {
click: props => {
if (!hasDimensions) {
// Allow cross-filter by dimensions OR by categorical X-axis (issue #25334)
if (!hasDimensions && !canCrossFilterByXAxis) {
return;
}
if (clickTimer.current) {
@@ -174,8 +227,14 @@ export default function EchartsTimeseries({
}
// Ensure that double-click events do not trigger single click event. So we put it in the timer.
clickTimer.current = setTimeout(() => {
const { seriesName: name } = props;
handleChange(name);
if (hasDimensions) {
// Cross-filter by dimension (original behavior)
const { seriesName: name } = props;
handleChange(name);
} else if (canCrossFilterByXAxis && props.data?.[0] != null) {
// Cross-filter by X-axis value when no dimensions (issue #25334)
handleXAxisChange(props.data[0]);
}
}, TIMER_DURATION);
},
mouseout: () => {
@@ -252,12 +311,18 @@ export default function EchartsTimeseries({
});
});
// Provide cross-filter for dimensions OR categorical X-axis (issue #25334)
let crossFilter;
if (hasDimensions) {
crossFilter = getCrossFilterDataMask(seriesName);
} else if (canCrossFilterByXAxis && data?.[0] != null) {
crossFilter = getXAxisCrossFilterDataMask(data[0]);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' },
crossFilter: hasDimensions
? getCrossFilterDataMask(seriesName)
: undefined,
crossFilter,
});
}
},

View File

@@ -419,6 +419,7 @@ export default function transformProps(
timeCompare: array,
timeShiftColor,
theme,
hasDimensions: (groupBy?.length ?? 0) > 0,
},
);
if (transformedSeries) {
@@ -573,8 +574,10 @@ export default function transformProps(
onLegendScroll,
} = hooks;
const addYAxisLabelOffset = !!yAxisTitle;
const addXAxisLabelOffset = !!xAxisTitle;
const addYAxisLabelOffset =
!!yAxisTitle && convertInteger(yAxisTitleMargin) !== 0;
const addXAxisLabelOffset =
!!xAxisTitle && convertInteger(xAxisTitleMargin) !== 0;
const padding = getPadding(
showLegend,
legendOrientation,

View File

@@ -196,6 +196,7 @@ export function transformSeries(
timeCompare?: string[];
timeShiftColor?: boolean;
theme?: SupersetTheme;
hasDimensions?: boolean;
},
): SeriesOption | undefined {
const { name, data } = series;
@@ -237,8 +238,12 @@ export function transformSeries(
const isConfidenceBand =
forecastSeries.type === ForecastSeriesEnum.ForecastLower ||
forecastSeries.type === ForecastSeriesEnum.ForecastUpper;
// When cross-filtering by X-axis (no dimensions), selectedValues contains
// X-axis values rather than series names, so skip series-level dimming.
const isFiltered =
filterState?.selectedValues && !filterState?.selectedValues.includes(name);
opts.hasDimensions !== false &&
filterState?.selectedValues &&
!filterState?.selectedValues.includes(name);
const opacity = isFiltered
? OpacityEnum.SemiTransparent
: opts.lineStyle?.opacity || OpacityEnum.NonTransparent;
@@ -656,7 +661,9 @@ export function getPadding(
top:
yAxisTitlePosition && yAxisTitlePosition === 'Top'
? TIMESERIES_CONSTANTS.gridOffsetTop + (Number(yAxisTitleMargin) || 0)
: TIMESERIES_CONSTANTS.gridOffsetTop + yAxisOffset,
: yAxisTitlePosition === 'Left'
? TIMESERIES_CONSTANTS.gridOffsetTop
: TIMESERIES_CONSTANTS.gridOffsetTop + yAxisOffset,
bottom:
zoomable && !isHorizontal
? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable + xAxisOffset

View File

@@ -35,6 +35,7 @@ const formData = {
a: 1,
},
compareLag: 1,
xAxis: '__timestamp',
timeGrainSqla: TimeGranularity.QUARTER,
granularitySqla: 'ds',
compareSuffix: 'over last quarter',
@@ -54,11 +55,13 @@ const rawFormData: BigNumberWithTrendlineFormData = {
a: 1,
},
compare_lag: 1,
x_axis: '__timestamp',
time_grain_sqla: TimeGranularity.QUARTER,
granularity_sqla: 'ds',
compare_suffix: 'over last quarter',
viz_type: VizType.BigNumber,
y_axis_format: '.3s',
xAxis: '__timestamp',
};
function generateProps(

View File

@@ -0,0 +1,82 @@
/**
* 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 { QueryFormData } from '@superset-ui/core';
import buildQuery from '../../src/Heatmap/buildQuery';
describe('Heatmap buildQuery - Rank Operation for Normalized Field', () => {
const baseFormData = {
datasource: '5__table',
granularity_sqla: 'ds',
metric: 'count',
x_axis: 'category',
groupby: ['region'],
viz_type: 'heatmap',
} as QueryFormData;
test('should ALWAYS include rank operation when normalized=true', () => {
const formData = {
...baseFormData,
normalized: true,
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
const rankOperation = query.post_processing?.find(
op => op?.operation === 'rank',
);
expect(rankOperation).toBeDefined();
expect(rankOperation?.operation).toBe('rank');
});
test('should ALWAYS include rank operation when normalized=false', () => {
const formData = {
...baseFormData,
normalized: false,
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
const rankOperation = query.post_processing?.find(
op => op?.operation === 'rank',
);
expect(rankOperation).toBeDefined();
expect(rankOperation?.operation).toBe('rank');
});
test('should ALWAYS include rank operation when normalized is undefined', () => {
const formData = {
...baseFormData,
// normalized not set
};
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
const rankOperation = query.post_processing?.find(
op => op?.operation === 'rank',
);
expect(rankOperation).toBeDefined();
expect(rankOperation?.operation).toBe('rank');
});
});

View File

@@ -291,4 +291,72 @@ describe('Heatmap transformProps', () => {
// Y-axis: numbers sorted numerically (1, 2, 10 NOT 1, 10, 2)
expect(yAxisData).toEqual([1, 2, 10]);
});
test('should include rank as 4th dimension when normalized is true', () => {
const dataWithRank = [
{ day_of_week: 'Monday', hour: 9, count: 10, rank: 0.33 },
{ day_of_week: 'Monday', hour: 14, count: 15, rank: 0.67 },
{ day_of_week: 'Wednesday', hour: 11, count: 8, rank: 0.17 },
{ day_of_week: 'Friday', hour: 16, count: 20, rank: 1.0 },
];
const chartProps = createChartProps({ normalized: true }, dataWithRank);
const result = transformProps(chartProps as HeatmapChartProps);
const seriesData = (result.echartOptions.series as any)[0].data;
// Each data point should be [xIndex, yIndex, metricValue, rankValue]
expect(Array.isArray(seriesData)).toBe(true);
expect(seriesData.length).toBe(4);
// Check that data points have 4 dimensions when normalized
seriesData.forEach((point: any) => {
expect(Array.isArray(point)).toBe(true);
expect(point.length).toBe(4);
// First two should be indices (numbers)
expect(typeof point[0]).toBe('number');
expect(typeof point[1]).toBe('number');
// Third should be the metric value
expect(typeof point[2]).toBe('number');
// Fourth should be the rank value
expect(typeof point[3]).toBe('number');
expect(point[3]).toBeGreaterThanOrEqual(0);
expect(point[3]).toBeLessThanOrEqual(1);
});
// visualMap should use dimension 3 (4th element) for coloring
expect((result.echartOptions.visualMap as any).dimension).toBe(3);
});
test('should use 3 dimensions when normalized is false', () => {
const chartProps = createChartProps({ normalized: false });
const result = transformProps(chartProps as HeatmapChartProps);
const seriesData = (result.echartOptions.series as any)[0].data;
// Each data point should be [xIndex, yIndex, metricValue]
seriesData.forEach((point: any) => {
expect(point.length).toBe(3);
});
// visualMap should use dimension 2 (3rd element) for coloring
expect((result.echartOptions.visualMap as any).dimension).toBe(2);
});
test('should always hide legend regardless of showLegend setting', () => {
// Test with showLegend: true
const chartPropsWithLegend = createChartProps({ showLegend: true });
const resultWithLegend = transformProps(
chartPropsWithLegend as HeatmapChartProps,
);
expect((resultWithLegend.echartOptions.legend as any).show).toBe(false);
// Test with showLegend: false
const chartPropsWithoutLegend = createChartProps({ showLegend: false });
const resultWithoutLegend = transformProps(
chartPropsWithoutLegend as HeatmapChartProps,
);
expect((resultWithoutLegend.echartOptions.legend as any).show).toBe(false);
});
});

View File

@@ -21,12 +21,16 @@ import { GenericDataType } from '@apache-superset/core/api/core';
import { supersetTheme } from '@apache-superset/core/ui';
import type { SeriesOption } from 'echarts';
import { EchartsTimeseriesSeriesType } from '../../src';
import { TIMESERIES_CONSTANTS } from '../../src/constants';
import { LegendOrientation } from '../../src/types';
import {
transformSeries,
transformNegativeLabelsPosition,
getPadding,
} from '../../src/Timeseries/transformers';
import transformProps from '../../src/Timeseries/transformProps';
import { EchartsTimeseriesChartProps } from '../../src/types';
import * as seriesUtils from '../../src/utils/series';
// Mock the colorScale function
const mockColorScale = jest.fn(
@@ -89,6 +93,34 @@ describe('transformSeries', () => {
expect((result as any).itemStyle.borderType).toBeUndefined();
expect((result as any).itemStyle.borderColor).toBeUndefined();
});
it('should dim series when selectedValues does not include series name (dimension-based filtering)', () => {
const opts = {
filterState: { selectedValues: ['other-series'] },
hasDimensions: true,
seriesType: EchartsTimeseriesSeriesType.Bar,
timeShiftColor: false,
};
const result = transformSeries(series, mockColorScale, 'test-key', opts);
// OpacityEnum.SemiTransparent = 0.3
expect((result as any).itemStyle.opacity).toBe(0.3);
});
it('should not dim series when hasDimensions is false (X-axis cross-filtering)', () => {
const opts = {
filterState: { selectedValues: ['Product A'] },
hasDimensions: false,
seriesType: EchartsTimeseriesSeriesType.Bar,
timeShiftColor: false,
};
const result = transformSeries(series, mockColorScale, 'test-key', opts);
// OpacityEnum.NonTransparent = 1 (not dimmed)
expect((result as any).itemStyle.opacity).toBe(1);
});
});
describe('transformNegativeLabelsPosition', () => {
@@ -237,3 +269,167 @@ test('should configure time axis labels to show max label for last month visibil
}),
);
});
function setupGetChartPaddingMock(): jest.SpyInstance {
// Mock getChartPadding to return the padding object as-is for easier testing
const getChartPaddingSpy = jest.spyOn(seriesUtils, 'getChartPadding');
getChartPaddingSpy.mockImplementation(
(
show: boolean,
orientation: LegendOrientation,
margin: string | number | null | undefined,
padding:
| {
bottom?: number;
left?: number;
right?: number;
top?: number;
}
| undefined,
) => {
return {
bottom: padding?.bottom ?? 0,
left: padding?.left ?? 0,
right: padding?.right ?? 0,
top: padding?.top ?? 0,
};
},
);
return getChartPaddingSpy;
}
test('getPadding should only affect left margin when Y axis title position is Left', () => {
const getChartPaddingSpy = setupGetChartPaddingMock();
try {
const result = getPadding(
false, // showLegend
LegendOrientation.Top, // legendOrientation
true, // addYAxisTitleOffset
false, // zoomable
null, // margin
false, // addXAxisTitleOffset
'Left', // yAxisTitlePosition
30, // yAxisTitleMargin
0, // xAxisTitleMargin
false, // isHorizontal
);
// Top should be base value, not affected by Left position
expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop);
// Left should include the margin
expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft + 30);
// Bottom should be base value
expect(result.bottom).toBe(TIMESERIES_CONSTANTS.gridOffsetBottom);
// Right should be base value
expect(result.right).toBe(TIMESERIES_CONSTANTS.gridOffsetRight);
} finally {
getChartPaddingSpy.mockRestore();
}
});
test('getPadding should only affect top margin when Y axis title position is Top', () => {
const getChartPaddingSpy = setupGetChartPaddingMock();
try {
const result = getPadding(
false, // showLegend
LegendOrientation.Top, // legendOrientation
true, // addYAxisTitleOffset
false, // zoomable
null, // margin
false, // addXAxisTitleOffset
'Top', // yAxisTitlePosition
30, // yAxisTitleMargin
0, // xAxisTitleMargin
false, // isHorizontal
);
// Top should include the margin
expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop + 30);
// Left should be base value, not affected by Top position
expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft);
// Bottom should be base value
expect(result.bottom).toBe(TIMESERIES_CONSTANTS.gridOffsetBottom);
// Right should be base value
expect(result.right).toBe(TIMESERIES_CONSTANTS.gridOffsetRight);
} finally {
getChartPaddingSpy.mockRestore();
}
});
test('getPadding should use yAxisOffset for top when position is not specified and addYAxisTitleOffset is true', () => {
const getChartPaddingSpy = setupGetChartPaddingMock();
try {
const result = getPadding(
false, // showLegend
LegendOrientation.Top, // legendOrientation
true, // addYAxisTitleOffset
false, // zoomable
null, // margin
false, // addXAxisTitleOffset
undefined, // yAxisTitlePosition (not specified)
0, // yAxisTitleMargin
0, // xAxisTitleMargin
false, // isHorizontal
);
// Top should include yAxisOffset
expect(result.top).toBe(
TIMESERIES_CONSTANTS.gridOffsetTop +
TIMESERIES_CONSTANTS.yAxisLabelTopOffset,
);
// Left should be base value
expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft);
} finally {
getChartPaddingSpy.mockRestore();
}
});
test('getPadding should not add yAxisOffset when addYAxisTitleOffset is false', () => {
const getChartPaddingSpy = setupGetChartPaddingMock();
try {
const result = getPadding(
false, // showLegend
LegendOrientation.Top, // legendOrientation
false, // addYAxisTitleOffset
false, // zoomable
null, // margin
false, // addXAxisTitleOffset
undefined, // yAxisTitlePosition
0, // yAxisTitleMargin
0, // xAxisTitleMargin
false, // isHorizontal
);
// Top should be base value only
expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop);
// Left should be base value
expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft);
} finally {
getChartPaddingSpy.mockRestore();
}
});
test('getPadding should handle Left position with zero margin correctly', () => {
const getChartPaddingSpy = setupGetChartPaddingMock();
try {
const result = getPadding(
false, // showLegend
LegendOrientation.Top, // legendOrientation
true, // addYAxisTitleOffset
false, // zoomable
null, // margin
false, // addXAxisTitleOffset
'Left', // yAxisTitlePosition
0, // yAxisTitleMargin (zero)
0, // xAxisTitleMargin
false, // isHorizontal
);
// Top should be base value, not affected
expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop);
// Left should be base value only (margin is 0)
expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft);
} finally {
getChartPaddingSpy.mockRestore();
}
});

View File

@@ -52,6 +52,7 @@ import {
SMART_DATE_ID,
validateMaxValue,
validateServerPagination,
withLabel,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/api/core';
import { isEmpty, last } from 'lodash';
@@ -407,7 +408,7 @@ const config: ControlPanelConfig = {
description: t('Rows per page, 0 means no pagination'),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.server_pagination?.value),
validators: [validateInteger],
validators: [withLabel(validateInteger, t('Server Page Length'))],
},
},
],
@@ -426,7 +427,7 @@ const config: ControlPanelConfig = {
state?.common?.conf?.SQL_MAX_ROW,
}),
validators: [
validateInteger,
withLabel(validateInteger, t('Row limit')),
(v, state) =>
validateMaxValue(
v,
@@ -448,9 +449,6 @@ const config: ControlPanelConfig = {
'Limits the number of the rows that are computed in the query that is the source of the data used for this chart.',
),
},
override: {
default: 1000,
},
},
],
[

View File

@@ -18,8 +18,10 @@
*/
import { ThemeProvider } from '@apache-superset/core/ui';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import { parse, stringify } from 'query-string';
import { BrowserRouter as Router } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
export function ProviderWrapper(props: any) {
const { children, theme } = props;
@@ -28,8 +30,12 @@ export function ProviderWrapper(props: any) {
<ThemeProvider theme={theme}>
<Router>
<QueryParamProvider
ReactRouterRoute={Route}
stringifyOptions={{ encode: false }}
adapter={ReactRouter5Adapter}
options={{
searchStringToObject: parse,
objectToSearchString: (object: Record<string, any>) =>
stringify(object, { encode: false }),
}}
>
{children}
</QueryParamProvider>

View File

@@ -31,6 +31,7 @@ export default class FixJSDOMEnvironment extends JSDOMEnvironment {
this.global.Response = Response;
this.global.AbortSignal = AbortSignal;
this.global.AbortController = AbortController;
this.global.ReadableStream = ReadableStream;
// Mock MessageChannel to prevent hanging Jest tests with rc-overflow@1.4.1
// Forces rc-overflow to use requestAnimationFrame fallback instead

View File

@@ -23,6 +23,7 @@ import jQuery from 'jquery';
// https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options
// in order to mock modules in test case, so avoid absolute import module
import { configure as configureTranslation } from '@apache-superset/core/ui';
import fetchMock from 'fetch-mock';
import { Worker } from './Worker';
import { IntersectionObserver } from './IntersectionObserver';
import { ResizeObserver } from './ResizeObserver';
@@ -43,6 +44,9 @@ if (defaultView != null) {
});
}
fetchMock.mockGlobal();
fetchMock.config.allowRelativeUrls = true;
const g = global as any;
g.window ??= Object.create(window);
g.window.location ??= { href: 'about:blank' };

View File

@@ -43,6 +43,7 @@ import { configureStore, Store } from '@reduxjs/toolkit';
import { api } from 'src/hooks/apiResources/queryApi';
import userEvent from '@testing-library/user-event';
import { ExtensionsProvider } from 'src/extensions/ExtensionsContext';
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
type Options = Omit<RenderOptions, 'queries'> & {
useRedux?: boolean;
@@ -109,7 +110,11 @@ export function createWrapper(options?: Options) {
}
if (useQueryParams) {
result = <QueryParamProvider>{result}</QueryParamProvider>;
result = (
<QueryParamProvider adapter={ReactRouter5Adapter}>
{result}
</QueryParamProvider>
);
}
if (useRouter) {

View File

@@ -86,21 +86,30 @@ describe('async actions', () => {
};
let dispatch;
const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*';
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
beforeEach(() => {
dispatch = sinon.spy();
fetchMock.removeRoute(fetchQueryEndpoint);
fetchMock.get(
fetchQueryEndpoint,
JSON.stringify({
data: mockBigNumber,
query: { sqlEditorId: 'dfsadfs' },
}),
{ name: fetchQueryEndpoint },
);
fetchMock.removeRoute(runQueryEndpoint);
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`, {
name: runQueryEndpoint,
});
});
afterEach(() => fetchMock.resetHistory());
const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*';
fetchMock.get(
fetchQueryEndpoint,
JSON.stringify({ data: mockBigNumber, query: { sqlEditorId: 'dfsadfs' } }),
);
const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/';
fetchMock.post(runQueryEndpoint, `{ "data": ${mockBigNumber} }`);
afterEach(() => {
fetchMock.clearHistory();
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveQuery', () => {
@@ -117,15 +126,15 @@ describe('async actions', () => {
const store = mockStore(initialState);
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(saveQueryEndpoint)).toHaveLength(1);
});
});
test('posts the correct query object', () => {
const store = mockStore(initialState);
return store.dispatch(actions.saveQuery(query, queryId)).then(() => {
const call = fetchMock.calls(saveQueryEndpoint)[0];
const formData = JSON.parse(call[1].body);
const call = fetchMock.callHistory.calls(saveQueryEndpoint)[0];
const formData = JSON.parse(call.options.body);
const mappedQueryToServer = actions.convertQueryToServer(query);
Object.keys(mappedQueryToServer).forEach(key => {
@@ -172,11 +181,12 @@ describe('async actions', () => {
const expectedSql = 'SELECT 1';
beforeEach(() => {
fetchMock.removeRoute(formatQueryEndpoint);
fetchMock.post(
formatQueryEndpoint,
{ result: expectedSql },
{
overwriteRoutes: true,
name: formatQueryEndpoint,
},
);
});
@@ -185,7 +195,9 @@ describe('async actions', () => {
const store = mockStore(initialState);
store.dispatch(actions.formatQuery(query, queryId));
await waitFor(() =>
expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
1,
),
);
expect(store.getActions()[0].type).toBe(actions.QUERY_EDITOR_SET_SQL);
expect(store.getActions()[0].sql).toBe(expectedSql);
@@ -209,11 +221,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithoutExtras));
await waitFor(() =>
expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
1,
),
);
const call = fetchMock.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call[1].body);
const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call.options.body);
expect(body).toEqual({ sql: 'SELECT * FROM table' });
expect(body.database_id).toBeUndefined();
@@ -238,11 +252,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithDb));
await waitFor(() =>
expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
1,
),
);
const call = fetchMock.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call[1].body);
const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call.options.body);
expect(body).toEqual({
sql: 'SELECT * FROM table',
@@ -268,11 +284,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithTemplateString));
await waitFor(() =>
expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
1,
),
);
const call = fetchMock.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call[1].body);
const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call.options.body);
expect(body).toEqual({
sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
@@ -299,11 +317,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(queryEditorWithTemplateObject));
await waitFor(() =>
expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
1,
),
);
const call = fetchMock.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call[1].body);
const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call.options.body);
expect(body).toEqual({
sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
@@ -314,12 +334,11 @@ describe('async actions', () => {
test('dispatches QUERY_EDITOR_SET_SQL with formatted result', async () => {
const formattedSql = 'SELECT\n *\nFROM\n table';
fetchMock.post(
fetchMock.removeRoute(formatQueryEndpoint);
fetchMock.route(
formatQueryEndpoint,
{ result: formattedSql },
{
overwriteRoutes: true,
},
{ name: formatQueryEndpoint },
);
const queryEditorToFormat = {
@@ -365,11 +384,13 @@ describe('async actions', () => {
store.dispatch(actions.formatQuery(outdatedQueryEditor));
await waitFor(() =>
expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength(
1,
),
);
const call = fetchMock.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call[1].body);
const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0];
const body = JSON.parse(call.options.body);
expect(body.sql).toBe('SELECT * FROM updated_table');
expect(body.database_id).toBe(10);
@@ -388,7 +409,7 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1);
});
});
@@ -402,7 +423,7 @@ describe('async actions', () => {
test.skip('parses large number result without losing precision', () =>
makeRequest().then(() => {
expect(fetchMock.calls(fetchQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2);
expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe(
mockBigNumber,
@@ -427,10 +448,11 @@ describe('async actions', () => {
test('calls queryFailed on fetch error', () => {
expect.assertions(1);
fetchMock.removeRoute(fetchQueryEndpoint);
fetchMock.get(
fetchQueryEndpoint,
{ throws: { message: 'error text' } },
{ overwriteRoutes: true },
{ name: fetchQueryEndpoint },
);
const store = mockStore({});
@@ -457,7 +479,7 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
});
});
@@ -469,9 +491,9 @@ describe('async actions', () => {
});
});
test.skip('parses large number result without losing precision', () =>
test('parses large number result without losing precision', () =>
makeRequest().then(() => {
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
expect(dispatch.callCount).toBe(2);
expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe(
mockBigNumber,
@@ -495,6 +517,7 @@ describe('async actions', () => {
test('calls queryFailed on fetch error and logs the error details', () => {
expect.assertions(2);
fetchMock.removeRoute(runQueryEndpoint);
fetchMock.post(
runQueryEndpoint,
{
@@ -504,7 +527,7 @@ describe('async actions', () => {
statusText: 'timeout',
},
},
{ overwriteRoutes: true },
{ name: runQueryEndpoint },
);
const store = mockStore({});
@@ -550,7 +573,9 @@ describe('async actions', () => {
`{ "data": ${mockBigNumber} }`,
);
await makeRequest().then(() => {
expect(fetchMock.calls(runQueryEndpointWithParams)).toHaveLength(1);
expect(
fetchMock.callHistory.calls(runQueryEndpointWithParams),
).toHaveLength(1);
});
});
});
@@ -591,7 +616,7 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
expect(fetchMock.calls(stopQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(stopQueryEndpoint)).toHaveLength(1);
});
});
@@ -607,8 +632,8 @@ describe('async actions', () => {
expect.assertions(1);
return makeRequest().then(() => {
const call = fetchMock.calls(stopQueryEndpoint)[0];
const body = JSON.parse(call[1].body);
const call = fetchMock.callHistory.calls(stopQueryEndpoint)[0];
const body = JSON.parse(call.options.body);
expect(body.client_id).toBe(baseQuery.id);
});
});
@@ -955,7 +980,7 @@ describe('async actions', () => {
isFeatureEnabled.mockRestore();
});
afterEach(() => fetchMock.resetHistory());
afterEach(() => fetchMock.clearHistory());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('addQueryEditor', () => {
@@ -978,7 +1003,9 @@ describe('async actions', () => {
store.dispatch(actions.addQueryEditor(queryEditor));
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(0);
});
});
@@ -1121,7 +1148,9 @@ describe('async actions', () => {
const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
return request(store.dispatch, store.getState).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(1);
});
});
});
@@ -1143,7 +1172,9 @@ describe('async actions', () => {
request(store.dispatch, store.getState);
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(0);
isFeatureEnabled.mockRestore();
});
});
@@ -1325,10 +1356,14 @@ describe('async actions', () => {
expectedActionTypes,
);
expect(store.getActions()[0].prepend).toBeFalsy();
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
expect(
fetchMock.callHistory.calls(updateTableSchemaEndpoint),
).toHaveLength(1);
// tab state is not updated, since no query was run
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(0);
});
});
});
@@ -1354,14 +1389,15 @@ describe('async actions', () => {
});
beforeEach(() => {
fetchMock.removeRoute(runQueryEndpoint);
fetchMock.post(runQueryEndpoint, JSON.stringify(results), {
overwriteRoutes: true,
name: runQueryEndpoint,
});
});
afterEach(() => {
store.clearActions();
fetchMock.resetHistory();
fetchMock.clearHistory();
});
test('updates and runs data preview query when configured', () => {
@@ -1382,9 +1418,11 @@ describe('async actions', () => {
expect(store.getActions().map(a => a.type)).toEqual(
expectedActionTypes,
);
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
// tab state is not updated, since the query is a data preview
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(0);
});
});
@@ -1406,9 +1444,11 @@ describe('async actions', () => {
expect(store.getActions().map(a => a.type)).toEqual(
expectedActionTypes,
);
expect(fetchMock.calls(runQueryEndpoint)).toHaveLength(1);
expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1);
// tab state is not updated, since the query is a data preview
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(0);
});
});
});
@@ -1428,13 +1468,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
call.url &&
call.url.includes('/tableschemaview/') &&
call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(1);
});
@@ -1454,7 +1494,7 @@ describe('async actions', () => {
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
@@ -1480,13 +1520,13 @@ describe('async actions', () => {
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
call.url &&
call.url.includes('/tableschemaview/') &&
call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
@@ -1510,13 +1550,13 @@ describe('async actions', () => {
return store.dispatch(actions.expandTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
// Check all POST calls to find the expanded endpoint
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
call.url &&
call.url.includes('/tableschemaview/') &&
call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
isFeatureEnabled.mockRestore();
@@ -1539,13 +1579,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
call.url &&
call.url.includes('/tableschemaview/') &&
call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(1);
});
@@ -1564,13 +1604,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
call.url &&
call.url.includes('/tableschemaview/') &&
call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
@@ -1589,13 +1629,13 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
call[0] &&
call[0].includes('/tableschemaview/') &&
call[0].includes('/expanded'),
call.url &&
call.url.includes('/tableschemaview/') &&
call.url.includes('/expanded'),
);
expect(expandedCalls).toHaveLength(0);
});
@@ -1618,7 +1658,7 @@ describe('async actions', () => {
];
return store.dispatch(actions.collapseTable(table)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
const expandedCalls = fetchMock
const expandedCalls = fetchMock.callHistory
.calls()
.filter(
call =>
@@ -1647,7 +1687,9 @@ describe('async actions', () => {
];
return store.dispatch(actions.removeTables([table])).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
expect(
fetchMock.callHistory.calls(updateTableSchemaEndpoint),
).toHaveLength(1);
});
});
@@ -1667,7 +1709,9 @@ describe('async actions', () => {
];
return store.dispatch(actions.removeTables(tables)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2);
expect(
fetchMock.callHistory.calls(updateTableSchemaEndpoint),
).toHaveLength(2);
});
});
@@ -1684,7 +1728,9 @@ describe('async actions', () => {
];
return store.dispatch(actions.removeTables(tables)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
expect(
fetchMock.callHistory.calls(updateTableSchemaEndpoint),
).toHaveLength(1);
});
});
});
@@ -1699,8 +1745,9 @@ describe('async actions', () => {
query: { sqlEditorId: 'null' },
query_id: 'efgh',
};
fetchMock.removeRoute(runQueryEndpoint);
fetchMock.post(runQueryEndpoint, JSON.stringify(results), {
overwriteRoutes: true,
name: runQueryEndpoint,
});
const oldQueryEditor = { ...queryEditor, inLocalStorage: true };
@@ -1777,10 +1824,14 @@ describe('async actions', () => {
.dispatch(actions.syncQueryEditor(oldQueryEditor))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(3);
expect(
fetchMock.callHistory.calls(updateTabStateEndpoint),
).toHaveLength(3);
// query editor has 2 tables loaded in the schema viewer
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(2);
expect(
fetchMock.callHistory.calls(updateTableSchemaEndpoint),
).toHaveLength(2);
});
});
});

View File

@@ -53,7 +53,11 @@ const StyledContainer = styled.div`
const StyledSidebar = styled.div`
position: relative;
padding: ${({ theme }) => theme.sizeUnit * 2.5}px;
padding: ${({ theme }) => theme.sizeUnit * 2.5}px 0;
margin: 0 ${({ theme }) => theme.sizeUnit * 2.5}px;
flex: 1;
height: 100%;
background-color: ${({ theme }) => theme.colorBgBase};
`;
const ContentWrapper = styled.div`

View File

@@ -74,13 +74,13 @@ beforeEach(() => {
});
afterEach(() => {
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
});
test('sync the unsaved editor tab state when there are new changes since the last update', async () => {
const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
fetchMock.put(updateEditorTabState, 200);
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
@@ -91,14 +91,14 @@ test('sync the unsaved editor tab state when there are new changes since the las
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(1);
fetchMock.restore();
expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(1);
fetchMock.clearHistory().removeRoutes();
});
test('sync the unsaved NEW editor state when there are new in local storage', async () => {
const createEditorTabState = `glob:*/tabstateview/`;
fetchMock.post(createEditorTabState, { id: 123 });
expect(fetchMock.calls(createEditorTabState)).toHaveLength(0);
expect(fetchMock.callHistory.calls(createEditorTabState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
@@ -119,12 +119,14 @@ test('sync the unsaved NEW editor state when there are new in local storage', as
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
expect(fetchMock.calls(createEditorTabState)).toHaveLength(1);
fetchMock.restore();
expect(fetchMock.callHistory.calls(createEditorTabState)).toHaveLength(1);
fetchMock.clearHistory().removeRoutes();
});
test('sync the active editor id when there are updates in tab history', async () => {
expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(0);
expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength(
0,
);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
@@ -147,18 +149,22 @@ test('sync the active editor id when there are updates in tab history', async ()
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1);
expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength(
1,
);
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
expect(fetchMock.calls(updateActiveEditorTabState)).toHaveLength(1);
expect(fetchMock.callHistory.calls(updateActiveEditorTabState)).toHaveLength(
1,
);
});
test('sync the destroyed editor id when there are updates in destroyed editors', async () => {
const removeId = 'removed-tab-id';
const deleteEditorState = `glob:*/tabstateview/${removeId}`;
fetchMock.delete(deleteEditorState, { id: removeId });
expect(fetchMock.calls(deleteEditorState)).toHaveLength(0);
expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
@@ -174,17 +180,17 @@ test('sync the destroyed editor id when there are updates in destroyed editors',
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(1);
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
expect(fetchMock.calls(deleteEditorState)).toHaveLength(1);
expect(fetchMock.callHistory.calls(deleteEditorState)).toHaveLength(1);
});
test('skip syncing the unsaved editor tab state when the updates are already synced', async () => {
const updateEditorTabState = `glob:*/tabstateview/${defaultQueryEditor.id}`;
fetchMock.put(updateEditorTabState, 200);
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
render(<EditorAutoSync />, {
useRedux: true,
initialState: {
@@ -203,8 +209,8 @@ test('skip syncing the unsaved editor tab state when the updates are already syn
await act(async () => {
jest.advanceTimersByTime(INTERVAL);
});
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
fetchMock.restore();
expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
fetchMock.clearHistory().removeRoutes();
});
test('renders an error toast when the sync failed', async () => {
@@ -212,7 +218,7 @@ test('renders an error toast when the sync failed', async () => {
fetchMock.put(updateEditorTabState, {
throws: new Error('errorMessage'),
});
expect(fetchMock.calls(updateEditorTabState)).toHaveLength(0);
expect(fetchMock.callHistory.calls(updateEditorTabState)).toHaveLength(0);
render(
<>
<EditorAutoSync />
@@ -235,5 +241,5 @@ test('renders an error toast when the sync failed', async () => {
'An error occurred while saving your editor state.',
expect.anything(),
);
fetchMock.restore();
fetchMock.clearHistory().removeRoutes();
});

View File

@@ -50,14 +50,16 @@ jest.mock('@superset-ui/core', () => ({
}));
afterEach(() => {
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
act(() => {
store.dispatch(api.util.resetApiState());
});
});
beforeEach(() => {
fetchMock.post(queryValidationApiRoute, fakeApiResult);
fetchMock.post(queryValidationApiRoute, fakeApiResult, {
name: queryValidationApiRoute,
});
});
const initialize = (withValidator = false) => {
@@ -115,13 +117,15 @@ const initialize = (withValidator = false) => {
test('skips fetching validation if validator is undefined', () => {
const { result } = initialize();
expect(result.current.data).toEqual([]);
expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(0);
expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength(0);
});
test('returns validation if validator is configured', async () => {
const { result, waitFor } = initialize(true);
await waitFor(() =>
expect(fetchMock.calls(queryValidationApiRoute)).toHaveLength(1),
expect(fetchMock.callHistory.calls(queryValidationApiRoute)).toHaveLength(
1,
),
);
expect(result.current.data).toEqual(
fakeApiResult.result.map(err => ({
@@ -135,13 +139,10 @@ test('returns validation if validator is configured', async () => {
test('returns server error description', async () => {
const errorMessage = 'Unexpected validation api error';
fetchMock.post(
queryValidationApiRoute,
{
throws: new Error(errorMessage),
},
{ overwriteRoutes: true },
);
fetchMock.removeRoute(queryValidationApiRoute);
fetchMock.post(queryValidationApiRoute, {
throws: new Error(errorMessage),
});
const { result, waitFor } = initialize(true);
await waitFor(
() =>
@@ -159,13 +160,10 @@ test('returns server error description', async () => {
test('returns session expire description when CSRF token expired', async () => {
const errorMessage = 'CSRF token expired';
fetchMock.post(
queryValidationApiRoute,
{
throws: new Error(errorMessage),
},
{ overwriteRoutes: true },
);
fetchMock.removeRoute(queryValidationApiRoute);
fetchMock.post(queryValidationApiRoute, {
throws: new Error(errorMessage),
});
const { result, waitFor } = initialize(true);
await waitFor(
() =>

View File

@@ -94,7 +94,7 @@ beforeEach(() => {
});
afterEach(() => {
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
act(() => {
store.dispatch(api.util.resetApiState());
});
@@ -120,7 +120,7 @@ test('returns keywords including fetched function_names data', async () => {
);
await waitFor(() =>
expect(fetchMock.calls(dbFunctionNamesApiRoute).length).toBe(1),
expect(fetchMock.callHistory.calls(dbFunctionNamesApiRoute).length).toBe(1),
);
fakeSchemaApiResult.forEach(schema => {
expect(result.current).toContainEqual(
@@ -171,7 +171,7 @@ test('skip fetching if autocomplete skipped', () => {
},
);
expect(result.current).toEqual([]);
expect(fetchMock.calls()).toEqual([]);
expect(fetchMock.callHistory.calls()).toEqual([]);
});
test('returns column keywords among selected tables', async () => {

View File

@@ -62,7 +62,7 @@ describe('ExploreCtasResultsButton', () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
fetchMock.post(getOrCreateTableEndpoint, { result: { table_id: 1234 } });
fireEvent.click(getByText('Explore'));
@@ -80,7 +80,7 @@ describe('ExploreCtasResultsButton', () => {
const { getByText } = setup({}, mockStore(initialState));
postFormSpy.mockClear();
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
fetchMock.post(getOrCreateTableEndpoint, {
throws: new Error('Unexpected all to v1 API'),
});

View File

@@ -57,7 +57,7 @@ beforeEach(() => {
});
afterEach(() => {
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
});
let replaceState = jest.spyOn(window.history, 'replaceState');
@@ -78,7 +78,7 @@ test('should handle id', async () => {
setup('/sqllab?id=1');
await waitFor(() =>
expect(
fetchMock.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`),
fetchMock.callHistory.calls(`glob:*/api/v1/sqllab/permalink/kv:${id}`),
).toHaveLength(1),
);
expect(replaceState).toHaveBeenCalledWith(
@@ -86,7 +86,7 @@ test('should handle id', async () => {
expect.anything(),
'/sqllab',
);
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
});
test('should handle permalink', async () => {
const key = '9sadkfl';
@@ -98,7 +98,7 @@ test('should handle permalink', async () => {
setup('/sqllab/p/9sadkfl');
await waitFor(() =>
expect(
fetchMock.calls(`glob:*/api/v1/sqllab/permalink/${key}`),
fetchMock.callHistory.calls(`glob:*/api/v1/sqllab/permalink/${key}`),
).toHaveLength(1),
);
expect(replaceState).toHaveBeenCalledWith(
@@ -106,12 +106,14 @@ test('should handle permalink', async () => {
expect.anything(),
'/sqllab',
);
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
});
test('should handle savedQueryId', async () => {
setup('/sqllab?savedQueryId=1');
await waitFor(() =>
expect(fetchMock.calls('glob:*/api/v1/saved_query/1')).toHaveLength(1),
expect(
fetchMock.callHistory.calls('glob:*/api/v1/saved_query/1'),
).toHaveLength(1),
);
expect(replaceState).toHaveBeenCalledWith(
expect.anything(),

View File

@@ -57,7 +57,7 @@ describe('QueryAutoRefresh', () => {
});
afterEach(() => {
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
cleanup();
jest.runOnlyPendingTimers();
jest.useRealTimers();
@@ -162,7 +162,7 @@ describe('QueryAutoRefresh', () => {
expect(
store.getActions().filter(({ type }) => type === REFRESH_QUERIES),
).toHaveLength(0);
expect(fetchMock.calls(refreshApi)).toHaveLength(1);
expect(fetchMock.callHistory.calls(refreshApi)).toHaveLength(1);
});
test('Does not fail and attempts to refresh with mixed valid/invalid queries', async () => {
@@ -217,7 +217,7 @@ describe('QueryAutoRefresh', () => {
),
);
expect(fetchMock.calls(refreshApi)).toHaveLength(0);
expect(fetchMock.callHistory.calls(refreshApi)).toHaveLength(0);
});
test('logs the failed error for async queries', async () => {

View File

@@ -81,7 +81,7 @@ const setup = (overrides = {}) => (
<QueryHistory {...mockedProps} {...overrides} />
);
afterEach(() => fetchMock.reset());
afterEach(() => fetchMock.clearHistory().removeRoutes());
test('Renders an empty state for query history', () => {
render(setup(), { useRedux: true, initialState });
@@ -102,7 +102,7 @@ test('fetches the query history when the persistence mode is enabled', async ()
fetchMock.get(editorQueryApiRoute, fakeApiResult);
render(setup(), { useRedux: true, initialState });
await waitFor(() =>
expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1),
expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1),
);
const queryResultText = screen.getByText(fakeApiResult.result[0].rows);
expect(queryResultText).toBeInTheDocument();
@@ -127,7 +127,7 @@ test('fetches the query history by the tabViewId', async () => {
},
});
await waitFor(() =>
expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1),
expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1),
);
const queryResultText = screen.getByText(fakeApiResult.result[0].rows);
expect(queryResultText).toBeInTheDocument();
@@ -213,7 +213,7 @@ test('displays multiple queries with newest query first', async () => {
const { container } = render(setup(), { useRedux: true, initialState });
await waitFor(() =>
expect(fetchMock.calls(editorQueryApiRoute).length).toBe(1),
expect(fetchMock.callHistory.calls(editorQueryApiRoute).length).toBe(1),
);
expect(screen.getByTestId('listview-table')).toBeVisible();

View File

@@ -127,7 +127,7 @@ fetchMock.post(reRunQueryEndpoint, { result: [] });
fetchMock.get('glob:*/api/v1/sqllab/results/*', { result: [] });
beforeEach(() => {
fetchMock.resetHistory();
fetchMock.clearHistory();
});
const middlewares = [thunk];
@@ -151,7 +151,7 @@ describe('ResultSet', () => {
// Add cleanup after each test
afterEach(async () => {
fetchMock.resetHistory();
fetchMock.clearHistory();
// Wait for any pending effects to complete
await new Promise(resolve => setTimeout(resolve, 0));
});
@@ -250,7 +250,7 @@ describe('ResultSet', () => {
},
});
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(0);
setup(mockedProps, store);
expect(store.getActions()).toHaveLength(1);
expect(store.getActions()[0].query.errorMessage).toEqual(
@@ -258,7 +258,7 @@ describe('ResultSet', () => {
);
expect(store.getActions()[0].type).toEqual('START_QUERY');
await waitFor(() =>
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(1),
expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(1),
);
});
@@ -276,7 +276,7 @@ describe('ResultSet', () => {
});
setup(mockedProps, store);
expect(store.getActions()).toEqual([]);
expect(fetchMock.calls(reRunQueryEndpoint)).toHaveLength(0);
expect(fetchMock.callHistory.calls(reRunQueryEndpoint)).toHaveLength(0);
});
test('should render cached query', async () => {
@@ -622,7 +622,9 @@ describe('ResultSet', () => {
});
// Verify the API was called
const resultsCalls = fetchMock.calls('glob:*/api/v1/sqllab/results/*');
const resultsCalls = fetchMock.callHistory.calls(
'glob:*/api/v1/sqllab/results/*',
);
expect(resultsCalls).toHaveLength(1);
});

View File

@@ -63,6 +63,7 @@ export type QueryPayload = {
const Styles = styled.span`
display: contents;
white-space: nowrap;
span[role='img']:not([aria-label='down']) {
display: flex;
margin: 0;

View File

@@ -82,18 +82,17 @@ describe('ShareSqlLabQuery', () => {
const storeQueryMockId = 'ci39c3';
beforeEach(async () => {
fetchMock.removeRoute(storeQueryUrl);
fetchMock.post(
storeQueryUrl,
() => ({ key: storeQueryMockId, url: `/p/${storeQueryMockId}` }),
{
overwriteRoutes: true,
},
{ name: storeQueryUrl },
);
fetchMock.resetHistory();
fetchMock.clearHistory();
jest.clearAllMocks();
});
afterAll(() => fetchMock.reset());
afterAll(() => fetchMock.hardReset());
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('via permalink api', () => {
@@ -116,10 +115,12 @@ describe('ShareSqlLabQuery', () => {
const expected = omit(mockQueryEditor, ['id', 'remoteId']);
userEvent.click(button);
await waitFor(() =>
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
expect(fetchMock.callHistory.calls(storeQueryUrl)).toHaveLength(1),
);
expect(
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
JSON.parse(
fetchMock.callHistory.calls(storeQueryUrl)[0].options?.body as string,
),
).toEqual(expected);
});
@@ -140,10 +141,12 @@ describe('ShareSqlLabQuery', () => {
const expected = omit(unsavedQueryEditor, ['id']);
userEvent.click(button);
await waitFor(() =>
expect(fetchMock.calls(storeQueryUrl)).toHaveLength(1),
expect(fetchMock.callHistory.calls(storeQueryUrl)).toHaveLength(1),
);
expect(
JSON.parse(fetchMock.calls(storeQueryUrl)[0][1]?.body as string),
JSON.parse(
fetchMock.callHistory.calls(storeQueryUrl)[0].options?.body as string,
),
).toEqual(expected);
});
});

View File

@@ -75,6 +75,15 @@ jest.mock('@superset-ui/core/components/AsyncAceEditor', () => ({
}));
jest.mock('src/SqlLab/components/ResultSet', () => jest.fn());
jest.mock('src/components/DatabaseSelector', () => ({
__esModule: true,
DatabaseSelector: ({ sqlLabMode }: { sqlLabMode?: boolean }) => (
<div data-test="mock-database-selector" data-sqllab-mode={sqlLabMode}>
Mock DatabaseSelector
</div>
),
}));
fetchMock.get('glob:*/api/v1/database/*/function_names/', {
function_names: [],
});
@@ -389,8 +398,8 @@ describe('SqlEditor', () => {
// click button
fireEvent.click(button);
await waitFor(() => {
expect(fetchMock.lastUrl()).toEqual(estimateApi);
expect(fetchMock.lastOptions()).toEqual(
expect(fetchMock.callHistory.lastCall()?.url).toEqual(estimateApi);
expect(fetchMock.callHistory.lastCall()?.options).toEqual(
expect.objectContaining({
body: JSON.stringify({
database_id: 2023,
@@ -402,11 +411,11 @@ describe('SqlEditor', () => {
cache: 'default',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRFToken': '1234',
accept: 'application/json',
'content-type': 'application/json',
'x-csrftoken': '1234',
},
method: 'POST',
method: 'post',
mode: 'same-origin',
redirect: 'follow',
signal: undefined,
@@ -443,10 +452,12 @@ describe('SqlEditor', () => {
const indicator = getByTestId('sqlEditor-loading');
expect(indicator).toBeInTheDocument();
await waitFor(() =>
expect(fetchMock.calls('glob:*/tabstateview/*').length).toBe(1),
expect(
fetchMock.callHistory.calls('glob:*/tabstateview/*').length,
).toBe(1),
);
// it will be called from EditorAutoSync
expect(fetchMock.calls(switchTabApi).length).toBe(0);
expect(fetchMock.callHistory.calls(switchTabApi).length).toBe(0);
});
});
});

View File

@@ -22,7 +22,6 @@ import {
screen,
userEvent,
waitFor,
within,
} from 'spec/helpers/testing-library';
import SqlEditorLeftBar, {
SqlEditorLeftBarProps,
@@ -31,12 +30,31 @@ import {
table,
initialState,
defaultQueryEditor,
extraQueryEditor1,
extraQueryEditor2,
} from 'src/SqlLab/fixtures';
import type { RootState } from 'src/views/store';
import type { Store } from 'redux';
// Mock TableExploreTree to avoid complex tree rendering in tests
jest.mock('../TableExploreTree', () => ({
__esModule: true,
default: () => (
<div data-test="mock-table-explore-tree">TableExploreTree</div>
),
}));
// Helper to switch from default TreeView to SelectView
const switchToSelectView = async () => {
const changeButton = screen.getByTestId('DatabaseSelector');
// Click Change button to open database selector modal
await userEvent.click(changeButton);
// Verify popup is opened
await waitFor(() => {
expect(screen.getByText('Select Database and Schema')).toBeInTheDocument();
});
};
const mockedProps = {
queryEditorId: defaultQueryEditor.id,
height: 0,
@@ -92,7 +110,7 @@ beforeEach(() => {
});
afterEach(() => {
fetchMock.restore();
fetchMock.clearHistory().removeRoutes();
jest.clearAllMocks();
});
@@ -109,81 +127,25 @@ const renderAndWait = (
}),
);
test('renders a TableElement', async () => {
const { findByText, getAllByTestId } = await renderAndWait(
mockedProps,
undefined,
{
...initialState,
sqlLab: {
...initialState.sqlLab,
tables: [table],
databases: { [mockData.database.id]: { ...mockData.database } },
},
},
);
expect(await findByText(/Database/i)).toBeInTheDocument();
const tableElement = getAllByTestId('table-element');
expect(tableElement.length).toBeGreaterThanOrEqual(1);
});
test('table should be visible when expanded is true', async () => {
const { container, getByText, getByRole, getAllByLabelText } =
await renderAndWait(mockedProps, undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
tables: [table],
databases: { [mockData.database.id]: { ...mockData.database } },
},
});
const dbSelect = getByRole('combobox', {
name: 'Select database or type to search databases',
});
const schemaSelect = getByRole('combobox', {
name: 'Select schema or type to search schemas: main',
});
const tableSelect = getAllByLabelText(
/Select table or type to search tables/i,
)[0];
const tableOption = within(tableSelect).getByText(/ab_user/i);
expect(getByText(/Database/i)).toBeInTheDocument();
expect(dbSelect).toBeInTheDocument();
expect(schemaSelect).toBeInTheDocument();
expect(tableSelect).toBeInTheDocument();
expect(tableOption).toBeInTheDocument();
expect(
container.querySelector('.ant-collapse-content-active'),
).toBeInTheDocument();
table.columns.forEach(({ name }) => {
expect(getByText(name)).toBeInTheDocument();
});
});
test('catalog selector should be visible when enabled in the database', async () => {
const { container, getByText, getByRole } = await renderAndWait(
mockedProps,
undefined,
{
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: mockedProps.queryEditorId,
dbId: mockData.database.id,
},
tables: [table],
databases: {
[mockData.database.id]: {
...mockData.database,
allow_multi_catalog: true,
},
const { getByRole } = await renderAndWait(mockedProps, undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: mockedProps.queryEditorId,
dbId: mockData.database.id,
},
tables: [table],
databases: {
[mockData.database.id]: {
...mockData.database,
allow_multi_catalog: true,
},
},
},
);
});
await switchToSelectView();
const dbSelect = getByRole('combobox', {
name: 'Select database or type to search databases',
@@ -191,131 +153,9 @@ test('catalog selector should be visible when enabled in the database', async ()
const catalogSelect = getByRole('combobox', {
name: 'Select catalog or type to search catalogs',
});
const schemaSelect = getByRole('combobox', {
name: 'Select schema or type to search schemas',
});
const dropdown = getByText(/Select table/i);
const abUser = getByText(/ab_user/i);
expect(getByText(/Database/i)).toBeInTheDocument();
expect(dbSelect).toBeInTheDocument();
expect(catalogSelect).toBeInTheDocument();
expect(schemaSelect).toBeInTheDocument();
expect(dropdown).toBeInTheDocument();
expect(abUser).toBeInTheDocument();
expect(
container.querySelector('.ant-collapse-content-active'),
).toBeInTheDocument();
table.columns.forEach(({ name }) => {
expect(getByText(name)).toBeInTheDocument();
});
});
test('should toggle the table when the header is clicked', async () => {
const { container } = await renderAndWait(mockedProps, undefined, {
...initialState,
sqlLab: {
...initialState.sqlLab,
tables: [table],
unsavedQueryEditor: {
id: mockedProps.queryEditorId,
dbId: mockData.database.id,
},
databases: {
[mockData.database.id]: {
...mockData.database,
allow_multi_catalog: true,
},
},
},
});
const header = container.querySelector('.ant-collapse-header');
expect(header).toBeInTheDocument();
if (header) {
userEvent.click(header);
}
await waitFor(() =>
expect(
container.querySelector('.ant-collapse-content-inactive'),
).toBeInTheDocument(),
);
});
test('When changing database the schema and table list must be updated', async () => {
const reduxState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
unsavedQueryEditor: {
id: defaultQueryEditor.id,
schema: 'db1_schema',
dbId: mockData.database.id,
},
queryEditors: [
defaultQueryEditor,
{
...extraQueryEditor1,
schema: 'new_schema',
dbId: 2,
},
],
tables: [
{
...table,
dbId: defaultQueryEditor.dbId,
schema: 'db1_schema',
},
{
...table,
dbId: 2,
schema: 'new_schema',
name: 'new_table',
queryEditorId: extraQueryEditor1.id,
},
],
databases: {
[mockData.database.id]: {
...mockData.database,
allow_multi_catalog: true,
},
2: {
id: 2,
database_name: 'new_db',
backend: 'postgresql',
},
},
},
};
const { rerender } = await renderAndWait(mockedProps, undefined, reduxState);
expect(screen.getAllByText(/main/i)[0]).toBeInTheDocument();
expect(screen.getAllByText(/ab_user/i)[0]).toBeInTheDocument();
rerender(
<SqlEditorLeftBar {...mockedProps} queryEditorId={extraQueryEditor1.id} />,
);
const updatedDbSelector = await screen.findAllByText(/new_db/i);
expect(updatedDbSelector[0]).toBeInTheDocument();
const select = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
});
userEvent.click(select);
expect(
await screen.findByRole('option', { name: 'main' }),
).toBeInTheDocument();
expect(
await screen.findByRole('option', { name: 'new_schema' }),
).toBeInTheDocument();
userEvent.click(screen.getByText('new_schema'));
const updatedTableSelector = await screen.findAllByText(/new_table/i);
expect(updatedTableSelector[0]).toBeInTheDocument();
});
test('display no compatible schema found when schema api throws errors', async () => {
@@ -351,10 +191,12 @@ test('display no compatible schema found when schema api throws errors', async (
undefined,
reduxState,
);
await switchToSelectView();
await waitFor(() =>
expect(fetchMock.calls('glob:*/api/v1/database/3/schemas/?*')).toHaveLength(
1,
),
expect(
fetchMock.callHistory.calls('glob:*/api/v1/database/3/schemas/?*').length,
).toBeGreaterThanOrEqual(1),
);
const select = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
@@ -384,17 +226,12 @@ test('ignore schema api when current schema is deprecated', async () => {
},
},
});
expect(await screen.findByText(/Database/i)).toBeInTheDocument();
expect(fetchMock.calls()).not.toContainEqual(
await switchToSelectView();
expect(fetchMock.callHistory.calls()).not.toContainEqual(
expect.arrayContaining([
expect.stringContaining(
`/tables/${mockData.database.id}/${invalidSchemaName}/`,
),
]),
);
// Deselect the deprecated schema selection
await waitFor(() =>
expect(screen.queryByText(/None/i)).not.toBeInTheDocument(),
);
});

View File

@@ -16,36 +16,35 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useCallback, useMemo, useState } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { SqlLabRootState, Table } from 'src/SqlLab/types';
import { resetState } from 'src/SqlLab/actions/sqlLab';
import {
addTable,
removeTables,
collapseTable,
expandTable,
resetState,
} from 'src/SqlLab/actions/sqlLab';
import { Button, EmptyState, Icons } from '@superset-ui/core/components';
Button,
EmptyState,
Flex,
Icons,
Popover,
Typography,
} from '@superset-ui/core/components';
import { t } from '@apache-superset/core';
import { styled, css } from '@apache-superset/core/ui';
import { TableSelectorMultiple } from 'src/components/TableSelector';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import { noop } from 'lodash';
import TableElement from '../TableElement';
import type { SchemaOption, CatalogOption } from 'src/hooks/apiResources';
import { DatabaseSelector, type DatabaseObject } from 'src/components';
import useDatabaseSelector from '../SqlEditorTopBar/useDatabaseSelector';
import TableExploreTree from '../TableExploreTree';
export interface SqlEditorLeftBarProps {
queryEditorId: string;
}
const StyledScrollbarContainer = styled.div`
flex: 1 1 auto;
overflow: auto;
`;
const LeftBarStyles = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.sizeUnit * 2}px;
${({ theme }) => css`
height: 100%;
display: flex;
@@ -53,117 +52,153 @@ const LeftBarStyles = styled.div`
.divider {
border-bottom: 1px solid ${theme.colorSplit};
margin: ${theme.sizeUnit * 4}px 0;
margin: ${theme.sizeUnit * 1}px 0;
}
`}
`;
const StyledDivider = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.colorSplit};
margin: 0 -${({ theme }) => theme.sizeUnit * 2.5}px 0;
`;
const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
const { db: userSelectedDb, ...dbSelectorProps } =
useDatabaseSelector(queryEditorId);
const allSelectedTables = useSelector<SqlLabRootState, Table[]>(
({ sqlLab }) =>
sqlLab.tables.filter(table => table.queryEditorId === queryEditorId),
shallowEqual,
);
const dbSelectorProps = useDatabaseSelector(queryEditorId);
const { db, catalog, schema, onDbChange, onCatalogChange, onSchemaChange } =
dbSelectorProps;
const dispatch = useDispatch();
const queryEditor = useQueryEditor(queryEditorId, [
'dbId',
'catalog',
'schema',
'tabViewId',
]);
const [_emptyResultsWithSearch, setEmptyResultsWithSearch] = useState(false);
const { dbId, schema } = queryEditor;
const tables = useMemo(
() =>
allSelectedTables.filter(
table => table.dbId === dbId && table.schema === schema,
),
[allSelectedTables, dbId, schema],
const shouldShowReset = window.location.search === '?reset=1';
// Modal state for Database/Catalog/Schema selector
const [selectorModalOpen, setSelectorModalOpen] = useState(false);
const [modalDb, setModalDb] = useState<DatabaseObject | undefined>(undefined);
const [modalCatalog, setModalCatalog] = useState<
CatalogOption | null | undefined
>(undefined);
const [modalSchema, setModalSchema] = useState<SchemaOption | undefined>(
undefined,
);
noop(_emptyResultsWithSearch); // This is to avoid unused variable warning, can be removed if not needed
const openSelectorModal = useCallback(() => {
setModalDb(db ?? undefined);
setModalCatalog(
catalog ? { label: catalog, value: catalog, title: catalog } : undefined,
);
setModalSchema(
schema ? { label: schema, value: schema, title: schema } : undefined,
);
setSelectorModalOpen(true);
}, [db, catalog, schema]);
const onEmptyResults = useCallback((searchText?: string) => {
setEmptyResultsWithSearch(!!searchText);
const closeSelectorModal = useCallback(() => {
setSelectorModalOpen(false);
}, []);
const selectedTableNames = useMemo(
() => tables?.map(table => table.name) || [],
[tables],
);
const onTablesChange = (
tableNames: string[],
catalogName: string | null,
schemaName: string,
) => {
if (!schemaName) {
return;
const handleModalOk = useCallback(() => {
if (modalDb && modalDb.id !== db?.id) {
onDbChange?.(modalDb);
}
const currentTables = [...tables];
const tablesToAdd = tableNames.filter(name => {
const index = currentTables.findIndex(table => table.name === name);
if (index >= 0) {
currentTables.splice(index, 1);
return false;
}
return true;
});
tablesToAdd.forEach(tableName => {
dispatch(addTable(queryEditor, tableName, catalogName, schemaName));
});
dispatch(removeTables(currentTables));
};
const onToggleTable = (updatedTables: string[]) => {
tables.forEach(table => {
if (!updatedTables.includes(table.id.toString()) && table.expanded) {
dispatch(collapseTable(table));
} else if (
updatedTables.includes(table.id.toString()) &&
!table.expanded
) {
dispatch(expandTable(table));
}
});
};
const shouldShowReset = window.location.search === '?reset=1';
if (modalCatalog?.value !== catalog) {
onCatalogChange?.(modalCatalog?.value);
}
if (modalSchema?.value !== schema) {
onSchemaChange?.(modalSchema?.value ?? '');
}
setSelectorModalOpen(false);
}, [
modalDb,
modalCatalog,
modalSchema,
db,
catalog,
schema,
onDbChange,
onCatalogChange,
onSchemaChange,
]);
const handleResetState = useCallback(() => {
dispatch(resetState());
}, [dispatch]);
const popoverContent = (
<Flex
vertical
gap="middle"
data-test="DatabaseSelector"
css={css`
min-width: 500px;
`}
>
<Typography.Title level={5} style={{ margin: 0 }}>
{t('Select Database and Schema')}
</Typography.Title>
<DatabaseSelector
key={modalDb ? modalDb.id : 'no-db'}
db={modalDb}
emptyState={<EmptyState />}
getDbList={dbSelectorProps.getDbList}
handleError={dbSelectorProps.handleError}
onDbChange={setModalDb}
onCatalogChange={cat =>
setModalCatalog(
cat ? { label: cat, value: cat, title: cat } : undefined,
)
}
catalog={modalCatalog?.value}
onSchemaChange={sch =>
setModalSchema(
sch ? { label: sch, value: sch, title: sch } : undefined,
)
}
schema={modalSchema?.value}
sqlLabMode={false}
/>
<Flex justify="flex-end" gap="small">
<Button
buttonStyle="tertiary"
onClick={e => {
e?.stopPropagation();
closeSelectorModal();
}}
>
{t('Cancel')}
</Button>
<Button
type="primary"
onClick={e => {
e?.stopPropagation();
handleModalOk();
}}
>
{t('Select')}
</Button>
</Flex>
</Flex>
);
return (
<LeftBarStyles data-test="sql-editor-left-bar">
<TableSelectorMultiple
{...dbSelectorProps}
onEmptyResults={onEmptyResults}
emptyState={<EmptyState />}
database={userSelectedDb}
onTableSelectChange={onTablesChange}
tableValue={selectedTableNames}
sqlLabMode
/>
<div className="divider" />
<StyledScrollbarContainer>
{tables.map(table => (
<TableElement
table={table}
key={table.id}
activeKey={tables
.filter(({ expanded }) => expanded)
.map(({ id }) => id)}
onChange={onToggleTable}
/>
))}
</StyledScrollbarContainer>
<Popover
content={popoverContent}
open={selectorModalOpen}
onOpenChange={open => !open && closeSelectorModal()}
placement="bottomLeft"
trigger="click"
>
<DatabaseSelector
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
schema ?? 'no-schema'
}`}
{...dbSelectorProps}
emptyState={<EmptyState />}
sqlLabMode
onOpenModal={openSelectorModal}
/>
</Popover>
<StyledDivider />
<TableExploreTree queryEditorId={queryEditorId} />
{shouldShowReset && (
<Button
buttonSize="small"

View File

@@ -38,12 +38,14 @@ const SqlEditorTopBar = ({
defaultSecondaryActions,
}: SqlEditorTopBarProps) => (
<StyledFlex justify="space-between" gap="small" id="js-sql-toolbar">
<Flex flex={1} gap="small" align="center">
<PanelToolbar
viewId={ViewContribution.Editor}
defaultPrimaryActions={defaultPrimaryActions}
defaultSecondaryActions={defaultSecondaryActions}
/>
<Flex gap="small" align="center">
<Flex gap="small" align="center">
<PanelToolbar
viewId={ViewContribution.Editor}
defaultPrimaryActions={defaultPrimaryActions}
defaultSecondaryActions={defaultSecondaryActions}
/>
</Flex>
</Flex>
</StyledFlex>
);

View File

@@ -67,9 +67,9 @@ export default function useDatabaseSelector(queryEditorId: string) {
);
const handleCatalogChange = useCallback(
(catalog: string | null) => {
(catalog?: string | null) => {
if (queryEditor) {
dispatch(queryEditorSetCatalog(queryEditor, catalog));
dispatch(queryEditorSetCatalog(queryEditor, catalog ?? null));
}
},
[dispatch, queryEditor],

View File

@@ -49,7 +49,7 @@ beforeEach(() => {
});
afterEach(() => {
fetchMock.reset();
fetchMock.clearHistory().removeRoutes();
});
test('should removeQueryEditor', async () => {

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