mirror of
https://github.com/apache/superset.git
synced 2026-05-02 14:34:22 +00:00
Compare commits
68 Commits
fix-docker
...
engine-man
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb4b5f3a6 | ||
|
|
c00fae53a5 | ||
|
|
99935fc035 | ||
|
|
d06ccf5152 | ||
|
|
62d2d82ed8 | ||
|
|
688224c4c0 | ||
|
|
de8c250f86 | ||
|
|
be31abeb7e | ||
|
|
5f61bb8d76 | ||
|
|
bb5a15dc5a | ||
|
|
929b0337f4 | ||
|
|
baf6e03d16 | ||
|
|
e82e06891b | ||
|
|
5753dfbb6e | ||
|
|
45f883c9cd | ||
|
|
8fd3401077 | ||
|
|
89a98ab9a4 | ||
|
|
2dfc770b0f | ||
|
|
6b7b23ed78 | ||
|
|
5ac5480f35 | ||
|
|
76889c1a69 | ||
|
|
569606635b | ||
|
|
66264856a7 | ||
|
|
3eb860a663 | ||
|
|
a44980da65 | ||
|
|
7112bce961 | ||
|
|
568486a304 | ||
|
|
fea135b46c | ||
|
|
601fcb3382 | ||
|
|
0d7cc88b2b | ||
|
|
32ee160c75 | ||
|
|
5914e83436 | ||
|
|
0b5e4dd5de | ||
|
|
3a565a6c16 | ||
|
|
f60c82e4a6 | ||
|
|
91131d5996 | ||
|
|
4b0d497513 | ||
|
|
86f690d17f | ||
|
|
e9b494163b | ||
|
|
be404f9b84 | ||
|
|
11257c0536 | ||
|
|
f2b6c395cd | ||
|
|
2d35ed2391 | ||
|
|
bd65469091 | ||
|
|
a6a66ca483 | ||
|
|
4a7cdccdad | ||
|
|
61bd8f0cf2 | ||
|
|
ae10e105c2 | ||
|
|
901dca58f7 | ||
|
|
d95a3d8426 | ||
|
|
70b95ca1b9 | ||
|
|
004f02746f | ||
|
|
5d20dc57d7 | ||
|
|
05c2354997 | ||
|
|
6043e7e7e3 | ||
|
|
1ee14c5993 | ||
|
|
9764a84402 | ||
|
|
570cc3e5f8 | ||
|
|
66519c3a85 | ||
|
|
1f43138888 | ||
|
|
652d029a2d | ||
|
|
e67b1f5326 | ||
|
|
fa79a467e4 | ||
|
|
2cce0308d4 | ||
|
|
c7fd1a2f65 | ||
|
|
ab4f646ef6 | ||
|
|
d6029f5c8a | ||
|
|
c16e8f747c |
2
.github/workflows/superset-docs-verify.yml
vendored
2
.github/workflows/superset-docs-verify.yml
vendored
@@ -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"
|
||||
|
||||
@@ -27,6 +27,7 @@ repos:
|
||||
args: [--check-untyped-defs]
|
||||
exclude: ^superset-extensions-cli/
|
||||
additional_dependencies: [
|
||||
types-cachetools,
|
||||
types-simplejson,
|
||||
types-python-dateutil,
|
||||
types-requests,
|
||||
|
||||
50
UPDATING.md
50
UPDATING.md
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": [
|
||||
|
||||
13
docs/static/feature-flags.json
vendored
13
docs/static/feature-flags.json
vendored
@@ -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
BIN
docs/static/img/databases/alloydb.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
57
docs/static/img/databases/apache-iotdb.svg
vendored
Normal file
57
docs/static/img/databases/apache-iotdb.svg
vendored
Normal 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 |
BIN
docs/static/img/databases/apache-phoenix.png
vendored
Normal file
BIN
docs/static/img/databases/apache-phoenix.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/static/img/databases/neon.png
vendored
Normal file
BIN
docs/static/img/databases/neon.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
1
docs/static/img/databases/supabase.svg
vendored
Normal file
1
docs/static/img/databases/supabase.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.7 KiB |
337
docs/yarn.lock
337
docs/yarn.lock
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
501
superset-frontend/package-lock.json
generated
501
superset-frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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')} />;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -42,10 +42,12 @@ const customIcons = [
|
||||
'Error',
|
||||
'Full',
|
||||
'Layers',
|
||||
'Move',
|
||||
'Multiple',
|
||||
'Queued',
|
||||
'Redo',
|
||||
'Running',
|
||||
'Sigma',
|
||||
'Slack',
|
||||
'Square',
|
||||
'SortAsc',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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' ||
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function validateServerPagination(
|
||||
serverPagination: boolean,
|
||||
maxValueWithoutServerPagination: number,
|
||||
maxServer: number,
|
||||
) {
|
||||
): string | false {
|
||||
if (
|
||||
Number(v) > +maxValueWithoutServerPagination &&
|
||||
Number(v) <= maxServer &&
|
||||
|
||||
@@ -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')]
|
||||
: [];
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
217
superset-frontend/playwright/components/core/Menu.ts
Normal file
217
superset-frontend/playwright/components/core/Menu.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -106,6 +106,7 @@ describe('BigNumberWithTrendline transformProps', () => {
|
||||
subtitleFontSize: 14,
|
||||
forceTimestampFormatting: false,
|
||||
timeFormat: 'YYYY-MM-DD',
|
||||
xAxis: '__timestamp',
|
||||
yAxisFormat: 'SMART_NUMBER',
|
||||
compareLag: 1,
|
||||
compareSuffix: 'WoW',
|
||||
|
||||
@@ -47,6 +47,7 @@ export type BigNumberWithTrendlineFormData = BigNumberTotalFormData & {
|
||||
b: number;
|
||||
};
|
||||
compareLag?: string | number;
|
||||
xAxis: string;
|
||||
showXAxis?: boolean;
|
||||
showXAxisMinMaxLabels?: boolean;
|
||||
showYAxis?: boolean;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'))],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' };
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user