Compare commits

..
Author SHA1 Message Date
Elizabeth Thompson 47f41ffa29 fix(reports): treat still-virtualized holders as not-ready
Drop "virtualized" from TERMINAL_CHART_HOLDER_STATES so an off-screen holder
that never mounted or painted is not counted as a ready/terminal state.

#42901 grows the standalone-capture viewport so off-screen holders mount, but a
holder still virtualized at readiness time -- e.g. a dashboard taller than the
capture viewport cap -- was previously counted terminal and could ship a blank
region. Treating it as not-ready makes the readiness gate fail loud (via the
webdriver unready check) and drops semantic_success rather than silently
delivering a partially blank capture.

virtualized_holders is still reported directly from the state counts, so the
diagnostic logging is unchanged; only ready/unready/semantic_success shift.
2026-08-15 01:38:24 +00:00
Elizabeth ThompsonandClaude 0e44fd6625 fix(reports): guard per-tile diagnostics and emit semantic status once per capture
Review fixes:
- Wrap the per-tile diagnostics evaluate on the readiness-succeeded path
  (same guard as the final block: diagnostics must not discard valid
  tiles). A navigation race or renderer crash during the diagnostics
  call no longer fails a capture whose tiles are already good.
- Emit report_semantic_status once per capture (final block only); the
  per-tile report_readiness_tile INFO lines already carry error_holders,
  and one error chart spanning N tiles was producing N+1 WARNINGs.
- Mixed-state tests pin the field-to-slot mapping of the enriched lines
  (2 rendered + 1 empty + 1 error + 1 unready), the once-per-capture
  semantic status, and the diagnostics-failure regression.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 01:38:24 +00:00
Elizabeth ThompsonandClaude 9bbf97b60d feat(reports): structured chart-holder diagnostics parity for capture logging
Enriches the report readiness/capture log lines with the per-state
holder breakdown proven out in production:

- ChartHolderDiagnostics (report_execution.py): structured counts
  separating capture readiness from semantic success, with
  TERMINAL_CHART_HOLDER_STATES and the explicit
  CHART_HOLDER_SEMANTIC_POLICY=deliver_terminal_errors_with_warning.
- report_readiness_poll/_ready/_terminal now carry rendered/empty/
  error/virtualized/unready counts and semantic_success on both the
  Playwright and Selenium paths.
- report_readiness_tile: per-tile readiness diagnostics during tiled
  capture, with each holder's strongest observed terminal state
  aggregated across tiles for the final ready line.
- report_semantic_status: a WARNING whenever capture readiness is
  satisfied but the artifact contains terminal chart errors — delivery
  is not semantic completeness, and this makes the distinction
  operator-visible (and alertable) per tile and per capture.
- Restores the explicit zero-holder thumbnail diagnostic on the
  no-context path.

