mirror of
https://github.com/apache/superset.git
synced 2026-09-01 13:01:33 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd89a41cb6 |
@@ -21,9 +21,6 @@ on:
|
||||
#schedule:
|
||||
# - cron: '0 0 * * *' # Runs daily at midnight UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
bump-python-package:
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
@@ -16,9 +16,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check_db_migration_conflict:
|
||||
name: Check DB migration conflict
|
||||
|
||||
@@ -14,9 +14,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-26.04
|
||||
|
||||
@@ -33,9 +33,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
enforce-single-migration-head:
|
||||
runs-on: ubuntu-26.04
|
||||
|
||||
@@ -7,9 +7,6 @@ on:
|
||||
pull_request:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
superbot-orglabel:
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
@@ -7,9 +7,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
|
||||
@@ -3,9 +3,6 @@ on:
|
||||
release:
|
||||
types: [published] # This makes it run only when a new released is published
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
latest-release:
|
||||
name: Add/update tag to new release
|
||||
|
||||
@@ -19,9 +19,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint-check:
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
@@ -17,9 +17,6 @@ env:
|
||||
GITHUB_ORG: ${{ github.repository_owner }}
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup-expired:
|
||||
name: Clean up expired showtime environments
|
||||
|
||||
@@ -26,9 +26,6 @@ env:
|
||||
GITHUB_REPO: ${{ github.event.repository.name }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: 🎪 Sync PR to desired state
|
||||
|
||||
@@ -26,9 +26,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-26.04
|
||||
|
||||
@@ -27,9 +27,6 @@ concurrency:
|
||||
group: helm-release
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-26.04
|
||||
|
||||
@@ -22,9 +22,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-26.04
|
||||
|
||||
@@ -14,9 +14,6 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-26.04
|
||||
|
||||
@@ -13,9 +13,6 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
supersetbot:
|
||||
runs-on: ubuntu-26.04
|
||||
|
||||
@@ -5,9 +5,6 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
+5
-49
@@ -46,43 +46,13 @@ import FAQSchema from '@site/src/components/FAQSchema';
|
||||
answer:
|
||||
'You need to register a free account at Mapbox.com, obtain an API key, and add it to your .env file at the key MAPBOX_API_KEY.',
|
||||
},
|
||||
{
|
||||
question: 'How to limit the timed refresh on a dashboard?',
|
||||
answer:
|
||||
'To exclude specific slices from the timed refresh process, add the timed_refresh_immune_slices key to the dashboard JSON Metadata field with the slice IDs to exclude.',
|
||||
},
|
||||
{
|
||||
question: "Why does 'flask fab' or Superset freeze, hang, or not respond when started (my home directory is NFS mounted)?",
|
||||
answer:
|
||||
'By default, Superset creates and uses an SQLite database at ~/.superset/superset.db. SQLite is known to not work well if used on NFS due to broken file locking. Override the path with the SUPERSET_HOME environment variable or change SQLALCHEMY_DATABASE_URI in superset_config.py.',
|
||||
},
|
||||
{
|
||||
question: 'What if the table schema changed?',
|
||||
answer:
|
||||
'Go to Data -> Datasets, click the edit icon next to the dataset whose schema has changed, and hit Sync columns from source from the Columns tab. The new columns will get merged.',
|
||||
},
|
||||
{
|
||||
question: 'What database engine can I use as a backend for Superset?',
|
||||
answer:
|
||||
'Superset is tested using MySQL, PostgreSQL, and SQLite backends for storing its internal metadata. While Superset supports many databases as data sources, only these are recommended for the metadata store in production.',
|
||||
},
|
||||
{
|
||||
question: 'How can I configure OAuth authentication and authorization?',
|
||||
answer:
|
||||
'Take a look at the Flask-AppBuilder OAuth configuration example, which shows how to configure OAuth authentication and authorization in Superset.',
|
||||
},
|
||||
{
|
||||
question: 'Is there a way to force the dashboard to use specific colors?',
|
||||
answer:
|
||||
'It is possible on a per-dashboard basis by providing a mapping of labels to colors in the JSON Metadata attribute using the label_colors key. You can use a full hex color, a named color, or the index in the current color palette.',
|
||||
},
|
||||
{
|
||||
question: 'How do I expand all chart descriptions on a dashboard by default?',
|
||||
answer:
|
||||
'Add the expand_all_slices key to the dashboard JSON Metadata field with a value of true. Charts that have already been manually expanded or collapsed keep that per-chart override regardless of the setting.',
|
||||
},
|
||||
{
|
||||
question: 'Does Superset work with [insert database engine here]?',
|
||||
question: 'Does Superset work with my database?',
|
||||
answer:
|
||||
'Superset supports any database with a Python SQLAlchemy dialect and DBAPI driver. Check the Connecting to Databases documentation for the full list of supported databases.',
|
||||
},
|
||||
@@ -91,30 +61,15 @@ import FAQSchema from '@site/src/components/FAQSchema';
|
||||
answer:
|
||||
'Yes, Superset has a public REST API documented using Swagger. Enable FAB_API_SWAGGER_UI in superset_config.py to access interactive API documentation at /swagger/v1.',
|
||||
},
|
||||
{
|
||||
question: 'How can I see usage statistics (e.g., monthly active users)?',
|
||||
answer:
|
||||
'This functionality is not included with Superset, but you can extract and analyze the logs table in Superset\u2019s metadata database to see what actions have occurred.',
|
||||
},
|
||||
{
|
||||
question: 'What does Hours Offset in the Edit Dataset view do?',
|
||||
answer:
|
||||
'In the Edit Dataset view, the hours offset lets you configure the number of hours to be added or subtracted from the time column. This can be used, for example, to convert UTC time to local time.',
|
||||
},
|
||||
{
|
||||
question: 'Does Superset collect any telemetry data?',
|
||||
answer:
|
||||
'Superset uses Scarf by default to collect basic telemetry data to help maintainers understand version usage. Users can opt out by setting the SCARF_ANALYTICS environment variable to false.',
|
||||
},
|
||||
{
|
||||
question: 'Does Superset have an archive panel or trash bin from which a user can recover deleted assets?',
|
||||
question: 'Does Superset have a trash bin to recover deleted assets?',
|
||||
answer:
|
||||
'No. Currently, there is no way to recover a deleted Superset dashboard, chart, dataset, or database from the UI. It is recommended to take periodic backups of the metadata database and use export functionality for recovery.',
|
||||
},
|
||||
{
|
||||
question: 'I ran a security scan of the Superset container image and it showed dozens of "high" and "critical" vulnerabilities! Can you release a version of Superset without these?',
|
||||
answer:
|
||||
'These are dependency CVEs in software that Superset uses, mostly in the Linux kernel or Python. Superset addresses them by regularly updating dependencies and welcomes pull requests that fix dependency CVEs. The Superset security team focuses primarily on vulnerabilities in Superset itself.',
|
||||
'No, there is no built-in way to recover deleted dashboards, charts, or datasets. It is recommended to take periodic backups of the metadata database and use export functionality for recovery.',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -259,7 +214,8 @@ frontend falls back to a built-in default of `5000` milliseconds.
|
||||
SUPERSET_DASHBOARD_MANUAL_REFRESH_STAGGER_MS = 0
|
||||
```
|
||||
|
||||
## Why does `flask fab` or Superset freeze, hang, or not respond when started (my home directory is NFS mounted)?
|
||||
**Why does ‘flask fab’ or superset freeze/hang/not responding when started (my home directory is
|
||||
NFS mounted)?**
|
||||
|
||||
By default, Superset creates and uses an SQLite database at `~/.superset/superset.db`. SQLite is
|
||||
known to [not work well if used on NFS](https://www.sqlite.org/lockingv3.html) due to broken file
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
+6
-6
@@ -42,7 +42,7 @@ dependencies = [
|
||||
# ``google-auth`` 2.53+ dropped it, so Superset must declare it
|
||||
# explicitly to keep fresh ``pip install apache-superset`` working
|
||||
# without the ``base.txt`` lock file (#40962).
|
||||
"cachetools>=7.1.7, <8",
|
||||
"cachetools>=7.1.6, <8",
|
||||
"celery>=5.6.3, <6.0.0",
|
||||
"click>=8.4.2",
|
||||
"click-option-group",
|
||||
@@ -75,7 +75,7 @@ dependencies = [
|
||||
"humanize",
|
||||
"isodate",
|
||||
"jsonpath-ng>=1.8.0, <2",
|
||||
"Mako>=1.4.1",
|
||||
"Mako>=1.2.2",
|
||||
"markdown>=3.10.3",
|
||||
# marshmallow 4 compatibility: see superset/marshmallow_compatibility.py for a
|
||||
# Flask-AppBuilder workaround. Tracking issue:
|
||||
@@ -135,10 +135,10 @@ athena = ["pyathena[pandas]>=3.35.4, <4"]
|
||||
# superset/db_engine_specs/aurora.py's known_incompatibilities metadata.
|
||||
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
|
||||
bigquery = [
|
||||
"pandas-gbq>=0.35.1",
|
||||
"pandas-gbq>=0.35.0",
|
||||
# 1.17.1 is likely the final release: googleapis/python-bigquery-sqlalchemy
|
||||
# was archived 2026-05-16. Both 1.17.0 and 1.17.1 support SQLAlchemy 1.4/2.0.
|
||||
"sqlalchemy-bigquery>=1.17.2",
|
||||
"sqlalchemy-bigquery>=1.17.1",
|
||||
"google-cloud-bigquery>=3.42.3",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.6.0, <2.0"]
|
||||
@@ -185,7 +185,7 @@ excel = ["xlrd>=2.0.2, <2.1"]
|
||||
# installing this extra is only required to actually run exports.
|
||||
excel-export = ["boto3"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.4.6,<4.0",
|
||||
"fastmcp>=3.4.5,<4.0",
|
||||
# tiktoken backs the response-size-guard token estimator. Without
|
||||
# it, the middleware falls back to a coarser character-based
|
||||
# heuristic that under-counts JSON-heavy MCP responses.
|
||||
@@ -221,7 +221,7 @@ ocient = [
|
||||
# unpinned sqlalchemy>=1.4 declared, but SQLAlchemy 2.0 support is
|
||||
# unverified. Lower confidence than the other bumps in this PR.
|
||||
"sqlalchemy-ocient>=3.0.0, <4",
|
||||
"pyocient>=3.9.0, <4",
|
||||
"pyocient>=1.0.15, <4",
|
||||
"shapely",
|
||||
"geojson",
|
||||
]
|
||||
|
||||
@@ -28,7 +28,7 @@ numexpr>=2.9.0
|
||||
# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation
|
||||
cryptography>=50.0.0,<51.0.0
|
||||
# Security: Snyk - XSS vulnerability in Mako templates
|
||||
mako>=1.4.1,<2.0.0
|
||||
mako>=1.3.11,<2.0.0
|
||||
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
|
||||
pyarrow>=24.0.0,<26.0.0
|
||||
# Security: CVE-2026-27459 - pyopenssl certificate validation
|
||||
|
||||
@@ -44,7 +44,7 @@ cachelib==0.13.0
|
||||
# via
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==7.1.7
|
||||
cachetools==7.1.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
cattrs==25.1.1
|
||||
# via requests-cache
|
||||
@@ -207,7 +207,7 @@ kombu==5.6.2
|
||||
# via celery
|
||||
limits==5.1.0
|
||||
# via flask-limiter
|
||||
mako==1.4.1
|
||||
mako==1.3.12
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
|
||||
@@ -99,7 +99,7 @@ cachelib==0.13.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==7.1.7
|
||||
cachetools==7.1.6
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -239,9 +239,9 @@ et-xmlfile==2.0.0
|
||||
# openpyxl
|
||||
exceptiongroup==1.3.0
|
||||
# via fastmcp-slim
|
||||
fastmcp==3.4.7
|
||||
fastmcp==3.4.5
|
||||
# via apache-superset
|
||||
fastmcp-slim==3.4.7
|
||||
fastmcp-slim==3.4.5
|
||||
# via fastmcp
|
||||
filelock==3.20.3
|
||||
# via
|
||||
@@ -510,7 +510,7 @@ limits==5.1.0
|
||||
# flask-limiter
|
||||
lz4==4.4.5
|
||||
# via trino
|
||||
mako==1.4.1
|
||||
mako==1.3.12
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# alembic
|
||||
@@ -640,7 +640,7 @@ pandas==2.3.3
|
||||
# db-dtypes
|
||||
# pandas-gbq
|
||||
# prophet
|
||||
pandas-gbq==0.35.1
|
||||
pandas-gbq==0.35.0
|
||||
# via apache-superset
|
||||
parameterized==0.9.0
|
||||
# via apache-superset
|
||||
@@ -964,7 +964,7 @@ sqlalchemy==2.0.51
|
||||
# sqlalchemy-bigquery
|
||||
# sqlalchemy-continuum
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.17.2
|
||||
sqlalchemy-bigquery==1.17.1
|
||||
# via apache-superset
|
||||
sqlalchemy-continuum==1.7.0
|
||||
# via
|
||||
|
||||
Generated
-506
@@ -19,7 +19,6 @@
|
||||
"@babel/preset-typescript": "7.29.7",
|
||||
"@types/node": "^25.4.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18",
|
||||
@@ -27,27 +26,6 @@
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/cli": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.29.7.tgz",
|
||||
@@ -1678,121 +1656,6 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz",
|
||||
@@ -2638,16 +2501,6 @@
|
||||
"acorn": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
@@ -3015,34 +2868,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -3061,13 +2886,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -3097,19 +2915,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/envinfo": {
|
||||
"version": "7.13.0",
|
||||
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
|
||||
@@ -3511,60 +3316,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/import-local": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
|
||||
@@ -3687,13 +3438,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -3754,46 +3498,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "26.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"nwsapi": "^2.2.16",
|
||||
"parse5": "^7.2.1",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^5.1.1",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.1.1",
|
||||
"ws": "^8.18.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -4273,13 +3977,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.24",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
|
||||
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
@@ -4336,19 +4033,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -4458,16 +4142,6 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -4640,33 +4314,6 @@
|
||||
"@rolldown/binding-win32-x64-msvc": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
@@ -4800,13 +4447,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
@@ -4952,26 +4592,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "6.1.86",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -4985,32 +4605,6 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tscw-config": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tscw-config/-/tscw-config-1.1.2.tgz",
|
||||
@@ -5337,19 +4931,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
|
||||
@@ -5363,16 +4944,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.105.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz",
|
||||
@@ -5499,44 +5070,6 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -5581,45 +5114,6 @@
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
|
||||
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"@babel/preset-typescript": "7.29.7",
|
||||
"@types/node": "^25.4.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18",
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { Switchboard } from "@superset-ui/switchboard";
|
||||
import { embedDashboard } from "./index";
|
||||
|
||||
vi.mock("@superset-ui/switchboard");
|
||||
|
||||
function makeFakeJWT(claims: any) {
|
||||
// not a valid jwt, but close enough for this code
|
||||
const tokenifiedClaims = Buffer.from(JSON.stringify(claims)).toString(
|
||||
"base64",
|
||||
);
|
||||
return `abc.${tokenifiedClaims}.xyz`;
|
||||
}
|
||||
|
||||
describe("embedDashboard", () => {
|
||||
let mountPoint: HTMLElement;
|
||||
let mockSwitchboard: Switchboard;
|
||||
|
||||
beforeEach(() => {
|
||||
mountPoint = document.createElement("div");
|
||||
document.body.appendChild(mountPoint);
|
||||
|
||||
mockSwitchboard = {
|
||||
emit: vi.fn(),
|
||||
get: vi.fn(),
|
||||
start: vi.fn(),
|
||||
defineMethod: vi.fn(),
|
||||
} as any;
|
||||
|
||||
// Constructor mocks must use `function`, since arrow functions cannot be
|
||||
// invoked with `new`.
|
||||
vi.mocked(Switchboard).mockImplementation(function () {
|
||||
return mockSwitchboard;
|
||||
} as any);
|
||||
|
||||
// Mock MessageChannel API
|
||||
globalThis.MessageChannel = vi.fn(function (this: any) {
|
||||
this.port1 = {};
|
||||
this.port2 = {};
|
||||
}) as any;
|
||||
|
||||
// Mock iframe load event and sandbox
|
||||
const originalCreateElement = document.createElement.bind(document);
|
||||
vi.spyOn(document, "createElement").mockImplementation(tagName => {
|
||||
const element = originalCreateElement(tagName);
|
||||
if (tagName === "iframe") {
|
||||
// Mock sandbox DOMTokenList
|
||||
(element as any).sandbox = {
|
||||
add: vi.fn(),
|
||||
};
|
||||
// Mock contentWindow for postMessage
|
||||
Object.defineProperty(element, "contentWindow", {
|
||||
writable: true,
|
||||
value: {
|
||||
postMessage: vi.fn(),
|
||||
},
|
||||
});
|
||||
setTimeout(() => {
|
||||
element.dispatchEvent(new Event("load"));
|
||||
}, 0);
|
||||
}
|
||||
return element;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(mountPoint);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("setDataMask sends dataMask to iframe", async () => {
|
||||
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
|
||||
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
|
||||
const testDataMask = {
|
||||
"NATIVE_FILTER-1": {
|
||||
filterState: {
|
||||
value: ["value1", "value2"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dashboard = await embedDashboard({
|
||||
id: "test-id",
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint,
|
||||
fetchGuestToken: mockFetchGuestToken,
|
||||
});
|
||||
|
||||
dashboard.setDataMask(testDataMask);
|
||||
|
||||
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
|
||||
dataMask: testDataMask,
|
||||
});
|
||||
});
|
||||
|
||||
test("setDataMask sends empty dataMask", async () => {
|
||||
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
|
||||
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
|
||||
const emptyDataMask = {};
|
||||
|
||||
const dashboard = await embedDashboard({
|
||||
id: "test-id",
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint,
|
||||
fetchGuestToken: mockFetchGuestToken,
|
||||
});
|
||||
|
||||
dashboard.setDataMask(emptyDataMask);
|
||||
|
||||
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
|
||||
dataMask: emptyDataMask,
|
||||
});
|
||||
});
|
||||
|
||||
test("setDataMask drops the change-trigger flags observeDataMask adds", async () => {
|
||||
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
|
||||
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
|
||||
const observedMask = {
|
||||
"NATIVE_FILTER-1": {
|
||||
filterState: {
|
||||
value: ["CA"],
|
||||
},
|
||||
},
|
||||
crossFiltersChanged: false,
|
||||
nativeFiltersChanged: true,
|
||||
};
|
||||
|
||||
const dashboard = await embedDashboard({
|
||||
id: "test-id",
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint,
|
||||
fetchGuestToken: mockFetchGuestToken,
|
||||
});
|
||||
|
||||
dashboard.setDataMask(observedMask);
|
||||
|
||||
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
|
||||
dataMask: {
|
||||
"NATIVE_FILTER-1": observedMask["NATIVE_FILTER-1"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("setDataMask sends complex dataMask with multiple filters", async () => {
|
||||
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
|
||||
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
|
||||
const complexDataMask = {
|
||||
"NATIVE_FILTER-1": {
|
||||
filterState: {
|
||||
value: ["CA", "NY"],
|
||||
},
|
||||
},
|
||||
"NATIVE_FILTER-2": {
|
||||
filterState: {
|
||||
value: [2023, 2024],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const dashboard = await embedDashboard({
|
||||
id: "test-id",
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint,
|
||||
fetchGuestToken: mockFetchGuestToken,
|
||||
});
|
||||
|
||||
dashboard.setDataMask(complexDataMask);
|
||||
|
||||
expect(mockSwitchboard.get).toHaveBeenCalledWith("setDataMask", {
|
||||
dataMask: complexDataMask,
|
||||
});
|
||||
});
|
||||
|
||||
test("setDataMask rejects when the embedded page does not support it", async () => {
|
||||
const fakeToken = makeFakeJWT({ exp: Date.now() / 1000 + 300 });
|
||||
const mockFetchGuestToken = vi.fn().mockResolvedValue(fakeToken);
|
||||
vi.mocked(mockSwitchboard.get).mockRejectedValue(
|
||||
new Error('Method "setDataMask" is not defined'),
|
||||
);
|
||||
|
||||
const dashboard = await embedDashboard({
|
||||
id: "test-id",
|
||||
supersetDomain: "https://superset.example.com",
|
||||
mountPoint,
|
||||
fetchGuestToken: mockFetchGuestToken,
|
||||
});
|
||||
|
||||
await expect(dashboard.setDataMask({})).rejects.toThrow(
|
||||
'Method "setDataMask" is not defined',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -115,12 +115,6 @@ export type EmbeddedDashboard = {
|
||||
getActiveTabs: () => Promise<string[]>;
|
||||
observeDataMask: (callbackFn: ObserveDataMaskCallbackFn) => void;
|
||||
getDataMask: () => Promise<Record<string, any>>;
|
||||
/**
|
||||
* Applies a data mask to the dashboard.
|
||||
* Rejects if the embedded Superset page does not support `setDataMask`,
|
||||
* so a version mismatch surfaces instead of silently doing nothing.
|
||||
*/
|
||||
setDataMask: (dataMask: Record<string, any>) => Promise<void>;
|
||||
getChartStates: () => Promise<Record<string, any>>;
|
||||
getChartDataPayloads: (params?: {
|
||||
chartId?: number;
|
||||
@@ -361,21 +355,6 @@ export async function embedDashboard({
|
||||
ourPort.get<string>("getDashboardPermalink", { anchor });
|
||||
const getActiveTabs = () => ourPort.get<string[]>("getActiveTabs");
|
||||
const getDataMask = () => ourPort.get<Record<string, any>>("getDataMask");
|
||||
// `observeDataMask` hands the host a mask with the change-trigger booleans
|
||||
// mixed in, so feeding that payload straight back into `setDataMask` is a
|
||||
// natural thing for a host to do. Keep only the entries that look like a
|
||||
// filter's mask, so those flags never reach the dashboard as filter ids.
|
||||
// Sent with `get` rather than `emit` so the iframe acknowledges the call:
|
||||
// an embedded page that predates `setDataMask` replies with an error instead
|
||||
// of dropping the message silently.
|
||||
const setDataMask = (dataMask: Record<string, any>) =>
|
||||
ourPort.get<void>("setDataMask", {
|
||||
dataMask: Object.fromEntries(
|
||||
Object.entries(dataMask).filter(
|
||||
([, mask]) => typeof mask === "object" && mask !== null,
|
||||
),
|
||||
),
|
||||
});
|
||||
const getChartStates = () =>
|
||||
ourPort.get<Record<string, any>>("getChartStates");
|
||||
const getChartDataPayloads = (params?: { chartId?: number }) =>
|
||||
@@ -417,7 +396,6 @@ export async function embedDashboard({
|
||||
getActiveTabs,
|
||||
observeDataMask,
|
||||
getDataMask,
|
||||
setDataMask,
|
||||
getChartStates,
|
||||
getChartDataPayloads,
|
||||
setThemeConfig,
|
||||
|
||||
Generated
+12
-31
@@ -33,7 +33,7 @@
|
||||
"@fontsource/fira-code": "^5.3.0",
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@googleapis/sheets": "^14.0.0",
|
||||
"@googleapis/sheets": "^13.0.2",
|
||||
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
||||
"@jsonforms/core": "^3.7.0",
|
||||
"@jsonforms/react": "^3.7.0",
|
||||
@@ -90,6 +90,8 @@
|
||||
"dom-to-image-more": "^3.10.2",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^6.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.4.0",
|
||||
"fuse.js": "^7.5.0",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
@@ -4140,9 +4142,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@googleapis/sheets": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@googleapis/sheets/-/sheets-14.0.0.tgz",
|
||||
"integrity": "sha512-fANEl4RQohsPYUWhcLSYyUyE8A8bRfvw/bp8h0t8VDQqTgdQ3itZBkty4nddtdqCAvNDpA+KM66OejVKDd6aFg==",
|
||||
"version": "13.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@googleapis/sheets/-/sheets-13.0.2.tgz",
|
||||
"integrity": "sha512-b1tBlMcfvNEziM4DZCikLOc9iqSlgCK1e5bMKtNQIADRXr1CQmbkHV3ZBVvTsFsjLErgihqO58Itn/kzCnSZ0A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"googleapis-common": "^8.0.0"
|
||||
@@ -6741,7 +6743,6 @@
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@@ -6755,7 +6756,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -6765,7 +6765,6 @@
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -16291,7 +16290,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
@@ -21061,7 +21059,6 @@
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -21185,7 +21182,6 @@
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
|
||||
"integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -21370,7 +21366,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
@@ -21944,7 +21939,6 @@
|
||||
"version": "11.4.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
|
||||
"integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
@@ -22922,7 +22916,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -23167,7 +23160,6 @@
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/h3-js": {
|
||||
@@ -24665,7 +24657,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -24728,7 +24719,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -24836,7 +24826,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
@@ -27348,7 +27337,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
@@ -29416,7 +29404,6 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -30005,7 +29992,6 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
@@ -33260,9 +33246,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -33702,7 +33688,6 @@
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -34970,9 +34955,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-checkbox-tree/node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -36463,7 +36448,6 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
|
||||
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
@@ -36533,7 +36517,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -39588,7 +39571,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
@@ -40658,7 +40640,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
"cover": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=4096\" jest --coverage",
|
||||
"dev": "webpack --mode=development --color --watch",
|
||||
"dev-server": "cross-env NODE_ENV=development BABEL_ENV=development node --max_old_space_size=4096 ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --mode=development",
|
||||
"format": "npm run _format -- --write",
|
||||
"format-check": "npm run _format -- --check",
|
||||
"format": "npm run _format --write",
|
||||
"format-check": "npm run _format --check",
|
||||
"lint": "npx oxlint --config oxlint.json --quiet",
|
||||
"lint:all": "npx oxlint --config oxlint.json && npm run type",
|
||||
"lint-fix": "npx oxlint --config oxlint.json --fix --quiet",
|
||||
@@ -65,6 +65,7 @@
|
||||
"ensure-oxc": "echo 'OXC linter is ready' && npx oxlint --version",
|
||||
"lint-stats": "node ./scripts/oxlint-metrics-uploader.js",
|
||||
"plugins:build": "node ./scripts/build.js",
|
||||
"plugins:build-assets": "node ./scripts/copyAssets.js",
|
||||
"plugins:create-conventional-version": "npm run prune && lerna version --conventional-commits --create-release github --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
|
||||
"plugins:create-minor-version": "npm run prune && lerna version minor --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
|
||||
"plugins:create-patch-version": "npm run prune && lerna version patch --no-private --yes --tag-version-prefix=\"plugins-and-packages-v\"",
|
||||
@@ -109,7 +110,7 @@
|
||||
"@fontsource/fira-code": "^5.3.0",
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@googleapis/sheets": "^14.0.0",
|
||||
"@googleapis/sheets": "^13.0.2",
|
||||
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
||||
"@jsonforms/core": "^3.7.0",
|
||||
"@jsonforms/react": "^3.7.0",
|
||||
@@ -166,6 +167,8 @@
|
||||
"dom-to-image-more": "^3.10.2",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^6.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.4.0",
|
||||
"fuse.js": "^7.5.0",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
@@ -412,7 +415,6 @@
|
||||
"minimatch@>=10": {
|
||||
"brace-expansion": ">=5.0.8"
|
||||
},
|
||||
"nanoid@>=3 <4": "3.3.18",
|
||||
"nwsapi": "^2.2.13",
|
||||
"puppeteer": "^22.4.1",
|
||||
"tar": "^7.5.16",
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
import { dirname, join } from 'path';
|
||||
// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments
|
||||
import helpers from 'yeoman-test';
|
||||
import { cpSync } from 'fs';
|
||||
// @ts-ignore -- fs-extra/esm has no type declarations
|
||||
import { copySync } from 'fs-extra/esm';
|
||||
import { fileURLToPath } from 'url';
|
||||
import pluginChartModule from '../generators/plugin-chart';
|
||||
|
||||
@@ -30,10 +31,9 @@ test('generator-superset:plugin-chart:creates files', async () => {
|
||||
.onTargetDirectory((dir: string) => {
|
||||
// `dir` is the path to the new temporary directory
|
||||
const generatorDirname = dirname(fileURLToPath(import.meta.url));
|
||||
cpSync(
|
||||
copySync(
|
||||
join(generatorDirname, '../generators/plugin-chart/templates'),
|
||||
join(dir, 'unknown/templates'),
|
||||
{ recursive: true },
|
||||
);
|
||||
})
|
||||
.withPrompts({
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* 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 { SqlaFormData } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import { EchartsBubbleChartProps } from './types';
|
||||
|
||||
const baseFormData: SqlaFormData = {
|
||||
datasource: '1__table',
|
||||
viz_type: 'bubble_v2',
|
||||
entity: 'customer_name',
|
||||
x: 'price',
|
||||
y: 'sales',
|
||||
size: 'count',
|
||||
};
|
||||
|
||||
const baseChartProps = {
|
||||
width: 400,
|
||||
height: 400,
|
||||
hooks: {},
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{ customer_name: 'A', price: 10, sales: 100, count: 5 },
|
||||
{ customer_name: 'B', price: 20, sales: 200, count: 8 },
|
||||
],
|
||||
},
|
||||
],
|
||||
theme: { colorText: '#000' },
|
||||
};
|
||||
|
||||
test('nests xAxisLabelInterval under axisLabel rather than the axis itself', () => {
|
||||
// Regression test: xAxis.interval forces echarts' IntervalScale into a
|
||||
// fixed-tick-spacing mode that expects a number and crashes on the
|
||||
// 'auto'/'0' strings this control actually produces (observed as an
|
||||
// uncaught assertion deep in echarts' axis "nice" tick calculation,
|
||||
// reproducing on every dashboard bubble chart). The interval belongs on
|
||||
// axisLabel, where it only controls how many labels are skipped.
|
||||
const { echartOptions } = transformProps({
|
||||
...baseChartProps,
|
||||
formData: baseFormData,
|
||||
} as unknown as EchartsBubbleChartProps);
|
||||
|
||||
expect((echartOptions.xAxis as any).interval).toBeUndefined();
|
||||
expect((echartOptions.xAxis as any).axisLabel.interval).toBe('auto');
|
||||
});
|
||||
|
||||
test('honors an explicit xAxisLabelInterval override', () => {
|
||||
const { echartOptions } = transformProps({
|
||||
...baseChartProps,
|
||||
formData: { ...baseFormData, xAxisLabelInterval: '0' },
|
||||
} as unknown as EchartsBubbleChartProps);
|
||||
|
||||
expect((echartOptions.xAxis as any).axisLabel.interval).toBe('0');
|
||||
});
|
||||
@@ -212,16 +212,13 @@ export default function transformProps(chartProps: EchartsBubbleChartProps) {
|
||||
const echartOptions: EChartsCoreOption = {
|
||||
series,
|
||||
xAxis: {
|
||||
axisLabel: {
|
||||
formatter: xAxisFormatter,
|
||||
rotate: xAxisLabelRotation,
|
||||
interval: xAxisLabelInterval,
|
||||
},
|
||||
axisLabel: { formatter: xAxisFormatter, rotate: xAxisLabelRotation },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
interval: xAxisLabelInterval,
|
||||
scale: true,
|
||||
name: bubbleXAxisTitle,
|
||||
nameLocation: 'middle',
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
process.env.PATH = `./node_modules/.bin:${process.env.PATH}`;
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
const { globSync } = require('fs');
|
||||
const fastGlob = require('fast-glob');
|
||||
const yargs = require('yargs');
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
|
||||
@@ -60,29 +60,26 @@ function getPackages(packagePattern, tsOnly = false) {
|
||||
// Find packages in both @superset-ui and @apache-superset scopes
|
||||
const supersetUiPackages = [
|
||||
...new Set(
|
||||
globSync(
|
||||
[
|
||||
fastGlob
|
||||
.sync([
|
||||
`./node_modules/@superset-ui/${pattern}/src/**/*.${
|
||||
tsOnly ? '{ts,tsx}' : '{ts,tsx,js,jsx}'
|
||||
}`,
|
||||
],
|
||||
{ followSymlinks: true },
|
||||
)
|
||||
.map(x => x.split('/')[2])
|
||||
])
|
||||
.map(x => x.split('/')[3])
|
||||
.filter(x => !META_PACKAGES.has(x)),
|
||||
),
|
||||
];
|
||||
|
||||
const apachePackages = [
|
||||
...new Set(
|
||||
globSync(
|
||||
[
|
||||
fastGlob
|
||||
.sync([
|
||||
`./node_modules/@apache-superset/${pattern}/src/**/*.${
|
||||
tsOnly ? '{ts,tsx}' : '{ts,tsx,js,jsx}'
|
||||
}`,
|
||||
],
|
||||
{ followSymlinks: true },
|
||||
).map(x => x.split('/')[2]),
|
||||
])
|
||||
.map(x => x.split('/')[3]),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
const { sync } = require('fast-glob');
|
||||
const { copy } = require('fs-extra');
|
||||
|
||||
const pkgGlob = process.argv[2] || '*';
|
||||
|
||||
const packages = sync([`{packages,plugins}/${pkgGlob}`], {
|
||||
onlyDirectories: true,
|
||||
});
|
||||
|
||||
console.log('Copying asset files from package {src} to {lib,esm}...');
|
||||
packages.forEach(pkg => {
|
||||
const assets = sync([`${pkg}/src/**/*.{png,gif,jpg,css,geojson}`]);
|
||||
assets.forEach(filePath => {
|
||||
['lib', 'esm']
|
||||
.map(dir => filePath.replace(`${pkg}/src`, `${pkg}/${dir}`))
|
||||
.forEach(newFilePath => {
|
||||
copy(filePath, newFilePath, err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
if (assets.length > 0) {
|
||||
console.log(
|
||||
` Copied ${assets.length
|
||||
.toString()
|
||||
.padStart(2)} asset files for ${pkg.replace(
|
||||
'packages/superset-ui-',
|
||||
'',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -35,43 +35,6 @@ if (SERVICE_ACCOUNT_KEY.client_email) {
|
||||
|
||||
const DATETIME = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
||||
|
||||
/**
|
||||
* Turn an oxlint diagnostic code into the canonical rule id used by the metrics
|
||||
* series.
|
||||
*
|
||||
* oxlint reports `<plugin>(<rule>)`, where the plugin is the linter the rule came
|
||||
* from: `eslint(no-console)`, `react-hooks(exhaustive-deps)`, `react(jsx-key)`,
|
||||
* `jest(no-conditional-expect)`, `oxc(erasing-op)`, and the legacy
|
||||
* `eslint-plugin-unicorn(no-new-array)` spelling.
|
||||
*
|
||||
* `eslint` is the implicit namespace, so its rules keep their bare name and stay
|
||||
* comparable with the rows recorded before the oxlint migration. Every other
|
||||
* plugin becomes `<plugin>/<rule>`, which is the id those rules are known by in
|
||||
* config and in the pre-migration history.
|
||||
*
|
||||
* @param {string | undefined} code the diagnostic's `code` field
|
||||
* @returns {string} the rule id to record
|
||||
*/
|
||||
function parseRuleId(code) {
|
||||
if (!code) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const match = code.match(/^([\w-]+)\(([^)]+)\)$/);
|
||||
if (!match) {
|
||||
return code;
|
||||
}
|
||||
|
||||
const [, namespace, rule] = match;
|
||||
if (namespace === 'eslint') {
|
||||
return rule;
|
||||
}
|
||||
|
||||
// `eslint-plugin-unicorn(...)` is the same rule as `unicorn/...`
|
||||
const plugin = namespace.replace(/^eslint-plugin-/, '');
|
||||
return `${plugin}/${rule}`;
|
||||
}
|
||||
|
||||
async function writeToGoogleSheet(data, range, headers, append = false) {
|
||||
if (!sheets) {
|
||||
console.log('No Google Sheets credentials, skipping upload');
|
||||
@@ -138,7 +101,17 @@ async function runOxlintAndProcess() {
|
||||
// OXC JSON format has diagnostics array
|
||||
if (results.diagnostics && Array.isArray(results.diagnostics)) {
|
||||
results.diagnostics.forEach(diagnostic => {
|
||||
const ruleId = parseRuleId(diagnostic.code);
|
||||
// Extract rule ID from code like "eslint(no-unused-vars)" or "eslint-plugin-unicorn(no-new-array)"
|
||||
const codeMatch = diagnostic.code?.match(
|
||||
/^(?:eslint(?:-plugin-(\w+))?\()([^)]+)\)$/,
|
||||
);
|
||||
let ruleId = diagnostic.code || 'unknown';
|
||||
|
||||
if (codeMatch) {
|
||||
const plugin = codeMatch[1];
|
||||
const rule = codeMatch[2];
|
||||
ruleId = plugin ? `${plugin}/${rule}` : rule;
|
||||
}
|
||||
|
||||
const file = diagnostic.filename || 'unknown';
|
||||
const line = diagnostic.labels?.[0]?.span?.line || 0;
|
||||
@@ -278,10 +251,5 @@ async function runOxlintAndProcess() {
|
||||
}
|
||||
}
|
||||
|
||||
// Run the process, unless this file was imported (e.g. by a test) rather than
|
||||
// executed, in which case nothing should be linted or uploaded on import.
|
||||
if (require.main === module) {
|
||||
runOxlintAndProcess().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = { parseRuleId };
|
||||
// Run the process
|
||||
runOxlintAndProcess().catch(console.error);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
const { parseRuleId } = require('../../scripts/oxlint-metrics-uploader');
|
||||
|
||||
test('eslint rules keep their bare name', () => {
|
||||
expect(parseRuleId('eslint(no-console)')).toBe('no-console');
|
||||
expect(parseRuleId('eslint(prefer-destructuring)')).toBe(
|
||||
'prefer-destructuring',
|
||||
);
|
||||
});
|
||||
|
||||
test('plugin rules are recorded under plugin/rule (#42981)', () => {
|
||||
// These are the codes oxlint actually emits. They previously fell through to
|
||||
// the raw `react-hooks(exhaustive-deps)` string, so the rows no longer lined
|
||||
// up with the ids the same rules were recorded under before the migration.
|
||||
expect(parseRuleId('react-hooks(exhaustive-deps)')).toBe(
|
||||
'react-hooks/exhaustive-deps',
|
||||
);
|
||||
expect(parseRuleId('react-hooks(rules-of-hooks)')).toBe(
|
||||
'react-hooks/rules-of-hooks',
|
||||
);
|
||||
expect(parseRuleId('react(jsx-key)')).toBe('react/jsx-key');
|
||||
expect(parseRuleId('jest(no-conditional-expect)')).toBe(
|
||||
'jest/no-conditional-expect',
|
||||
);
|
||||
expect(parseRuleId('oxc(erasing-op)')).toBe('oxc/erasing-op');
|
||||
expect(parseRuleId('typescript(no-explicit-any)')).toBe(
|
||||
'typescript/no-explicit-any',
|
||||
);
|
||||
});
|
||||
|
||||
test('the legacy eslint-plugin- prefix still collapses to the plugin name', () => {
|
||||
expect(parseRuleId('eslint-plugin-unicorn(no-new-array)')).toBe(
|
||||
'unicorn/no-new-array',
|
||||
);
|
||||
});
|
||||
|
||||
test('an unrecognized or missing code is passed through rather than dropped', () => {
|
||||
expect(parseRuleId('something-unparseable')).toBe('something-unparseable');
|
||||
expect(parseRuleId(undefined)).toBe('unknown');
|
||||
expect(parseRuleId('')).toBe('unknown');
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
/**
|
||||
* 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 { DataMaskStateWithId } from '@superset-ui/core';
|
||||
|
||||
// Mock factories must build their own jest.fn()s: jest.mock calls are hoisted
|
||||
// above this file's declarations, so a factory closing over a const would read
|
||||
// it before initialization.
|
||||
jest.mock('@apache-superset/core/utils', () => ({
|
||||
logging: { debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
jest.mock('../views/store', () => ({
|
||||
store: { dispatch: jest.fn(), getState: jest.fn(), subscribe: jest.fn() },
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import { embeddedApi } from './api';
|
||||
// eslint-disable-next-line import/first
|
||||
import { updateDataMask } from '../dataMask/actions';
|
||||
|
||||
const { logging: mockLogging } = jest.requireMock(
|
||||
'@apache-superset/core/utils',
|
||||
);
|
||||
const { store: mockStore } = jest.requireMock('../views/store');
|
||||
const mockDispatch = mockStore.dispatch;
|
||||
const mockGetState = mockStore.getState;
|
||||
|
||||
const nativeFilterMask = { filterState: { value: ['CA'] } };
|
||||
const crossFilterMask = { filterState: { value: [2024] } };
|
||||
|
||||
// `dashboardInfo.id` is only set once HYDRATE_DASHBOARD lands, so it doubles as
|
||||
// the "dashboard is hydrated" signal setDataMask waits for.
|
||||
function stateWithFilters(filterIds: string[]) {
|
||||
return {
|
||||
dashboardInfo: { id: 1 },
|
||||
dataMask: Object.fromEntries(filterIds.map(id => [id, { id }])),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('setDataMask dispatches an update for each known filter', () => {
|
||||
mockGetState.mockReturnValue(
|
||||
stateWithFilters(['NATIVE_FILTER-1', 'NATIVE_FILTER-2']),
|
||||
);
|
||||
|
||||
embeddedApi.setDataMask({
|
||||
dataMask: {
|
||||
'NATIVE_FILTER-1': nativeFilterMask,
|
||||
'NATIVE_FILTER-2': crossFilterMask,
|
||||
} as unknown as DataMaskStateWithId,
|
||||
});
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
|
||||
);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
updateDataMask('NATIVE_FILTER-2', crossFilterMask),
|
||||
);
|
||||
expect(mockLogging.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('setDataMask ignores filter ids the dashboard does not know', () => {
|
||||
mockGetState.mockReturnValue(stateWithFilters(['NATIVE_FILTER-1']));
|
||||
|
||||
embeddedApi.setDataMask({
|
||||
dataMask: {
|
||||
'NATIVE_FILTER-1': nativeFilterMask,
|
||||
'NATIVE_FILTER-from-another-dashboard': crossFilterMask,
|
||||
} as unknown as DataMaskStateWithId,
|
||||
});
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
|
||||
);
|
||||
expect(mockLogging.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('unknown filter ids'),
|
||||
'NATIVE_FILTER-from-another-dashboard',
|
||||
);
|
||||
});
|
||||
|
||||
test('setDataMask ignores the change-trigger flags observeDataMask emits', () => {
|
||||
mockGetState.mockReturnValue(stateWithFilters(['NATIVE_FILTER-1']));
|
||||
|
||||
embeddedApi.setDataMask({
|
||||
dataMask: {
|
||||
'NATIVE_FILTER-1': nativeFilterMask,
|
||||
crossFiltersChanged: false,
|
||||
nativeFiltersChanged: true,
|
||||
} as unknown as DataMaskStateWithId,
|
||||
});
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
|
||||
);
|
||||
});
|
||||
|
||||
test('setDataMask dispatches nothing when no filter id is known', () => {
|
||||
mockGetState.mockReturnValue(stateWithFilters([]));
|
||||
|
||||
embeddedApi.setDataMask({
|
||||
dataMask: {
|
||||
'NATIVE_FILTER-1': nativeFilterMask,
|
||||
} as unknown as DataMaskStateWithId,
|
||||
});
|
||||
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockLogging.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('setDataMask queues the mask until the dashboard hydrates', () => {
|
||||
let notifyStoreSubscribers = () => {};
|
||||
mockStore.subscribe.mockImplementation((listener: () => void) => {
|
||||
notifyStoreSubscribers = listener;
|
||||
return jest.fn();
|
||||
});
|
||||
mockGetState.mockReturnValue({ dataMask: {} });
|
||||
|
||||
embeddedApi.setDataMask({
|
||||
dataMask: {
|
||||
'NATIVE_FILTER-1': nativeFilterMask,
|
||||
} as unknown as DataMaskStateWithId,
|
||||
});
|
||||
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
expect(mockLogging.warn).not.toHaveBeenCalled();
|
||||
|
||||
mockGetState.mockReturnValue(stateWithFilters(['NATIVE_FILTER-1']));
|
||||
notifyStoreSubscribers();
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
updateDataMask('NATIVE_FILTER-1', nativeFilterMask),
|
||||
);
|
||||
});
|
||||
@@ -17,15 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { DataMaskStateWithId, JsonObject } from '@superset-ui/core';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import getBootstrapData from 'src/utils/getBootstrapData';
|
||||
import { batch } from 'react-redux';
|
||||
import { store } from '../views/store';
|
||||
import { getDashboardPermalink as getDashboardPermalinkUtil } from '../utils/urlUtils';
|
||||
import { DashboardChartStates } from '../dashboard/types/chartState';
|
||||
import { hasStatefulCharts } from '../dashboard/util/chartStateConverter';
|
||||
import { getChartDataPayloads as getChartDataPayloadsUtil } from './utils';
|
||||
import { updateDataMask } from '../dataMask/actions';
|
||||
|
||||
const bootstrapData = getBootstrapData();
|
||||
|
||||
@@ -43,7 +40,6 @@ type EmbeddedSupersetApi = {
|
||||
getChartDataPayloads: (params?: {
|
||||
chartId?: number;
|
||||
}) => Promise<Record<string, JsonObject>>;
|
||||
setDataMask: ({ dataMask }: { dataMask: DataMaskStateWithId }) => void;
|
||||
};
|
||||
|
||||
const getScrollSize = (): Size => ({
|
||||
@@ -87,58 +83,6 @@ const getActiveTabs = () => store?.getState()?.dashboardState?.activeTabs || [];
|
||||
|
||||
const getDataMask = () => store?.getState()?.dataMask || {};
|
||||
|
||||
const isDashboardHydrated = () => Boolean(store?.getState()?.dashboardInfo?.id);
|
||||
|
||||
const applyDataMask = (dataMask: DataMaskStateWithId) => {
|
||||
// The dashboard's own data mask holds an entry for every native filter and
|
||||
// every cross-filter-capable chart, so it doubles as the set of filter ids
|
||||
// this dashboard can accept. Anything else — a filter id from a different
|
||||
// dashboard, or the change-trigger flags that `observeDataMask` emits
|
||||
// alongside the mask — would otherwise be inserted as a bogus filter and
|
||||
// treated as a globally scoped filter by the active-filter derivation.
|
||||
const knownFilterIds = new Set(Object.keys(getDataMask()));
|
||||
const entries = Object.entries(dataMask);
|
||||
const applicable = entries.filter(([id]) => knownFilterIds.has(id));
|
||||
const ignored = entries.filter(([id]) => !knownFilterIds.has(id));
|
||||
|
||||
if (ignored.length) {
|
||||
logging.warn(
|
||||
'[superset] setDataMask ignored unknown filter ids:',
|
||||
ignored.map(([id]) => id).join(', '),
|
||||
);
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
applicable.forEach(([filterId, mask]) => {
|
||||
store?.dispatch(updateDataMask(filterId, mask));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// A mask requested before the dashboard hydrates cannot be applied yet: the
|
||||
// store holds no filter entries to validate the ids against, and hydration
|
||||
// would replace anything dispatched in the meantime. Hold the request and
|
||||
// replay it once hydration lands.
|
||||
let queuedDataMask: DataMaskStateWithId | undefined;
|
||||
let unsubscribeFromHydration: (() => void) | undefined;
|
||||
|
||||
const setDataMask = ({ dataMask }: { dataMask: DataMaskStateWithId }) => {
|
||||
if (isDashboardHydrated()) {
|
||||
applyDataMask(dataMask);
|
||||
return;
|
||||
}
|
||||
|
||||
queuedDataMask = { ...queuedDataMask, ...dataMask };
|
||||
unsubscribeFromHydration ??= store?.subscribe(() => {
|
||||
if (!isDashboardHydrated()) return;
|
||||
unsubscribeFromHydration?.();
|
||||
unsubscribeFromHydration = undefined;
|
||||
const pending = queuedDataMask;
|
||||
queuedDataMask = undefined;
|
||||
if (pending) applyDataMask(pending);
|
||||
});
|
||||
};
|
||||
|
||||
const getChartStates = () =>
|
||||
store?.getState()?.dashboardState?.chartStates || {};
|
||||
|
||||
@@ -158,5 +102,4 @@ export const embeddedApi: EmbeddedSupersetApi = {
|
||||
getDataMask,
|
||||
getChartStates,
|
||||
getChartDataPayloads,
|
||||
setDataMask,
|
||||
};
|
||||
|
||||
@@ -298,7 +298,6 @@ window.addEventListener('message', function embeddedPageInitializer(event) {
|
||||
Switchboard.defineMethod('getActiveTabs', embeddedApi.getActiveTabs);
|
||||
Switchboard.defineMethod('getDataMask', embeddedApi.getDataMask);
|
||||
Switchboard.defineMethod('getChartStates', embeddedApi.getChartStates);
|
||||
Switchboard.defineMethod('setDataMask', embeddedApi.setDataMask);
|
||||
Switchboard.defineMethod(
|
||||
'getChartDataPayloads',
|
||||
embeddedApi.getChartDataPayloads,
|
||||
|
||||
+4
-5
@@ -22,11 +22,10 @@ import FixedOrMetricControl from '.';
|
||||
jest.mock(
|
||||
'@superset-ui/core/components/Icons/AsyncIcon',
|
||||
() =>
|
||||
({ fileName }: { fileName: string }) =>
|
||||
(
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
({ fileName }: { fileName: string }) => (
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
const createProps = () => ({
|
||||
|
||||
+4
-5
@@ -38,11 +38,10 @@ import {
|
||||
jest.mock(
|
||||
'@superset-ui/core/components/Icons/AsyncIcon',
|
||||
() =>
|
||||
({ fileName }: { fileName: string }) =>
|
||||
(
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
({ fileName }: { fileName: string }) => (
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
|
||||
+4
-5
@@ -29,11 +29,10 @@ import DatasetPanelWrapper from 'src/features/datasets/AddDataset/DatasetPanel';
|
||||
jest.mock(
|
||||
'@superset-ui/core/components/Icons/AsyncIcon',
|
||||
() =>
|
||||
({ fileName }: { fileName: string }) =>
|
||||
(
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
({ fileName }: { fileName: string }) => (
|
||||
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
|
||||
<span role="img" aria-label={fileName.replace('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
const errorMessageRegistry = getErrorMessageComponentRegistry();
|
||||
|
||||
@@ -21,7 +21,6 @@ from functools import partial
|
||||
from typing import cast
|
||||
from uuid import UUID
|
||||
|
||||
from superset import db
|
||||
from superset.commands.base import BaseCommand
|
||||
from superset.commands.database.exceptions import DatabaseNotFoundError
|
||||
from superset.daos.database import DatabaseUserOAuth2TokensDAO
|
||||
@@ -97,11 +96,6 @@ class OAuth2StoreTokenCommand(BaseCommand):
|
||||
database_id=self._state["database_id"],
|
||||
):
|
||||
DatabaseUserOAuth2TokensDAO.delete([existing])
|
||||
# flush the delete before inserting the replacement -- the unit
|
||||
# of work otherwise emits INSERTs before DELETEs within a single
|
||||
# flush, which would trip the (user_id, database_id) unique
|
||||
# index below on the old row.
|
||||
db.session.flush()
|
||||
|
||||
# store tokens
|
||||
expiration = datetime.now() + timedelta(seconds=token_response["expires_in"])
|
||||
|
||||
@@ -318,39 +318,27 @@ def import_tag(
|
||||
|
||||
for tag_name in target_tag_names:
|
||||
try:
|
||||
# Isolate each tag operation in a SAVEPOINT so a failure (e.g. a
|
||||
# concurrent unique-constraint violation) rolls back only the failed
|
||||
# tag and leaves the session usable for the remaining tags, instead
|
||||
# of poisoning the session with a pending-rollback state.
|
||||
with db_session.begin_nested():
|
||||
tag = existing_tags.get(tag_name)
|
||||
tag = existing_tags.get(tag_name)
|
||||
|
||||
# If tag does not exist, create it
|
||||
if tag is None:
|
||||
description = tag_descriptions.get(tag_name, None)
|
||||
tag = Tag(name=tag_name, description=description, type="custom")
|
||||
db_session.add(tag)
|
||||
existing_tags[tag_name] = tag # Update the existing_tags dictionary
|
||||
# If tag does not exist, create it
|
||||
if tag is None:
|
||||
description = tag_descriptions.get(tag_name, None)
|
||||
tag = Tag(name=tag_name, description=description, type="custom")
|
||||
db_session.add(tag)
|
||||
existing_tags[tag_name] = tag # Update the existing_tags dictionary
|
||||
|
||||
# Ensure the association with the object
|
||||
tagged_object = (
|
||||
db_session.query(TaggedObject)
|
||||
.filter_by(
|
||||
object_id=object_id, object_type=object_type, tag_id=tag.id
|
||||
)
|
||||
.first()
|
||||
# Ensure the association with the object
|
||||
tagged_object = (
|
||||
db_session.query(TaggedObject)
|
||||
.filter_by(object_id=object_id, object_type=object_type, tag_id=tag.id)
|
||||
.first()
|
||||
)
|
||||
if not tagged_object:
|
||||
new_tagged_object = TaggedObject(
|
||||
tag_id=tag.id, object_id=object_id, object_type=object_type
|
||||
)
|
||||
if not tagged_object:
|
||||
new_tagged_object = TaggedObject(
|
||||
tag_id=tag.id, object_id=object_id, object_type=object_type
|
||||
)
|
||||
db_session.add(new_tagged_object)
|
||||
db_session.add(new_tagged_object)
|
||||
|
||||
# Only record the tag as imported once the SAVEPOINT has been
|
||||
# released (and its pending inserts flushed) without error; the
|
||||
# nested block's own flush can still fail on a concurrent
|
||||
# unique-constraint violation, in which case this line must not
|
||||
# run.
|
||||
new_tag_ids.append(tag.id)
|
||||
|
||||
except SQLAlchemyError as err:
|
||||
@@ -361,9 +349,7 @@ def import_tag(
|
||||
object_id,
|
||||
err,
|
||||
)
|
||||
# The SAVEPOINT was rolled back by begin_nested(); the session is
|
||||
# still usable for the remaining tags.
|
||||
continue
|
||||
continue # No need for manual rollback, handled by transaction decorator
|
||||
|
||||
# Remove old tags not in the new config
|
||||
for tag in existing_assocs:
|
||||
|
||||
@@ -59,7 +59,7 @@ params:
|
||||
expressionType: SIMPLE
|
||||
label: SUM(SP_POP_TOTL)
|
||||
optionName: metric_size_life_expectancy_vs_rural
|
||||
time_range: '2011-01-01 : 2011-01-02'
|
||||
time_range: '2014-01-01 : 2014-01-02'
|
||||
tooltipSizeFormat: SMART_NUMBER
|
||||
truncateXAxis: true
|
||||
viz_type: bubble_v2
|
||||
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
# 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.
|
||||
"""Enforce one OAuth2 token per (user_id, database_id).
|
||||
|
||||
`OAuth2StoreTokenCommand` always deletes any existing token for a
|
||||
user+database pair before storing a new one, so the table is only ever
|
||||
meant to carry a single live row per pair -- but that invariant was only
|
||||
enforced in application code, via a plain (non-unique) lookup index. A
|
||||
race between two concurrent OAuth2 callbacks for the same user+database
|
||||
can leave duplicate rows behind, and nothing downstream picks a
|
||||
deterministic one of them. Flagged as a follow-up during review of #42211
|
||||
(which fixed an unrelated `purge_oauth2_tokens` filter bug on this same
|
||||
table).
|
||||
|
||||
Pre-flight: deletes any pre-existing duplicate rows, keeping the
|
||||
highest-id row per (user_id, database_id) pair, since
|
||||
`OAuth2StoreTokenCommand` always deletes-then-inserts and a higher id is
|
||||
therefore the more recently issued token.
|
||||
|
||||
Revision ID: da0e3f0081bf
|
||||
Revises: b8d2f4a6c901
|
||||
Create Date: 2026-08-07 09:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
from superset.migrations.shared.utils import create_index, drop_index
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "da0e3f0081bf"
|
||||
down_revision: str = "b8d2f4a6c901"
|
||||
|
||||
TABLE_NAME = "database_user_oauth2_tokens"
|
||||
INDEX_NAME = "idx_user_id_database_id"
|
||||
# Temporary name for the unique index while it and the old non-unique index
|
||||
# briefly coexist -- see the comment in ``upgrade`` below.
|
||||
TMP_INDEX_NAME = "idx_user_id_database_id_tmp_unique"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
metadata = sa.MetaData()
|
||||
table = sa.Table(TABLE_NAME, metadata, autoload_with=bind)
|
||||
|
||||
# Find the highest id per (user_id, database_id) pair by reading into
|
||||
# Python first, rather than deleting via a subquery on the same table:
|
||||
# MySQL rejects a DELETE whose WHERE clause subqueries the target table
|
||||
# (error 1093).
|
||||
rows = bind.execute(
|
||||
sa.select(table.c.id, table.c.user_id, table.c.database_id)
|
||||
).fetchall()
|
||||
max_id_by_pair: dict[tuple[int, int], int] = {}
|
||||
for row in rows:
|
||||
pair = (row.user_id, row.database_id)
|
||||
if row.id > max_id_by_pair.get(pair, -1):
|
||||
max_id_by_pair[pair] = row.id
|
||||
keep_ids = set(max_id_by_pair.values())
|
||||
dupe_ids = [row.id for row in rows if row.id not in keep_ids]
|
||||
if dupe_ids:
|
||||
bind.execute(table.delete().where(table.c.id.in_(dupe_ids)))
|
||||
|
||||
# MySQL's InnoDB won't drop an index that's still needed to satisfy a
|
||||
# foreign key (error 1553) -- the existing (user_id, database_id) index
|
||||
# is the only one covering the `user_id` FK. Create the new unique
|
||||
# index (which covers the same leading column) before dropping the old
|
||||
# one, so an FK-satisfying index always exists, then swap it into its
|
||||
# final name.
|
||||
create_index(TABLE_NAME, TMP_INDEX_NAME, ["user_id", "database_id"], unique=True)
|
||||
drop_index(TABLE_NAME, INDEX_NAME)
|
||||
create_index(TABLE_NAME, INDEX_NAME, ["user_id", "database_id"], unique=True)
|
||||
drop_index(TABLE_NAME, TMP_INDEX_NAME)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# The pre-flight dedupe above is not reversible -- any rows it removed
|
||||
# stay removed -- but that only ever discards rows that violated the
|
||||
# single-token-per-pair invariant the application already assumed.
|
||||
create_index(TABLE_NAME, TMP_INDEX_NAME, ["user_id", "database_id"])
|
||||
drop_index(TABLE_NAME, INDEX_NAME)
|
||||
create_index(TABLE_NAME, INDEX_NAME, ["user_id", "database_id"])
|
||||
drop_index(TABLE_NAME, TMP_INDEX_NAME)
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
# 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.
|
||||
"""merge oauth2 token uniqueness with report_schedule include_cta
|
||||
|
||||
Revision ID: 1072de5ed955
|
||||
Revises: ('da0e3f0081bf', '2d6ad72e4af6')
|
||||
Create Date: 2026-08-15 01:39:00.000000
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1072de5ed955"
|
||||
down_revision = ("da0e3f0081bf", "2d6ad72e4af6")
|
||||
|
||||
|
||||
def upgrade():
|
||||
pass
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
||||
@@ -1607,9 +1607,7 @@ class DatabaseUserOAuth2Tokens(Model, AuditMixinNullable):
|
||||
"""
|
||||
|
||||
__tablename__ = "database_user_oauth2_tokens"
|
||||
__table_args__ = (
|
||||
sqla.Index("idx_user_id_database_id", "user_id", "database_id", unique=True),
|
||||
)
|
||||
__table_args__ = (sqla.Index("idx_user_id_database_id", "user_id", "database_id"),)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
|
||||
@@ -19,14 +19,12 @@
|
||||
import copy
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from flask_appbuilder.security.sqla.models import Role, User
|
||||
from pytest_mock import MockerFixture
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from superset import security_manager
|
||||
@@ -508,71 +506,3 @@ def test_import_tag_logic_for_charts(session_with_schema: Session):
|
||||
.all()
|
||||
)
|
||||
assert len(associated_tags) == 0
|
||||
|
||||
|
||||
def test_import_tag_savepoint_keeps_session_usable(
|
||||
mocker: MockerFixture, session_with_schema: Session
|
||||
) -> None:
|
||||
"""
|
||||
When a single tag operation fails with a SQLAlchemyError (e.g. a unique
|
||||
constraint violation from a concurrent import), the per-tag SAVEPOINT
|
||||
isolates the failure so the session is not left in a pending-rollback
|
||||
state, and the remaining tags still import successfully.
|
||||
"""
|
||||
contents = {
|
||||
"tags.yaml": yaml.dump(
|
||||
{
|
||||
"tags": [
|
||||
{"tag_name": "tag_1", "description": "Description for tag_1"},
|
||||
{"tag_name": "tag_2", "description": "Description for tag_2"},
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
object_id = 1
|
||||
object_type = "chart"
|
||||
|
||||
# Simulate a unique-constraint violation discovered when the first
|
||||
# TaggedObject's SAVEPOINT is flushed (e.g. a concurrent import already
|
||||
# created the same association) -- not synchronously from Session.add().
|
||||
# `import_tag`'s own pre-insert existence check would normally catch a
|
||||
# real duplicate row, so the failure is injected at the point SQLAlchemy
|
||||
# actually persists the pending row: `begin_nested()`'s implicit flush
|
||||
# on a successful `with` exit, which calls `Session.flush()` directly
|
||||
# (see `SessionTransaction._prepare_impl`).
|
||||
pending_tagged_objects: list[TaggedObject] = []
|
||||
original_add = session_with_schema.add
|
||||
|
||||
def tracking_add(obj: object) -> None:
|
||||
if isinstance(obj, TaggedObject):
|
||||
pending_tagged_objects.append(obj)
|
||||
original_add(obj)
|
||||
|
||||
original_flush = session_with_schema.flush
|
||||
|
||||
def flaky_flush(*args: Any, **kwargs: Any) -> None:
|
||||
# Only the first TaggedObject ever added should fail, and only while
|
||||
# it's still pending -- once its SAVEPOINT rolls back, SQLAlchemy
|
||||
# expunges it from the session, so this does not also fail tag_2's
|
||||
# flush.
|
||||
if (
|
||||
pending_tagged_objects
|
||||
and pending_tagged_objects[0] is not None
|
||||
and pending_tagged_objects[0] in session_with_schema.new
|
||||
):
|
||||
raise SQLAlchemyError("UNIQUE constraint failed: tagged_object")
|
||||
original_flush(*args, **kwargs)
|
||||
|
||||
mocker.patch.object(session_with_schema, "add", side_effect=tracking_add)
|
||||
mocker.patch.object(session_with_schema, "flush", side_effect=flaky_flush)
|
||||
|
||||
with patch.object(feature_flag_manager, "is_feature_enabled", return_value=True):
|
||||
new_tag_ids = import_tag(
|
||||
["tag_1", "tag_2"], contents, object_id, object_type, session_with_schema
|
||||
)
|
||||
|
||||
# tag_1 failed on the unique violation, but tag_2 succeeded.
|
||||
assert len(new_tag_ids) == 1
|
||||
# The session is still usable — no PendingRollbackError.
|
||||
assert session_with_schema.query(TaggedObject).count() == 1
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
# 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.
|
||||
"""Tests for migration ``da0e3f0081bf_enforce_oauth2_token_uniqueness``.
|
||||
|
||||
Runs the migration's ``upgrade()`` and ``downgrade()`` against an
|
||||
in-memory SQLite engine seeded with pre-existing duplicate
|
||||
(user_id, database_id) rows -- the state a deployment could be in today,
|
||||
since the prior plain index never stopped them from accumulating.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
import pytest
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
create_engine,
|
||||
inspect,
|
||||
Integer,
|
||||
MetaData,
|
||||
select,
|
||||
Table,
|
||||
)
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
migration = import_module(
|
||||
"superset.migrations.versions."
|
||||
"2026-08-07_09-00_da0e3f0081bf_enforce_oauth2_token_uniqueness"
|
||||
)
|
||||
|
||||
TABLE_NAME: str = migration.TABLE_NAME # "database_user_oauth2_tokens"
|
||||
INDEX_NAME: str = migration.INDEX_NAME # "idx_user_id_database_id"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def engine() -> Engine:
|
||||
"""In-memory SQLite seeded with a minimal pre-migration token table,
|
||||
including duplicate (user_id, database_id) rows -- id 2 and id 3 both
|
||||
belong to user 1 + database 10, which the plain index never prevented.
|
||||
"""
|
||||
engine = create_engine("sqlite:///:memory:", future=True)
|
||||
md = MetaData()
|
||||
table = Table(
|
||||
TABLE_NAME,
|
||||
md,
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("user_id", Integer, nullable=False),
|
||||
Column("database_id", Integer, nullable=False),
|
||||
)
|
||||
md.create_all(engine)
|
||||
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
table.insert(),
|
||||
[
|
||||
{"id": 1, "user_id": 1, "database_id": 20},
|
||||
{"id": 2, "user_id": 1, "database_id": 10},
|
||||
{"id": 3, "user_id": 1, "database_id": 10},
|
||||
{"id": 4, "user_id": 2, "database_id": 10},
|
||||
],
|
||||
)
|
||||
|
||||
return engine
|
||||
|
||||
|
||||
def _indexes(engine: Engine) -> dict[str, bool]:
|
||||
"""Map of index name -> ``unique`` flag on the target table."""
|
||||
return {ix["name"]: ix["unique"] for ix in inspect(engine).get_indexes(TABLE_NAME)}
|
||||
|
||||
|
||||
def _rows(engine: Engine) -> list[tuple[int, int, int]]:
|
||||
with engine.begin() as conn:
|
||||
table = Table(TABLE_NAME, MetaData(), autoload_with=conn)
|
||||
return sorted(
|
||||
(r.id, r.user_id, r.database_id)
|
||||
for r in conn.execute(select(table)).fetchall()
|
||||
)
|
||||
|
||||
|
||||
def test_upgrade_deletes_duplicates_keeping_highest_id(engine: Engine) -> None:
|
||||
"""Only the highest-id row survives per (user_id, database_id) pair;
|
||||
unrelated pairs are untouched.
|
||||
"""
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
|
||||
assert _rows(engine) == [
|
||||
(1, 1, 20),
|
||||
(3, 1, 10),
|
||||
(4, 2, 10),
|
||||
], "upgrade() must keep only the highest-id row per duplicate pair"
|
||||
|
||||
|
||||
def test_upgrade_makes_the_index_unique(engine: Engine) -> None:
|
||||
"""upgrade() replaces the plain lookup index with a unique one."""
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
|
||||
assert _indexes(engine) == {INDEX_NAME: True}
|
||||
|
||||
|
||||
def test_upgrade_index_rejects_new_duplicates(engine: Engine) -> None:
|
||||
"""After upgrade(), inserting a second row for an existing
|
||||
(user_id, database_id) pair is rejected at the DB level.
|
||||
"""
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
|
||||
table = Table(TABLE_NAME, MetaData(), autoload_with=engine)
|
||||
with pytest.raises(IntegrityError):
|
||||
with engine.begin() as conn:
|
||||
conn.execute(table.insert().values(id=99, user_id=1, database_id=20))
|
||||
|
||||
|
||||
def test_downgrade_restores_a_plain_non_unique_index(engine: Engine) -> None:
|
||||
"""downgrade() drops the unique index and recreates a plain one --
|
||||
the dedupe deletions from upgrade() are not reversed, since those rows
|
||||
already violated the single-token-per-pair invariant the application
|
||||
assumed.
|
||||
"""
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
migration.downgrade()
|
||||
|
||||
assert _indexes(engine) == {INDEX_NAME: False}
|
||||
assert _rows(engine) == [
|
||||
(1, 1, 20),
|
||||
(3, 1, 10),
|
||||
(4, 2, 10),
|
||||
]
|
||||
|
||||
# Now that the index is plain again, duplicates are allowed once more.
|
||||
table = Table(TABLE_NAME, MetaData(), autoload_with=engine)
|
||||
with engine.begin() as conn:
|
||||
conn.execute(table.insert().values(id=99, user_id=1, database_id=20))
|
||||
assert (99, 1, 20) in _rows(engine)
|
||||
|
||||
|
||||
def test_upgrade_is_idempotent(engine: Engine) -> None:
|
||||
"""``create_index``/``drop_index`` are skip-if-exists; running
|
||||
``upgrade()`` twice must not raise.
|
||||
"""
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
migration.upgrade()
|
||||
@@ -1513,22 +1513,12 @@ def test_purge_oauth2_tokens_scoped_by_database_id(session: Session) -> None:
|
||||
session.add(database1)
|
||||
session.flush()
|
||||
|
||||
# Insert several tokens on database1, one per distinct user, so token PKs
|
||||
# advance past 1 and drift away from any single database PK. Each token
|
||||
# is for a different user to respect the unique (user_id, database_id)
|
||||
# constraint -- PK drift is what this test needs, not repeat users.
|
||||
# Insert several tokens on database1 so token PKs advance past 1 and drift
|
||||
# away from any single database PK.
|
||||
for i in range(5):
|
||||
drift_user = User(
|
||||
first_name="Drift",
|
||||
last_name=str(i),
|
||||
email=f"drift{i}@example.org",
|
||||
username=f"drift{i}",
|
||||
)
|
||||
session.add(drift_user)
|
||||
session.flush()
|
||||
session.add(
|
||||
DatabaseUserOAuth2Tokens(
|
||||
user_id=drift_user.id,
|
||||
user_id=user.id,
|
||||
database_id=database1.id,
|
||||
access_token=f"db1_access_{i}", # noqa: S106
|
||||
access_token_expiration=datetime(2023, 1, 1),
|
||||
@@ -1583,59 +1573,6 @@ def test_purge_oauth2_tokens_scoped_by_database_id(session: Session) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_oauth2_tokens_unique_per_user_and_database(session: Session) -> None:
|
||||
"""
|
||||
``database_user_oauth2_tokens`` allows at most one row per
|
||||
(user_id, database_id) pair. `OAuth2StoreTokenCommand` always
|
||||
deletes any existing token before storing a new one, so a second row for
|
||||
the same pair can only appear via a lost race between two concurrent
|
||||
callbacks -- the DB-level constraint is what makes that impossible rather
|
||||
than merely unlikely.
|
||||
"""
|
||||
from flask_appbuilder.security.sqla.models import Role, User # noqa: F401
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from superset.models.core import Database, DatabaseUserOAuth2Tokens
|
||||
|
||||
Database.metadata.create_all(session.get_bind()) # pylint: disable=no-member
|
||||
|
||||
user = User(
|
||||
first_name="Alice",
|
||||
last_name="Doe",
|
||||
email="adoe3@example.org",
|
||||
username="adoe3",
|
||||
)
|
||||
session.add(user)
|
||||
session.flush()
|
||||
|
||||
database = Database(database_name="my_unique_oauth2_db", sqlalchemy_uri="sqlite://")
|
||||
session.add(database)
|
||||
session.flush()
|
||||
|
||||
session.add(
|
||||
DatabaseUserOAuth2Tokens(
|
||||
user_id=user.id,
|
||||
database_id=database.id,
|
||||
access_token="first_access_token", # noqa: S106
|
||||
access_token_expiration=datetime(2023, 1, 1),
|
||||
refresh_token="first_refresh_token", # noqa: S106
|
||||
)
|
||||
)
|
||||
session.flush()
|
||||
|
||||
session.add(
|
||||
DatabaseUserOAuth2Tokens(
|
||||
user_id=user.id,
|
||||
database_id=database.id,
|
||||
access_token="second_access_token", # noqa: S106
|
||||
access_token_expiration=datetime(2023, 1, 1),
|
||||
refresh_token="second_refresh_token", # noqa: S106
|
||||
)
|
||||
)
|
||||
with pytest.raises(IntegrityError):
|
||||
session.flush()
|
||||
|
||||
|
||||
def test_compile_sqla_query_no_optimization(query: Select) -> None:
|
||||
"""
|
||||
Test the `compile_sqla_query` method.
|
||||
|
||||
Reference in New Issue
Block a user