Logging-only: no readiness predicate, budget, or delivery behavior
changes. Unit tests pin the diagnostics counts and the enriched line
formats.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 01:38:24 +00:00
174 changed files with 2025 additions and 8034 deletions
+1 -1
View File
@@ -29,7 +29,7 @@
"dependencies:python":
- changed-files:
- any-glob-to-any-file:
- 'requirements/**'
- 'superset/requirements/**'
- 'superset/translations/requirements.txt'
- 'RELEASING/requirements.txt'
@@ -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
-3
View File
@@ -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
-3
View File
@@ -7,9 +7,6 @@ on:
pull_request:
types: [opened, edited]
permissions:
contents: read
jobs:
superbot-orglabel:
runs-on: ubuntu-slim
-3
View File
@@ -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
View File
@@ -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
-3
View File
@@ -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
-3
View File
@@ -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
-3
View File
@@ -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
-3
View File
@@ -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
-3
View File
@@ -13,9 +13,6 @@ on:
required: true
type: string
permissions:
contents: read
jobs:
supersetbot:
runs-on: ubuntu-26.04
-3
View File
@@ -5,9 +5,6 @@ on:
pull_request_target:
types: [opened]
permissions:
contents: read
jobs:
welcome:
runs-on: ubuntu-slim
-3
View File
@@ -24,8 +24,6 @@ assists people when migrating to a new version.
## Next
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
### OAuth2 database callback metrics include their outcome
The unqualified `DatabaseRestApi.oauth2` StatsD counter has been replaced with
@@ -33,7 +31,6 @@ The unqualified `DatabaseRestApi.oauth2` StatsD counter has been replaced with
`DatabaseRestApi.oauth2.error`. Update monitoring rules and dashboards that consume
the old counter to use the outcome-specific replacements.
- [42930](https://github.com/apache/superset/pull/42930): Dataset import data-URI fetches no longer honor an HTTP(S) proxy when `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS` is `False` (the default): the connection is now made directly to the destination so the peer-address check validates the real target instead of a proxy's. Deployments that require an egress proxy to reach legitimate external data URLs for dataset import should set `DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS = True` or otherwise ensure those URLs resolve without one.
- [42935](https://github.com/apache/superset/pull/42935): The MCP service now refuses to start (`MCPAuthConfigError`) when `MCP_JWT_ISSUER` trusts more than one issuer and no `MCP_USER_RESOLVER` is configured, instead of only logging a warning. This was already a documented misconfiguration (the default resolver isn't issuer-scoped, so distinct trusted issuers minting the same username/email would resolve to the same Superset user); deployments trusting multiple issuers must configure an `MCP_USER_RESOLVER` that derives its identity from the token's `iss` claim before upgrading. Single-issuer deployments are unaffected.
- [42393](https://github.com/apache/superset/pull/42393): Exported dataset YAML now carries a `uuid` for each metric and column so that custom folder assignments (which reference metrics/columns by UUID) survive an import into another workspace. This affects any export bundle that contains datasets, not just a dataset export: chart, dashboard, database and full-asset exports all embed the same dataset YAML, so a dashboard exported from this release also fails to import into an older one even though no dataset was exported directly. As with `folders` and `currency_code_column`, the affected `datasets/` files fail schema validation (`Unknown field: uuid`) when imported into Superset releases that predate this change; regenerate or hand-edit exports for older targets in mixed-version fleets.
- [42300](https://github.com/apache/superset/pull/42300): Timeseries charts (line/area/bar) with a Y-axis bound in effect — either an explicit `yAxisBounds` or one derived from `truncateYAxis` — now clamp out-of-range data points to that bound instead of letting ECharts drop the point (and the line segments around it) entirely. Any existing chart with a configured Y-axis bound and data outside it will look different after upgrading: a gap becomes a point pinned to the boundary. The clamp also rewrites the value ECharts reads for that point's tooltip and data label, so the displayed value is the bound rather than the true observation.
@@ -97,54 +97,6 @@ for more information on how to configure it.
At the very least, you'll want to change `SECRET_KEY` and `SQLALCHEMY_DATABASE_URI`. Continue reading for more about each of these.
## Localizing D3 date and time labels
`BABEL_DEFAULT_LOCALE` controls Superset's application translations, while
`D3_TIME_FORMAT` provides localized date and time names to visualizations that
use the D3 formatter registry, including Calendar Heatmap. Configure both when
you want the application and chart labels to use the same locale.
`D3_TIME_FORMAT` accepts partial overrides. For example, Russian month names
can be configured in `superset_config.py` as follows:
```python
BABEL_DEFAULT_LOCALE = "ru"
D3_TIME_FORMAT = {
"months": [
"Январь",
"Февраль",
"Март",
"Апрель",
"Май",
"Июнь",
"Июль",
"Август",
"Сентябрь",
"Октябрь",
"Ноябрь",
"Декабрь",
],
"shortMonths": [
"Янв",
"Фев",
"Мар",
"Апр",
"Май",
"Июн",
"Июл",
"Авг",
"Сен",
"Окт",
"Ноя",
"Дек",
],
}
```
Restart Superset after changing `superset_config.py` so the frontend receives
the updated formatter configuration.
## Chart-data query timing
Set `CHART_DATA_INCLUDE_TIMING = True` to add an optional versioned timing object
+1 -13
View File
@@ -400,7 +400,7 @@ Once enabled, each user manages their own keys from their profile page:
1. Open the user menu (top-right) and click **Info** to navigate to the User Info page
2. Expand the **API Keys** section
3. Click **+ API Key**
4. Enter a name and optionally select resource scopes
4. Enter a name and (optionally) an expiration date
5. Copy the generated token — it is shown only once
Only users with the `can_read` and `can_write` permissions on `ApiKey` (granted by default to Admins) can manage API keys.
@@ -415,18 +415,6 @@ Authorization: Bearer <your-api-key>
This works for all REST API endpoints and the MCP server. The request is executed with the permissions of the user who created the key.
#### API Key Scopes
The creation dialog can restrict an API key to MCP resource actions such as
`superset:dashboard:read` or `superset:chart:write`. A scope is an additional
restriction: it never grants a permission that the creating user does not
already have through Superset RBAC. Write scopes also cover update and delete
operations for that resource; `superset:sqllab:write` covers SQL execution.
Keys created without scopes retain legacy RBAC-only behavior. The scoped-key
restrictions described here are enforced by the MCP server; regular REST API
routes continue to apply their existing Superset RBAC checks.
#### Use Cases
- **CI/CD pipelines** — automated chart/dashboard exports and imports
+5 -49
View File
@@ -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
+7 -7
View File
@@ -61,9 +61,9 @@
"@storybook/addon-docs": "^10.5.7",
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.15.47",
"antd": "^6.6.0",
"baseline-browser-mapping": "^2.11.13",
"caniuse-lite": "^1.0.30001809",
"antd": "^6.5.4",
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001807",
"docusaurus-plugin-openapi-docs": "^5.1.3",
"docusaurus-theme-openapi-docs": "^5.1.3",
"js-yaml": "^5.2.3",
@@ -89,14 +89,14 @@
"@eslint/js": "^9.39.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"@typescript-eslint/eslint-plugin": "^8.66.0",
"@typescript-eslint/parser": "^8.66.0",
"eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5",
"globals": "^17.9.0",
"oxfmt": "^0.63.0",
"oxfmt": "^0.62.0",
"typescript": "~6.0.3",
"typescript-eslint": "^8.67.0",
"typescript-eslint": "^8.66.0",
"webpack": "^5.109.2"
},
"browserslist": {
+32 -44
View File
@@ -3407,26 +3407,22 @@
"nullable": true,
"type": "string"
},
"description": {
"nullable": true,
"type": "string"
},
"editors": {
"items": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Subject"
},
"type": "array"
},
"id": {
"type": "integer"
},
"is_managed_externally": {
"type": "boolean"
},
"owners": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.User2"
},
"published": {
"nullable": true,
"type": "boolean"
},
"roles": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Role"
},
"slug": {
"maxLength": 255,
"nullable": true,
@@ -3436,10 +3432,10 @@
"readOnly": true
},
"tags": {
"items": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Tag"
},
"type": "array"
"$ref": "#/components/schemas/DashboardRestApi.get_list.Tag"
},
"thumbnail_url": {
"readOnly": true
},
"url": {
"readOnly": true
@@ -3448,46 +3444,21 @@
"format": "uuid",
"nullable": true,
"type": "string"
},
"viewers": {
"items": {
"$ref": "#/components/schemas/DashboardRestApi.get_list.Subject1"
},
"type": "array"
}
},
"type": "object"
},
"DashboardRestApi.get_list.Subject": {
"DashboardRestApi.get_list.Role": {
"properties": {
"id": {
"type": "integer"
},
"label": {
"maxLength": 255,
"name": {
"maxLength": 64,
"type": "string"
},
"type": {
"type": "integer"
}
},
"required": ["label", "type"],
"type": "object"
},
"DashboardRestApi.get_list.Subject1": {
"properties": {
"id": {
"type": "integer"
},
"label": {
"maxLength": 255,
"type": "string"
},
"type": {
"type": "integer"
}
},
"required": ["label", "type"],
"required": ["name"],
"type": "object"
},
"DashboardRestApi.get_list.Tag": {
@@ -3540,6 +3511,23 @@
"required": ["first_name", "last_name"],
"type": "object"
},
"DashboardRestApi.get_list.User2": {
"properties": {
"first_name": {
"maxLength": 64,
"type": "string"
},
"id": {
"type": "integer"
},
"last_name": {
"maxLength": 64,
"type": "string"
}
},
"required": ["first_name", "last_name"],
"type": "object"
},
"DashboardRestApi.post": {
"properties": {
"certification_details": {
@@ -16518,7 +16506,7 @@
},
"result": {
"items": {
"$ref": "#/components/schemas/DashboardRestApi.get_list"
"type": "object"
},
"type": "array"
}
+244 -262
View File
@@ -1142,11 +1142,6 @@
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768"
integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==
"@babel/runtime@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-8.0.0.tgz#d7bd513e6843662346552c2798ab895716cf97f2"
integrity sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==
"@babel/template@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700"
@@ -3175,100 +3170,100 @@
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.23.0.tgz#8b66dbfa7b796139e719063fc0e44084e80a1c15"
integrity sha512-gUGJpr+Rn6zMxm5juApV0K3U845i8t47o8k+rbO0BHbi4PoJIfSPeQmrE2dgohQm2g5k6iviNFyXCGqvmaYUpw==
"@oxfmt/binding-android-arm-eabi@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz#136176dc94fdc41e21415cc770d86f5066282e0f"
integrity sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==
"@oxfmt/binding-android-arm-eabi@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.62.0.tgz#3f5b9d3ba944f42ad3fa2697b9fef88a8c9d4ce0"
integrity sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw==
"@oxfmt/binding-android-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz#10bc42457179210061c801122a64304619e3bdab"
integrity sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==
"@oxfmt/binding-android-arm64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.62.0.tgz#4c7e2c567f645ed051be100318e9e3f716630c1b"
integrity sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog==
"@oxfmt/binding-darwin-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz#5f9084d9a760a1836387f8970a7f9d614ec3d909"
integrity sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==
"@oxfmt/binding-darwin-arm64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.62.0.tgz#6c8007ae65ed17f9d1ecc6c680da19ec19276c67"
integrity sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q==
"@oxfmt/binding-darwin-x64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz#badd4a02218a9a62319817d5c337b30159a54a21"
integrity sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==
"@oxfmt/binding-darwin-x64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.62.0.tgz#0661a0274e8625921c5a054aeb21a36251946e6b"
integrity sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ==
"@oxfmt/binding-freebsd-x64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz#a17261e95c8ebef1f76d8aaac746a64fdb6ba51e"
integrity sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==
"@oxfmt/binding-freebsd-x64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.62.0.tgz#f3345001102ac3e6c2947920d6d1676e9cf97e75"
integrity sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g==
"@oxfmt/binding-linux-arm-gnueabihf@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz#baeee34bb08e0769af878623f442e83bc0aacd7a"
integrity sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==
"@oxfmt/binding-linux-arm-gnueabihf@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.62.0.tgz#ddc03bc2a899f2071d6706c06dfdec3a7f3e8b5a"
integrity sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g==
"@oxfmt/binding-linux-arm-musleabihf@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz#e70d5697ec4b6bb5f87a3f019e01b3f956b8e44b"
integrity sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==
"@oxfmt/binding-linux-arm-musleabihf@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.62.0.tgz#5e82208d612c4caf64ada75e129e34d1a9eefb2c"
integrity sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog==
"@oxfmt/binding-linux-arm64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz#638a8ed4f3d256c50aeb6d2c19cfc65792c902e1"
integrity sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==
"@oxfmt/binding-linux-arm64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.62.0.tgz#eb379bc58aa962e753d58b4cc68ff4081bc19a5d"
integrity sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg==
"@oxfmt/binding-linux-arm64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz#af5a9b787f5233f27a3360ad56235fc1b011f760"
integrity sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==
"@oxfmt/binding-linux-arm64-musl@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.62.0.tgz#dc1c62510405e874bf6a53a34f548032eb6dfed7"
integrity sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew==
"@oxfmt/binding-linux-ppc64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz#c1a211206134a5577e355a495989e0d733218d60"
integrity sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==
"@oxfmt/binding-linux-ppc64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.62.0.tgz#9f9afee327090024db86b70ec81a57ad06bb2f00"
integrity sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw==
"@oxfmt/binding-linux-riscv64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz#4863f0311e5c1b88f75ef822959b3ca4fd938937"
integrity sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==
"@oxfmt/binding-linux-riscv64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.62.0.tgz#4427d42ee3bad0e55b38dc76fe14a7e5318c360c"
integrity sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA==
"@oxfmt/binding-linux-riscv64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz#ad05a017d12553e2f544743c4940adb552aa1d1c"
integrity sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==
"@oxfmt/binding-linux-riscv64-musl@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.62.0.tgz#a117f82909f075cf07c333842d89a5638429e21d"
integrity sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A==
"@oxfmt/binding-linux-s390x-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz#2803f539db15bc66db115888fa8f84d6531ed2b9"
integrity sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==
"@oxfmt/binding-linux-s390x-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.62.0.tgz#3fac79fefe7ffc3f0a9393678ebd782aac918fcd"
integrity sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A==
"@oxfmt/binding-linux-x64-gnu@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz#c22a06a60ae2d6b3de522095e0c50a816040a033"
integrity sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==
"@oxfmt/binding-linux-x64-gnu@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.62.0.tgz#8da207bef27941f0265c129d1c7c82c7cf91d1ce"
integrity sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w==
"@oxfmt/binding-linux-x64-musl@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz#48d3eeaf8e3757f638cf92de5ee4858befc9c0a3"
integrity sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==
"@oxfmt/binding-linux-x64-musl@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.62.0.tgz#e55cf9b7c8c2204fdbb5d4818f8c5ba02aa49360"
integrity sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw==
"@oxfmt/binding-openharmony-arm64@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz#02be9e140ae35ba30f52bdce27612fece4a01ab3"
integrity sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==
"@oxfmt/binding-openharmony-arm64@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.62.0.tgz#4998769ee1b5894efcd6cb99729d5a75f4c09dd1"
integrity sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA==
"@oxfmt/binding-win32-arm64-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz#2226eaf52b6345a2cb926499216b2486cf0dbec2"
integrity sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==
"@oxfmt/binding-win32-arm64-msvc@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.62.0.tgz#e09eaabdde76c885c4f8a190518c2eb2de08548a"
integrity sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw==
"@oxfmt/binding-win32-ia32-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz#58d263bb5ecd7330c02f9dcd8cda10f66e42e74b"
integrity sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==
"@oxfmt/binding-win32-ia32-msvc@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.62.0.tgz#f36e306308923977365270d8b26290f4ca2fcfa5"
integrity sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw==
"@oxfmt/binding-win32-x64-msvc@0.63.0":
version "0.63.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz#02a166c8a8049c55d0096d1ba9d8e73f3a4d26a7"
integrity sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==
"@oxfmt/binding-win32-x64-msvc@0.62.0":
version "0.62.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.62.0.tgz#bb6545e581d5ee7111084dbabeec7fe548bae418"
integrity sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ==
"@parcel/watcher-android-arm64@2.5.6":
version "2.5.6"
@@ -3537,13 +3532,13 @@
dependencies:
"@babel/runtime" "^7.24.4"
"@rc-component/cascader@~1.22.0":
version "1.22.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.22.0.tgz#eec0b6f4d2df5903aa12cfed321a578705926937"
integrity sha512-SffrA57aS9oub3VuI7ajPhJTPtaNxngSvtRhD40Rd8dwJ5vfWPSrVanWgeepdWFGBt7EHftIK5RUU0u3rCTwWw==
"@rc-component/cascader@~1.17.0":
version "1.17.0"
resolved "https://registry.yarnpkg.com/@rc-component/cascader/-/cascader-1.17.0.tgz#52c0eceada2c7b4b37ebe822c19a6544b9562edf"
integrity sha512-3cVNG0zrQF1PoXq262L3wGCU+/YLEC1mGSVHDl577dQmA0ZKkXFbY6nwyXo+beCcM7buo49t24jkr+QZdL7O8w==
dependencies:
"@rc-component/select" "~1.10.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/select" "~1.8.0"
"@rc-component/tree" "~1.3.2"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
@@ -3619,14 +3614,14 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/image@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.10.0.tgz#5d7a82d20e4c91f75875ea64eb1dadd7af676b1d"
integrity sha512-BjeZCRQ+hw+4WAhvrw8rJvy5fckA2xpf/X2XQEOABUHvLTNB9inB98X3Mp54jYQ7g10DfWERQWHXeC4ylxp1Uw==
"@rc-component/image@~1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@rc-component/image/-/image-1.9.0.tgz#110785d735d20336afcdbac84e8fbfd059a7a44e"
integrity sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/portal" "^2.1.2"
"@rc-component/util" "^1.11.1"
"@rc-component/util" "^1.10.1"
clsx "^2.1.1"
"@rc-component/input-number@~1.6.2":
@@ -3638,7 +3633,7 @@
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/input@~1.3.1":
"@rc-component/input@~1.3.0", "@rc-component/input@~1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@rc-component/input/-/input-1.3.1.tgz#230b8b59cdde8521d50f0eede63ddacb61cc0cd3"
integrity sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==
@@ -3647,27 +3642,15 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/listy@~1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@rc-component/listy/-/listy-1.2.3.tgz#e9c8ef4f409c231b44dded37e63ae2875bbe0334"
integrity sha512-IXiMjV5s0rczLBlfh7G5nB4M3365mrEeedjwKtf5I+Ns3PqRUsebR2h5u8CeFarsVfLUPC2I5p0h09TNoOWyvQ==
"@rc-component/mentions@~1.10.0":
version "1.10.0"
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.10.0.tgz#46b1117cfb0c716b476e97f342555eccc2f41c97"
integrity sha512-CI1njYUVY0NjHtLhNoVmXlJyy568Sfep9Wsak6vmGjtT6uazx98djGYlCXz2xkHhEm73g91Y3MTvzUyE5avI7w==
dependencies:
"@rc-component/motion" "^1.1.4"
"@rc-component/portal" "^2.0.0"
"@rc-component/resize-observer" "^1.0.0"
"@rc-component/util" "^1.3.1"
"@rc-component/virtual-list" "^1.4.0"
clsx "^2.1.1"
"@rc-component/mentions@~1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/mentions/-/mentions-1.11.0.tgz#cee0c4710f26766ad8550d386cfec5ff86fd58d9"
integrity sha512-IC2qXuEBMFHxPIXEFfYWj6Sr7UiDZnOqJHCYQBbwPzopBJOPZIR6mV9U4QH1bYQRlKYlYnIsajWDMgVGgWQyWQ==
dependencies:
"@rc-component/input" "~1.3.1"
"@rc-component/input" "~1.3.0"
"@rc-component/menu" "~1.4.0"
"@rc-component/trigger" "^3.0.0"
"@rc-component/util" "^1.11.1"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/menu@~1.4.0", "@rc-component/menu@~1.4.1":
@@ -3741,7 +3724,7 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/portal@^2.0.0", "@rc-component/portal@^2.1.0", "@rc-component/portal@^2.1.2", "@rc-component/portal@^2.1.3", "@rc-component/portal@^2.2.0", "@rc-component/portal@^2.2.1":
"@rc-component/portal@^2.1.0", "@rc-component/portal@^2.1.2", "@rc-component/portal@^2.1.3", "@rc-component/portal@^2.2.0", "@rc-component/portal@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@rc-component/portal/-/portal-2.2.1.tgz#37c34b4c8cd73f53cc7072c96dd0e9ac332669ec"
integrity sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==
@@ -3789,10 +3772,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/select@~1.10.0":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.10.1.tgz#323b2f458a637e8e752f8341094783741c613c34"
integrity sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==
"@rc-component/select@~1.8.0", "@rc-component/select@~1.8.2":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@rc-component/select/-/select-1.8.2.tgz#f016992dae5c57186535512d73783e2fc7e4c59e"
integrity sha512-HQ9zuYqjfZTlcEMWlU1GAPBajd2OHIMVHyjZSGVTCVARwkfCgvXZMTEn0cduy3L+ejAKkaZluOQvxovZoaJaQw==
dependencies:
"@rc-component/overflow" "^1.0.0"
"@rc-component/trigger" "^3.0.0"
@@ -3824,10 +3807,10 @@
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/table@~1.11.0":
version "1.11.1"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.11.1.tgz#7b5c2a7c26fd37b6a403082029b5a72fcb330a4d"
integrity sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw==
"@rc-component/table@~1.10.4":
version "1.10.4"
resolved "https://registry.yarnpkg.com/@rc-component/table/-/table-1.10.4.tgz#8c4e33bc150aa39f579c15426421348a789de326"
integrity sha512-HwoTnrwc29zeoXkXGhWqzJh8FIibGUxi1jM4LtoSzmR9d5Vv5osUQpZxnXKBP8iOCvyD6BQzZm1nXJRcnrxpAg==
dependencies:
"@rc-component/context" "^2.0.1"
"@rc-component/resize-observer" "^1.0.0"
@@ -3835,10 +3818,10 @@
"@rc-component/virtual-list" "^1.0.1"
clsx "^2.1.1"
"@rc-component/tabs@~1.12.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.12.0.tgz#41a1a77ed1afc4f1b8b727003a058c631aceea1b"
integrity sha512-XL7Kqy5fnUE2WTlO1/fCGrrfNlGFebdr7JseGkEIjzcVMAtIFQJ8sqCSOmxcXstjU6fonD/4rnhZHxj7sDTajQ==
"@rc-component/tabs@~1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/tabs/-/tabs-1.11.0.tgz#c157b2fadcdc2f3ab6c69d0098f73e03c6aa0c12"
integrity sha512-hA/drZYOVa/MMIb4M2fWf3yaTyTG4qVuIABmghvEhyfw2nBob5VTH69lMCDjSVKmgODjO6nWlCV+gVn3xBrj5Q==
dependencies:
"@rc-component/dropdown" "~1.0.0"
"@rc-component/menu" "~1.4.0"
@@ -3847,13 +3830,13 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/tooltip@~1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@rc-component/tooltip/-/tooltip-1.5.0.tgz#422aa0760b310e0a1d0f9f7223e7f0d455de57a2"
integrity sha512-agQ/+mBqrEQfTX4D3KhQ7j+ZbX4/VHjoJ7Noa2wIdZ1/FbQTOd7Sn92rp+jtCoqAVTLUgSOydePIgZ204gi2EQ==
"@rc-component/tooltip@~1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/tooltip/-/tooltip-1.4.0.tgz#c8cf15c6773218a5a36271467f06e663f99c28e7"
integrity sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==
dependencies:
"@rc-component/trigger" "^3.10.0"
"@rc-component/util" "^1.11.1"
"@rc-component/trigger" "^3.7.1"
"@rc-component/util" "^1.3.0"
clsx "^2.1.1"
"@rc-component/tour@~2.4.0":
@@ -3866,27 +3849,27 @@
"@rc-component/util" "^1.7.0"
clsx "^2.1.1"
"@rc-component/tree-select@~1.16.0":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.16.1.tgz#dcaea96e396e98108cb29cc051840d4fbdda38cc"
integrity sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg==
"@rc-component/tree-select@~1.11.0":
version "1.11.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree-select/-/tree-select-1.11.0.tgz#9080cdf1d28f2ddd6d8a4879b7aa90d3170f7db9"
integrity sha512-EhS0X0wtUhBfK4S5TlpSY3MR9ndPMGgujtt1PJW3Ej+ToAlnS/6ohYURtCoXBYGqazUwHmgQGVUDsfpVwhWPkg==
dependencies:
"@rc-component/select" "~1.10.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/select" "~1.8.0"
"@rc-component/tree" "~1.3.2"
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/tree@~1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.4.0.tgz#c0031180e681389bf0bdcb867a0087525b45c8a9"
integrity sha512-dGsJGDJQedA0BqqVgj3F8BvHXTSZijyhTXdbAdkcx8lynzZkty/CV3Z3LOm/fxz+BCfl3dfGiAQpb7Q5XNvl0Q==
"@rc-component/tree@~1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@rc-component/tree/-/tree-1.3.2.tgz#4b0c13564314eff61ca948c18ef923b87c9d7e44"
integrity sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA==
dependencies:
"@rc-component/motion" "^1.0.0"
"@rc-component/util" "^1.11.1"
"@rc-component/virtual-list" "^1.2.0"
clsx "^2.1.1"
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.10.0", "@rc-component/trigger@^3.10.1", "@rc-component/trigger@^3.6.15":
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.10.1", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-3.10.1.tgz#cb28e1bc0745a2af6897dd7ec774f9b56dc88f86"
integrity sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==
@@ -3905,7 +3888,7 @@
"@rc-component/util" "^1.11.1"
clsx "^2.1.1"
"@rc-component/util@^1.11.0", "@rc-component/util@^1.11.1", "@rc-component/util@^1.12.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.3.1", "@rc-component/util@^1.4.0", "@rc-component/util@^1.7.0", "@rc-component/util@^1.9.0":
"@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.11.1", "@rc-component/util@^1.12.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.7.0", "@rc-component/util@^1.9.0":
version "1.12.0"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.12.0.tgz#58e453585810bcb8a35ff1aafd5e01187457b86f"
integrity sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==
@@ -3923,16 +3906,6 @@
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@rc-component/virtual-list@^1.4.0":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@rc-component/virtual-list/-/virtual-list-1.5.1.tgz#71c5844a8d6bd5b3501dfb66419d3a4612b2bb18"
integrity sha512-boqHxdtyWC88u8quYgEO49bcBy5fzRiOcnBge+N4nLzs2k8hUQ/yw7JE9dM6yCBE4jSm5YSHVCVMS+suBuJGKA==
dependencies:
"@babel/runtime" "^8.0.0"
"@rc-component/resize-observer" "^1.0.1"
"@rc-component/util" "^1.4.0"
clsx "^2.1.1"
"@redocly/ajv@^8.18.1":
version "8.18.3"
resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.18.3.tgz#a925753d9a33375219f1b2ba91aef320f9929577"
@@ -5685,100 +5658,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.67.0", "@typescript-eslint/eslint-plugin@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz#52f9f0e47d5a7571c4336e69bfeea581509ef2cf"
integrity sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==
"@typescript-eslint/eslint-plugin@8.66.0", "@typescript-eslint/eslint-plugin@^8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz#76e86aa5a2459fbf5bbd7a839c0dc0cce1d56224"
integrity sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/type-utils" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
"@typescript-eslint/scope-manager" "8.66.0"
"@typescript-eslint/type-utils" "8.66.0"
"@typescript-eslint/utils" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.67.0", "@typescript-eslint/parser@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.67.0.tgz#0158022ec9927e0afcd58a8cc2ad57e01d892f5c"
integrity sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==
"@typescript-eslint/parser@8.66.0", "@typescript-eslint/parser@^8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.66.0.tgz#88e3865ecf73b0118134e7cb831da87a961a57a1"
integrity sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==
dependencies:
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
"@typescript-eslint/scope-manager" "8.66.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.67.0.tgz#1552db007ca9206a1c6c7acf49e210bd17a8c56f"
integrity sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==
"@typescript-eslint/project-service@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.66.0.tgz#828f788895df52d9eb2b543445a3a5a13e35ab4e"
integrity sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.67.0"
"@typescript-eslint/types" "^8.67.0"
"@typescript-eslint/tsconfig-utils" "^8.66.0"
"@typescript-eslint/types" "^8.66.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz#4d4c2da09560d10dd7d947cba2d29d14d25af16d"
integrity sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==
"@typescript-eslint/scope-manager@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz#4fffcc6ebd0df9fe7983c0256967567ea6f5ac63"
integrity sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==
dependencies:
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
"@typescript-eslint/tsconfig-utils@8.67.0", "@typescript-eslint/tsconfig-utils@^8.67.0":
"@typescript-eslint/tsconfig-utils@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz#3a89066c507aa30541dc176804685b4b444e1e52"
integrity sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==
"@typescript-eslint/tsconfig-utils@^8.66.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz#f45a3eba6b9132fb47141ec03ce2f275f1ea991d"
integrity sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==
"@typescript-eslint/type-utils@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz#96bed105275559df3bcf0449b73a6414d35c59ce"
integrity sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==
"@typescript-eslint/type-utils@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz#b2315303eca72fad9afa7be4f58f053c8f2a0479"
integrity sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==
dependencies:
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/utils" "8.66.0"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.67.0", "@typescript-eslint/types@^8.67.0":
"@typescript-eslint/types@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.66.0.tgz#3cacab94d3b564c1d48c56eb37b89f89a6d48479"
integrity sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==
"@typescript-eslint/types@^8.66.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.67.0.tgz#4a8d00cc1faba5c14feabc60f85b7a32652f34b6"
integrity sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==
"@typescript-eslint/typescript-estree@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz#116c3a47c06119c5a050e8851861d6497dd64bc2"
integrity sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==
"@typescript-eslint/typescript-estree@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz#1a38c3a97dc6c669b66d585d7f90ebc4fbb32a50"
integrity sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==
dependencies:
"@typescript-eslint/project-service" "8.67.0"
"@typescript-eslint/tsconfig-utils" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
"@typescript-eslint/project-service" "8.66.0"
"@typescript-eslint/tsconfig-utils" "8.66.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/visitor-keys" "8.66.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.67.0.tgz#3e478a3d69d330a1fc50c12746cc2ee0732ccfcd"
integrity sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==
"@typescript-eslint/utils@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.66.0.tgz#e277d67427043cdca2580ee91aa62921e4689969"
integrity sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/scope-manager" "8.66.0"
"@typescript-eslint/types" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/visitor-keys@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz#601d40af9acf82a28da2286f3edafc69bba9017f"
integrity sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==
"@typescript-eslint/visitor-keys@8.66.0":
version "8.66.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz#4c494e94745fb2724a4f37a310091e56b644d18a"
integrity sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==
dependencies:
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/types" "8.66.0"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -6181,10 +6164,10 @@ ansis@^3.2.0:
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
antd@^6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.6.0.tgz#8acb84c54b36594b5c1a9084c8acb6a03b79961b"
integrity sha512-UDwWIbpmrCHB9ZQ+bPh4vQfB6DTI2ulIyoQ0Tc9xxalFblttiNGHl3ySBD9SyV/8+gUjFzfSx1+iU1Fog2i46w==
antd@^6.5.4:
version "6.5.4"
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.4.tgz#b41665e86a5f46ca761abd3b0abef7460116ca0d"
integrity sha512-jchA6i0rEwHjLpgC+l6HeLHP0gL4Q4yjs6Mxqt6PlhGD5ArxCj3ZH+fKFbNquCtd6Rlzzi+emfNFpP2dGLwZzg==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/cssinjs" "^2.1.2"
@@ -6193,7 +6176,7 @@ antd@^6.6.0:
"@ant-design/icons" "^6.3.2"
"@ant-design/react-slick" "~2.0.0"
"@babel/runtime" "^7.29.2"
"@rc-component/cascader" "~1.22.0"
"@rc-component/cascader" "~1.17.0"
"@rc-component/checkbox" "~2.0.0"
"@rc-component/collapse" "~1.2.0"
"@rc-component/color-picker" "~3.1.1"
@@ -6201,11 +6184,10 @@ antd@^6.6.0:
"@rc-component/drawer" "~1.4.2"
"@rc-component/dropdown" "~1.0.3"
"@rc-component/form" "~1.8.6"
"@rc-component/image" "~1.10.0"
"@rc-component/image" "~1.9.0"
"@rc-component/input" "~1.3.1"
"@rc-component/input-number" "~1.6.2"
"@rc-component/listy" "~1.2.3"
"@rc-component/mentions" "~1.11.0"
"@rc-component/mentions" "~1.10.0"
"@rc-component/menu" "~1.4.1"
"@rc-component/motion" "^1.3.3"
"@rc-component/mutate-observer" "^2.0.1"
@@ -6217,16 +6199,16 @@ antd@^6.6.0:
"@rc-component/rate" "~1.0.1"
"@rc-component/resize-observer" "^1.1.2"
"@rc-component/segmented" "~1.3.0"
"@rc-component/select" "~1.10.0"
"@rc-component/select" "~1.8.2"
"@rc-component/slider" "~1.1.1"
"@rc-component/steps" "~1.2.2"
"@rc-component/switch" "~1.0.3"
"@rc-component/table" "~1.11.0"
"@rc-component/tabs" "~1.12.0"
"@rc-component/tooltip" "~1.5.0"
"@rc-component/table" "~1.10.4"
"@rc-component/tabs" "~1.11.0"
"@rc-component/tooltip" "~1.4.0"
"@rc-component/tour" "~2.4.0"
"@rc-component/tree" "~1.4.0"
"@rc-component/tree-select" "~1.16.0"
"@rc-component/tree" "~1.3.2"
"@rc-component/tree-select" "~1.11.0"
"@rc-component/trigger" "^3.10.1"
"@rc-component/upload" "~1.1.1"
"@rc-component/util" "^1.12.0"
@@ -6522,10 +6504,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.13, baseline-browser-mapping@^2.9.19:
version "2.11.13"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz#660073103c1bee93e54df55f117b7528adf6af19"
integrity sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.12, baseline-browser-mapping@^2.9.19:
version "2.11.12"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz#42ac48770bf73d292f60ce8ba4dc5e7ebb242ec3"
integrity sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==
batch@0.6.1:
version "0.6.1"
@@ -6763,10 +6745,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001809:
version "1.0.30001809"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz#e6cf71f14ddfe008f114dd2a846923be3c03a07b"
integrity sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001807:
version "1.0.30001807"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz#a113854941fb45b4c1f51793f4636920489079b4"
integrity sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==
ccount@^2.0.0:
version "2.0.1"
@@ -12262,32 +12244,32 @@ oxc-resolver@^11.19.1:
"@oxc-resolver/binding-win32-arm64-msvc" "11.23.0"
"@oxc-resolver/binding-win32-x64-msvc" "11.23.0"
oxfmt@^0.63.0:
version "0.63.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.63.0.tgz#c7338e6c43a68d5cf8dc61c08b617d77cb54e323"
integrity sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==
oxfmt@^0.62.0:
version "0.62.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.62.0.tgz#9945728022d26dc0a1d5bc486db112e7e340507a"
integrity sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ==
dependencies:
tinypool "2.1.0"
optionalDependencies:
"@oxfmt/binding-android-arm-eabi" "0.63.0"
"@oxfmt/binding-android-arm64" "0.63.0"
"@oxfmt/binding-darwin-arm64" "0.63.0"
"@oxfmt/binding-darwin-x64" "0.63.0"
"@oxfmt/binding-freebsd-x64" "0.63.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.63.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.63.0"
"@oxfmt/binding-linux-arm64-gnu" "0.63.0"
"@oxfmt/binding-linux-arm64-musl" "0.63.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.63.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.63.0"
"@oxfmt/binding-linux-riscv64-musl" "0.63.0"
"@oxfmt/binding-linux-s390x-gnu" "0.63.0"
"@oxfmt/binding-linux-x64-gnu" "0.63.0"
"@oxfmt/binding-linux-x64-musl" "0.63.0"
"@oxfmt/binding-openharmony-arm64" "0.63.0"
"@oxfmt/binding-win32-arm64-msvc" "0.63.0"
"@oxfmt/binding-win32-ia32-msvc" "0.63.0"
"@oxfmt/binding-win32-x64-msvc" "0.63.0"
"@oxfmt/binding-android-arm-eabi" "0.62.0"
"@oxfmt/binding-android-arm64" "0.62.0"
"@oxfmt/binding-darwin-arm64" "0.62.0"
"@oxfmt/binding-darwin-x64" "0.62.0"
"@oxfmt/binding-freebsd-x64" "0.62.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.62.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.62.0"
"@oxfmt/binding-linux-arm64-gnu" "0.62.0"
"@oxfmt/binding-linux-arm64-musl" "0.62.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.62.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.62.0"
"@oxfmt/binding-linux-riscv64-musl" "0.62.0"
"@oxfmt/binding-linux-s390x-gnu" "0.62.0"
"@oxfmt/binding-linux-x64-gnu" "0.62.0"
"@oxfmt/binding-linux-x64-musl" "0.62.0"
"@oxfmt/binding-openharmony-arm64" "0.62.0"
"@oxfmt/binding-win32-arm64-msvc" "0.62.0"
"@oxfmt/binding-win32-ia32-msvc" "0.62.0"
"@oxfmt/binding-win32-x64-msvc" "0.62.0"
p-cancelable@^3.0.0:
version "3.0.0"
@@ -15485,15 +15467,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.67.0:
version "8.67.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.67.0.tgz#1e92de09ee0ff2d96cc0848f5e9f345ea930d963"
integrity sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==
typescript-eslint@^8.66.0:
version "8.66.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.66.0.tgz#0809b6d25c8a0924690ba30dc1f05607093c11fb"
integrity sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==
dependencies:
"@typescript-eslint/eslint-plugin" "8.67.0"
"@typescript-eslint/parser" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
"@typescript-eslint/eslint-plugin" "8.66.0"
"@typescript-eslint/parser" "8.66.0"
"@typescript-eslint/typescript-estree" "8.66.0"
"@typescript-eslint/utils" "8.66.0"
typescript@~6.0.3:
version "6.0.3"
+6 -6
View File
@@ -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",
]
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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)
+6 -6
View File
@@ -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
@@ -18,9 +18,8 @@
from __future__ import annotations
import enum
from dataclasses import dataclass, field
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from typing import Any
import isodate
import pyarrow as pa
@@ -91,8 +90,6 @@ class Dimension:
definition: str | None = None
description: str | None = None
grain: Grain | None = None
verbose_name: str | None = field(default=None, compare=False)
metadata: dict[str, Any] = field(default_factory=dict, compare=False)
class AggregationType(str, enum.Enum):
@@ -124,9 +121,6 @@ class Metric:
definition: str
description: str | None = None
aggregation: AggregationType | None = None
verbose_name: str | None = field(default=None, compare=False)
d3format: str | None = field(default=None, compare=False)
metadata: dict[str, Any] = field(default_factory=dict, compare=False)
@dataclass(frozen=True)
@@ -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.
import pyarrow as pa
from superset_core.semantic_layers.types import Dimension, Metric
def test_dimension_metadata_is_not_part_of_identity() -> None:
first = Dimension(
"sales.region",
"region",
pa.utf8(),
verbose_name="Region",
metadata={"display_name": "Region"},
)
second = Dimension(
"sales.region",
"region",
pa.utf8(),
verbose_name="Sales region",
metadata={"display_name": "Sales region"},
)
assert first == second
assert {first, second} == {first}
def test_metric_metadata_is_not_part_of_identity() -> None:
first = Metric(
"sales.total_revenue",
"total_revenue",
pa.float64(),
"SUM(revenue)",
verbose_name="Total revenue",
d3format="$,.2f",
metadata={"unit": {"kind": "currency", "code": "USD"}},
)
second = Metric(
"sales.total_revenue",
"total_revenue",
pa.float64(),
"SUM(revenue)",
verbose_name="Revenue",
d3format=",.0f",
metadata={"unit": {"kind": "currency", "code": "EUR"}},
)
assert first == second
assert {first, second} == {first}
def test_metric_accepts_superset_presentation_fields() -> None:
metric = Metric(
"sales.total_revenue",
"total_revenue",
pa.float64(),
"SUM(revenue)",
verbose_name="Total revenue",
d3format="$,.2f",
)
assert metric.verbose_name == "Total revenue"
assert metric.d3format == "$,.2f"
def test_dimension_accepts_superset_presentation_fields() -> None:
dimension = Dimension(
"sales.region",
"region",
pa.utf8(),
verbose_name="Region",
)
assert dimension.verbose_name == "Region"
def test_metadata_defaults_are_not_shared() -> None:
first = Metric("first", "first", pa.int64(), "COUNT(*)")
second = Metric("second", "second", pa.int64(), "COUNT(*)")
first.metadata["display_name"] = "First"
assert second.metadata == {}
-506
View File
@@ -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",
-1
View File
@@ -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",
-213
View File
@@ -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',
);
});
});
-22
View File
@@ -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,
+632 -352
View File
File diff suppressed because it is too large Load Diff
+11 -12
View File
@@ -110,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",
@@ -158,7 +158,7 @@
"@visx/xychart": "^4.0.0",
"ag-grid-community": "36.1.0",
"ag-grid-react": "36.1.0",
"antd": "^6.6.0",
"antd": "^6.5.4",
"chrono-node": "^2.10.1",
"classnames": "^2.2.5",
"content-disposition": "^2.0.1",
@@ -176,7 +176,7 @@
"geostyler-openlayers-parser": "^5.7.1",
"geostyler-style": "11.0.2",
"geostyler-wfs-parser": "^3.0.1",
"google-auth-library": "^11.0.1",
"google-auth-library": "^11.0.0",
"immer": "^11.1.16",
"interweave": "^13.1.1",
"jquery": "^4.0.0",
@@ -266,7 +266,7 @@
"@swc/plugin-emotion": "^14.15.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^15.0.0",
"@testing-library/user-event": "^12.8.3",
"@types/content-disposition": "^0.5.9",
@@ -277,7 +277,7 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.2.0",
"@types/node": "^26.1.2",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
@@ -289,19 +289,19 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/eslint-plugin": "^8.66.0",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.11.13",
"baseline-browser-mapping": "^2.11.12",
"cheerio": "1.2.0",
"concurrently": "^10.0.4",
"copy-webpack-plugin": "^14.0.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.4",
"eslint": "^10.8.1",
"eslint": "^10.8.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^4.4.5",
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
@@ -331,8 +331,8 @@
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.6.1",
"open-cli": "^9.0.0",
"oxfmt": "^0.63.0",
"oxlint": "^1.78.0",
"oxfmt": "^0.62.0",
"oxlint": "^1.77.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
"process": "^0.11.10",
@@ -349,7 +349,7 @@
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.12",
"tscw-config": "^1.1.2",
"tsx": "^4.23.12",
"tsx": "^4.23.10",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
@@ -415,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",
@@ -103,7 +103,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.25",
"@types/node": "^26.2.0",
"@types/node": "^26.1.2",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -107,14 +107,12 @@ const getAllSelectOptions = () =>
const findSelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.ant-select-dropdown-list')).getByText(text),
within(getElementByClassName('.rc-virtual-list')).getByText(text),
);
const querySelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.ant-select-dropdown-list')).queryByText(
text,
),
within(getElementByClassName('.rc-virtual-list')).queryByText(text),
);
const findAllSelectOptions = () =>
@@ -646,7 +644,7 @@ test('does not add a new option if the option already exists', async () => {
await type(option);
await waitFor(() => {
const array = within(
getElementByClassName('.ant-select-dropdown-list'),
getElementByClassName('.rc-virtual-list'),
).getAllByText(option);
expect(array.length).toBe(1);
});
@@ -1400,7 +1398,7 @@ test('appends page>1 results during an active search and discards them when sear
// scrollTop via e.currentTarget in its onFallbackScroll handler, which
// then forwards to onPopupScroll (handlePagination here).
const holder = document.querySelector(
'.ant-select-dropdown-list-holder',
'.rc-virtual-list-holder',
) as HTMLElement | null;
if (!holder) throw new Error('virtual-list holder not rendered');
Object.defineProperty(holder, 'scrollHeight', {
@@ -93,14 +93,12 @@ const deselectAllButtonText = (length: number) =>
const findSelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.ant-select-dropdown-list')).getByText(text),
within(getElementByClassName('.rc-virtual-list')).getByText(text),
);
const querySelectOption = (text: string) =>
waitFor(() =>
within(getElementByClassName('.ant-select-dropdown-list')).queryByText(
text,
),
within(getElementByClassName('.rc-virtual-list')).queryByText(text),
);
const getAllSelectOptions = () =>
@@ -19,71 +19,19 @@
import { t } from '@apache-superset/core/translation';
import { sanitizeHtml } from './html';
export type TooltipTruncationMode = 'off' | 'end' | 'start' | 'middle';
export const TRUNCATION_MAX_CHARS = 40;
const TRUNCATION_STYLE = `
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
`;
const NOWRAP_STYLE = `
white-space: nowrap;
`;
/**
* Shortens plain text so a tooltip label stays readable, placing the ellipsis
* where the caller asked for it.
*
* Only 'start' and 'middle' slice. 'end' is handled by CSS in tooltipHtml, and
* 'off' means no truncation at all, so both return the input untouched.
*
* The input must be plain text. Callers are responsible for truncating before
* any markup (such as the ECharts series marker) is prepended, and before
* sanitization slicing a string that already contains markup would cut into
* a tag.
*/
export function truncateLabel(
text: string,
mode: TooltipTruncationMode = 'end',
): string {
if (
(mode !== 'start' && mode !== 'middle') ||
text.length <= TRUNCATION_MAX_CHARS
) {
return text;
}
const budget = TRUNCATION_MAX_CHARS - 1;
if (mode === 'start') {
return `${text.slice(-budget)}`;
}
const head = Math.ceil(budget / 2);
const tail = Math.floor(budget / 2);
return `${text.slice(0, head)}${text.slice(-tail)}`;
}
function getTruncationStyle(mode: TooltipTruncationMode): string {
if (mode === 'end') {
return TRUNCATION_STYLE;
}
if (mode === 'off') {
return '';
}
// 'start' and 'middle' are already sliced upstream; keep them on one line.
return NOWRAP_STYLE;
}
export function tooltipHtml(
data: string[][],
title?: string,
focusedRow?: number,
truncation: TooltipTruncationMode = 'end',
) {
const truncationStyle = getTruncationStyle(truncation);
const titleRow = title
? `<span style="font-weight: 700;${truncationStyle}">${title}</span>`
? `<span style="font-weight: 700;${TRUNCATION_STYLE}">${title}</span>`
: '';
return sanitizeHtml(`
<div>
@@ -98,7 +46,7 @@ export function tooltipHtml(
const cellStyle = `
text-align: ${j > 0 ? 'right' : 'left'};
padding-left: ${j === 0 ? 0 : 16}px;
${truncationStyle}
${TRUNCATION_STYLE}
`;
return `<td style="${cellStyle}">${cell}</td>`;
});
@@ -16,12 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
sanitizeHtml,
tooltipHtml,
truncateLabel,
TRUNCATION_MAX_CHARS,
} from '@superset-ui/core';
import { sanitizeHtml, tooltipHtml } from '@superset-ui/core';
const TITLE_STYLE =
'style="font-weight: 700;max-width:300px;overflow:hidden;text-overflow:ellipsis;"';
@@ -187,88 +182,3 @@ test('should preserve table styling after sanitization (fixes ECharts tooltip fo
expect(html).toContain('padding-left:16px');
expect(html).toContain('max-width:300px');
});
describe('truncateLabel', () => {
const long = 'prod-us-east-1-service-checkout-latency-p99'; // 43 chars
test('returns text unchanged for off and end', () => {
expect(truncateLabel(long, 'off')).toBe(long);
expect(truncateLabel(long, 'end')).toBe(long);
});
test('defaults to end, which does not slice', () => {
expect(truncateLabel(long)).toBe(long);
});
test('truncates the start, keeping the distinguishing suffix', () => {
expect(truncateLabel(long, 'start')).toBe(
'…-us-east-1-service-checkout-latency-p99',
);
expect(truncateLabel(long, 'start')).toHaveLength(TRUNCATION_MAX_CHARS);
});
test('truncates the middle, keeping both ends', () => {
expect(truncateLabel(long, 'middle')).toBe(
'prod-us-east-1-servi…heckout-latency-p99',
);
expect(truncateLabel(long, 'middle')).toHaveLength(TRUNCATION_MAX_CHARS);
});
test('leaves text at or under the limit untouched', () => {
const atLimit = 'x'.repeat(TRUNCATION_MAX_CHARS);
expect(truncateLabel(atLimit, 'start')).toBe(atLimit);
expect(truncateLabel(atLimit, 'middle')).toBe(atLimit);
expect(truncateLabel('short', 'start')).toBe('short');
expect(truncateLabel('', 'middle')).toBe('');
});
test('truncates text one character over the limit', () => {
const overLimit = 'x'.repeat(TRUNCATION_MAX_CHARS + 1);
expect(truncateLabel(overLimit, 'start')).toBe(
`${'x'.repeat(TRUNCATION_MAX_CHARS - 1)}`,
);
});
});
describe('tooltipHtml truncation modes', () => {
const rows = [['label', 'value']];
// sanitizeHtml normalizes spacing inside style attributes, and it does so
// differently across versions, so compare with whitespace stripped.
const styles = (
title: string | undefined,
truncation?: 'off' | 'end' | 'start' | 'middle',
) => removeWhitespaces(tooltipHtml(rows, title, undefined, truncation));
test('emits the 300px cap for end and for the default', () => {
expect(styles('Title', 'end')).toContain('max-width:300px');
expect(tooltipHtml(rows, 'Title')).toBe(
tooltipHtml(rows, 'Title', undefined, 'end'),
);
});
test('emits no truncation style for off', () => {
const html = styles('Title', 'off');
expect(html).not.toContain('max-width');
expect(html).not.toContain('text-overflow');
expect(html).not.toContain('white-space');
});
test.each(['start', 'middle'] as const)(
'emits nowrap instead of a cap for %s',
mode => {
const html = styles('Title', mode);
expect(html).toContain('white-space:nowrap');
expect(html).not.toContain('max-width');
},
);
test('never slices cell text itself, whatever the mode', () => {
const longCell = 'y'.repeat(TRUNCATION_MAX_CHARS + 20);
(['off', 'end', 'start', 'middle'] as const).forEach(mode => {
expect(tooltipHtml([[longCell]], undefined, undefined, mode)).toContain(
longCell,
);
});
});
});
@@ -22,7 +22,7 @@ import { getSequentialSchemeRegistry } from '@superset-ui/core';
import { SupersetTheme } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import CalHeatMapImport from './vendor/cal-heatmap';
import { convertUTCTimestampToLocal, getFormattedUTCTime } from './utils';
import { convertUTCTimestampToLocal } from './utils';
// The vendor file is @ts-nocheck, so its export lacks type info.
// Define a minimal constructor interface for use in this file.
@@ -103,8 +103,6 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
const subDomainTextFormat = showValues
? (_date: Date, value: number) => valueFormatter(value)
: null;
const dateFormatter = (date: Date, format: string) =>
getFormattedUTCTime(date.getTime(), format);
const metricsData = data.data;
@@ -168,7 +166,6 @@ function Calendar(element: HTMLElement, props: CalendarProps) {
itemName: '',
valueFormatter,
timeFormatter,
dateFormatter,
subDomainTextFormat,
});
});
@@ -76,8 +76,6 @@ var CalHeatMap = function () {
timeFormatter: d => d,
dateFormatter: null,
domain: 'hour',
subDomain: 'min',
@@ -1992,14 +1990,10 @@ CalHeatMap.prototype = {
if (typeof format === 'function') {
return format(d);
} else {
var f = d3.time.format(format);
return f(d);
}
if (typeof this.options.dateFormatter === 'function') {
return this.options.dateFormatter(d, format);
}
var f = d3.time.format(format);
return f(d);
},
getSubDomainTitle: function (d) {
@@ -25,11 +25,9 @@ import {
waitFor,
} from 'spec/helpers/testing-library';
import { CALENDAR_TOOLTIP_CLASS } from '../src/tooltip';
import { convertUTCTimestampToLocal } from '../src/utils';
interface MockCalHeatMapConfig {
itemSelector: Element;
dateFormatter?: (date: Date, format: string) => string;
}
type MetricNameInput = string | string[];
@@ -40,7 +38,6 @@ let mockInitCallCount = 0;
let mockThrowOnInitCall: number | null = null;
let mockDestroyCallCount = 0;
let mockDestroyedInstanceIds: string[] = [];
let mockDateFormatter: MockCalHeatMapConfig['dateFormatter'];
const mockTheme = {
colorBgElevated: '#ffffff',
@@ -59,7 +56,6 @@ jest.mock('../src/vendor/cal-heatmap', () => ({
} = require('../src/tooltip');
mockInitCallCount += 1;
mockDateFormatter = config.dateFormatter;
if (mockThrowOnInitCall === mockInitCallCount) {
throw new Error('Mock CalHeatMap init failure');
}
@@ -288,28 +284,9 @@ afterEach(() => {
mockThrowOnInitCall = null;
mockDestroyCallCount = 0;
mockDestroyedInstanceIds = [];
mockDateFormatter = undefined;
document.body.innerHTML = '';
});
test('Calendar provides a timezone-safe date formatter to CalHeatMap', () => {
const calendarOwner = document.createElement('div');
document.body.appendChild(calendarOwner);
Calendar(calendarOwner, {
...createCalendarProps('localized-metric'),
theme: mockTheme,
});
if (!mockDateFormatter) {
throw new Error('Expected Calendar to configure a date formatter');
}
const localDate = new Date(convertUTCTimestampToLocal(Date.UTC(2024, 0, 1)));
expect(mockDateFormatter(localDate, '%Y-%m-%d')).toBe('2024-01-01');
});
test('rerender and unmount clean up only the affected calendar tooltips', () => {
jest.useFakeTimers();
@@ -1,61 +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 CalHeatMapImport from '../src/vendor/cal-heatmap';
type DateFormatter = (date: Date, format: string) => string;
type FunctionalDateFormat = (date: Date) => string;
interface CalHeatMapInstance {
options: {
dateFormatter: DateFormatter | null;
};
formatDate(date: Date, format: string | FunctionalDateFormat): string;
}
const CalHeatMap = CalHeatMapImport as unknown as new () => CalHeatMapInstance;
test('CalHeatMap delegates string date formats to the configured formatter', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
const dateFormatter = jest.fn<string, [Date, string]>(() => 'Январь');
calendar.options.dateFormatter = dateFormatter;
expect(calendar.formatDate(date, '%B')).toBe('Январь');
expect(dateFormatter).toHaveBeenCalledWith(date, '%B');
});
test('CalHeatMap preserves functional formatters over the configured formatter', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
const dateFormatter = jest.fn<string, [Date, string]>(() => 'localized');
const functionalFormat = jest.fn<string, [Date]>(() => 'custom');
calendar.options.dateFormatter = dateFormatter;
expect(calendar.formatDate(date, functionalFormat)).toBe('custom');
expect(functionalFormat).toHaveBeenCalledWith(date);
expect(dateFormatter).not.toHaveBeenCalled();
});
test('CalHeatMap keeps the D3 formatter fallback', () => {
const calendar = new CalHeatMap();
const date = new Date(2024, 0, 1);
expect(calendar.formatDate(date, '%B')).toBe('January');
});
@@ -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',
@@ -40,7 +40,6 @@ import {
TimeseriesChartDataResponseResult,
TimeseriesDataRecord,
tooltipHtml,
truncateLabel,
ValueFormatter,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
@@ -208,7 +207,6 @@ export default function transformProps(
zoomable,
richTooltip,
tooltipSortByMetric,
tooltipTruncation,
xAxisBounds,
xAxisLabelRotation,
xAxisLabelInterval,
@@ -909,19 +907,13 @@ export default function transformProps(
formatter: primarySeries.has(key)
? tooltipFormatter
: tooltipFormatterSecondary,
truncation: tooltipTruncation,
});
rows.push(row);
if (key === focusedSeries) {
focusedRow = rows.length - 1;
}
});
return tooltipHtml(
rows,
truncateLabel(tooltipFormatter(xValue), tooltipTruncation),
focusedRow,
tooltipTruncation,
);
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
},
},
legend: {
@@ -24,7 +24,6 @@ import {
ContributionType,
TimeFormatter,
AxisType,
TooltipTruncationMode,
} from '@superset-ui/core';
import {
BaseChartProps,
@@ -60,7 +59,6 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & {
timeGrainSqla?: TimeGranularity;
forceMaxInterval?: boolean;
tooltipTimeFormat?: string;
tooltipTruncation?: TooltipTruncationMode;
zoomable: boolean;
richTooltip: boolean;
showQueryIdentifiers?: boolean;
@@ -110,7 +108,6 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = {
yAxisFormatSecondary: TIMESERIES_DEFAULTS.yAxisFormat,
yAxisTitleSecondary: DEFAULT_TITLE_FORM_DATA.yAxisTitle,
tooltipTimeFormat: TIMESERIES_DEFAULTS.tooltipTimeFormat,
tooltipTruncation: TIMESERIES_DEFAULTS.tooltipTruncation,
xAxisBounds: TIMESERIES_DEFAULTS.xAxisBounds,
xAxisForceCategorical: TIMESERIES_DEFAULTS.xAxisForceCategorical,
xAxisTimeFormat: TIMESERIES_DEFAULTS.xAxisTimeFormat,
@@ -73,7 +73,6 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
seriesType: EchartsTimeseriesSeriesType.Line,
stack: false,
tooltipTimeFormat: 'smart_date',
tooltipTruncation: 'end',
xAxisTimeFormat: 'smart_date',
xAxisNumberFormat: 'SMART_NUMBER',
truncateXAxis: true,
@@ -30,7 +30,6 @@ import {
DTTM_ALIAS,
ensureIsArray,
tooltipHtml,
truncateLabel,
getCustomFormatter,
getMetricLabel,
getNumberFormatter,
@@ -304,7 +303,6 @@ export default function transformProps(
tooltipSortByMetric,
showTooltipTotal,
showTooltipPercentage,
tooltipTruncation,
truncateXAxis,
truncateYAxis,
xAxis: xAxisOrig,
@@ -1451,7 +1449,6 @@ export default function transformProps(
seriesName: key,
formatter,
marker,
truncation: tooltipTruncation,
});
const annotationRow = annotationLayers.some(
@@ -1485,12 +1482,7 @@ export default function transformProps(
}
rows.push(totalRow);
}
return tooltipHtml(
rows,
truncateLabel(tooltipFormatter(xValue), tooltipTruncation),
focusedRow,
tooltipTruncation,
);
return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow);
},
},
legend: {
@@ -25,7 +25,6 @@ import {
QueryFormMetric,
TimeFormatter,
TimeGranularity,
TooltipTruncationMode,
} from '@superset-ui/core';
import {
BaseChartProps,
@@ -83,7 +82,6 @@ export type EchartsTimeseriesFormData = QueryFormData & {
tooltipTimeFormat?: string;
showTooltipTotal?: boolean;
showTooltipPercentage?: boolean;
tooltipTruncation?: TooltipTruncationMode;
truncateXAxis: boolean;
truncateYAxis: boolean;
yAxisFormat?: string;
@@ -16,13 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { EChartsCoreOption } from 'echarts/core';
import { render, waitFor } from '../../../../spec/helpers/testing-library';
import Echart, {
ECHARTS_HOST_CLASS,
ECHARTS_RENDER_FINISHED_CLASS,
isReportScreenshotMode,
} from './Echart';
import type { EChartsCoreOption } from 'echarts/core';
import Echart, { isReportScreenshotMode } from './Echart';
import type { EchartsProps } from '../types';
type Handler = (params: unknown) => void;
@@ -276,31 +272,3 @@ test('keeps animation enabled when not in report screenshot mode', async () => {
const lastOptions = mockChart.setOption.mock.calls.at(-1)?.[0];
expect(lastOptions.animation).not.toBe(false);
});
test('tags the ECharts canvas host with the readiness-gate class', async () => {
const { container } = render(renderEchart(), {
initialState,
useRedux: true,
});
await waitFor(() => expect(mockChart.setOption).toHaveBeenCalled());
expect(container.querySelector(`.${ECHARTS_HOST_CLASS}`)).not.toBeNull();
});
test('marks the host painted only on the ECharts `finished` event', async () => {
const { container } = render(renderEchart(), {
initialState,
useRedux: true,
});
await waitFor(() => expect(mockChart.setOption).toHaveBeenCalled());
const host = container.querySelector(`.${ECHARTS_HOST_CLASS}`) as HTMLElement;
expect(host).not.toBeNull();
// `setOption` ran during mount, which clears the marker; `finished` has not
// fired yet, so the host must NOT be flagged as painted.
expect(host).not.toHaveClass(ECHARTS_RENDER_FINISHED_CLASS);
// Simulate ECharts completing its draw -> the host is flagged painted.
trigger('finished');
expect(host).toHaveClass(ECHARTS_RENDER_FINISHED_CLASS);
});
@@ -138,15 +138,6 @@ export function isReportScreenshotMode(): boolean {
}
}
// Report-screenshot readiness contract (see superset/utils/screenshot_utils.py).
// `echarts-host` marks the canvas host element; `echarts-render-finished` is
// toggled OFF before each setOption and ON in the ECharts `finished` event --
// the only signal that the canvas is fully painted (chartStatus/onRenderSuccess
// both fire pre-paint). The readiness gate treats a host that lacks
// `echarts-render-finished` as not-yet-painted so it never captures a blank chart.
export const ECHARTS_HOST_CLASS = 'echarts-host';
export const ECHARTS_RENDER_FINISHED_CLASS = 'echarts-render-finished';
function Echart(
{
width,
@@ -210,11 +201,6 @@ function Echart(
width,
height,
});
// Paint marker for the report-screenshot readiness gate. `finished`
// is the only event that guarantees the canvas is fully drawn.
chartRef.current.on('finished', () => {
divRef.current?.classList.add(ECHARTS_RENDER_FINISHED_CLASS);
});
}
// did mount
handleSizeChange({ width, height });
@@ -335,9 +321,6 @@ function Echart(
}
)?.dataZoom
: undefined;
// Clear the paint marker before (re)drawing; the `finished` handler
// re-adds it once the new frame is fully rendered.
divRef.current?.classList.remove(ECHARTS_RENDER_FINISHED_CLASS);
chartRef.current?.setOption(themedEchartOptions, {
notMerge,
replaceMerge: notMerge ? undefined : ['series'],
@@ -429,14 +412,7 @@ function Echart(
handleSizeChange({ width, height });
}, [width, height, handleSizeChange]);
return (
<Styles
ref={divRef}
className={ECHARTS_HOST_CLASS}
height={height}
width={width}
/>
);
return <Styles ref={divRef} height={height} width={width} />;
}
export default forwardRef(Echart);
@@ -315,27 +315,6 @@ const tooltipPercentageControl: ControlSetItem = {
},
};
const tooltipTruncationControl: ControlSetItem = {
name: 'tooltipTruncation',
config: {
type: 'SelectControl',
freeForm: false,
label: t('Truncate labels'),
renderTrigger: true,
default: 'end',
clearable: false,
choices: [
['off', t('Off')],
['end', t('End')],
['start', t('Start')],
['middle', t('Middle')],
],
description: t(
'Where to place the ellipsis when a tooltip label is too long. Choose Off to always show the full label, or Start when labels share a common prefix.',
),
},
};
export const richTooltipSection: ControlSetRow[] = [
[<ControlSubSectionHeader>{t('Tooltip')}</ControlSubSectionHeader>],
[richTooltipControl],
@@ -343,7 +322,6 @@ export const richTooltipSection: ControlSetRow[] = [
[tooltipPercentageControl],
[tooltipSortByMetricControl],
[tooltipTimeFormatControl],
[tooltipTruncationControl],
];
const sortSeriesType: ControlSetItem = {
@@ -16,13 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
DataRecord,
DTTM_ALIAS,
truncateLabel,
TooltipTruncationMode,
ValueFormatter,
} from '@superset-ui/core';
import { DataRecord, DTTM_ALIAS, ValueFormatter } from '@superset-ui/core';
import type { OptionName, SeriesOption } from 'echarts/types/src/util/types';
import type { TooltipMarker } from 'echarts/types/src/util/format';
import {
@@ -97,16 +91,12 @@ export const formatForecastTooltipSeries = ({
forecastUpper,
marker,
formatter,
truncation = 'end',
}: ForecastValue & {
seriesName: string;
marker: TooltipMarker;
formatter: ValueFormatter;
truncation?: TooltipTruncationMode;
}): string[] => {
// Truncate before sanitizing and before the marker is prepended: slicing a
// string that already contains markup would cut into the marker's tag.
const name = `${marker}${sanitizeHtml(truncateLabel(seriesName, truncation))}`;
const name = `${marker}${sanitizeHtml(seriesName)}`;
let value = typeof observation === 'number' ? formatter(observation) : '';
// Use finite-number checks rather than truthiness so that legitimate
// zero values (e.g. a forecast that crosses zero, or a confidence bound of
@@ -27,7 +27,6 @@ import {
VizType,
ChartDataResponseResult,
TimeGranularity,
TooltipTruncationMode,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import {
@@ -1296,59 +1295,3 @@ test('y-axis title position: non-Left sets nameLocation to end', () => {
expect(yAxis[1].nameGap).toEqual(30);
expect(yAxis[1].nameLocation).toEqual('end');
});
describe('EchartsMixedTimeseries tooltip truncation', () => {
const longSeriesName = 'prod-us-east-1-service-checkout-latency-p99';
const marker = '<span style="background-color:#1f77b4;"></span>';
const buildTooltip = (tooltipTruncation?: TooltipTruncationMode) => {
const chartProps = createEchartsTimeseriesTestChartProps<
EchartsMixedTimeseriesFormData,
EchartsMixedTimeseriesProps
>({
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
defaultQueriesData: queriesData,
formData: {
...formData,
...(tooltipTruncation ? { tooltipTruncation } : {}),
},
queriesData,
});
const { echartOptions } = transformProps(chartProps);
const { formatter } = echartOptions.tooltip as {
formatter: (params: unknown) => string;
};
// richTooltip is false in this fixture, so the trigger is 'item' and the
// formatter receives a single param object rather than an array.
return formatter({
seriesId: longSeriesName,
seriesName: longSeriesName,
value: [599616000000, 1],
marker,
});
};
test('keeps full text with the CSS cap by default', () => {
const html = buildTooltip();
expect(html.replace(/\s/g, '')).toContain('max-width:300px');
expect(html).toContain(longSeriesName);
});
test('removes the cap and keeps full text when off', () => {
const html = buildTooltip('off');
expect(html).not.toContain('max-width');
expect(html).toContain(longSeriesName);
});
test('drops the shared prefix when truncating from the start', () => {
const html = buildTooltip('start');
expect(html).not.toContain('prod-us-east');
expect(html).toContain('latency-p99');
expect(html).toContain('background-color:#1f77b4');
});
test('keeps both ends when truncating the middle', () => {
const html = buildTooltip('middle');
expect(html).toContain('prod-us-east-1-servi…heckout-latency-p99');
expect(html).not.toContain(longSeriesName);
});
});
@@ -32,7 +32,6 @@ import {
TimeseriesAnnotationLayer,
ChartDataResponseResult,
TimeGranularity,
TooltipTruncationMode,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { supersetTheme } from '@apache-superset/core/theme';
@@ -2438,94 +2437,3 @@ test('honors the snake_case flag the compare-chart migration stores in params',
[BASE_TIMESTAMP + 300000000, 2],
]);
});
describe('EchartsTimeseries tooltip truncation', () => {
const longSeriesName = 'prod-us-east-1-service-checkout-latency-p99';
const marker = '<span style="background-color:#1f77b4;"></span>';
const buildTooltip = (
tooltipTruncation?: TooltipTruncationMode,
xValue: string | number = 599616000000,
) => {
const chartProps = new ChartProps({
formData: {
colorScheme: 'bnbColors',
datasource: '3__table',
granularity_sqla: 'ds',
metric: 'sum__num',
groupby: ['foo'],
viz_type: 'my_viz',
...(tooltipTruncation ? { tooltipTruncation } : {}),
} as SqlaFormData,
width: 800,
height: 600,
queriesData: [
{
data: [
{ [longSeriesName]: 1, __timestamp: 599616000000 },
{ [longSeriesName]: 3, __timestamp: 599916000000 },
],
},
],
theme: supersetTheme,
});
const { echartOptions } = transformProps(
chartProps as EchartsTimeseriesChartProps,
);
const { formatter } = echartOptions.tooltip as {
formatter: (params: unknown) => string;
};
return formatter([
{
seriesId: longSeriesName,
seriesName: longSeriesName,
value: [xValue, 1],
marker,
},
]);
};
test('applies the CSS cap and keeps full text by default', () => {
const html = buildTooltip();
expect(html).toContain(longSeriesName);
// sanitizeHtml normalizes spacing inside style attributes, so compare with
// whitespace stripped rather than hard-coding one version's formatting.
expect(html.replace(/\s/g, '')).toContain('max-width:300px');
});
test('removes the cap and keeps full text when off', () => {
const html = buildTooltip('off');
expect(html).not.toContain('max-width');
expect(html).toContain(longSeriesName);
});
test('drops the shared prefix when truncating from the start', () => {
const html = buildTooltip('start');
expect(html).not.toContain('prod-us-east');
expect(html).toContain('latency-p99');
expect(html.replace(/\s/g, '')).toContain('white-space:nowrap');
});
test('keeps both ends when truncating the middle', () => {
const html = buildTooltip('middle');
expect(html).toContain('prod-us-east-1-servi…heckout-latency-p99');
expect(html).not.toContain(longSeriesName);
});
test('preserves the echarts marker in every mode', () => {
(['off', 'end', 'start', 'middle'] as const).forEach(mode => {
expect(buildTooltip(mode)).toContain('background-color:#1f77b4');
});
});
test('truncates a long non-temporal x-axis title', () => {
const longCategory = 'prod-us-east-1-service-checkout-cohort-2026';
const html = buildTooltip('start', longCategory);
expect(html).not.toContain(longCategory);
expect(html).toContain('cohort-2026');
});
test('leaves a long title alone in the default mode', () => {
const longCategory = 'prod-us-east-1-service-checkout-cohort-2026';
expect(buildTooltip(undefined, longCategory)).toContain(longCategory);
});
});
@@ -16,11 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
getNumberFormatter,
NumberFormats,
TRUNCATION_MAX_CHARS,
} from '@superset-ui/core';
import { getNumberFormatter, NumberFormats } from '@superset-ui/core';
import { SeriesOption } from 'echarts';
import {
extractForecastSeriesContext,
@@ -415,52 +411,3 @@ test('formatForecastTooltipSeries should skip non-finite forecast values', () =>
}),
).toEqual(['<img>qwerty', '10']);
});
describe('formatForecastTooltipSeries truncation', () => {
const marker =
'<span style="display:inline-block;width:10px;height:10px;background-color:#1f77b4;"></span>';
const longName = 'prod-us-east-1-service-checkout-latency-p99'; // 43 chars
const intFormatter = getNumberFormatter(NumberFormats.INTEGER);
const format = (truncation?: 'off' | 'end' | 'start' | 'middle') =>
formatForecastTooltipSeries({
seriesName: longName,
observation: 1,
marker,
formatter: intFormatter,
...(truncation ? { truncation } : {}),
})[0];
test('leaves the name intact by default and for off/end', () => {
expect(format()).toContain(longName);
expect(format('off')).toContain(longName);
expect(format('end')).toContain(longName);
});
test('slices the start of the name without harming the marker', () => {
const cell = format('start');
expect(cell).toContain(marker);
expect(cell).toContain('…-us-east-1-service-checkout-latency-p99');
expect(cell).not.toContain('prod-us-east');
});
test('slices the middle of the name without harming the marker', () => {
const cell = format('middle');
expect(cell).toContain(marker);
expect(cell).toContain('prod-us-east-1-servi…heckout-latency-p99');
});
test('measures the budget against the name, not the marker markup', () => {
// The marker alone is far longer than the budget. If truncation were
// applied to the concatenated cell, a short name would be mangled.
expect(marker.length).toBeGreaterThan(TRUNCATION_MAX_CHARS);
const [cell] = formatForecastTooltipSeries({
seriesName: 'cpu',
observation: 1,
marker,
formatter: intFormatter,
truncation: 'start',
});
expect(cell).toBe(`${marker}cpu`);
});
});
@@ -33,7 +33,7 @@
"mapbox-gl": "^3.28.1",
"maplibre-gl": "^5.24.0",
"react-map-gl": "^8.1.2",
"supercluster": "^9.0.0"
"supercluster": "^8.0.1"
},
"peerDependencies": {
"@apache-superset/core": "*",
@@ -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);
@@ -164,7 +164,7 @@ export async function selectOption(option: string, selectName?: string) {
const item = await waitFor(() =>
within(
// eslint-disable-next-line testing-library/no-node-access
document.querySelector('.ant-select-dropdown-list')!,
document.querySelector('.rc-virtual-list')!,
).getByText(option),
);
await userEvent.click(item);
@@ -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');
});
@@ -17,7 +17,6 @@
* under the License.
*/
import { useState } from 'react';
import fetchMock from 'fetch-mock';
import {
cleanup,
render,
@@ -36,10 +35,6 @@ import { useDrillDetailMenuItems, DrillDetailMenuItemsProps } from './index';
/* eslint jest/expect-expect: ["warn", { "assertFunctionNames": ["expect*"] }] */
// Opening the context menu logs an event, and an unmatched request makes
// fetch-mock throw inside the component.
fetchMock.post('glob:*/log/?*', {});
jest.mock(
'../DrillDetail/DrillDetailPane',
() =>
@@ -867,7 +867,7 @@ function DatasourceEditor({
return {
...metric,
certification_details: certificationDetails || details,
warning_markdown: warningMarkdown || metric.warning_markdown || '',
warning_markdown: warningMarkdown || '',
certified_by: certifiedBy || certifiedByMetric,
};
}),
@@ -1,73 +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 fetchMock from 'fetch-mock';
import { screen, userEvent, waitFor } from 'spec/helpers/testing-library';
import {
createProps,
DATASOURCE_ENDPOINT,
setupDatasourceEditorMocks,
cleanupAsyncOperations,
fastRender,
dismissDatasourceWarning,
} from './DatasourceEditor.test.utils';
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT });
setupDatasourceEditorMocks();
});
afterEach(async () => {
await cleanupAsyncOperations();
fetchMock.clearHistory().removeRoutes();
});
// Certifying a metric fills two adjacent fields in one visit to the expanded
// row. Both are committed through TextControl's debounce, so the second one
// used to land on the item as it looked before the first had been applied,
// leaving the saved metric with details but no certifier.
test('certifying a metric keeps both certified_by and certification_details', async () => {
const testProps = createProps();
fastRender(testProps);
await dismissDatasourceWarning();
await userEvent.click(await screen.findByTestId('collection-tab-Metrics'));
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
await userEvent.type(
await screen.findByPlaceholderText('Certified by'),
'Metric Certifier',
);
await userEvent.type(
await screen.findByPlaceholderText('Certification details'),
'Metric cert details',
);
await waitFor(() => {
const { calls } = testProps.onChange.mock;
const savedMetrics = calls[calls.length - 1]?.[0]?.metrics ?? [];
const saved = savedMetrics.find(metric => metric.metric_name === 'count');
expect(saved).toEqual(
expect.objectContaining({
certified_by: 'Metric Certifier',
certification_details: 'Metric cert details',
}),
);
});
});
@@ -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.
*/
import fetchMock from 'fetch-mock';
import { screen, userEvent } from 'spec/helpers/testing-library';
import {
createProps,
DATASOURCE_ENDPOINT,
setupDatasourceEditorMocks,
cleanupAsyncOperations,
fastRender,
dismissDatasourceWarning,
} from './DatasourceEditor.test.utils';
// Stub the Ace-backed control with a plain textarea. Ace spreads its document
// across many spans and keeps only the keystroke buffer in its own textarea,
// so asserting on the value the control receives is less brittle than
// reaching into Ace's DOM.
jest.mock('src/explore/components/controls/TextAreaControl', () => ({
__esModule: true,
default: ({
controlId,
value,
onChange,
}: {
controlId?: string;
value?: string;
onChange?: (value: string) => void;
}) => (
<textarea
data-test={`mock-textarea-${controlId}`}
value={value ?? ''}
onChange={event => onChange?.(event.target.value)}
/>
),
}));
beforeEach(() => {
fetchMock.get(DATASOURCE_ENDPOINT, [], { name: DATASOURCE_ENDPOINT });
setupDatasourceEditorMocks();
});
afterEach(async () => {
await cleanupAsyncOperations();
fetchMock.clearHistory().removeRoutes();
});
// Regression test for #42704. Explore's datasource payload (SqlMetric.data on
// the backend) exposes warning_markdown as a flattened top-level field and
// omits the raw `extra` JSON string that the /api/v1/dataset/{id} endpoint
// backing the Datasets page provides. Deriving warning_markdown purely from
// `extra` therefore dropped the saved text when the modal was opened from
// Explore, leaving the Warning field blank on reopen.
test('keeps a pre-existing top-level warning_markdown when the metric has no extra', async () => {
const baseProps = createProps();
const testProps = {
...baseProps,
datasource: {
...baseProps.datasource,
metrics: [
{
...baseProps.datasource.metrics[0],
warning_markdown: 'existing warning',
extra: undefined,
},
],
},
};
fastRender(testProps);
await dismissDatasourceWarning();
const metricsTab = await screen.findByTestId('collection-tab-Metrics');
await userEvent.click(metricsTab);
const expandToggles = await screen.findAllByLabelText(/expand row/i);
await userEvent.click(expandToggles[0]);
expect(
await screen.findByTestId('mock-textarea-warning_markdown'),
).toHaveValue('existing warning');
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ReactNode, useCallback, useEffect, useRef } from 'react';
import { ReactNode, useCallback } from 'react';
import { Divider, Form, Typography } from '@superset-ui/core/components';
import { css } from '@apache-superset/core/theme';
import { recurseReactClone } from '../../utils';
@@ -39,24 +39,14 @@ export default function Fieldset({
title = null,
compact = false,
}: FieldsetProps) {
// Controls report their edits asynchronously - TextControl debounces by
// FAST_DEBOUNCE - so the callback that eventually fires was built during an
// earlier render. Spreading that render's `item` rebuilds the whole record
// from a snapshot taken before a sibling field committed, dropping the value
// the user typed first. Reading off a ref merges into the latest commit.
const itemRef = useRef(item);
useEffect(() => {
itemRef.current = item;
}, [item]);
const handleChange = useCallback(
(fieldKey: fieldKeyType, val: any) => {
onChange?.({
...itemRef.current,
...item,
[fieldKey]: val,
});
},
[onChange],
[onChange, item],
);
const propExtender = (field: { props: { fieldKey: fieldKeyType } }) => ({
@@ -135,15 +135,6 @@ describe('dashboardState actions', () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('saveDashboardRequest', () => {
const findDangerToast = (dispatch: jest.Mock) =>
dispatch.mock.calls
.map(call => call[0])
.find(
action =>
action?.type === ADD_TOAST &&
action.payload.toastType === ToastType.Danger,
);
test('should dispatch UPDATE_COMPONENTS_PARENTS_LIST action', () => {
const { getState, dispatch } = setup({
dashboardState: { hasUnsavedChanges: false },
@@ -236,89 +227,6 @@ describe('dashboardState actions', () => {
const { body } = putStub.mock.calls[0][0];
expect(body).toBe(JSON.stringify(confirmedDashboardData));
});
test('warns about the overwrite values when a diff is detected', async () => {
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() =>
expect(findDangerToast(dispatch)?.payload.text).toBe(
'Please confirm the overwrite values.',
),
);
expect(putStub.mock.calls.length).toBe(0);
});
test('reports the actual error when the overwrite precheck fails', async () => {
getStub.mockRestore();
getStub = jest
.spyOn(SupersetClient, 'get')
.mockRejectedValue(new Error('precheck exploded'));
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() =>
expect(findDangerToast(dispatch)?.payload.text).toContain(
'precheck exploded',
),
);
expect(putStub.mock.calls.length).toBe(0);
});
});
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('when FeatureFlag.CONFIRM_DASHBOARD_DIFF is disabled', () => {
beforeEach(() => {
mockIsFeatureEnabled.mockImplementation(() => false);
});
afterEach(() => {
mockIsFeatureEnabled.mockRestore();
});
test('never runs the overwrite precheck', async () => {
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() => expect(putStub.mock.calls.length).toBe(1));
expect(getStub).not.toHaveBeenCalledWith(
expect.objectContaining({ endpoint: '/api/v1/dashboard/192' }),
);
});
// An unexpected failure used to reach the overwrite-confirm handler,
// which reported it as "Please confirm the overwrite values." even with
// the feature flag off, hiding the real error.
test('reports the actual error when the update throws unexpectedly', async () => {
putStub.mockRestore();
putStub = jest.spyOn(SupersetClient, 'put').mockImplementation(() => {
throw new Error('unexpected boom');
});
const { getState, dispatch } = setup();
const thunk = saveDashboardRequest(
newDashboardData,
192,
SAVE_TYPE_OVERWRITE,
);
thunk(dispatch, getState);
await waitFor(() =>
expect(findDangerToast(dispatch)?.payload.text).toContain(
'unexpected boom',
),
);
});
});
test('should navigate to the new dashboard after Save As', async () => {
@@ -471,6 +379,15 @@ describe('dashboardState actions', () => {
// permission-denied copy, while a 403 from outside Superset (reverse proxy,
// WAF, SSO gateway) carries a non-JSON body and must fall back to the
// generic status-derived toast. See #42239.
const findDangerToast = (dispatch: jest.Mock) =>
dispatch.mock.calls
.map(call => call[0])
.find(
action =>
action?.type === ADD_TOAST &&
action.payload.toastType === ToastType.Danger,
);
test('maps a non-JSON 403 save failure to the generic error toast', async () => {
const { getState, dispatch } = setup();
putStub.mockRestore();
@@ -646,7 +646,6 @@ export function saveDashboardRequest(
};
const onError = async (response: Response): Promise<void> => {
logging.error(response);
const { error, message } = await getClientErrorObject(response);
let errorText = t('Sorry, an unknown error occurred');
@@ -690,64 +689,64 @@ export function saveDashboardRequest(
}),
};
const updateDashboard = async (): Promise<JsonObject | void> => {
try {
const response = await SupersetClient.put({
endpoint: `/api/v1/dashboard/${id}`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedDashboard),
});
return await onUpdateSuccess(response);
} catch (error) {
return onError(error as Response);
const updateDashboard = (): Promise<JsonObject | void> =>
SupersetClient.put({
endpoint: `/api/v1/dashboard/${id}`,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedDashboard),
})
.then(response => onUpdateSuccess(response))
.catch(response => onError(response));
return new Promise<void>((resolve, reject) => {
if (
!isFeatureEnabled(FeatureFlag.ConfirmDashboardDiff) ||
saveType === SAVE_TYPE_OVERWRITE_CONFIRMED
) {
// skip overwrite precheck
resolve();
return;
}
};
if (
!isFeatureEnabled(FeatureFlag.ConfirmDashboardDiff) ||
saveType === SAVE_TYPE_OVERWRITE_CONFIRMED
) {
// skip overwrite precheck
return updateDashboard();
}
// precheck for overwrite items
return SupersetClient.get({
endpoint: `/api/v1/dashboard/${id}`,
})
.then((response: JsonObject) => {
// precheck for overwrite items
SupersetClient.get({
endpoint: `/api/v1/dashboard/${id}`,
}).then((response: JsonObject) => {
const dashboard = (response.json as JsonObject).result as JsonObject;
const overwriteConfirmItems = getOverwriteItems(
dashboard,
updatedDashboard,
);
if (overwriteConfirmItems.length === 0) {
return updateDashboard();
if (overwriteConfirmItems.length > 0) {
dispatch(
setOverrideConfirm({
updatedAt: dashboard.changed_on as string,
updatedBy: dashboard.changed_by_name as string,
overwriteConfirmItems:
overwriteConfirmItems as DashboardState['overwriteConfirmMetadata'] extends
| { overwriteConfirmItems: infer I }
| undefined
? I
: never,
dashboardId: id,
data: updatedDashboard,
}),
);
return reject(overwriteConfirmItems);
}
dispatch(
setOverrideConfirm({
updatedAt: dashboard.changed_on as string,
updatedBy: dashboard.changed_by_name as string,
overwriteConfirmItems:
overwriteConfirmItems as DashboardState['overwriteConfirmMetadata'] extends
| { overwriteConfirmItems: infer I }
| undefined
? I
: never,
dashboardId: id,
data: updatedDashboard,
}),
);
return resolve();
});
})
.then(updateDashboard)
.catch((overwriteConfirmItems: JsonObject[]) => {
const errorText = t('Please confirm the overwrite values.');
dispatch(
logEvent(LOG_ACTIONS_CONFIRM_OVERWRITE_DASHBOARD_METADATA, {
dashboard_id: id,
items: overwriteConfirmItems,
}),
);
dispatch(addDangerToast(t('Please confirm the overwrite values.')));
return undefined;
})
.catch(onError);
dispatch(addDangerToast(errorText));
});
}
// changing the data as the endpoint requires
if (
@@ -27,7 +27,7 @@ import {
} from '@superset-ui/core';
import { Dispatch } from 'redux';
import { RootState } from 'src/dashboard/types';
import { cloneDeep, omit } from 'lodash-es';
import { cloneDeep } from 'lodash-es';
import { setDataMaskForFilterChangesComplete } from 'src/dataMask/actions';
import { HYDRATE_DASHBOARD } from './hydrate';
import {
@@ -90,20 +90,12 @@ export const setFilterConfiguration =
});
try {
const response = await updateFilters(filterChanges);
// chartsInScope/tabsInScope are derived from the live layout, and the
// response carries the persisted copy for every filter - including the
// ones this save never touched, whose copy is whatever was stored when
// the dashboard was last saved. Dropping them lets the reducers keep the
// scopes calculateScopes already computed for this session.
const savedFilters = response.result.map(
filter => omit(filter, ['chartsInScope', 'tabsInScope']) as Filter,
);
dispatch({
type: SET_NATIVE_FILTERS_CONFIG_COMPLETE,
filterChanges: savedFilters,
filterChanges: response.result,
deletedIds: filterChanges.deleted,
});
dispatch(nativeFiltersConfigChanged(savedFilters));
dispatch(nativeFiltersConfigChanged(response.result));
dispatch(setDataMaskForFilterChangesComplete(filterChanges, oldFilters));
} catch (err) {
dispatch({
@@ -16,20 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
ChartCustomizationType,
type ChartCustomization,
} from '@superset-ui/core';
import { LabeledValue } from '@superset-ui/core/components';
import { render, screen } from 'spec/helpers/testing-library';
import GroupByFilterCard, {
createLabelSortComparator,
} from './GroupByFilterCard';
jest.mock('src/utils/cachedSupersetGet', () => ({
// Never resolves, pinning the card in its column-loading state.
cachedSupersetGet: jest.fn(() => new Promise(() => {})),
}));
import { createLabelSortComparator } from './GroupByFilterCard';
const apple: LabeledValue = { value: 'a', label: 'Apple' };
const banana: LabeledValue = { value: 'b', label: 'Banana' };
@@ -51,27 +39,3 @@ test('preserves source order when sortAscending is unset', () => {
expect(compare(apple, banana)).toBe(0);
expect(compare(banana, apple)).toBe(0);
});
const groupByCustomization: ChartCustomization = {
id: 'groupby-1',
name: 'Group By',
filterType: 'filter_groupby',
type: ChartCustomizationType.ChartCustomization,
targets: [{ datasetId: 1 }],
scope: { rootPath: [], excluded: [] },
controlValues: {},
defaultDataMask: {},
};
test('renders the column-loading spinner small and muted', async () => {
render(<GroupByFilterCard customizationItem={groupByCustomization} />, {
useRedux: true,
initialState: {
dataMask: {},
nativeFilters: { filters: {} },
},
});
const spinner = await screen.findByTestId('loading-indicator');
expect(spinner).toHaveClass('inline');
expect(spinner).toHaveStyle({ opacity: 0.25, width: '40px' });
});
@@ -645,7 +645,7 @@ const GroupByFilterCard: FC<GroupByFilterCardProps> = ({
{loading && (
<div style={{ textAlign: 'center', marginTop: 8 }}>
<Loading position="inline" size="s" muted />
<Loading position="inline" />
</div>
)}
</div>
@@ -76,7 +76,7 @@ const typeIntoSelect = async (text: string) => {
const findOption = (text: string) =>
waitFor(() => {
// eslint-disable-next-line testing-library/no-node-access
const virtualList = document.querySelector('.ant-select-dropdown-list');
const virtualList = document.querySelector('.rc-virtual-list');
if (!virtualList) {
throw new Error('Virtual list not found');
}
@@ -72,13 +72,3 @@ test('omits datasourceType when undefined', () => {
});
expect(target).not.toHaveProperty('datasourceType');
});
test('omits datasourceType when there is no dataset', () => {
// The modal stamps a hidden ``datasourceType`` field on every filter form,
// including dataset-less types. Without a dataset there is nothing for it to
// describe, and emitting it would diverge from the ``{}`` target the import
// and seed paths write.
expect(
buildNativeFilterTarget({ datasourceType: DatasourceType.Table }),
).toEqual({});
});
@@ -33,9 +33,9 @@ export interface TargetFormInputs {
* Build the ``NativeFilterTarget`` carried by a native filter or chart
* customization from its form inputs.
*
* Consolidates what used to live in ``filterTransformer`` and
* ``customizationTransformer`` so changes to the target shape only need to
* happen here.
* Consolidates what used to live in three places ``filterTransformer``,
* ``customizationTransformer``, and ``createHandleSave`` so changes to the
* target shape only need to happen here.
*/
export function buildNativeFilterTarget(
formInputs: TargetFormInputs,
@@ -49,11 +49,7 @@ export function buildNativeFilterTarget(
: formInputs.dataset;
}
// ``datasourceType`` describes the selected dataset, so it only belongs on a
// target that has one. Emitting it for a dataset-less filter (e.g.
// ``filter_time``) would make a UI save serialize a target the import and
// seed paths write as ``{}``.
if (formInputs.dataset != null && formInputs.datasourceType) {
if (formInputs.datasourceType) {
target.datasourceType = formInputs.datasourceType;
}
@@ -1,102 +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 { ChartCustomization, ChartCustomizationType } from '@superset-ui/core';
import { ChartCustomizationsFormItem } from '../types';
import { transformCustomizationForSave } from './customizationTransformer';
const baseFormItem = {
type: ChartCustomizationType.ChartCustomization,
scope: { rootPath: ['ROOT_ID'], excluded: [] },
controlValues: {},
requiredFirst: {},
defaultValue: null,
defaultDataMask: { filterState: {}, extraFormData: {} },
sortMetric: null,
description: '',
// form-only field that must never leak into the saved customization
defaultValueQueriesData: null,
} as unknown as ChartCustomizationsFormItem;
test('serializes a dataset-less customization into a full ChartCustomization', () => {
// Customization plugins declaring ``datasourceCount: 0`` render no dataset
// control, so their form item carries neither ``dataset`` nor ``targets``.
const formItem = {
...baseFormItem,
name: 'Layer visibility',
filterType: 'customization_deckgl_layer_visibility',
} as unknown as ChartCustomizationsFormItem;
const result = transformCustomizationForSave(
'CHART_CUSTOMIZATION-abc',
formItem,
) as ChartCustomization;
expect(result.targets).toEqual([{}]);
expect(result.defaultDataMask).toBeDefined();
expect(result.removed).toBe(false);
expect(result).not.toHaveProperty('defaultValueQueriesData');
});
test('serializes a dataset-backed customization into a full ChartCustomization', () => {
const formItem = {
...baseFormItem,
name: 'Group by',
filterType: 'customization_dynamic_group_by',
dataset: { value: 42, label: 'sales' },
column: 'region',
} as unknown as ChartCustomizationsFormItem;
const result = transformCustomizationForSave(
'CHART_CUSTOMIZATION-def',
formItem,
) as ChartCustomization;
expect(result.targets).toEqual([
{ datasetId: 42, column: { name: 'region' } },
]);
expect(result).not.toHaveProperty('defaultValueQueriesData');
});
test('passes an already-saved ChartCustomization through untouched', () => {
const saved: ChartCustomization = {
id: 'CHART_CUSTOMIZATION-ghi',
name: 'Group by',
filterType: 'customization_dynamic_group_by',
type: ChartCustomizationType.ChartCustomization,
targets: [{ datasetId: 42, column: { name: 'region' } }],
defaultDataMask: { filterState: {}, extraFormData: {} },
controlValues: {},
scope: { rootPath: ['ROOT_ID'], excluded: [] },
description: ' needs trim ',
chartsInScope: [1, 2],
tabsInScope: ['TAB-1'],
};
const result = transformCustomizationForSave(
'CHART_CUSTOMIZATION-ghi',
saved,
) as ChartCustomization;
expect(result.targets).toEqual([
{ datasetId: 42, column: { name: 'region' } },
]);
expect(result.chartsInScope).toEqual([1, 2]);
expect(result.tabsInScope).toEqual(['TAB-1']);
expect(result.description).toBe('needs trim');
});
@@ -69,10 +69,7 @@ function isDividerType(
function isFormInput(
formInputs: ChartCustomizationFormOrSaved,
): formInputs is ChartCustomizationsFormItem {
// Mirrors `filterTransformer`: a saved customization always carries a
// serialized `targets` array, and dataset-less types (e.g. the deck.gl layer
// visibility customization) have no `dataset` to discriminate on.
return !('targets' in formInputs);
return 'dataset' in formInputs && typeof formInputs.dataset === 'object';
}
function transformCustomizationDivider(
@@ -1,156 +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 { DatasourceType, Filter, NativeFilterType } from '@superset-ui/core';
import { NativeFiltersFormItem } from '../types';
import { transformFilterForSave } from './filterTransformer';
const baseFormItem = {
type: NativeFilterType.NativeFilter,
scope: { rootPath: ['ROOT_ID'], excluded: [] },
controlValues: {},
requiredFirst: {},
defaultValue: null,
defaultDataMask: { filterState: {}, extraFormData: {} },
description: '',
// form-only fields that must never leak into the saved filter
defaultValueQueriesData: null,
} as unknown as NativeFiltersFormItem;
test('serializes a dataset-less filter (filter_time) into a full Filter', () => {
// A ``filter_time`` filter has no dataset/column controls, so its form item
// carries neither a ``dataset`` nor a ``targets`` key. It must still be
// transformed like any other native filter rather than persisted verbatim.
const formItem: NativeFiltersFormItem = {
...baseFormItem,
name: 'Time Range',
filterType: 'filter_time',
dependencies: ['NATIVE_FILTER-parent'],
// the modal stamps this on every filter form, dataset or not
datasourceType: DatasourceType.Table,
};
const result = transformFilterForSave(
'NATIVE_FILTER-abc',
formItem,
) as Filter;
// Keys the bug used to strip are present and well-formed. The target matches
// the ``{}`` the import and seed paths write, so one logical filter has one
// serialization regardless of provenance.
expect(result.targets).toEqual([{}]);
expect(result.defaultDataMask).toBeDefined();
expect(result.cascadeParentIds).toEqual(['NATIVE_FILTER-parent']);
// Form-only keys must not leak into the persisted config.
expect(result).not.toHaveProperty('defaultValueQueriesData');
expect(result).not.toHaveProperty('dependencies');
// Empty requiredFirst collapses to undefined instead of the raw form object.
expect(result.requiredFirst).toBeUndefined();
// A dataset-less filter has no sort metric control, so the persisted document
// must not gain a ``sortMetric`` key it never had. Asserted on the serialized
// form because ``undefined`` values survive in the object but not in JSON.
expect(JSON.parse(JSON.stringify(result))).not.toHaveProperty('sortMetric');
expect(result.name).toBe('Time Range');
expect(result.filterType).toBe('filter_time');
});
test('serializes a dataset-backed filter (filter_select) into a full Filter', () => {
const formItem: NativeFiltersFormItem = {
...baseFormItem,
name: 'Region',
filterType: 'filter_select',
dataset: { value: 42, label: 'sales' },
column: 'region',
dependencies: [],
};
const result = transformFilterForSave(
'NATIVE_FILTER-def',
formItem,
) as Filter;
expect(result.targets).toEqual([
{ datasetId: 42, column: { name: 'region' } },
]);
expect(result.defaultDataMask).toBeDefined();
expect(result.cascadeParentIds).toEqual([]);
expect(result).not.toHaveProperty('defaultValueQueriesData');
});
test('passes an already-saved Filter through untouched (aside from trimming)', () => {
// Values coming from the stored filter config map (e.g. cascade-parent
// cleanup) already carry a ``targets`` array and must be preserved as-is.
const savedFilter: Filter = {
id: 'NATIVE_FILTER-ghi',
name: 'Time Range',
filterType: 'filter_time',
type: NativeFilterType.NativeFilter,
targets: [{}],
defaultDataMask: { filterState: {}, extraFormData: {} },
cascadeParentIds: ['NATIVE_FILTER-parent'],
controlValues: {},
scope: { rootPath: ['ROOT_ID'], excluded: [] },
description: ' needs trim ',
chartsInScope: [1, 2],
tabsInScope: ['TAB-1'],
};
const result = transformFilterForSave(
'NATIVE_FILTER-ghi',
savedFilter,
) as Filter;
expect(result.targets).toEqual([{}]);
expect(result.cascadeParentIds).toEqual(['NATIVE_FILTER-parent']);
expect(result.chartsInScope).toEqual([1, 2]);
expect(result.tabsInScope).toEqual(['TAB-1']);
expect(result.description).toBe('needs trim');
});
test('rebuilds a saved filter whose targets were already stripped', () => {
// Dashboards affected by this bug hold ``filter_time`` entries with no
// ``targets``. They no longer match the saved-filter branch, so they take the
// form-item path and are repaired on the next save. ``cascadeParentIds`` is
// read from the form's ``dependencies``, which such an entry does not carry —
// the same write that stripped ``targets`` stripped ``cascadeParentIds`` too.
const strippedFilter = {
id: 'NATIVE_FILTER-jkl',
name: 'Time Range',
filterType: 'filter_time',
type: NativeFilterType.NativeFilter,
scope: { rootPath: ['ROOT_ID'], excluded: [] },
controlValues: { timeShift: false },
description: '',
requiredFirst: { 'NATIVE_FILTER-jkl': true },
defaultValueQueriesData: null,
} as unknown as NativeFiltersFormItem;
const result = transformFilterForSave(
'NATIVE_FILTER-jkl',
strippedFilter,
) as Filter;
expect(result.targets).toEqual([{}]);
expect(result.defaultDataMask).toBeDefined();
expect(result.requiredFirst).toBe(true);
expect(result.cascadeParentIds).toEqual([]);
expect(result).not.toHaveProperty('defaultValueQueriesData');
});
@@ -67,10 +67,7 @@ function isDividerType(
function isFormInput(
formInputs: NativeFilterFormOrSaved,
): formInputs is NativeFiltersFormItem {
// A saved filter always carries a serialized `targets` array; a form item
// never does. Keying this off `dataset` misclassified filter types with no
// dataset control (e.g. `filter_time`) as already saved.
return !('targets' in formInputs);
return 'dataset' in formInputs;
}
function transformDivider(
@@ -118,7 +115,7 @@ function transformFormInput(
adhoc_filters: formInputs.adhoc_filters,
time_range: formInputs.time_range,
granularity_sqla: formInputs.granularity_sqla,
sortMetric: formInputs.sortMetric,
sortMetric: formInputs.sortMetric ?? null,
requiredFirst: formInputs.requiredFirst
? Object.values(formInputs.requiredFirst).find(rf => rf)
: undefined,
@@ -18,15 +18,21 @@
*/
import type { FormInstance } from '@superset-ui/core/components';
import { nanoid } from 'nanoid';
import { getInitialDataMask } from 'src/dataMask/reducer';
import {
FilterConfiguration,
NativeFilterType,
NativeFilterTarget,
Filter,
Divider,
ChartCustomizationType,
ChartCustomizationConfiguration,
ChartCustomization,
ChartCustomizationDivider,
} from '@superset-ui/core';
import { logging } from '@apache-superset/core/utils';
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
import { buildNativeFilterTarget } from './transformers/buildTarget';
import {
ChartCustomizationsForm,
FilterChangesType,
@@ -95,6 +101,70 @@ export const validateForm = async (
}
};
export const createHandleSave =
(
saveForm: Function,
filterChanges: FilterChangesType,
values: NativeFiltersForm,
filterConfigMap: Record<string, Filter | Divider>,
) =>
async () => {
const transformFilter = (id: string) => {
const formInputs = values.filters?.[id] || filterConfigMap[id];
if (!formInputs) {
return undefined;
}
if (formInputs.type === NativeFilterType.Divider) {
return {
id,
type: NativeFilterType.Divider,
scope: {
rootPath: [DASHBOARD_ROOT_ID],
excluded: [],
},
title: formInputs.title,
description: formInputs.description,
};
}
const target: Partial<NativeFilterTarget> =
buildNativeFilterTarget(formInputs);
return {
id,
adhoc_filters: formInputs.adhoc_filters,
time_range: formInputs.time_range,
controlValues: formInputs.controlValues ?? {},
granularity_sqla: formInputs.granularity_sqla,
...(formInputs.time_grains?.length
? { time_grains: formInputs.time_grains }
: {}),
requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
rf => rf,
),
name: formInputs.name,
filterType: formInputs.filterType,
targets: [target],
defaultDataMask: formInputs.defaultDataMask ?? getInitialDataMask(),
cascadeParentIds: formInputs.dependencies || [],
scope: formInputs.scope,
sortMetric: formInputs.sortMetric,
type: formInputs.type,
description: (formInputs.description || '').trim(),
};
};
const transformedModified = filterChanges.modified
.map(transformFilter)
.filter(Boolean);
const newFilterChanges = {
...filterChanges,
modified: transformedModified,
};
await saveForm(newFilterChanges);
};
export const createHandleRemoveItem =
(
setRemovedFilters: (
-155
View File
@@ -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),
);
});
-57
View File
@@ -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,
};
-1
View File
@@ -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,
@@ -214,9 +214,7 @@ test('Should filter simple columns by column_name and verbose_name', async () =>
await userEvent.type(combobox, 'revenue');
let dropdown = document.querySelector(
'.ant-select-dropdown-list',
) as HTMLElement;
let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Total Sales')).toBeInTheDocument();
expect(
within(dropdown).queryByText('User Identifier'),
@@ -228,7 +226,7 @@ test('Should filter simple columns by column_name and verbose_name', async () =>
await userEvent.clear(combobox);
await userEvent.type(combobox, 'Identifier');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('User Identifier')).toBeInTheDocument();
expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument();
expect(within(dropdown).queryByText('Creation Date')).not.toBeInTheDocument();
@@ -236,7 +234,7 @@ test('Should filter simple columns by column_name and verbose_name', async () =>
await userEvent.clear(combobox);
await userEvent.type(combobox, '_at');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Creation Date')).toBeInTheDocument();
expect(within(dropdown).getByText('Last Update')).toBeInTheDocument();
expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument();
@@ -290,9 +288,7 @@ test('Should filter saved expressions by column_name and verbose_name', async ()
await userEvent.type(combobox, 'revenue');
let dropdown = document.querySelector(
'.ant-select-dropdown-list',
) as HTMLElement;
let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Total Sales')).toBeInTheDocument();
expect(within(dropdown).queryByText('Tax Amount')).not.toBeInTheDocument();
expect(within(dropdown).queryByText('Net Profit')).not.toBeInTheDocument();
@@ -302,7 +298,7 @@ test('Should filter saved expressions by column_name and verbose_name', async ()
await userEvent.clear(combobox);
await userEvent.type(combobox, 'Rate');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Discount Rate')).toBeInTheDocument();
expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument();
expect(within(dropdown).queryByText('Tax Amount')).not.toBeInTheDocument();
@@ -310,7 +306,7 @@ test('Should filter saved expressions by column_name and verbose_name', async ()
await userEvent.clear(combobox);
await userEvent.type(combobox, 'profit');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Net Profit')).toBeInTheDocument();
expect(within(dropdown).getByText('Profit Margin')).toBeInTheDocument();
expect(within(dropdown).queryByText('Total Sales')).not.toBeInTheDocument();
@@ -207,38 +207,6 @@ describe('AdhocFilter', () => {
expect(adhocFilter10.isValid()).toBe(true);
});
test('is invalid when a comparator-taking operator has no comparator', () => {
// A comparator that was never set, or that was cleared through the value
// Select's clear affordance, is `undefined` rather than `null` or `[]`.
const adhocFilter1 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'is_intro',
operator: 'IN',
comparator: undefined,
clause: Clauses.Where,
});
expect(adhocFilter1.isValid()).toBe(false);
const adhocFilter2 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'is_intro',
operator: '==',
comparator: undefined,
clause: Clauses.Where,
});
expect(adhocFilter2.isValid()).toBe(false);
// `false` is a legitimate boolean comparator, not a missing value
const adhocFilter3 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
subject: 'is_intro',
operator: '==',
comparator: false,
clause: Clauses.Where,
});
expect(adhocFilter3.isValid()).toBe(true);
});
test('can translate from simple expressions to sql expressions', () => {
const adhocFilter1 = new AdhocFilter({
expressionType: ExpressionTypes.Simple,
@@ -163,10 +163,8 @@ export default class AdhocFilter {
// A non-empty array of values ('IN' or 'NOT IN' clauses)
return this.comparator.length > 0;
}
// A value has been selected or typed. An unset comparator is
// `undefined` rather than `null`: picking a new subject resets it, and
// the value Select's clear affordance emits `undefined` too.
return this.comparator != null;
// A value has been selected or typed
return this.comparator !== null;
}
}
@@ -181,29 +181,6 @@ describe('AdhocFilterEditPopover', () => {
expect(saveButton).toBeDisabled();
});
test('disables save button when a boolean column has no value selected', async () => {
const booleanColumn = { type: 'BOOL', column_name: 'is_intro' };
renderPopover({
adhocFilter: new AdhocFilter({
expressionType: ExpressionTypes.Simple,
clause: Clauses.Where,
}),
options: [booleanColumn],
datasource: { columns: [booleanColumn], filter_select: false },
});
// Picking the subject resets the comparator to `undefined`; the value
// control is then left untouched, mirroring the reported repro.
await userEvent.click(screen.getByTestId('select-element'));
await userEvent.click(
await screen.findByRole('option', { name: /is_intro/ }),
);
expect(
screen.getByTestId('adhoc-filter-edit-popover-save-button'),
).toBeDisabled();
});
test('initiates resize when resize handle is dragged', async () => {
const onResize = jest.fn();
renderPopover({ onResize });
@@ -340,9 +340,7 @@ test('Should filter saved metrics by metric_name and verbose_name', async () =>
await userEvent.type(combobox, 'revenue');
let dropdown = document.querySelector(
'.ant-select-dropdown-list',
) as HTMLElement;
let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Gross Revenue')).toBeInTheDocument();
expect(within(dropdown).queryByText('Total Count')).not.toBeInTheDocument();
expect(within(dropdown).queryByText('Average Price')).not.toBeInTheDocument();
@@ -354,7 +352,7 @@ test('Should filter saved metrics by metric_name and verbose_name', async () =>
await userEvent.clear(combobox);
await userEvent.type(combobox, 'Unique');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Unique Users')).toBeInTheDocument();
expect(within(dropdown).queryByText('Total Count')).not.toBeInTheDocument();
expect(within(dropdown).queryByText('Gross Revenue')).not.toBeInTheDocument();
@@ -362,7 +360,7 @@ test('Should filter saved metrics by metric_name and verbose_name', async () =>
await userEvent.clear(combobox);
await userEvent.type(combobox, 'total');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Total Count')).toBeInTheDocument();
expect(within(dropdown).getByText('Total Quantity')).toBeInTheDocument();
expect(within(dropdown).queryByText('Gross Revenue')).not.toBeInTheDocument();
@@ -423,9 +421,7 @@ test('Should filter columns by column_name and verbose_name in Simple tab', asyn
await userEvent.type(columnCombobox, 'product');
let dropdown = document.querySelector(
'.ant-select-dropdown-list',
) as HTMLElement;
let dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Product Title')).toBeInTheDocument();
expect(
within(dropdown).queryByText('User Identifier'),
@@ -439,7 +435,7 @@ test('Should filter columns by column_name and verbose_name in Simple tab', asyn
await userEvent.clear(columnCombobox);
await userEvent.type(columnCombobox, 'Modified');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Last Modified')).toBeInTheDocument();
expect(
within(dropdown).queryByText('User Identifier'),
@@ -449,7 +445,7 @@ test('Should filter columns by column_name and verbose_name in Simple tab', asyn
await userEvent.clear(columnCombobox);
await userEvent.type(columnCombobox, '_at');
dropdown = document.querySelector('.ant-select-dropdown-list') as HTMLElement;
dropdown = document.querySelector('.rc-virtual-list') as HTMLElement;
expect(within(dropdown).getByText('Creation Timestamp')).toBeInTheDocument();
expect(within(dropdown).getByText('Last Modified')).toBeInTheDocument();
expect(
@@ -27,15 +27,8 @@ import {
Input,
Button,
Modal,
Select,
} from '@superset-ui/core/components';
import { useToasts } from 'src/components/MessageToasts/withToasts';
import copyTextToClipboard from 'src/utils/copy';
import {
API_KEY_SCOPE_OPTIONS,
getApiKeyScopesHelpText,
serializeApiKeyScopes,
} from './apiKeyScopes';
interface ApiKeyCreateModalProps {
show: boolean;
@@ -45,7 +38,6 @@ interface ApiKeyCreateModalProps {
interface FormValues {
name: string;
scopes?: string[];
}
export function ApiKeyCreateModal({
@@ -70,13 +62,9 @@ export function ApiKeyCreateModal({
const handleFormSubmit = async (values: FormValues) => {
try {
const scopes = serializeApiKeyScopes(values.scopes);
const response = await SupersetClient.post({
endpoint: '/api/v1/security/api_keys/',
jsonPayload: {
name: values.name,
...(scopes && { scopes }),
},
jsonPayload: values,
});
const key = response.json?.result?.key;
if (!key) {
@@ -95,7 +83,7 @@ export function ApiKeyCreateModal({
return;
}
try {
await copyTextToClipboard(() => Promise.resolve(createdKey));
await navigator.clipboard.writeText(createdKey);
setCopied(true);
if (copyTimerRef.current) {
clearTimeout(copyTimerRef.current);
@@ -182,24 +170,6 @@ export function ApiKeyCreateModal({
placeholder={t('e.g., CI/CD Pipeline, Analytics Script')}
/>
</FormItem>
<FormItem
name="scopes"
label={t('MCP scopes')}
help={getApiKeyScopesHelpText()}
>
<Select
name="scopes"
mode="multiple"
allowClear
showSearch
options={API_KEY_SCOPE_OPTIONS}
placeholder={t('Select MCP resource scopes (optional)')}
data-test="api-key-scopes-select"
getPopupContainer={(trigger: HTMLElement) =>
trigger.closest<HTMLElement>('.ant-modal-container') ?? trigger
}
/>
</FormItem>
</FormModal>
);
}
@@ -162,19 +162,6 @@ export function ApiKeyList() {
key: 'status',
render: (_: unknown, record: ApiKey) => getStatusBadge(record),
},
{
title: t('MCP scopes'),
dataIndex: 'scopes',
key: 'scopes',
render: (scopes: string | null) =>
scopes ? (
<Tooltip title={scopes}>
<Tag>{t('%s MCP scopes', scopes.split(',').length)}</Tag>
</Tooltip>
) : (
<Tag>{t('RBAC only')}</Tag>
),
},
{
title: t('Actions'),
key: 'actions',
@@ -1,50 +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 {
API_KEY_SCOPE_OPTIONS,
getApiKeyScopesHelpText,
serializeApiKeyScopes,
} from './apiKeyScopes';
test('offers read and write scopes for every supported resource', () => {
expect(API_KEY_SCOPE_OPTIONS).toHaveLength(32);
expect(API_KEY_SCOPE_OPTIONS).toContainEqual({
label: 'superset:dashboard:read',
value: 'superset:dashboard:read',
});
expect(API_KEY_SCOPE_OPTIONS).toContainEqual({
label: 'superset:sqllab:write',
value: 'superset:sqllab:write',
});
});
test('serializes selected scopes for the FAB API', () => {
expect(
serializeApiKeyScopes(['superset:dashboard:read', 'superset:chart:write']),
).toBe('superset:dashboard:read,superset:chart:write');
expect(serializeApiKeyScopes([])).toBeUndefined();
expect(serializeApiKeyScopes()).toBeUndefined();
});
test('explains that scopes apply to MCP rather than REST APIs', () => {
expect(getApiKeyScopesHelpText()).toContain('MCP resources');
expect(getApiKeyScopesHelpText()).toContain(
'do not restrict REST API requests',
);
});
@@ -1,55 +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 { t } from '@apache-superset/core/translation';
const API_KEY_SCOPE_RESOURCES = [
'annotation',
'chart',
'dashboard',
'database',
'dataset',
'explore',
'query',
'report',
'role',
'rls',
'savedquery',
'sqllab',
'tag',
'task',
'theme',
'user',
] as const;
const API_KEY_SCOPE_ACTIONS = ['read', 'write'] as const;
export const API_KEY_SCOPE_OPTIONS = API_KEY_SCOPE_RESOURCES.flatMap(resource =>
API_KEY_SCOPE_ACTIONS.map(action => {
const value = `superset:${resource}:${action}`;
return { label: value, value };
}),
);
export const serializeApiKeyScopes = (scopes?: string[]) =>
scopes?.length ? scopes.join(',') : undefined;
export const getApiKeyScopesHelpText = () =>
t(
'Limit which MCP resources and actions this key can access. These scopes do not restrict REST API requests and never grant permissions the user does not already have. Leave empty for legacy RBAC-only behavior.',
);
@@ -88,9 +88,9 @@ test('PermissionsField shows a permission matched by its raw name even though th
),
);
expect(
await within(
document.querySelector('.ant-select-dropdown-list')!,
).findByText('stg silver'),
await within(document.querySelector('.rc-virtual-list')!).findByText(
'stg silver',
),
).toBeInTheDocument();
});
+156 -156
View File
@@ -15,24 +15,24 @@
"jsonwebtoken": "^9.0.3",
"lodash-es": "^4.18.1",
"winston": "^3.19.0",
"ws": "^8.21.3"
"ws": "^8.21.2"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
"@types/eslint__js": "^8.42.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.2.0",
"@types/node": "^26.1.2",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^10.8.1",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.66.0",
"eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.9.0",
"oxfmt": "^0.63.0",
"oxfmt": "^0.62.0",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.67.0",
"typescript-eslint": "^8.66.0",
"vitest": "^4.1.10"
},
"engines": {
@@ -310,9 +310,9 @@
}
},
"node_modules/@oxfmt/binding-android-arm-eabi": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz",
"integrity": "sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.62.0.tgz",
"integrity": "sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw==",
"cpu": [
"arm"
],
@@ -327,9 +327,9 @@
}
},
"node_modules/@oxfmt/binding-android-arm64": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz",
"integrity": "sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.62.0.tgz",
"integrity": "sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog==",
"cpu": [
"arm64"
],
@@ -344,9 +344,9 @@
}
},
"node_modules/@oxfmt/binding-darwin-arm64": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz",
"integrity": "sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.62.0.tgz",
"integrity": "sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q==",
"cpu": [
"arm64"
],
@@ -361,9 +361,9 @@
}
},
"node_modules/@oxfmt/binding-darwin-x64": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz",
"integrity": "sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.62.0.tgz",
"integrity": "sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ==",
"cpu": [
"x64"
],
@@ -378,9 +378,9 @@
}
},
"node_modules/@oxfmt/binding-freebsd-x64": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz",
"integrity": "sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.62.0.tgz",
"integrity": "sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g==",
"cpu": [
"x64"
],
@@ -395,9 +395,9 @@
}
},
"node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz",
"integrity": "sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.62.0.tgz",
"integrity": "sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g==",
"cpu": [
"arm"
],
@@ -412,9 +412,9 @@
}
},
"node_modules/@oxfmt/binding-linux-arm-musleabihf": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz",
"integrity": "sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.62.0.tgz",
"integrity": "sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog==",
"cpu": [
"arm"
],
@@ -429,9 +429,9 @@
}
},
"node_modules/@oxfmt/binding-linux-arm64-gnu": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz",
"integrity": "sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.62.0.tgz",
"integrity": "sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg==",
"cpu": [
"arm64"
],
@@ -449,9 +449,9 @@
}
},
"node_modules/@oxfmt/binding-linux-arm64-musl": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz",
"integrity": "sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.62.0.tgz",
"integrity": "sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew==",
"cpu": [
"arm64"
],
@@ -469,9 +469,9 @@
}
},
"node_modules/@oxfmt/binding-linux-ppc64-gnu": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz",
"integrity": "sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.62.0.tgz",
"integrity": "sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw==",
"cpu": [
"ppc64"
],
@@ -489,9 +489,9 @@
}
},
"node_modules/@oxfmt/binding-linux-riscv64-gnu": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz",
"integrity": "sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.62.0.tgz",
"integrity": "sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA==",
"cpu": [
"riscv64"
],
@@ -509,9 +509,9 @@
}
},
"node_modules/@oxfmt/binding-linux-riscv64-musl": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz",
"integrity": "sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.62.0.tgz",
"integrity": "sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A==",
"cpu": [
"riscv64"
],
@@ -529,9 +529,9 @@
}
},
"node_modules/@oxfmt/binding-linux-s390x-gnu": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz",
"integrity": "sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.62.0.tgz",
"integrity": "sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A==",
"cpu": [
"s390x"
],
@@ -549,9 +549,9 @@
}
},
"node_modules/@oxfmt/binding-linux-x64-gnu": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz",
"integrity": "sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.62.0.tgz",
"integrity": "sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w==",
"cpu": [
"x64"
],
@@ -569,9 +569,9 @@
}
},
"node_modules/@oxfmt/binding-linux-x64-musl": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz",
"integrity": "sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.62.0.tgz",
"integrity": "sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw==",
"cpu": [
"x64"
],
@@ -589,9 +589,9 @@
}
},
"node_modules/@oxfmt/binding-openharmony-arm64": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz",
"integrity": "sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.62.0.tgz",
"integrity": "sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA==",
"cpu": [
"arm64"
],
@@ -606,9 +606,9 @@
}
},
"node_modules/@oxfmt/binding-win32-arm64-msvc": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz",
"integrity": "sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.62.0.tgz",
"integrity": "sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw==",
"cpu": [
"arm64"
],
@@ -623,9 +623,9 @@
}
},
"node_modules/@oxfmt/binding-win32-ia32-msvc": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz",
"integrity": "sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.62.0.tgz",
"integrity": "sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw==",
"cpu": [
"ia32"
],
@@ -640,9 +640,9 @@
}
},
"node_modules/@oxfmt/binding-win32-x64-msvc": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz",
"integrity": "sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.62.0.tgz",
"integrity": "sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ==",
"cpu": [
"x64"
],
@@ -1044,9 +1044,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
"version": "26.1.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1070,17 +1070,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz",
"integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz",
"integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/type-utils": "8.67.0",
"@typescript-eslint/utils": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0",
"@typescript-eslint/scope-manager": "8.66.0",
"@typescript-eslint/type-utils": "8.66.0",
"@typescript-eslint/utils": "8.66.0",
"@typescript-eslint/visitor-keys": "8.66.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1093,22 +1093,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.67.0",
"@typescript-eslint/parser": "^8.66.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz",
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz",
"integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/typescript-estree": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0",
"@typescript-eslint/scope-manager": "8.66.0",
"@typescript-eslint/types": "8.66.0",
"@typescript-eslint/typescript-estree": "8.66.0",
"@typescript-eslint/visitor-keys": "8.66.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1124,14 +1124,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz",
"integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz",
"integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.67.0",
"@typescript-eslint/types": "^8.67.0",
"@typescript-eslint/tsconfig-utils": "^8.66.0",
"@typescript-eslint/types": "^8.66.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1146,14 +1146,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz",
"integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz",
"integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0"
"@typescript-eslint/types": "8.66.0",
"@typescript-eslint/visitor-keys": "8.66.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1164,9 +1164,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz",
"integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz",
"integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1181,15 +1181,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz",
"integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz",
"integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/typescript-estree": "8.67.0",
"@typescript-eslint/utils": "8.67.0",
"@typescript-eslint/types": "8.66.0",
"@typescript-eslint/typescript-estree": "8.66.0",
"@typescript-eslint/utils": "8.66.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1206,9 +1206,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz",
"integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz",
"integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1220,16 +1220,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz",
"integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz",
"integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.67.0",
"@typescript-eslint/tsconfig-utils": "8.67.0",
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/visitor-keys": "8.67.0",
"@typescript-eslint/project-service": "8.66.0",
"@typescript-eslint/tsconfig-utils": "8.66.0",
"@typescript-eslint/types": "8.66.0",
"@typescript-eslint/visitor-keys": "8.66.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1248,16 +1248,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz",
"integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz",
"integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/typescript-estree": "8.67.0"
"@typescript-eslint/scope-manager": "8.66.0",
"@typescript-eslint/types": "8.66.0",
"@typescript-eslint/typescript-estree": "8.66.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1272,13 +1272,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz",
"integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz",
"integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.67.0",
"@typescript-eslint/types": "8.66.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1689,9 +1689,9 @@
}
},
"node_modules/eslint": {
"version": "10.8.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz",
"integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==",
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz",
"integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
"dev": true,
"license": "MIT",
"workspaces": [
@@ -2709,9 +2709,9 @@
}
},
"node_modules/oxfmt": {
"version": "0.63.0",
"resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.63.0.tgz",
"integrity": "sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==",
"version": "0.62.0",
"resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.62.0.tgz",
"integrity": "sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2727,25 +2727,25 @@
"url": "https://github.com/sponsors/Boshen"
},
"optionalDependencies": {
"@oxfmt/binding-android-arm-eabi": "0.63.0",
"@oxfmt/binding-android-arm64": "0.63.0",
"@oxfmt/binding-darwin-arm64": "0.63.0",
"@oxfmt/binding-darwin-x64": "0.63.0",
"@oxfmt/binding-freebsd-x64": "0.63.0",
"@oxfmt/binding-linux-arm-gnueabihf": "0.63.0",
"@oxfmt/binding-linux-arm-musleabihf": "0.63.0",
"@oxfmt/binding-linux-arm64-gnu": "0.63.0",
"@oxfmt/binding-linux-arm64-musl": "0.63.0",
"@oxfmt/binding-linux-ppc64-gnu": "0.63.0",
"@oxfmt/binding-linux-riscv64-gnu": "0.63.0",
"@oxfmt/binding-linux-riscv64-musl": "0.63.0",
"@oxfmt/binding-linux-s390x-gnu": "0.63.0",
"@oxfmt/binding-linux-x64-gnu": "0.63.0",
"@oxfmt/binding-linux-x64-musl": "0.63.0",
"@oxfmt/binding-openharmony-arm64": "0.63.0",
"@oxfmt/binding-win32-arm64-msvc": "0.63.0",
"@oxfmt/binding-win32-ia32-msvc": "0.63.0",
"@oxfmt/binding-win32-x64-msvc": "0.63.0"
"@oxfmt/binding-android-arm-eabi": "0.62.0",
"@oxfmt/binding-android-arm64": "0.62.0",
"@oxfmt/binding-darwin-arm64": "0.62.0",
"@oxfmt/binding-darwin-x64": "0.62.0",
"@oxfmt/binding-freebsd-x64": "0.62.0",
"@oxfmt/binding-linux-arm-gnueabihf": "0.62.0",
"@oxfmt/binding-linux-arm-musleabihf": "0.62.0",
"@oxfmt/binding-linux-arm64-gnu": "0.62.0",
"@oxfmt/binding-linux-arm64-musl": "0.62.0",
"@oxfmt/binding-linux-ppc64-gnu": "0.62.0",
"@oxfmt/binding-linux-riscv64-gnu": "0.62.0",
"@oxfmt/binding-linux-riscv64-musl": "0.62.0",
"@oxfmt/binding-linux-s390x-gnu": "0.62.0",
"@oxfmt/binding-linux-x64-gnu": "0.62.0",
"@oxfmt/binding-linux-x64-musl": "0.62.0",
"@oxfmt/binding-openharmony-arm64": "0.62.0",
"@oxfmt/binding-win32-arm64-msvc": "0.62.0",
"@oxfmt/binding-win32-ia32-msvc": "0.62.0",
"@oxfmt/binding-win32-x64-msvc": "0.62.0"
},
"peerDependencies": {
"svelte": "^5.0.0",
@@ -3211,16 +3211,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.67.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz",
"integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==",
"version": "8.66.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz",
"integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.67.0",
"@typescript-eslint/parser": "8.67.0",
"@typescript-eslint/typescript-estree": "8.67.0",
"@typescript-eslint/utils": "8.67.0"
"@typescript-eslint/eslint-plugin": "8.66.0",
"@typescript-eslint/parser": "8.66.0",
"@typescript-eslint/typescript-estree": "8.66.0",
"@typescript-eslint/utils": "8.66.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3520,9 +3520,9 @@
}
},
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"version": "8.21.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz",
"integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
+7 -7
View File
@@ -23,24 +23,24 @@
"jsonwebtoken": "^9.0.3",
"lodash-es": "^4.18.1",
"winston": "^3.19.0",
"ws": "^8.21.3"
"ws": "^8.21.2"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
"@types/eslint__js": "^8.42.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/lodash-es": "^4.17.12",
"@types/node": "^26.2.0",
"@types/node": "^26.1.2",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.67.0",
"@typescript-eslint/parser": "^8.67.0",
"eslint": "^10.8.1",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.66.0",
"eslint": "^10.8.0",
"eslint-config-prettier": "^10.1.8",
"globals": "^17.9.0",
"oxfmt": "^0.63.0",
"oxfmt": "^0.62.0",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.67.0",
"typescript-eslint": "^8.66.0",
"vitest": "^4.1.10"
},
"engines": {
-15
View File
@@ -21,7 +21,6 @@ from functools import partial
from typing import cast
from uuid import UUID
from superset import db, security_manager
from superset.commands.base import BaseCommand
from superset.commands.database.exceptions import DatabaseNotFoundError
from superset.daos.database import DatabaseUserOAuth2TokensDAO
@@ -31,7 +30,6 @@ from superset.exceptions import OAuth2Error
from superset.key_value.types import JsonKeyValueCodec, KeyValueResource
from superset.models.core import Database, DatabaseUserOAuth2Tokens
from superset.superset_typing import OAuth2State
from superset.utils.core import get_user_id
from superset.utils.decorators import on_error, transaction
from superset.utils.oauth2 import decode_oauth2_state
@@ -98,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"])
@@ -122,14 +115,6 @@ class OAuth2StoreTokenCommand(BaseCommand):
self._state = decode_oauth2_state(self._parameters["state"])
# Bind the callback to the current session: require an authenticated,
# non-guest user whose id matches the one carried in the state.
user_id = get_user_id()
if user_id is None or security_manager.is_guest_user():
raise OAuth2Error("The OAuth2 callback requires an authenticated user")
if user_id != self._state["user_id"]:
raise OAuth2Error("The OAuth2 state belongs to a different user")
if database := DatabaseUserOAuth2TokensDAO.get_database(
self._state["database_id"]
):
@@ -15,12 +15,9 @@
# specific language governing permissions and limitations
# under the License.
import gzip
import ipaddress
import logging
import os
import re
import socket
from http.client import HTTPConnection, HTTPResponse, HTTPSConnection
from typing import Any
from urllib import request
from urllib.parse import urljoin, urlparse
@@ -50,7 +47,7 @@ from superset.models.helpers import ChildMultipleResultsFound
from superset.sql.parse import Table
from superset.utils import json
from superset.utils.core import get_user
from superset.utils.network import is_safe_host, is_safe_ip
from superset.utils.network import is_safe_host
logger = logging.getLogger(__name__)
@@ -79,47 +76,6 @@ class _ValidatingRedirectHandler(HTTPRedirectHandler):
return super().redirect_request(req, fp, code, msg, headers, newurl)
def _raise_for_unsafe_peer(sock: socket.socket) -> None:
"""
Validate that an established connection's actual peer is publicly
routable, so the address reached matches the policy applied to the host.
"""
peer = sock.getpeername()[0]
if not is_safe_ip(ipaddress.ip_address(peer)):
raise DatasetForbiddenDataURI()
class _PeerValidatingHTTPConnection(HTTPConnection):
"""HTTP connection that validates the peer address on connect."""
def connect(self) -> None:
super().connect()
_raise_for_unsafe_peer(self.sock)
class _PeerValidatingHTTPSConnection(HTTPSConnection):
"""HTTPS connection that validates the peer address after the handshake."""
def connect(self) -> None:
super().connect()
_raise_for_unsafe_peer(self.sock)
class _PeerValidatingHTTPHandler(request.HTTPHandler):
"""Opens HTTP connections through the peer-validating connection class."""
def http_open(self, req: request.Request) -> HTTPResponse:
return self.do_open(_PeerValidatingHTTPConnection, req)
class _PeerValidatingHTTPSHandler(request.HTTPSHandler):
"""Opens HTTPS connections through the peer-validating connection class."""
def https_open(self, req: request.Request) -> HTTPResponse:
context = self._context # type: ignore[attr-defined]
return self.do_open(_PeerValidatingHTTPSConnection, req, context=context)
CHUNKSIZE = 512
VARCHAR = re.compile(r"VARCHAR\((\d+)\)", re.IGNORECASE)
@@ -625,17 +581,7 @@ def load_data(data_uri: str, dataset: SqlaTable, database: Database) -> None:
validate_data_uri(data_uri)
logger.info("Downloading data from %s", data_uri)
handlers: list[request.BaseHandler | type[request.BaseHandler]] = [
_ValidatingRedirectHandler
]
if not app.config["DATASET_IMPORT_ALLOW_INTERNAL_DATA_URLS"]:
# Also enforce the policy at the socket layer: re-check the peer of
# every connection, including each redirect hop. Disable proxies so the
# connection is made directly to the destination and the peer check
# validates the destination address rather than a proxy's.
handlers.append(request.ProxyHandler({}))
handlers.extend([_PeerValidatingHTTPHandler, _PeerValidatingHTTPSHandler])
opener = request.build_opener(*handlers)
opener = request.build_opener(_ValidatingRedirectHandler)
data = opener.open(data_uri) # pylint: disable=consider-using-with # noqa: S310
if data_uri.endswith(".gz"):
data = gzip.open(data)
+18 -32
View File
@@ -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:
+6 -72
View File
@@ -137,29 +137,6 @@ def resolve_executor_user(model: ReportSchedule) -> tuple["User", str]:
return user, username
def _should_build_execution_context(model: ReportSchedule) -> bool:
"""
Whether an execution should run under a :class:`ReportExecutionContext`.
Reports always do their behavior is unchanged. Alerts join them only when
they deliver a rendered PNG/PDF screenshot to recipients, which happens when
``ALERTS_ATTACH_REPORTS`` is enabled. Delivered screenshots must fail closed:
the context selects the fail-closed readiness predicate and disables
partial-tile fallback, so a blank or incomplete capture raises instead of
being delivered.
CSV/text alerts, alerts without the attach flag, the non-delivered
query-context capture, and UI thumbnails are deliberately excluded and keep
their lenient capture contract.
"""
if model.type == ReportScheduleType.REPORT:
return True
return model.report_format in (
ReportDataFormat.PNG,
ReportDataFormat.PDF,
) and feature_flag_manager.is_feature_enabled("ALERTS_ATTACH_REPORTS")
def log_report_delivery_phase(
report_context: ReportExecutionContext | None,
recipient_type: ReportRecipientType | None,
@@ -1995,13 +1972,13 @@ class ReportSuccessState(BaseReportState):
try:
self.send()
except Exception as first_ex: # pylint: disable=broad-except
if self._handle_retry_or_error(str(first_ex), first_ex):
except Exception as ex: # pylint: disable=broad-except
if self._handle_retry_or_error(str(ex), ex):
return # retry scheduled — exit cleanly
try:
self.update_report_schedule_and_log(
ReportState.ERROR, error_message=str(first_ex)
ReportState.ERROR, error_message=str(ex)
)
except (ReportScheduleUnexpectedError, SQLAlchemyError) as logging_ex:
# Logging failed (likely StaleDataError), but we still want to
@@ -2014,45 +1991,7 @@ class ReportSuccessState(BaseReportState):
exc_info=True,
)
# Re-raise the original exception, not the logging failure
raise first_ex from logging_ex
# A delivery failure from the Success/Grace path must notify the
# owner just like the first-run path (ReportNotTriggeredErrorState).
# Without this, a schedule whose previous run succeeded would fail
# silently — e.g. once a screenshot capture starts failing closed.
# The error grace period still throttles repeated notifications.
if not self.is_in_error_grace_period():
second_error_message = REPORT_SCHEDULE_ERROR_NOTIFICATION_MARKER
try:
self.send_error(
f"Error occurred for {self._report_schedule.type}:"
f" {self._report_schedule.name}",
str(first_ex),
)
except SupersetErrorsException as second_ex:
second_error_message = ";".join(
[error.message for error in second_ex.errors]
)
except ReportScheduleUnexpectedError:
# send_error failed due to logging issue; log and continue
# to raise the original error
logger.warning(
"Failed to send error notification due to database issue",
exc_info=True,
)
except Exception as second_ex: # pylint: disable=broad-except
second_error_message = str(second_ex)
finally:
try:
self.update_report_schedule_and_log(
ReportState.ERROR, error_message=second_error_message
)
except ReportScheduleUnexpectedError:
# Logging failed again; log it but don't hide first_ex
logger.warning(
"Failed to log final error state due to database issue",
exc_info=True,
)
raise ex from logging_ex
raise
# send() succeeded — clear retry state and log success. Any execution
@@ -2119,18 +2058,13 @@ class AsyncExecuteReportScheduleCommand(BaseCommand):
if not self._model:
raise ReportScheduleExecuteUnexpectedError()
# Reports always run under an execution context; alerts join them
# only when they deliver a rendered screenshot, so a blank/partial
# capture fails closed instead of being delivered. Ownership and
# terminal-error persistence remain report-only recovery semantics.
if _should_build_execution_context(self._model):
if self._model.type == ReportScheduleType.REPORT:
# An invocation that enters on WORKING is a duplicate or stale
# recovery, not the owner that created the active row. Its state
# handler may terminalize a stale execution, but the command
# boundary must never infer ownership from a replayed UUID.
owns_report_working_state = (
self._model.type == ReportScheduleType.REPORT
and self._model.last_state != ReportState.WORKING
self._model.last_state != ReportState.WORKING
)
total_seconds = resolve_report_execution_budget_seconds(
app.config,

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