mirror of
https://github.com/apache/superset.git
synced 2026-07-29 18:12:29 +00:00
Compare commits
3 Commits
dependabot
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
336f9b464c | ||
|
|
5650ff8a73 | ||
|
|
e484e3e7b3 |
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
|
||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e0647621c2984b5ed2f768cb892365bf2a616ad1 # v4.37.2
|
||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
1
.github/workflows/dependency-review.yml
vendored
1
.github/workflows/dependency-review.yml
vendored
@@ -32,6 +32,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
# compatible/incompatible licenses addressed here: https://www.apache.org/legal/resolved.html
|
||||
|
||||
6
.github/workflows/issue_creation.yml
vendored
6
.github/workflows/issue_creation.yml
vendored
@@ -2,10 +2,10 @@ name: supersetbot orglabel based on author
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
types: [created, edited]
|
||||
|
||||
pull_request:
|
||||
types: [opened, edited]
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
superbot-orglabel:
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
# Label the issue with the appropriate org using supersetbot
|
||||
# - this requires for the author to be publicly associated with their org
|
||||
# - and for the org to be listed in `supersetbot/src/metadata.js`
|
||||
supersetbot orglabel --issue ${{ github.event.number || github.event.issue.number }} --repo ${{ github.repository }} || true
|
||||
supersetbot orglabel --issue ${{ github.event.number }} --repo ${{ github.repository }} || true
|
||||
|
||||
77
.github/workflows/superset-docs-verify.yml
vendored
77
.github/workflows/superset-docs-verify.yml
vendored
@@ -21,8 +21,8 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lychee:
|
||||
# See docs here: https://github.com/lycheeverse/lychee-action
|
||||
linkinator:
|
||||
# See docs here: https://github.com/marketplace/actions/linkinator
|
||||
# Only run on pull_request, not workflow_run
|
||||
if: github.event_name == 'pull_request'
|
||||
name: Link Checking
|
||||
@@ -31,52 +31,37 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Restores the on-disk request cache (.lycheecache) lychee writes when
|
||||
# `--cache` is set, so re-runs don't re-check every external link from
|
||||
# scratch. Key never matches exactly (github.run_id is unique per run),
|
||||
# so actions/cache always saves a fresh copy afterwards; restore-keys
|
||||
# falls back to the most recent prior cache on lookup.
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: .lycheecache
|
||||
key: lychee-cache-${{ github.run_id }}
|
||||
restore-keys: lychee-cache-
|
||||
# v2.8.0 and v2.9.0 are both on ASF Infra's action allowlist
|
||||
# (apache/infrastructure-actions approved_patterns.yml) as of this
|
||||
# writing. Do not bump past v2.9.0 without opening an Infra ticket
|
||||
# to allow the new SHA first!
|
||||
- uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# an ASF Infra ticket to allow the new version first!
|
||||
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
|
||||
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
|
||||
with:
|
||||
fail: false
|
||||
args: >-
|
||||
--verbose --no-progress
|
||||
--cache --max-cache-age 7d
|
||||
--exclude '^https://github.com/apache/(superset|incubator-superset)/(pull|issues)/\d+'
|
||||
--exclude '^https://github.com/apache/(superset|incubator-superset)/commit/[a-f0-9]+'
|
||||
--exclude 'superset-frontend/.*CHANGELOG\.md'
|
||||
--exclude 'http://localhost:8088/'
|
||||
--exclude 'http://127.0.0.1:3000/'
|
||||
--exclude 'http://localhost:9001/'
|
||||
--exclude 'https://charts.bitnami.com/bitnami'
|
||||
--exclude 'https://www.li.me/'
|
||||
--exclude 'https://www.fanatics.com/'
|
||||
--exclude 'https://tails.com/gb/'
|
||||
--exclude 'https://www.techaudit.info/'
|
||||
--exclude 'https://avetilearning.com/'
|
||||
--exclude 'https://www.udemy.com/'
|
||||
--exclude 'https://trustmedis.com/'
|
||||
--exclude 'http://theiconic.com.au/'
|
||||
--exclude 'https://dev.mysql.com/doc/refman/5.7/en/innodb-limits.html'
|
||||
--exclude '^https://img\.shields\.io/.*'
|
||||
--exclude 'https://vkusvill.ru/'
|
||||
--exclude 'https://www.linkedin.com/in/mark-thomas-b16751158/'
|
||||
--exclude 'https://theiconic.com.au/'
|
||||
--exclude 'https://wattbewerb.de/'
|
||||
--exclude 'https://timbr.ai/'
|
||||
--exclude 'https://opensource.org/license/apache-2-0'
|
||||
--exclude 'https://www.plaidcloud.com/'
|
||||
'./**/*.md' './**/*.mdx'
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
linksToSkip: >-
|
||||
^https://github.com/apache/(superset|incubator-superset)/(pull|issues)/\d+,
|
||||
^https://github.com/apache/(superset|incubator-superset)/commit/[a-f0-9]+,
|
||||
superset-frontend/.*CHANGELOG\.md,
|
||||
http://localhost:8088/,
|
||||
http://127.0.0.1:3000/,
|
||||
http://localhost:9001/,
|
||||
https://charts.bitnami.com/bitnami,
|
||||
https://www.li.me/,
|
||||
https://www.fanatics.com/,
|
||||
https://tails.com/gb/,
|
||||
https://www.techaudit.info/,
|
||||
https://avetilearning.com/,
|
||||
https://www.udemy.com/,
|
||||
https://trustmedis.com/,
|
||||
http://theiconic.com.au/,
|
||||
https://dev.mysql.com/doc/refman/5.7/en/innodb-limits.html,
|
||||
^https://img\.shields\.io/.*,
|
||||
https://vkusvill.ru/,
|
||||
https://www.linkedin.com/in/mark-thomas-b16751158/,
|
||||
https://theiconic.com.au/,
|
||||
https://wattbewerb.de/,
|
||||
https://timbr.ai/,
|
||||
https://opensource.org/license/apache-2-0,
|
||||
https://www.plaidcloud.com/
|
||||
|
||||
build-on-pr:
|
||||
# Build docs when PR changes docs/** (uses committed databases.json)
|
||||
|
||||
@@ -28,10 +28,8 @@ x-superset-image: &superset-image apachesuperset.docker.scarf.sh/apache/superset
|
||||
x-superset-volumes:
|
||||
&superset-volumes # /app/pythonpath_docker will be appended to the PYTHONPATH in the final container
|
||||
- ./docker:/app/docker
|
||||
- ./superset-core:/app/superset-core
|
||||
- superset_home:/app/superset_home
|
||||
x-superset-environment: &superset-environment
|
||||
# Tagged images must use their bundled packages rather than host bind mounts.
|
||||
DEV_MODE: "false"
|
||||
|
||||
services:
|
||||
redis:
|
||||
@@ -60,7 +58,6 @@ services:
|
||||
required: true
|
||||
- path: docker/.env-local # optional override
|
||||
required: false
|
||||
environment: *superset-environment
|
||||
image: *superset-image
|
||||
container_name: superset_app
|
||||
command: ["/app/docker/docker-bootstrap.sh", "app-gunicorn"]
|
||||
@@ -82,7 +79,6 @@ services:
|
||||
required: true
|
||||
- path: docker/.env-local # optional override
|
||||
required: false
|
||||
environment: *superset-environment
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_started
|
||||
@@ -102,7 +98,6 @@ services:
|
||||
required: true
|
||||
- path: docker/.env-local # optional override
|
||||
required: false
|
||||
environment: *superset-environment
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
superset-init:
|
||||
@@ -125,7 +120,6 @@ services:
|
||||
required: true
|
||||
- path: docker/.env-local # optional override
|
||||
required: false
|
||||
environment: *superset-environment
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
superset-init:
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^10.5.3",
|
||||
"@storybook/addon-docs": "^10.5.0",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.46",
|
||||
"antd": "^6.5.1",
|
||||
"baseline-browser-mapping": "^2.10.44",
|
||||
"baseline-browser-mapping": "^2.10.43",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.2",
|
||||
@@ -77,7 +77,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.2.0",
|
||||
"storybook": "^10.5.3",
|
||||
"storybook": "^10.5.0",
|
||||
"swagger-ui-react": "^5.32.9",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
@@ -89,16 +89,16 @@
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.64.0",
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.7.0",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier": "^3.9.5",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"typescript-eslint": "^8.64.0",
|
||||
"webpack": "^5.108.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
194
docs/yarn.lock
194
docs/yarn.lock
@@ -4005,23 +4005,23 @@
|
||||
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
|
||||
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
|
||||
|
||||
"@storybook/addon-docs@^10.5.3":
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.3.tgz#b5666cf20361d85c98cc836b2bfcdbbb72af3e97"
|
||||
integrity sha512-MI1VDMSMQk78YxjIdt7WlrVOiA3TzTP00lRed1LeXh0fCvA9jxz9YXJI2+XigsLaxCSuOAEf/l35/GTLDMHD8A==
|
||||
"@storybook/addon-docs@^10.5.0":
|
||||
version "10.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.2.tgz#e8023d1a68f637de9188be093f719895a8252d19"
|
||||
integrity sha512-MoBANDsh5qEA14U+JaBoQcYsKbayJDDMopigFN0NdVAsZTdBfVIsL7cnjTFBL6ubB3ifb5M0tCXbScpml1KqiQ==
|
||||
dependencies:
|
||||
"@mdx-js/react" "^3.0.0"
|
||||
"@storybook/csf-plugin" "10.5.3"
|
||||
"@storybook/csf-plugin" "10.5.2"
|
||||
"@storybook/icons" "^2.0.2"
|
||||
"@storybook/react-dom-shim" "10.5.3"
|
||||
"@storybook/react-dom-shim" "10.5.2"
|
||||
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/csf-plugin@10.5.3":
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.3.tgz#0818709c2761910f1e42710f6231f773eb97fb41"
|
||||
integrity sha512-mkPq6zru8fN5+46uC1cZEbKW2ws1hh9KvF4g4/Gu8pNbKnvqULPhk0/Bf0ZCtlr7zI7DvcFhyCy3dbvN+2n4Gw==
|
||||
"@storybook/csf-plugin@10.5.2":
|
||||
version "10.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.2.tgz#a359dffe0b842dfffacb0e94ad98da0ccb13ba69"
|
||||
integrity sha512-PK/wXiALFf88mt4HmDtiZJ6NRvhExSXEM9uFIN+OIHxGqg7Xbp6MB0SPdhsTbMY9720ahiu/DJx5iIzkidcA3w==
|
||||
dependencies:
|
||||
unplugin "^2.3.5"
|
||||
|
||||
@@ -4035,10 +4035,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
|
||||
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
|
||||
|
||||
"@storybook/react-dom-shim@10.5.3":
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.3.tgz#e176a6549aa02002e375cb66467a00c5403745f8"
|
||||
integrity sha512-eUWBsRRax5R3MDJVFs/CrFDF1bYS58AMB9tX02lLRuiZe6xy1cKh3CRFS+2xH571l0fNaXQ+7j69TOJ0fk2tmA==
|
||||
"@storybook/react-dom-shim@10.5.2":
|
||||
version "10.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.2.tgz#993077c517ee1a1c065c82bf0143ae08856a3c90"
|
||||
integrity sha512-TbdYVLuD7gwj1CFsDJhCHUiwfVmzFWzalKEUGy9XgXyNpyOV1CYRsdmRdhaOHgmn2ljQZuTAxSnG7NlElghVaw==
|
||||
|
||||
"@superset-ui/core@^0.20.4":
|
||||
version "0.20.4"
|
||||
@@ -5527,100 +5527,100 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.65.0", "@typescript-eslint/eslint-plugin@^8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz#0a58df6fea8c0bf6b396f518077099bc8b762bb5"
|
||||
integrity sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==
|
||||
"@typescript-eslint/eslint-plugin@8.64.0", "@typescript-eslint/eslint-plugin@^8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz#71a0c3d5f8a5e6c5dfdb4f0f04bd1bfb572d5e24"
|
||||
integrity sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.65.0"
|
||||
"@typescript-eslint/type-utils" "8.65.0"
|
||||
"@typescript-eslint/utils" "8.65.0"
|
||||
"@typescript-eslint/visitor-keys" "8.65.0"
|
||||
"@typescript-eslint/scope-manager" "8.64.0"
|
||||
"@typescript-eslint/type-utils" "8.64.0"
|
||||
"@typescript-eslint/utils" "8.64.0"
|
||||
"@typescript-eslint/visitor-keys" "8.64.0"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.65.0", "@typescript-eslint/parser@^8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.65.0.tgz#5295c1058c0a1dd746ef28baaf9c0341dbdf03dc"
|
||||
integrity sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==
|
||||
"@typescript-eslint/parser@8.64.0", "@typescript-eslint/parser@^8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.64.0.tgz#c9864a1cc28a13ff29a7314fbdef0528bb122f72"
|
||||
integrity sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.65.0"
|
||||
"@typescript-eslint/types" "8.65.0"
|
||||
"@typescript-eslint/typescript-estree" "8.65.0"
|
||||
"@typescript-eslint/visitor-keys" "8.65.0"
|
||||
"@typescript-eslint/scope-manager" "8.64.0"
|
||||
"@typescript-eslint/types" "8.64.0"
|
||||
"@typescript-eslint/typescript-estree" "8.64.0"
|
||||
"@typescript-eslint/visitor-keys" "8.64.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.65.0.tgz#65fbbc9a1591abffaeab5513200f848271cb0aa5"
|
||||
integrity sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==
|
||||
"@typescript-eslint/project-service@8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.64.0.tgz#14c4e29390d7325a7f8a1218c2788fd649b85da6"
|
||||
integrity sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.65.0"
|
||||
"@typescript-eslint/types" "^8.65.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.64.0"
|
||||
"@typescript-eslint/types" "^8.64.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz#9547202ce7e608e7b6283df585703b980a0ea70d"
|
||||
integrity sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==
|
||||
"@typescript-eslint/scope-manager@8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz#d45f15304a94c85c39db317b717b158fb6259958"
|
||||
integrity sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.65.0"
|
||||
"@typescript-eslint/visitor-keys" "8.65.0"
|
||||
"@typescript-eslint/types" "8.64.0"
|
||||
"@typescript-eslint/visitor-keys" "8.64.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.65.0", "@typescript-eslint/tsconfig-utils@^8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz#36f168fcdbb1295f7446ff0379667f98c3cf1bf3"
|
||||
integrity sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==
|
||||
"@typescript-eslint/tsconfig-utils@8.64.0", "@typescript-eslint/tsconfig-utils@^8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz#c62ac8ea9173c3cac8b38b8e66e30a046b548851"
|
||||
integrity sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==
|
||||
|
||||
"@typescript-eslint/type-utils@8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz#d316d7522d93cff4cd14f305e02f3df2d804f9c1"
|
||||
integrity sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==
|
||||
"@typescript-eslint/type-utils@8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz#106fa7d58cf9cf7758f3dd8e426ac8237eceacf3"
|
||||
integrity sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.65.0"
|
||||
"@typescript-eslint/typescript-estree" "8.65.0"
|
||||
"@typescript-eslint/utils" "8.65.0"
|
||||
"@typescript-eslint/types" "8.64.0"
|
||||
"@typescript-eslint/typescript-estree" "8.64.0"
|
||||
"@typescript-eslint/utils" "8.64.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.65.0", "@typescript-eslint/types@^8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.65.0.tgz#3e86738416a777c8b8925ab46745f48ecf904c9f"
|
||||
integrity sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==
|
||||
"@typescript-eslint/types@8.64.0", "@typescript-eslint/types@^8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.64.0.tgz#b41f8ef5dd40616908658b991197a9d486cda60b"
|
||||
integrity sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz#f1f514808f6aa713e2d678ae8ff592a65e1632af"
|
||||
integrity sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==
|
||||
"@typescript-eslint/typescript-estree@8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz#b8d51255e2d726eb4bd80d397a4fb4170c02eecc"
|
||||
integrity sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.65.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.65.0"
|
||||
"@typescript-eslint/types" "8.65.0"
|
||||
"@typescript-eslint/visitor-keys" "8.65.0"
|
||||
"@typescript-eslint/project-service" "8.64.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.64.0"
|
||||
"@typescript-eslint/types" "8.64.0"
|
||||
"@typescript-eslint/visitor-keys" "8.64.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.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.65.0.tgz#afedd974a0c8deeef553b509df5800bafd615a72"
|
||||
integrity sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==
|
||||
"@typescript-eslint/utils@8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.64.0.tgz#98bb2010cfb754b41985b9c93e6e8b3dcd7bd600"
|
||||
integrity sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.65.0"
|
||||
"@typescript-eslint/types" "8.65.0"
|
||||
"@typescript-eslint/typescript-estree" "8.65.0"
|
||||
"@typescript-eslint/scope-manager" "8.64.0"
|
||||
"@typescript-eslint/types" "8.64.0"
|
||||
"@typescript-eslint/typescript-estree" "8.64.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.65.0":
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz#e3704c13cb4a1c22454c1abf28ff4737e15018c6"
|
||||
integrity sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==
|
||||
"@typescript-eslint/visitor-keys@8.64.0":
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz#7a08421d10e54960733352cd7c95fab1784e8473"
|
||||
integrity sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.65.0"
|
||||
"@typescript-eslint/types" "8.64.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -6368,10 +6368,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.10.44, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.44"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz#2e074675e640b67cb3835c54c11aa2dd9119c0f8"
|
||||
integrity sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==
|
||||
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.10.43, baseline-browser-mapping@^2.9.19:
|
||||
version "2.10.43"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz#7b5d11590ce5acdbe4859443e3c940e81ce8c02d"
|
||||
integrity sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
@@ -13029,10 +13029,10 @@ prettier-linter-helpers@^1.0.1:
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@^3.9.6:
|
||||
version "3.9.6"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.6.tgz#b3ea5146515d40fc53f18aa63f74dfab1e10dbf6"
|
||||
integrity sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==
|
||||
prettier@^3.9.5:
|
||||
version "3.9.5"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.5.tgz#4fec97736e33b9d0b620b48914fe93b530e835ad"
|
||||
integrity sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==
|
||||
|
||||
pretty-error@^4.0.0:
|
||||
version "4.0.0"
|
||||
@@ -14627,10 +14627,10 @@ stop-iteration-iterator@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
internal-slot "^1.1.0"
|
||||
|
||||
storybook@^10.5.3:
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.3.tgz#72ee7cc02e3b6353eeec9633c2367437cf2ac86e"
|
||||
integrity sha512-c8Wumu5qz0N2fnzWBxcPzUsY+8BpKBKChNyl4BEh9qhMV6KW587gL8il8emRB+4Hay+zMjDHA7cIeTkl4FKYuw==
|
||||
storybook@^10.5.0:
|
||||
version "10.5.2"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.2.tgz#751aca53cbeea086886d84ceb46c8a29a5f802f3"
|
||||
integrity sha512-zkYxVZoDMj8njzZc3EH5UyY7885wpi9a1mmWVwFiNHSo+i5r2Go84E2OI1cdOctRymLkNvgs1j5jqAKA0ftBqg==
|
||||
dependencies:
|
||||
"@storybook/global" "^5.0.0"
|
||||
"@storybook/icons" "^2.0.2"
|
||||
@@ -15309,15 +15309,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.65.0:
|
||||
version "8.65.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.65.0.tgz#754c953fd6a9a3d36fa54015bdba80a6eb2a4957"
|
||||
integrity sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==
|
||||
typescript-eslint@^8.64.0:
|
||||
version "8.64.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.64.0.tgz#4984dae4de9dc8bf892acf5c394d0a2a5f08c3e1"
|
||||
integrity sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.65.0"
|
||||
"@typescript-eslint/parser" "8.65.0"
|
||||
"@typescript-eslint/typescript-estree" "8.65.0"
|
||||
"@typescript-eslint/utils" "8.65.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.64.0"
|
||||
"@typescript-eslint/parser" "8.64.0"
|
||||
"@typescript-eslint/typescript-estree" "8.64.0"
|
||||
"@typescript-eslint/utils" "8.64.0"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
@@ -39,7 +39,6 @@ RETRYABLE_STATUS_CODES: frozenset[int] = frozenset({403, 429})
|
||||
PATTERNS = {
|
||||
"python": [
|
||||
r"^\.github/workflows/.*python",
|
||||
r"^docker-compose-image-tag\.yml$",
|
||||
r"^tests/",
|
||||
r"^superset/",
|
||||
r"^scripts/",
|
||||
|
||||
564
superset-frontend/package-lock.json
generated
564
superset-frontend/package-lock.json
generated
@@ -111,7 +111,7 @@
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.26.0",
|
||||
"markdown-to-jsx": "^9.9.0",
|
||||
"markdown-to-jsx": "^9.8.2",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -182,9 +182,9 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.3",
|
||||
"@storybook/addon-links": "10.5.3",
|
||||
"@storybook/react-webpack5": "10.5.3",
|
||||
"@storybook/addon-docs": "10.5.2",
|
||||
"@storybook/addon-links": "10.5.2",
|
||||
"@storybook/react-webpack5": "10.5.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.46",
|
||||
@@ -215,13 +215,13 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.64.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.10.44",
|
||||
"baseline-browser-mapping": "^2.10.43",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
@@ -240,7 +240,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.3",
|
||||
"eslint-plugin-storybook": "10.5.2",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -262,7 +262,7 @@
|
||||
"oxlint": "^1.74.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"prettier": "3.9.6",
|
||||
"prettier": "3.9.5",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"process": "^0.11.10",
|
||||
"react-dnd-test-backend": "^16.0.1",
|
||||
@@ -272,7 +272,7 @@
|
||||
"source-map": "^0.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.3",
|
||||
"storybook": "10.5.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"swc-loader": "^0.2.7",
|
||||
@@ -10329,16 +10329,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.3.tgz",
|
||||
"integrity": "sha512-MI1VDMSMQk78YxjIdt7WlrVOiA3TzTP00lRed1LeXh0fCvA9jxz9YXJI2+XigsLaxCSuOAEf/l35/GTLDMHD8A==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.2.tgz",
|
||||
"integrity": "sha512-MoBANDsh5qEA14U+JaBoQcYsKbayJDDMopigFN0NdVAsZTdBfVIsL7cnjTFBL6ubB3ifb5M0tCXbScpml1KqiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.5.3",
|
||||
"@storybook/csf-plugin": "10.5.2",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@storybook/react-dom-shim": "10.5.3",
|
||||
"@storybook/react-dom-shim": "10.5.2",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -10349,7 +10349,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10358,9 +10358,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.3.tgz",
|
||||
"integrity": "sha512-mkPq6zru8fN5+46uC1cZEbKW2ws1hh9KvF4g4/Gu8pNbKnvqULPhk0/Bf0ZCtlr7zI7DvcFhyCy3dbvN+2n4Gw==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.2.tgz",
|
||||
"integrity": "sha512-PK/wXiALFf88mt4HmDtiZJ6NRvhExSXEM9uFIN+OIHxGqg7Xbp6MB0SPdhsTbMY9720ahiu/DJx5iIzkidcA3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10373,7 +10373,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "10.5.3",
|
||||
"storybook": "10.5.2",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -10393,9 +10393,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.3.tgz",
|
||||
"integrity": "sha512-eUWBsRRax5R3MDJVFs/CrFDF1bYS58AMB9tX02lLRuiZe6xy1cKh3CRFS+2xH571l0fNaXQ+7j69TOJ0fk2tmA==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.2.tgz",
|
||||
"integrity": "sha512-TbdYVLuD7gwj1CFsDJhCHUiwfVmzFWzalKEUGy9XgXyNpyOV1CYRsdmRdhaOHgmn2ljQZuTAxSnG7NlElghVaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -10407,7 +10407,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10419,9 +10419,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.3.tgz",
|
||||
"integrity": "sha512-awu6nBV/MRFv+zu9hIqFrqnKa37LnWfZ3/UHeS68il0QxWfy6uSU1dfpxqiYbCjs9Ct0+bsaO6ZaPamA+jlFLA==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.2.tgz",
|
||||
"integrity": "sha512-9Xk0gTpe8CJNR8a2UfgJtCPIZx1hQGUzYx7iTdfzJFQ9Y79ljGeOdq+d/O1zJ3G2uVqHhjHyD8/V+iolhfhdxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10434,7 +10434,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10528,15 +10528,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.3.tgz",
|
||||
"integrity": "sha512-5uGVxywkT+/Bge4JwaReIj3TMKeceS6trkFwRc70RJnTWUQtlAbUyodI1OMQifRAs/1190+ptKBl0qOAbDyINw==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.2.tgz",
|
||||
"integrity": "sha512-nffAS7ipJY0ltaPuM7ELBO0S4SiSLxAR0f9oUrYVbSKpYUVC0r1MzHVTyj/eqzzS6Dwq5yvmvCrwgc3M3Htdmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-webpack5": "10.5.3",
|
||||
"@storybook/preset-react-webpack": "10.5.3",
|
||||
"@storybook/react": "10.5.3"
|
||||
"@storybook/builder-webpack5": "10.5.2",
|
||||
"@storybook/preset-react-webpack": "10.5.2",
|
||||
"@storybook/react": "10.5.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -10545,7 +10545,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.3",
|
||||
"storybook": "10.5.2",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -10555,13 +10555,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.3.tgz",
|
||||
"integrity": "sha512-IY8OlaOFRgsOCA4K+fBmTfEbVArvpqhjc/TYKC7Tq9hgY449stMmj89XNDeviL+ZXc7fL+S80EMiZnCPoF2Acw==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.2.tgz",
|
||||
"integrity": "sha512-iFPMPeRCinW5pQJlcQT1EiNv0+eKXuLu/3t53g/xY9cGY0QfQtAMzxf0c2QIeAnJOxu3BWTrkdL3MKDHuDnPwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.5.3",
|
||||
"@storybook/core-webpack": "10.5.2",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"cjs-module-lexer": "^1.2.3",
|
||||
"css-loader": "^7.1.2",
|
||||
@@ -10583,7 +10583,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -10592,9 +10592,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5/node_modules/@storybook/core-webpack": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.3.tgz",
|
||||
"integrity": "sha512-A7DIGq4XcOXkL7g4Dc/XyntlfStvYvBp+iIkblA5mVOJ/I0n7MxI0jgrLoG+thP5jA72HLdSpjedXa3GUQOPrA==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.2.tgz",
|
||||
"integrity": "sha512-XNzXTJmvAKXpvcrWHDWnIC8jZR4GBgTUH+FF9T5lSmU9ZL46T2wVaJAu2GUXfDLmNwLHZXusbYljbTD7Kswamg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10605,17 +10605,17 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.3.tgz",
|
||||
"integrity": "sha512-i93PdwMFAO0hqFhqdrNEhcwBnPb416zELssZTnxjjClJq4qRIGrGxch1NZ33IN4LGaF3fEY68byGuX6AHcHo6g==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.2.tgz",
|
||||
"integrity": "sha512-XV5XV0PqgPEzBwilcEnowqbWkjt7r8qvTm7Zi3vSjrr1Aq6uLpc+9rV51bXsoOgcAFLZv9VQMbAu4B7LzHU/4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.5.3",
|
||||
"@storybook/core-webpack": "10.5.2",
|
||||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -10632,7 +10632,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -10641,9 +10641,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack/node_modules/@storybook/core-webpack": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.3.tgz",
|
||||
"integrity": "sha512-A7DIGq4XcOXkL7g4Dc/XyntlfStvYvBp+iIkblA5mVOJ/I0n7MxI0jgrLoG+thP5jA72HLdSpjedXa3GUQOPrA==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.2.tgz",
|
||||
"integrity": "sha512-XNzXTJmvAKXpvcrWHDWnIC8jZR4GBgTUH+FF9T5lSmU9ZL46T2wVaJAu2GUXfDLmNwLHZXusbYljbTD7Kswamg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10654,18 +10654,18 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.3.tgz",
|
||||
"integrity": "sha512-d/CK78xgA7DDvqnxkqcYmiTjomE4ch2TWvk0O8/xHQWW6y0nMjKtsZbmUBfZ0QcdYdWq7dErzfbG7YAzxDi7Ig==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.2.tgz",
|
||||
"integrity": "sha512-DQkTEvQ3Tn5ndyZnOyNTnYuJWBZdnUsVFuvImtt1H6QyHsZGhl35/3CqRwERa/P10Sw/6vLP5DS+KhDCz1hWbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.5.3",
|
||||
"@storybook/react-dom-shim": "10.5.2",
|
||||
"react-docgen": "^8.0.2",
|
||||
"react-docgen-typescript": "^2.2.2"
|
||||
},
|
||||
@@ -10678,7 +10678,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.3",
|
||||
"storybook": "10.5.2",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -10694,9 +10694,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.3.tgz",
|
||||
"integrity": "sha512-eUWBsRRax5R3MDJVFs/CrFDF1bYS58AMB9tX02lLRuiZe6xy1cKh3CRFS+2xH571l0fNaXQ+7j69TOJ0fk2tmA==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.2.tgz",
|
||||
"integrity": "sha512-TbdYVLuD7gwj1CFsDJhCHUiwfVmzFWzalKEUGy9XgXyNpyOV1CYRsdmRdhaOHgmn2ljQZuTAxSnG7NlElghVaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -10708,7 +10708,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -12776,17 +12776,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
|
||||
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/type-utils": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/type-utils": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -12799,7 +12799,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -12839,6 +12839,69 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
@@ -12861,15 +12924,29 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
|
||||
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -12897,15 +12974,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -12921,10 +12998,116 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
||||
"@typescript-eslint/types": "^8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -12963,6 +13146,38 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
@@ -12986,6 +13201,19 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
@@ -13003,16 +13231,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
|
||||
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
|
||||
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0"
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -13026,14 +13254,120 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
||||
"@typescript-eslint/types": "^8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/brace-expansion": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
|
||||
"integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
|
||||
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -15299,9 +15633,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.10.44",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz",
|
||||
"integrity": "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==",
|
||||
"version": "2.10.43",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz",
|
||||
"integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -19886,18 +20220,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-storybook": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.3.tgz",
|
||||
"integrity": "sha512-dreVGgQvTTOvPTrnn71uogs3E7skbODW3Ya1cHxupznzuIofLCzy+7zWJ4ObsEZ1MsOcofQlSaQuV9oJBszLoA==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.2.tgz",
|
||||
"integrity": "sha512-BPTbb8OKNAlQ2XubEQ6H1TeHG9nMygLrHC8cSVTfdIat65qbS568WYgNg//zlJWtZ9ZamyVkCp/na+LBbFPseA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "^8.60.0",
|
||||
"@typescript-eslint/utils": "^8.60.0"
|
||||
"@typescript-eslint/types": "^8.48.0",
|
||||
"@typescript-eslint/utils": "^8.48.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8",
|
||||
"storybook": "10.5.3"
|
||||
"storybook": "10.5.2"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-testing-library": {
|
||||
@@ -28751,9 +29085,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-to-jsx": {
|
||||
"version": "9.9.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.9.0.tgz",
|
||||
"integrity": "sha512-3YRQuriODgdMr9EeY//Ya/104p978bY0nOwmc85IGxHixqOlhNDgq7IJTsLOdmFxjMFKJ6bVzUav30wWVJZOJQ==",
|
||||
"version": "9.8.2",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.8.2.tgz",
|
||||
"integrity": "sha512-rWUuxKB5NsuJmSfUOuXkQ0O5qk0J/Lr3Lk6dzxKoKQI/jeHYlsVfz3zJdMLAhI46hHoXDYERWhtBOiqtWDZ4LA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@@ -33326,9 +33660,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||
"version": "3.9.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz",
|
||||
"integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -38364,9 +38698,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.5.3",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.3.tgz",
|
||||
"integrity": "sha512-c8Wumu5qz0N2fnzWBxcPzUsY+8BpKBKChNyl4BEh9qhMV6KW587gL8il8emRB+4Hay+zMjDHA7cIeTkl4FKYuw==",
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.2.tgz",
|
||||
"integrity": "sha512-zkYxVZoDMj8njzZc3EH5UyY7885wpi9a1mmWVwFiNHSo+i5r2Go84E2OI1cdOctRymLkNvgs1j5jqAKA0ftBqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -41086,9 +41420,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/unplugin/node_modules/acorn": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
|
||||
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.26.0",
|
||||
"markdown-to-jsx": "^9.9.0",
|
||||
"markdown-to-jsx": "^9.8.2",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -267,9 +267,9 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.3",
|
||||
"@storybook/addon-links": "10.5.3",
|
||||
"@storybook/react-webpack5": "10.5.3",
|
||||
"@storybook/addon-docs": "10.5.2",
|
||||
"@storybook/addon-links": "10.5.2",
|
||||
"@storybook/react-webpack5": "10.5.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.46",
|
||||
@@ -300,13 +300,13 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.64.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.10.44",
|
||||
"baseline-browser-mapping": "^2.10.43",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
@@ -325,7 +325,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.3",
|
||||
"eslint-plugin-storybook": "10.5.2",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -347,7 +347,7 @@
|
||||
"oxlint": "^1.74.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"prettier": "3.9.6",
|
||||
"prettier": "3.9.5",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"process": "^0.11.10",
|
||||
"react-dnd-test-backend": "^16.0.1",
|
||||
@@ -357,7 +357,7 @@
|
||||
"source-map": "^0.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.3",
|
||||
"storybook": "10.5.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"swc-loader": "^0.2.7",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// The geojson is loaded via fs rather than a normal import because the
|
||||
// webpack/jest loaders treat *.geojson as an opaque asset (a URL string in
|
||||
// the browser build, an empty mock object under jest), so neither exposes
|
||||
// the actual feature data to a unit test.
|
||||
const iranGeojsonPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'src',
|
||||
'countries',
|
||||
'iran.geojson',
|
||||
);
|
||||
|
||||
type IranFeature = {
|
||||
properties: { ISO: string; NAME_1: string };
|
||||
};
|
||||
|
||||
const getIranFeatures = (): IranFeature[] =>
|
||||
JSON.parse(fs.readFileSync(iranGeojsonPath, 'utf-8')).features;
|
||||
|
||||
test('every province in the Iran map has a unique, non-empty ISO code', () => {
|
||||
const features = getIranFeatures();
|
||||
// Iran has 31 provinces; assert the count so a deleted feature is caught,
|
||||
// not just a duplicated ISO code among whatever features remain.
|
||||
expect(features).toHaveLength(31);
|
||||
|
||||
const isoCodes = features.map(feature => feature.properties.ISO);
|
||||
isoCodes.forEach(iso => expect(iso).toBeTruthy());
|
||||
|
||||
const uniqueIsoCodes = new Set(isoCodes);
|
||||
expect(uniqueIsoCodes.size).toBe(isoCodes.length);
|
||||
});
|
||||
|
||||
test('Tehran, Alborz and Razavi Khorasan have their correct, distinct ISO codes', () => {
|
||||
const provincesByIso = Object.fromEntries(
|
||||
getIranFeatures().map(feature => [
|
||||
feature.properties.NAME_1,
|
||||
feature.properties.ISO,
|
||||
]),
|
||||
);
|
||||
|
||||
expect(provincesByIso.Tehran).toBe('IR-07');
|
||||
expect(provincesByIso.Alborz).toBe('IR-30');
|
||||
expect(provincesByIso['Razavi Khorasan']).toBe('IR-09');
|
||||
});
|
||||
@@ -457,23 +457,13 @@ export default function transformProps(
|
||||
const seriesName = inverted[entryName] || entryName;
|
||||
const colorScaleKey = getOriginalSeries(seriesName, array);
|
||||
|
||||
const labelMapValues = rawLabelMap?.[seriesName];
|
||||
|
||||
let displayName: string;
|
||||
|
||||
if (groupby.length > 0) {
|
||||
// When we have groupby, format as "metric, dimension". Each series
|
||||
// belongs to the metric recorded in its label-map tuple
|
||||
// ([metric, ...dimensions]) — always using the first metric would
|
||||
// prepend it to every other metric's series (#37921). Tuples without
|
||||
// a metric part fall back to the first metric as before.
|
||||
const metricDisplayName =
|
||||
labelMapValues && labelMapValues.length > 1
|
||||
? getMetricDisplayName(labelMapValues[0], verboseMap)
|
||||
: MetricDisplayNameA;
|
||||
// When we have groupby, format as "metric, dimension"
|
||||
const metricPart: string = showQueryIdentifiers
|
||||
? `${metricDisplayName} (Query A)`
|
||||
: metricDisplayName;
|
||||
? `${MetricDisplayNameA} (Query A)`
|
||||
: MetricDisplayNameA;
|
||||
displayName = entryName.includes(metricPart)
|
||||
? entryName
|
||||
: `${metricPart}, ${entryName}`;
|
||||
@@ -481,6 +471,8 @@ export default function transformProps(
|
||||
// When no groupby, format as just the entry name with optional query identifier
|
||||
displayName = showQueryIdentifiers ? `${entryName} (Query A)` : entryName;
|
||||
}
|
||||
|
||||
const labelMapValues = rawLabelMap?.[seriesName];
|
||||
if (labelMapValues) {
|
||||
displayLabelMap[displayName] = labelMapValues;
|
||||
}
|
||||
@@ -544,23 +536,13 @@ export default function transformProps(
|
||||
const seriesEntry = inverted[entryName] || entryName;
|
||||
const colorScaleKey = getOriginalSeries(seriesEntry, array);
|
||||
|
||||
const labelMapValuesB = rawLabelMapB?.[seriesEntry];
|
||||
|
||||
let displayName: string;
|
||||
|
||||
if (groupbyB.length > 0) {
|
||||
// When we have groupby, format as "metric, dimension". Each series
|
||||
// belongs to the metric recorded in its label-map tuple
|
||||
// ([metric, ...dimensions]) — always using the first metric would
|
||||
// prepend it to every other metric's series (#37921). Tuples without
|
||||
// a metric part fall back to the first metric as before.
|
||||
const metricDisplayName =
|
||||
labelMapValuesB && labelMapValuesB.length > 1
|
||||
? getMetricDisplayName(labelMapValuesB[0], verboseMap)
|
||||
: MetricDisplayNameB;
|
||||
// When we have groupby, format as "metric, dimension"
|
||||
const metricPart: string = showQueryIdentifiers
|
||||
? `${metricDisplayName} (Query B)`
|
||||
: metricDisplayName;
|
||||
? `${MetricDisplayNameB} (Query B)`
|
||||
: MetricDisplayNameB;
|
||||
displayName = entryName.includes(metricPart)
|
||||
? entryName
|
||||
: `${metricPart}, ${entryName}`;
|
||||
@@ -568,6 +550,8 @@ export default function transformProps(
|
||||
// When no groupby, format as just the entry name with optional query identifier
|
||||
displayName = showQueryIdentifiers ? `${entryName} (Query B)` : entryName;
|
||||
}
|
||||
|
||||
const labelMapValuesB = rawLabelMapB?.[seriesEntry];
|
||||
if (labelMapValuesB) {
|
||||
displayLabelMapB[displayName] = labelMapValuesB;
|
||||
}
|
||||
|
||||
@@ -325,8 +325,9 @@ const config: ControlPanelConfig = {
|
||||
description: t(
|
||||
'Total angle covered by the chart, in degrees. ' +
|
||||
'360° draws a full circle and 180° draws a half donut. ' +
|
||||
'Partial arcs are automatically re-centered and scaled ' +
|
||||
'to make use of the available space.',
|
||||
'When the sweep is 180° or less and the start angle is a ' +
|
||||
'multiple of 90°, the chart is automatically re-centered ' +
|
||||
'to make use of the empty space.',
|
||||
),
|
||||
renderTrigger: true,
|
||||
default: DEFAULT_FORM_DATA.sweptAngle,
|
||||
|
||||
@@ -39,6 +39,9 @@ import {
|
||||
EchartsPieLabelType,
|
||||
PieChartDataItem,
|
||||
PieChartTransformedProps,
|
||||
TotalValuePaddingProps,
|
||||
PaddingResult,
|
||||
HalfDonut,
|
||||
} from './types';
|
||||
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
|
||||
import {
|
||||
@@ -73,153 +76,120 @@ export function parseParams({
|
||||
return [name, formattedValue, formattedPercent];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounding box of the pie arc in unit coordinates: outer radius = 1,
|
||||
* mathematical y-up convention matching ECharts' angle convention
|
||||
* (0° points right, 90° points up, angles sweep clockwise from
|
||||
* `startAngle` to `startAngle - sweptAngle`).
|
||||
*/
|
||||
export interface ArcBoundingBox {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
}
|
||||
const HALF_DONUT_SWEEP_LIMIT = 180;
|
||||
|
||||
/**
|
||||
* Computes the bounding box of an annular sector from its angles.
|
||||
* Geometric configuration for each type of semi-circular layout.
|
||||
*
|
||||
* The box is spanned by the outer arc endpoints, the inner arc endpoints
|
||||
* (which collapse to the pie origin when `innerRatio` is 0), and every axis
|
||||
* extreme (0°/90°/180°/270°) the arc sweeps through.
|
||||
* - `centerOffset` — offset of the chart center from the baseline 50% on the X and Y axes.
|
||||
* Resulting position: `50% + offset`.
|
||||
* - `totalBase` — base position of the "Total" text as a percentage on the X and Y axes.
|
||||
*
|
||||
* @param startAngle - The start angle of the arc in degrees.
|
||||
* @param sweptAngle - The total angle covered by the arc in degrees.
|
||||
* @param innerRatio - Inner radius as a fraction of the outer radius (0–1).
|
||||
* The values are empirically tuned so that the "Total" text visually remains
|
||||
* at the geometric center of the arc after the chart is re-centered. The
|
||||
* `left`/`right` totalBase values sit 5% inside the shifted chart center
|
||||
* (60% and 40% respectively) to compensate for the text being positioned by
|
||||
* its left edge rather than its midpoint.
|
||||
*/
|
||||
export function getArcBoundingBox(
|
||||
const HALF_DONUT_LAYOUT: Record<
|
||||
HalfDonut,
|
||||
{
|
||||
centerOffset: { x: number; y: number };
|
||||
totalBase: { left: number; top: number };
|
||||
}
|
||||
> = {
|
||||
top: { centerOffset: { x: 0, y: 20 }, totalBase: { left: 50, top: 68.5 } },
|
||||
bottom: { centerOffset: { x: 0, y: -20 }, totalBase: { left: 50, top: 30 } },
|
||||
left: { centerOffset: { x: 10, y: 0 }, totalBase: { left: 55, top: 50 } },
|
||||
right: { centerOffset: { x: -10, y: 0 }, totalBase: { left: 35, top: 50 } },
|
||||
none: { centerOffset: { x: 0, y: 0 }, totalBase: { left: 50, top: 50 } },
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the type of semicircular layout based on the start angle and swept angle.
|
||||
*
|
||||
* All four semicircle orientations are supported:
|
||||
* - `'top'` — the arc is positioned at the top; the chart center shifts downwards.
|
||||
* - `'bottom'` — the arc is positioned at the bottom; the chart center shifts upwards.
|
||||
* - `'left'` — the arc is positioned at the left; the chart center shifts right.
|
||||
* - `'right'` — the arc is positioned at the right; the chart center shifts left.
|
||||
*
|
||||
* @param startAngle - The start angle of the arc in degrees (0–360).
|
||||
* @param sweptAngle - The swept angle of the arc in degrees (10–360).
|
||||
* @returns The type of semicircular layout.
|
||||
*/
|
||||
export const getHalfDonut = (
|
||||
startAngle: number,
|
||||
sweptAngle: number,
|
||||
innerRatio: number,
|
||||
): ArcBoundingBox {
|
||||
if (sweptAngle >= 360) {
|
||||
return { minX: -1, maxX: 1, minY: -1, maxY: 1 };
|
||||
}
|
||||
const toRad = (deg: number) => (deg * Math.PI) / 180;
|
||||
const endAngle = startAngle - sweptAngle;
|
||||
const points: [number, number][] = [];
|
||||
[startAngle, endAngle].forEach(angle => {
|
||||
const x = Math.cos(toRad(angle));
|
||||
const y = Math.sin(toRad(angle));
|
||||
points.push([x, y], [innerRatio * x, innerRatio * y]);
|
||||
});
|
||||
for (
|
||||
let axis = Math.ceil(endAngle / 90) * 90;
|
||||
axis <= startAngle;
|
||||
axis += 90
|
||||
) {
|
||||
points.push([Math.cos(toRad(axis)), Math.sin(toRad(axis))]);
|
||||
}
|
||||
const xs = points.map(([x]) => x);
|
||||
const ys = points.map(([, y]) => y);
|
||||
return {
|
||||
minX: Math.min(...xs),
|
||||
maxX: Math.max(...xs),
|
||||
minY: Math.min(...ys),
|
||||
maxY: Math.max(...ys),
|
||||
};
|
||||
}
|
||||
): HalfDonut => {
|
||||
if (sweptAngle > HALF_DONUT_SWEEP_LIMIT) return 'none';
|
||||
|
||||
/**
|
||||
* Arcs covering only a sliver of the circle would otherwise scale up without
|
||||
* bound; cap the fit scale at the factor a quarter arc reaches naturally.
|
||||
*/
|
||||
const MAX_RADIUS_SCALE = 2;
|
||||
const normalized = startAngle % 360;
|
||||
|
||||
export interface PieLayout {
|
||||
/** Pie origin in px, relative to the padded series rect. */
|
||||
center: [number, number];
|
||||
/** Inner/outer radius percent strings, scaled to fit the arc's box. */
|
||||
radius: [string, string];
|
||||
/** Pie origin in px, in container coordinates (for the graphic component). */
|
||||
totalAnchor: { x: number; y: number };
|
||||
}
|
||||
if (normalized === 180) return 'top';
|
||||
if (normalized === 0) return 'bottom';
|
||||
if (normalized === 270) return 'left';
|
||||
if (normalized === 90) return 'right';
|
||||
|
||||
/**
|
||||
* Lays out the pie geometrically for any start/sweep angle combination:
|
||||
* scales the radius until the arc's bounding box fills the available rect
|
||||
* (so partial arcs reclaim the space a full circle would leave empty) and
|
||||
* shifts the pie origin so that box is centered. A full circle reproduces
|
||||
* ECharts' default layout exactly.
|
||||
*/
|
||||
export function getPieLayout({
|
||||
return 'none';
|
||||
};
|
||||
|
||||
const getHalfDonutLayout = (startAngle: number, sweptAngle: number) =>
|
||||
HALF_DONUT_LAYOUT[getHalfDonut(startAngle, sweptAngle)];
|
||||
|
||||
export function getTotalValuePadding({
|
||||
chartPadding,
|
||||
donut,
|
||||
width,
|
||||
height,
|
||||
padding,
|
||||
startAngle,
|
||||
sweptAngle,
|
||||
donut,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
}: {
|
||||
width: number;
|
||||
height: number;
|
||||
padding: { top: number; bottom: number; left: number; right: number };
|
||||
startAngle: number;
|
||||
sweptAngle: number;
|
||||
donut: boolean;
|
||||
innerRadius: number;
|
||||
outerRadius: number;
|
||||
}): PieLayout {
|
||||
const rectWidth = Math.max(width - padding.left - padding.right, 1);
|
||||
const rectHeight = Math.max(height - padding.top - padding.bottom, 1);
|
||||
const innerRatio = donut ? innerRadius / Math.max(outerRadius, 1) : 0;
|
||||
const box = getArcBoundingBox(startAngle, sweptAngle, innerRatio);
|
||||
const boxWidth = box.maxX - box.minX;
|
||||
const boxHeight = box.maxY - box.minY;
|
||||
}: TotalValuePaddingProps): PaddingResult {
|
||||
const safeHeight = height || 1;
|
||||
const safeWidth = width || 1;
|
||||
|
||||
// ECharts resolves percentage radii against min(rect width, height) / 2.
|
||||
// Grow that basis until the arc's bounding box hits the rect on one axis.
|
||||
const fullBasis = Math.min(rectWidth, rectHeight) / 2;
|
||||
const fitBasis = Math.min(rectWidth / boxWidth, rectHeight / boxHeight);
|
||||
const scale = Math.min(fitBasis / fullBasis, MAX_RADIUS_SCALE);
|
||||
const outerPx = (outerRadius / 100) * fullBasis * scale;
|
||||
const halfType = getHalfDonut(startAngle, sweptAngle);
|
||||
const layout = HALF_DONUT_LAYOUT[halfType];
|
||||
const isHalf = halfType !== 'none';
|
||||
|
||||
// Place the pie origin so the arc's box is centered in the rect. Unit y
|
||||
// points up while screen y points down, hence the sign flip.
|
||||
const round = (value: number) => Math.round(value * 100) / 100;
|
||||
const centerX = rectWidth / 2 - ((box.minX + box.maxX) / 2) * outerPx;
|
||||
const centerY = rectHeight / 2 + ((box.minY + box.maxY) / 2) * outerPx;
|
||||
const calculateTop = (): string => {
|
||||
if (chartPadding.bottom) {
|
||||
return donut
|
||||
? `${layout.totalBase.top - (chartPadding.bottom / safeHeight) * 50}%`
|
||||
: '0';
|
||||
}
|
||||
|
||||
// The pie origin is the natural spot for the "Total" text (the middle of
|
||||
// the hole, or the flat edge of a half donut), but for narrow arcs it can
|
||||
// fall far outside the drawn shape, even off-canvas. Clamp it into the
|
||||
// arc's bounding box, which always sits within the rect.
|
||||
const clamp = (value: number, min: number, max: number) =>
|
||||
Math.min(Math.max(value, min), max);
|
||||
const halfBoxWidth = (boxWidth / 2) * outerPx;
|
||||
const halfBoxHeight = (boxHeight / 2) * outerPx;
|
||||
const anchorX = clamp(
|
||||
centerX,
|
||||
rectWidth / 2 - halfBoxWidth,
|
||||
rectWidth / 2 + halfBoxWidth,
|
||||
);
|
||||
const anchorY = clamp(
|
||||
centerY,
|
||||
rectHeight / 2 - halfBoxHeight,
|
||||
rectHeight / 2 + halfBoxHeight,
|
||||
);
|
||||
if (chartPadding.top || isHalf) {
|
||||
if (donut) {
|
||||
return `${layout.totalBase.top + (chartPadding.top / safeHeight) * 50}%`;
|
||||
}
|
||||
return `${(chartPadding.top / safeHeight) * 100}%`;
|
||||
}
|
||||
|
||||
return donut ? 'middle' : '0';
|
||||
};
|
||||
|
||||
const calculateLeft = (): string => {
|
||||
if (chartPadding.right) {
|
||||
const rightPercent = (chartPadding.right / safeWidth) * 100;
|
||||
return `${layout.totalBase.left - rightPercent * 0.75}%`;
|
||||
}
|
||||
|
||||
if (chartPadding.left) {
|
||||
const leftPercent = (chartPadding.left / safeWidth) * 100;
|
||||
return `${layout.totalBase.left + leftPercent * 0.25}%`;
|
||||
}
|
||||
|
||||
if (isHalf && (halfType === 'left' || halfType === 'right')) {
|
||||
return `${layout.totalBase.left}%`;
|
||||
}
|
||||
|
||||
return 'center';
|
||||
};
|
||||
|
||||
return {
|
||||
center: [round(centerX), round(centerY)],
|
||||
radius: [
|
||||
`${round(donut ? innerRadius * scale : 0)}%`,
|
||||
`${round(outerRadius * scale)}%`,
|
||||
],
|
||||
totalAnchor: {
|
||||
x: round(padding.left + anchorX),
|
||||
y: round(padding.top + anchorY),
|
||||
},
|
||||
top: calculateTop(),
|
||||
left: calculateLeft(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -508,16 +478,7 @@ export default function transformProps(
|
||||
effectiveLegendMargin,
|
||||
);
|
||||
|
||||
const pieLayout = getPieLayout({
|
||||
width,
|
||||
height,
|
||||
padding: chartPadding,
|
||||
startAngle,
|
||||
sweptAngle,
|
||||
donut,
|
||||
innerRadius,
|
||||
outerRadius,
|
||||
});
|
||||
const { centerOffset } = getHalfDonutLayout(startAngle, sweptAngle);
|
||||
|
||||
const series: PieSeriesOption[] = [
|
||||
{
|
||||
@@ -525,8 +486,8 @@ export default function transformProps(
|
||||
...chartPadding,
|
||||
animation: false,
|
||||
roseType: roseType || undefined,
|
||||
radius: pieLayout.radius,
|
||||
center: pieLayout.center,
|
||||
radius: [`${donut ? innerRadius : 0}%`, `${outerRadius}%`],
|
||||
center: [`${50 + centerOffset.x}%`, `${50 + centerOffset.y}%`],
|
||||
startAngle,
|
||||
endAngle: startAngle - sweptAngle,
|
||||
avoidLabelOverlap: true,
|
||||
@@ -589,18 +550,16 @@ export default function transformProps(
|
||||
graphic: showTotal
|
||||
? {
|
||||
type: 'text',
|
||||
// Donut: center the text on the pie origin (the middle of the
|
||||
// hole, or the flat edge of a partial arc). Pie: park it at the
|
||||
// top center of the padded rect so it doesn't overlap the slices.
|
||||
x: donut
|
||||
? pieLayout.totalAnchor.x
|
||||
: chartPadding.left +
|
||||
(width - chartPadding.left - chartPadding.right) / 2,
|
||||
y: donut ? pieLayout.totalAnchor.y : chartPadding.top,
|
||||
...getTotalValuePadding({
|
||||
chartPadding,
|
||||
donut,
|
||||
width,
|
||||
height,
|
||||
startAngle,
|
||||
sweptAngle,
|
||||
}),
|
||||
style: {
|
||||
text: t('Total: %s', numberFormatter(totalValue)),
|
||||
align: 'center',
|
||||
verticalAlign: donut ? 'middle' : 'top',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
fill: theme.colorText,
|
||||
|
||||
@@ -103,3 +103,36 @@ export interface PieChartDataItem {
|
||||
};
|
||||
isOther?: boolean;
|
||||
}
|
||||
|
||||
interface ChartPadding {
|
||||
top: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
}
|
||||
|
||||
export interface TotalValuePaddingProps {
|
||||
chartPadding: ChartPadding;
|
||||
donut: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
sweptAngle: number;
|
||||
startAngle: number;
|
||||
}
|
||||
|
||||
export interface PaddingResult {
|
||||
top?: string;
|
||||
left?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Semicircular chart layout type.
|
||||
*
|
||||
* - `'top'` — arc at the top, center shifted downwards.
|
||||
* - `'bottom'` — arc at the bottom, center shifted upwards.
|
||||
* - `'left'` — arc on the left, center shifted to the right.
|
||||
* - `'right'` — arc on the right, center shifted to the left.
|
||||
* - `'none'` — full circle (no recentering).
|
||||
* @see getHalfDonut
|
||||
*/
|
||||
export type HalfDonut = 'top' | 'bottom' | 'left' | 'right' | 'none';
|
||||
|
||||
@@ -1161,83 +1161,3 @@ test('x-axis dedup keeps the forced min label when the endpoints format identica
|
||||
|
||||
expect(formatter(min)).toBe('May');
|
||||
});
|
||||
|
||||
test('regression #37921: multi-metric Query A with groupby does not duplicate first metric in series names', () => {
|
||||
// Regression test for https://github.com/apache/superset/issues/37921
|
||||
// ("Residual" follow-up to #37055).
|
||||
//
|
||||
// When Query A has multiple metrics + at least one Group By dimension,
|
||||
// the display-name builder in transformProps.ts used to prepend the FIRST
|
||||
// metric's display name to every series that didn't literally contain it:
|
||||
// name: `${MetricDisplayNameA}, ${entryName}`
|
||||
// For series belonging to the *second* metric, this produced a
|
||||
// cross-contaminated label like `score_one, score_two, A` — the
|
||||
// user-visible "first metric duplicated" symptom in the legend / tooltip.
|
||||
// The fix derives each series' metric from its label-map tuple instead.
|
||||
const multiMetricRows = [
|
||||
{
|
||||
'score_one, A': 1,
|
||||
'score_one, B': 2,
|
||||
'score_two, A': 3,
|
||||
'score_two, B': 4,
|
||||
ds: 599616000000,
|
||||
},
|
||||
{
|
||||
'score_one, A': 5,
|
||||
'score_one, B': 6,
|
||||
'score_two, A': 7,
|
||||
'score_two, B': 8,
|
||||
ds: 599916000000,
|
||||
},
|
||||
];
|
||||
const multiMetricLabelMap = {
|
||||
ds: ['ds'],
|
||||
'score_one, A': ['score_one', 'A'],
|
||||
'score_one, B': ['score_one', 'B'],
|
||||
'score_two, A': ['score_two', 'A'],
|
||||
'score_two, B': ['score_two', 'B'],
|
||||
};
|
||||
|
||||
const queryAData = createTestQueryData(multiMetricRows, {
|
||||
label_map: multiMetricLabelMap,
|
||||
});
|
||||
// Query B keeps the existing single-metric shape — the bug is on
|
||||
// Query A's path so we just need a valid Query B alongside.
|
||||
const queryBData = createTestQueryData(defaultQueryRows, {
|
||||
label_map: defaultLabelMap,
|
||||
});
|
||||
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps
|
||||
>({
|
||||
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
|
||||
defaultQueriesData: [queryAData, queryBData],
|
||||
formData: {
|
||||
...formData,
|
||||
metrics: ['score_one', 'score_two'],
|
||||
groupby: ['category'],
|
||||
},
|
||||
queriesData: [queryAData, queryBData],
|
||||
});
|
||||
const transformed = transformProps(chartProps);
|
||||
|
||||
const queryASeriesNames = (transformed.echartOptions.series as any[])
|
||||
.map((s: any) => String(s.name))
|
||||
.filter((n: string) => n.includes('score_'));
|
||||
|
||||
// Each (metric, dim_value) combo from Query A should appear exactly once
|
||||
// with the *correct* metric prefix — not the first-metric-prepended-to-
|
||||
// everything-else form. Comparing the sorted array (rather than using
|
||||
// separate toContain assertions) also catches a regression that emits
|
||||
// duplicate series for the same name.
|
||||
expect([...queryASeriesNames].sort()).toEqual(
|
||||
['score_one, A', 'score_one, B', 'score_two, A', 'score_two, B'].sort(),
|
||||
);
|
||||
|
||||
// And explicitly: no series name should contain *both* metric names —
|
||||
// that's the smoking gun for the duplication bug.
|
||||
for (const name of queryASeriesNames) {
|
||||
expect(name).not.toMatch(/score_one,\s+score_two/);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -29,20 +29,12 @@ import type {
|
||||
} from 'echarts/types/src/util/types';
|
||||
import transformProps, {
|
||||
parseParams,
|
||||
getArcBoundingBox,
|
||||
getPieLayout,
|
||||
getHalfDonut,
|
||||
getTotalValuePadding,
|
||||
} from '../../src/Pie/transformProps';
|
||||
import { EchartsPieChartProps, PieChartDataItem } from '../../src/Pie/types';
|
||||
import { LegendOrientation, LegendType } from '../../src/types';
|
||||
|
||||
const getGraphic = (transformed: ReturnType<typeof transformProps>) =>
|
||||
transformed.echartOptions.graphic as {
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
style: { text: string; align: string; verticalAlign: string };
|
||||
};
|
||||
|
||||
describe('Pie transformProps', () => {
|
||||
const formData: SqlaFormData = {
|
||||
colorScheme: 'bnbColors',
|
||||
@@ -360,60 +352,109 @@ describe('Total value positioning with legends', () => {
|
||||
|
||||
test('should center total text when legend is on the right', () => {
|
||||
const props = getChartPropsWithLegend(true, true, 'right', true);
|
||||
const graphic = getGraphic(transformProps(props));
|
||||
const transformed = transformProps(props);
|
||||
|
||||
expect(graphic.type).toBe('text');
|
||||
expect(graphic.style.text).toContain('Total:');
|
||||
expect(graphic.style.align).toBe('center');
|
||||
expect(graphic.style.verticalAlign).toBe('middle');
|
||||
// Anchored on the pie origin, which shifts left of the container center
|
||||
// because the right legend narrows the series rect.
|
||||
expect(graphic.x).toBeLessThan(400);
|
||||
expect(graphic.y).toBe(300);
|
||||
expect(transformed.echartOptions.graphic).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
left: expect.stringMatching(/^\d+(\.\d+)?%$/),
|
||||
top: 'middle',
|
||||
style: expect.objectContaining({
|
||||
text: expect.stringContaining('Total:'),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// The left position should be less than 50% (shifted left)
|
||||
const leftValue = parseFloat(
|
||||
(transformed.echartOptions.graphic as any).left.replace('%', ''),
|
||||
);
|
||||
expect(leftValue).toBeLessThan(50);
|
||||
expect(leftValue).toBeGreaterThan(30); // Should be reasonable positioning
|
||||
});
|
||||
|
||||
test('should center total text when legend is on the left', () => {
|
||||
const props = getChartPropsWithLegend(true, true, 'left', true);
|
||||
const graphic = getGraphic(transformProps(props));
|
||||
const transformed = transformProps(props);
|
||||
|
||||
// The left legend pads the rect, pushing the pie origin right.
|
||||
expect(graphic.x).toBeGreaterThan(400);
|
||||
expect(graphic.y).toBe(300);
|
||||
expect(transformed.echartOptions.graphic).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
left: expect.stringMatching(/^\d+(\.\d+)?%$/),
|
||||
top: 'middle',
|
||||
}),
|
||||
);
|
||||
|
||||
// The left position should be greater than 50% (shifted right)
|
||||
const leftValue = parseFloat(
|
||||
(transformed.echartOptions.graphic as any).left.replace('%', ''),
|
||||
);
|
||||
expect(leftValue).toBeGreaterThan(50);
|
||||
expect(leftValue).toBeLessThan(70); // Should be reasonable positioning
|
||||
});
|
||||
|
||||
test('should center total text when legend is on top', () => {
|
||||
const props = getChartPropsWithLegend(true, true, 'top', true);
|
||||
const graphic = getGraphic(transformProps(props));
|
||||
const transformed = transformProps(props);
|
||||
|
||||
expect(graphic.x).toBe(400);
|
||||
expect(graphic.y).toBeGreaterThan(300);
|
||||
expect(transformed.echartOptions.graphic).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: expect.stringMatching(/^\d+(\.\d+)?%$/),
|
||||
}),
|
||||
);
|
||||
|
||||
// The top position should be adjusted for top legend
|
||||
const topValue = parseFloat(
|
||||
(transformed.echartOptions.graphic as any).top.replace('%', ''),
|
||||
);
|
||||
expect(topValue).toBeGreaterThan(50); // Shifted down for top legend
|
||||
});
|
||||
|
||||
test('should center total text when legend is on bottom', () => {
|
||||
const props = getChartPropsWithLegend(true, true, 'bottom', true);
|
||||
const graphic = getGraphic(transformProps(props));
|
||||
const transformed = transformProps(props);
|
||||
|
||||
expect(graphic.x).toBe(400);
|
||||
expect(graphic.y).toBeLessThan(300);
|
||||
expect(transformed.echartOptions.graphic).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: expect.stringMatching(/^\d+(\.\d+)?%$/),
|
||||
}),
|
||||
);
|
||||
|
||||
// The top position should be adjusted for bottom legend
|
||||
const topValue = parseFloat(
|
||||
(transformed.echartOptions.graphic as any).top.replace('%', ''),
|
||||
);
|
||||
expect(topValue).toBeLessThan(50); // Shifted up for bottom legend
|
||||
});
|
||||
|
||||
test('should center on the pie origin when no legend is shown', () => {
|
||||
test('should use default positioning when no legend is shown', () => {
|
||||
const props = getChartPropsWithLegend(true, false, 'right', true);
|
||||
const graphic = getGraphic(transformProps(props));
|
||||
const transformed = transformProps(props);
|
||||
|
||||
expect(graphic.x).toBe(400);
|
||||
expect(graphic.y).toBe(300);
|
||||
expect(graphic.style.verticalAlign).toBe('middle');
|
||||
expect(transformed.echartOptions.graphic).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should park total at the top of the rect for non-donut charts', () => {
|
||||
test('should handle regular pie chart (non-donut) positioning', () => {
|
||||
const props = getChartPropsWithLegend(true, true, 'right', false);
|
||||
const graphic = getGraphic(transformProps(props));
|
||||
const transformed = transformProps(props);
|
||||
|
||||
expect(graphic.y).toBe(0);
|
||||
expect(graphic.style.verticalAlign).toBe('top');
|
||||
// Horizontally centered over the narrowed rect, not the container.
|
||||
expect(graphic.x).toBeLessThan(400);
|
||||
expect(transformed.echartOptions.graphic).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'text',
|
||||
top: '0', // Non-donut charts use '0' as default top position
|
||||
left: expect.stringMatching(/^\d+(\.\d+)?%$/), // Should still adjust left for right legend
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('should not show total graphic when showTotal is false', () => {
|
||||
@@ -614,7 +655,6 @@ const getAngleChartProps = (
|
||||
startAngle,
|
||||
sweptAngle,
|
||||
show_total: true,
|
||||
show_legend: false,
|
||||
};
|
||||
|
||||
return new ChartProps({
|
||||
@@ -633,110 +673,217 @@ const getAngleChartProps = (
|
||||
}) as EchartsPieChartProps;
|
||||
};
|
||||
|
||||
const getSeries = (props: EchartsPieChartProps) =>
|
||||
transformProps(props).echartOptions.series as PieSeriesOption[];
|
||||
|
||||
test('keeps ECharts default layout for a full donut', () => {
|
||||
const series = getSeries(getAngleChartProps(true, 360, 90));
|
||||
expect(series[0].center).toEqual([400, 300]);
|
||||
expect(series[0].radius).toEqual(['30%', '70%']);
|
||||
test('sets center to 70% for half-donut', () => {
|
||||
const props = getAngleChartProps(true, 180);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].center).toEqual(['50%', '70%']);
|
||||
});
|
||||
|
||||
test('recenters and scales up a top half-donut', () => {
|
||||
// Bounding box is 2 wide x 1 tall, so an 800x600 canvas fits a radius
|
||||
// basis of min(800/2, 600/1) = 400 instead of 300: scale 4/3.
|
||||
const series = getSeries(getAngleChartProps(true, 180, 180));
|
||||
expect(series[0].center).toEqual([400, 440]);
|
||||
expect(series[0].radius).toEqual(['40%', '93.33%']);
|
||||
test('keeps center at 50% for full donut', () => {
|
||||
const props = getAngleChartProps(true, 360);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].center).toEqual(['50%', '50%']);
|
||||
});
|
||||
|
||||
test('recenters a bottom half-donut upwards', () => {
|
||||
const series = getSeries(getAngleChartProps(true, 180, 0));
|
||||
expect(series[0].center).toEqual([400, 160]);
|
||||
test('calculates endAngle for a quarter donut', () => {
|
||||
const props = getAngleChartProps(true, 90);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].endAngle).toBe(90);
|
||||
});
|
||||
|
||||
test('recenters a right half-donut leftwards without scaling', () => {
|
||||
// A lateral half is 1 wide x 2 tall; height binds at the full-circle
|
||||
// basis, so the radius stays put and only the center shifts.
|
||||
const series = getSeries(getAngleChartProps(true, 180, 90));
|
||||
expect(series[0].center).toEqual([295, 300]);
|
||||
expect(series[0].radius).toEqual(['30%', '70%']);
|
||||
test('sets center to 30% for bottom half-donut (startAngle=0)', () => {
|
||||
const props = getAngleChartProps(true, 180, 0);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].center).toEqual(['50%', '30%']);
|
||||
});
|
||||
|
||||
test('recenters a left half-donut rightwards', () => {
|
||||
const series = getSeries(getAngleChartProps(true, 180, 270));
|
||||
expect(series[0].center).toEqual([505, 300]);
|
||||
test('sets center to 30% for bottom half-donut (startAngle=360)', () => {
|
||||
const props = getAngleChartProps(true, 180, 360);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].center).toEqual(['50%', '30%']);
|
||||
});
|
||||
|
||||
test('recenters non-cardinal start angles too', () => {
|
||||
const series = getSeries(getAngleChartProps(true, 180, 170));
|
||||
expect(series[0].center).not.toEqual([400, 300]);
|
||||
test('shifts center left for right half-donut (startAngle=90)', () => {
|
||||
const props = getAngleChartProps(true, 180, 90);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].center).toEqual(['40%', '50%']);
|
||||
});
|
||||
|
||||
test('scales a quarter donut to the fit cap', () => {
|
||||
const series = getSeries(getAngleChartProps(true, 90, 180));
|
||||
expect(series[0].center).toEqual([610, 510]);
|
||||
expect(series[0].radius).toEqual(['60%', '140%']);
|
||||
test('shifts center right for left half-donut (startAngle=270)', () => {
|
||||
const props = getAngleChartProps(true, 180, 270);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].center).toEqual(['60%', '50%']);
|
||||
});
|
||||
|
||||
test('passes startAngle through and derives endAngle from the sweep', () => {
|
||||
const series = getSeries(getAngleChartProps(true, 180, 90));
|
||||
test('keeps center at 50% for non-cardinal start angle even when sweep ≤ 180', () => {
|
||||
const props = getAngleChartProps(true, 180, 45);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].center).toEqual(['50%', '50%']);
|
||||
});
|
||||
|
||||
test('allows endAngle to go negative for right half-donut', () => {
|
||||
const props = getAngleChartProps(true, 180, 90);
|
||||
const transformed = transformProps(props);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
expect(series[0].startAngle).toBe(90);
|
||||
expect(series[0].endAngle).toBe(-90);
|
||||
});
|
||||
|
||||
test('anchors the total on the pie origin for a top half-donut', () => {
|
||||
const graphic = getGraphic(
|
||||
transformProps(getAngleChartProps(true, 180, 180)),
|
||||
);
|
||||
expect(graphic.x).toBe(400);
|
||||
expect(graphic.y).toBe(440);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['full circle', 90, 360, 0.3, { minX: -1, maxX: 1, minY: -1, maxY: 1 }],
|
||||
['top half', 180, 180, 0, { minX: -1, maxX: 1, minY: 0, maxY: 1 }],
|
||||
['bottom half', 0, 180, 0, { minX: -1, maxX: 1, minY: -1, maxY: 0 }],
|
||||
['right half', 90, 180, 0, { minX: 0, maxX: 1, minY: -1, maxY: 1 }],
|
||||
['left half', 270, 180, 0, { minX: -1, maxX: 0, minY: -1, maxY: 1 }],
|
||||
['top-left quarter', 180, 90, 0.5, { minX: -1, maxX: 0, minY: 0, maxY: 1 }],
|
||||
])('getArcBoundingBox: %s', (_label, start, sweep, inner, expected) => {
|
||||
const box = getArcBoundingBox(start, sweep, inner);
|
||||
expect(box.minX).toBeCloseTo(expected.minX, 10);
|
||||
expect(box.maxX).toBeCloseTo(expected.maxX, 10);
|
||||
expect(box.minY).toBeCloseTo(expected.minY, 10);
|
||||
expect(box.maxY).toBeCloseTo(expected.maxY, 10);
|
||||
[180, 180, 'top'],
|
||||
[180, 90, 'top'],
|
||||
[180, 45, 'top'],
|
||||
[0, 180, 'bottom'],
|
||||
[360, 180, 'bottom'],
|
||||
[360, 90, 'bottom'],
|
||||
[90, 180, 'right'],
|
||||
[90, 90, 'right'],
|
||||
[270, 180, 'left'],
|
||||
[270, 90, 'left'],
|
||||
[45, 180, 'none'],
|
||||
[170, 180, 'none'],
|
||||
[180, 360, 'none'],
|
||||
[180, 181, 'none'],
|
||||
[0, 360, 'none'],
|
||||
])('startAngle=%i, sweptAngle=%i → %s', (start, swept, expected) => {
|
||||
expect(getHalfDonut(start, swept)).toBe(expected);
|
||||
});
|
||||
|
||||
test('getArcBoundingBox includes inner arc endpoints for narrow donuts', () => {
|
||||
// A 20-degree sliver straddling 12 o'clock: the lowest point of the
|
||||
// annular sector is an inner endpoint, not an outer one.
|
||||
const box = getArcBoundingBox(100, 20, 0.5);
|
||||
expect(box.minY).toBeCloseTo(0.5 * Math.sin((80 * Math.PI) / 180), 10);
|
||||
expect(box.maxY).toBeCloseTo(1, 10);
|
||||
});
|
||||
const baseProps = {
|
||||
donut: true,
|
||||
width: 800,
|
||||
height: 600,
|
||||
startAngle: 180,
|
||||
sweptAngle: 360,
|
||||
};
|
||||
|
||||
test('getPieLayout centers the bounding box within legend padding', () => {
|
||||
const layout = getPieLayout({
|
||||
width: 800,
|
||||
height: 600,
|
||||
padding: { top: 0, bottom: 0, left: 0, right: 200 },
|
||||
startAngle: 90,
|
||||
sweptAngle: 360,
|
||||
donut: true,
|
||||
innerRadius: 30,
|
||||
outerRadius: 70,
|
||||
test('returns "middle" for donut without padding and not half', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
chartPadding: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
// Rect is 600x600; the pie centers within it and the total anchor is
|
||||
// reported in container coordinates.
|
||||
expect(layout.center).toEqual([300, 300]);
|
||||
expect(layout.totalAnchor).toEqual({ x: 300, y: 300 });
|
||||
expect(result.top).toBe('middle');
|
||||
});
|
||||
|
||||
test('clamps the total anchor into the arc box for narrow arcs', () => {
|
||||
// A 20-degree sliver's pie origin falls far below the drawn wedge; the
|
||||
// anchor must stay within the arc's bounding box so the text is visible.
|
||||
const graphic = getGraphic(transformProps(getAngleChartProps(true, 20, 100)));
|
||||
expect(graphic.x).toBe(400);
|
||||
expect(graphic.y).toBeCloseTo(421.37, 1);
|
||||
test('returns "0" for non-donut without padding and not half', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
donut: false,
|
||||
chartPadding: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.top).toBe('0');
|
||||
});
|
||||
|
||||
test('adjusts top for donut with bottom padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
chartPadding: { top: 0, bottom: 60, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.top).toBe('45%');
|
||||
});
|
||||
|
||||
test('returns "0" for non-donut with bottom padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
donut: false,
|
||||
chartPadding: { top: 0, bottom: 60, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.top).toBe('0');
|
||||
});
|
||||
|
||||
test('adjusts top for donut with top padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
chartPadding: { top: 60, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.top).toBe('55%');
|
||||
});
|
||||
|
||||
test('adjusts top for non-donut with top padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
donut: false,
|
||||
chartPadding: { top: 60, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.top).toBe('10%');
|
||||
});
|
||||
|
||||
test('positions total at 68.5% for top half-donut without padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
sweptAngle: 180,
|
||||
chartPadding: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.top).toBe('68.5%');
|
||||
});
|
||||
|
||||
test('adjusts total position from 68.5% base for top half-donut with top padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
sweptAngle: 180,
|
||||
chartPadding: { top: 60, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.top).toBe('73.5%');
|
||||
});
|
||||
|
||||
test('returns "center" when no left/right padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
chartPadding: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.left).toBe('center');
|
||||
});
|
||||
|
||||
test('adjusts left for left padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
chartPadding: { top: 0, bottom: 0, left: 80, right: 0 },
|
||||
});
|
||||
expect(result.left).toBe('52.5%');
|
||||
});
|
||||
|
||||
test('adjusts left for right padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
chartPadding: { top: 0, bottom: 0, left: 0, right: 80 },
|
||||
});
|
||||
expect(result.left).toBe('42.5%');
|
||||
});
|
||||
|
||||
test('prioritizes right padding over left padding', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
chartPadding: { top: 0, bottom: 0, left: 80, right: 80 },
|
||||
});
|
||||
expect(result.left).toBe('42.5%');
|
||||
});
|
||||
|
||||
test('positions total inside the shifted center for left half-donut', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
startAngle: 270,
|
||||
sweptAngle: 180,
|
||||
chartPadding: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.left).toBe('55%');
|
||||
expect(result.top).toBe('50%');
|
||||
});
|
||||
|
||||
test('positions total inside the shifted center for right half-donut', () => {
|
||||
const result = getTotalValuePadding({
|
||||
...baseProps,
|
||||
startAngle: 90,
|
||||
sweptAngle: 180,
|
||||
chartPadding: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
});
|
||||
expect(result.left).toBe('35%');
|
||||
expect(result.top).toBe('50%');
|
||||
});
|
||||
|
||||
@@ -713,8 +713,8 @@ export function handleChartDataResponse(
|
||||
// Query is running asynchronously and we must await the results.
|
||||
// When status is 202, result contains async event data (job_id, channel_id, etc.)
|
||||
// which differs from QueryData. We cast through unknown to handle this safely.
|
||||
// The optional signal lets a caller abort the wait (Stop pressed, chart
|
||||
// superseded or unmounted), cancelling the job and avoiding leaked listeners.
|
||||
// The optional signal lets a caller (e.g. StatefulChart) cancel the wait
|
||||
// when its chart is superseded or unmounted, avoiding leaked listeners.
|
||||
if (useLegacyApi) {
|
||||
return waitForAsyncData(
|
||||
result[0] as unknown as Parameters<typeof waitForAsyncData>[0],
|
||||
@@ -786,25 +786,15 @@ export function exploreJSON(
|
||||
const [useLegacyApi] = getQuerySettings(formData);
|
||||
const chartDataRequestCaught = chartDataRequest
|
||||
.then(({ response, json }) =>
|
||||
handleChartDataResponse(
|
||||
response,
|
||||
json,
|
||||
useLegacyApi,
|
||||
controller.signal,
|
||||
),
|
||||
handleChartDataResponse(response, json, useLegacyApi),
|
||||
)
|
||||
.then(queriesResponse => {
|
||||
// Drop stale responses: if this request was aborted (Stop, or a newer
|
||||
// query that aborted ours), or a newer query has since replaced our
|
||||
// controller in state, ignore the result so we don't clobber newer
|
||||
// data or a 'stopped' status. Checking the signal is authoritative
|
||||
// because the reducer nulls out queryController when a query stops.
|
||||
// Drop stale responses: if a newer query has started for this chart,
|
||||
// its controller will have replaced ours in state, so ignore this
|
||||
// response to avoid clobbering newer data with older results.
|
||||
if (key != null) {
|
||||
const currentController = getState().charts?.[key]?.queryController;
|
||||
if (
|
||||
controller.signal.aborted ||
|
||||
(currentController != null && currentController !== controller)
|
||||
) {
|
||||
if (currentController && currentController !== controller) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -863,10 +853,7 @@ export function exploreJSON(
|
||||
// so a slow earlier request can't mark a newer one as failed.
|
||||
if (key != null) {
|
||||
const currentController = getState().charts?.[key]?.queryController;
|
||||
if (
|
||||
controller.signal.aborted ||
|
||||
(currentController != null && currentController !== controller)
|
||||
) {
|
||||
if (currentController && currentController !== controller) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,6 @@ export const URL_PARAMS = {
|
||||
name: 'show_filters',
|
||||
type: 'boolean',
|
||||
},
|
||||
showDownload: {
|
||||
name: 'show_download',
|
||||
type: 'boolean',
|
||||
},
|
||||
expandFilters: {
|
||||
name: 'expand_filters',
|
||||
type: 'boolean',
|
||||
|
||||
@@ -547,7 +547,7 @@ test('Clear All stages filter_select clear without dispatching until Apply', asy
|
||||
});
|
||||
expect(updateDataMaskSpy).toHaveBeenCalledWith(filterId, {
|
||||
id: filterId,
|
||||
filterState: { value: null, validateStatus: undefined },
|
||||
filterState: { value: undefined, validateStatus: undefined },
|
||||
extraFormData: {},
|
||||
});
|
||||
updateDataMaskSpy.mockRestore();
|
||||
@@ -685,95 +685,12 @@ test('Clear All + Apply only dispatches for filters present in dataMask', async
|
||||
expect(updateDataMaskSpy).toHaveBeenCalledTimes(1);
|
||||
expect(updateDataMaskSpy).toHaveBeenCalledWith(idInMask, {
|
||||
id: idInMask,
|
||||
filterState: { value: null, validateStatus: undefined },
|
||||
filterState: { value: undefined, validateStatus: undefined },
|
||||
extraFormData: {},
|
||||
});
|
||||
updateDataMaskSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('Clear All in horizontal bar does not re-apply default values', async () => {
|
||||
fetchMock.post(
|
||||
'glob:*/api/v1/chart/data',
|
||||
{
|
||||
result: [
|
||||
{
|
||||
data: [{ test_column: 'East' }, { test_column: 'West' }],
|
||||
colnames: ['test_column'],
|
||||
coltypes: [1],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: 'horizontal-clear-chart-data' },
|
||||
);
|
||||
const filterId = 'NATIVE_FILTER-horizontal-default';
|
||||
const updateDataMaskSpy = jest.spyOn(dataMaskActions, 'updateDataMask');
|
||||
const filterWithDefault = createFilter({
|
||||
id: filterId,
|
||||
name: 'Region',
|
||||
filterType: 'filter_select',
|
||||
targets: [{ datasetId: 7, column: { name: 'test_column' } }],
|
||||
defaultDataMask: {
|
||||
filterState: { value: ['East'] },
|
||||
extraFormData: {
|
||||
filters: [{ col: 'test_column', op: 'IN', val: ['East'] }],
|
||||
},
|
||||
},
|
||||
chartsInScope: [18],
|
||||
});
|
||||
const stateHorizontal = {
|
||||
...stateWithoutNativeFilters,
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
dash_edit_perm: true,
|
||||
filterBarOrientation: FilterBarOrientation.Horizontal,
|
||||
metadata: {
|
||||
native_filter_configuration: [filterWithDefault],
|
||||
chart_configuration: {},
|
||||
},
|
||||
},
|
||||
dashboardState: {
|
||||
...stateWithoutNativeFilters.dashboardState,
|
||||
activeTabs: ['ROOT_ID'],
|
||||
},
|
||||
dataMask: {
|
||||
[filterId]: createDataMask(filterId, ['East'], {
|
||||
filters: [{ col: 'test_column', op: 'IN', val: ['East'] }],
|
||||
}),
|
||||
},
|
||||
nativeFilters: {
|
||||
filters: { [filterId]: filterWithDefault },
|
||||
filtersState: {},
|
||||
},
|
||||
};
|
||||
|
||||
render(<FilterBar orientation={FilterBarOrientation.Horizontal} />, {
|
||||
initialState: stateHorizontal,
|
||||
useDnd: true,
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
});
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
const clearBtn = screen.getByTestId(getTestId('clear-button'));
|
||||
expect(clearBtn).not.toBeDisabled();
|
||||
await act(async () => {
|
||||
userEvent.click(clearBtn);
|
||||
});
|
||||
// Let the clear-all trigger round-trip through the filter plugin
|
||||
await act(async () => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
// The staged clear must survive the trigger completing: the default value
|
||||
// must not be re-applied and Apply must stay enabled
|
||||
expect(updateDataMaskSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTitle('East')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(getTestId('apply-button'))).not.toBeDisabled();
|
||||
updateDataMaskSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('FilterBar Clear All only clears in-scope filters, not out-of-scope ones', async () => {
|
||||
const inScopeFilterId = 'NATIVE_FILTER-in-scope';
|
||||
const outOfScopeRequiredFilterId = 'NATIVE_FILTER-out-of-scope-required';
|
||||
@@ -914,7 +831,7 @@ test('FilterBar Clear All only clears in-scope filters, not out-of-scope ones',
|
||||
|
||||
expect(updateDataMaskSpy).toHaveBeenCalledWith(inScopeFilterId, {
|
||||
id: inScopeFilterId,
|
||||
filterState: { value: null, validateStatus: undefined },
|
||||
filterState: { value: undefined, validateStatus: undefined },
|
||||
extraFormData: {},
|
||||
});
|
||||
|
||||
|
||||
@@ -517,21 +517,22 @@ const FilterBar: FC<FiltersBarProps> = ({
|
||||
// Only clear in-scope filters
|
||||
if (!inScopeFilterIds.has(id)) return;
|
||||
|
||||
// Cleared values stage as explicit null ([null, null] for ranges), never
|
||||
// undefined: the select plugin's init effect treats undefined as
|
||||
// "uninitialized" and would re-apply default values once the clear-all
|
||||
// trigger completes.
|
||||
const clearedValue = filterType === 'filter_range' ? [null, null] : null;
|
||||
// Range filters use [null, null] as the cleared value; others use undefined
|
||||
const clearedValue =
|
||||
filterType === 'filter_range' ? [null, null] : undefined;
|
||||
const isRequired = !!filter.controlValues?.enableEmptyFilter;
|
||||
if (dataMaskSelected[id]) {
|
||||
// Stage the cleared value locally; do NOT dispatch to Redux here.
|
||||
// Persistence happens when the user clicks Apply.
|
||||
setDataMaskSelected(draft => {
|
||||
if (draft[id].filterState?.value !== undefined) {
|
||||
draft[id].filterState!.value = clearedValue;
|
||||
}
|
||||
draft[id].extraFormData = {};
|
||||
const { filterState } = draft[id];
|
||||
if (filterState) {
|
||||
filterState.value = clearedValue;
|
||||
filterState.validateStatus = isRequired ? 'error' : undefined;
|
||||
if (draft[id].filterState) {
|
||||
draft[id].filterState!.validateStatus = isRequired
|
||||
? 'error'
|
||||
: undefined;
|
||||
}
|
||||
});
|
||||
newClearAllTriggers[id] = true;
|
||||
|
||||
@@ -31,15 +31,6 @@ import {
|
||||
import ChartContainerComponent from 'src/explore/components/ExploreChartPanel';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
|
||||
jest.mock('./StandaloneDownloadControl', () => ({
|
||||
__esModule: true,
|
||||
default: () => (
|
||||
<button type="button" data-test="standalone-download-button">
|
||||
Download
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
// Cast to accept partial mock props in tests
|
||||
const ChartContainer = ChartContainerComponent as unknown as React.FC<
|
||||
Record<string, any>
|
||||
@@ -84,10 +75,6 @@ const createProps = (overrides = {}) => ({
|
||||
describe('ChartContainer', () => {
|
||||
jest.setTimeout(10000);
|
||||
|
||||
afterEach(() => {
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
});
|
||||
|
||||
test('renders when vizType is line', () => {
|
||||
const props = createProps();
|
||||
expect(isValidElement(<ChartContainer {...props} />)).toBe(true);
|
||||
@@ -201,77 +188,4 @@ describe('ChartContainer', () => {
|
||||
expect(await screen.findByRole('timer')).toBeInTheDocument();
|
||||
expect(gutter).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('does not render standalone download control when show_download is absent', () => {
|
||||
const props = createProps({
|
||||
standalone: true,
|
||||
can_download: true,
|
||||
});
|
||||
|
||||
render(<ChartContainer {...props} />, { useRedux: true });
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('standalone-download-button'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render standalone download control when show_download is disabled', () => {
|
||||
window.history.replaceState({}, '', '?show_download=0');
|
||||
|
||||
const props = createProps({
|
||||
standalone: true,
|
||||
can_download: true,
|
||||
});
|
||||
|
||||
render(<ChartContainer {...props} />, { useRedux: true });
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('standalone-download-button'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders standalone download control when show_download is enabled and user can download', () => {
|
||||
window.history.replaceState({}, '', '?show_download=1');
|
||||
|
||||
const props = createProps({
|
||||
standalone: true,
|
||||
can_download: true,
|
||||
});
|
||||
|
||||
render(<ChartContainer {...props} />, { useRedux: true });
|
||||
|
||||
expect(
|
||||
screen.getByTestId('standalone-download-button'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render standalone download control when user cannot download', () => {
|
||||
window.history.replaceState({}, '', '?show_download=1');
|
||||
|
||||
const props = createProps({
|
||||
standalone: true,
|
||||
can_download: false,
|
||||
});
|
||||
|
||||
render(<ChartContainer {...props} />, { useRedux: true });
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('standalone-download-button'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render standalone download control outside standalone mode', () => {
|
||||
window.history.replaceState({}, '', '?show_download=1');
|
||||
|
||||
const props = createProps({
|
||||
standalone: false,
|
||||
can_download: true,
|
||||
});
|
||||
|
||||
render(<ChartContainer {...props} />, { useRedux: true });
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('standalone-download-button'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,147 +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 { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import { VizType } from '@superset-ui/core';
|
||||
import { ExportStatus } from 'src/components/StreamingExportModal/StreamingExportModal';
|
||||
import { useExploreDataExport } from '../useExploreAdditionalActionsMenu/useExploreDataExport';
|
||||
import StandaloneDownloadControl from './StandaloneDownloadControl';
|
||||
|
||||
jest.mock('../useExploreAdditionalActionsMenu/useExploreDataExport', () => ({
|
||||
useExploreDataExport: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseExploreDataExport = useExploreDataExport as jest.MockedFunction<
|
||||
typeof useExploreDataExport
|
||||
>;
|
||||
|
||||
const exportCSV = jest.fn();
|
||||
const exportJson = jest.fn();
|
||||
const exportExcel = jest.fn();
|
||||
|
||||
const streamingExportState = {
|
||||
isVisible: false,
|
||||
progress: {
|
||||
rowsProcessed: 0,
|
||||
totalRows: undefined,
|
||||
totalSize: 0,
|
||||
speed: 0,
|
||||
mbPerSecond: 0,
|
||||
elapsedTime: 0,
|
||||
status: ExportStatus.STREAMING,
|
||||
},
|
||||
onCancel: jest.fn(),
|
||||
onRetry: jest.fn(),
|
||||
onDownload: jest.fn(),
|
||||
};
|
||||
|
||||
const latestQueryFormData = {
|
||||
viz_type: VizType.Histogram,
|
||||
datasource: '49__table',
|
||||
};
|
||||
|
||||
describe('StandaloneDownloadControl', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockUseExploreDataExport.mockReturnValue({
|
||||
exportCSV,
|
||||
exportCSVPivoted: jest.fn(),
|
||||
exportJson,
|
||||
exportExcel,
|
||||
handleExportError: jest.fn(),
|
||||
streamingExportState,
|
||||
});
|
||||
});
|
||||
|
||||
test('renders the download button', () => {
|
||||
render(
|
||||
<StandaloneDownloadControl
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
canDownload
|
||||
/>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('standalone-download-button'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders export options when download button is clicked', async () => {
|
||||
render(
|
||||
<StandaloneDownloadControl
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
canDownload
|
||||
/>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('standalone-download-button'));
|
||||
|
||||
expect(await screen.findByText('Export to .CSV')).toBeInTheDocument();
|
||||
expect(screen.getByText('Export to .JSON')).toBeInTheDocument();
|
||||
expect(screen.getByText('Export to Excel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('exports CSV when CSV option is clicked', async () => {
|
||||
render(
|
||||
<StandaloneDownloadControl
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
canDownload
|
||||
/>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('standalone-download-button'));
|
||||
userEvent.click(await screen.findByText('Export to .CSV'));
|
||||
|
||||
expect(exportCSV).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('exports JSON when JSON option is clicked', async () => {
|
||||
render(
|
||||
<StandaloneDownloadControl
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
canDownload
|
||||
/>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('standalone-download-button'));
|
||||
userEvent.click(await screen.findByText('Export to .JSON'));
|
||||
|
||||
expect(exportJson).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('exports Excel when Excel option is clicked', async () => {
|
||||
render(
|
||||
<StandaloneDownloadControl
|
||||
latestQueryFormData={latestQueryFormData}
|
||||
canDownload
|
||||
/>,
|
||||
{ useRedux: true },
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('standalone-download-button'));
|
||||
userEvent.click(await screen.findByText('Export to Excel'));
|
||||
|
||||
expect(exportExcel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,108 +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 { useMemo } from 'react';
|
||||
import { JsonObject, LatestQueryFormData } from '@superset-ui/core';
|
||||
import { Button, Dropdown } from '@superset-ui/core/components';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { StreamingExportModal } from 'src/components/StreamingExportModal';
|
||||
import { Slice } from 'src/types/Chart';
|
||||
import { useExploreDataExport } from '../useExploreAdditionalActionsMenu/useExploreDataExport';
|
||||
|
||||
interface StandaloneDownloadControlProps {
|
||||
latestQueryFormData: LatestQueryFormData;
|
||||
canDownload: boolean;
|
||||
slice?: Slice;
|
||||
ownState?: JsonObject;
|
||||
}
|
||||
|
||||
const StandaloneDownloadControl = ({
|
||||
latestQueryFormData,
|
||||
canDownload,
|
||||
slice,
|
||||
ownState,
|
||||
}: StandaloneDownloadControlProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { exportCSV, exportJson, exportExcel, streamingExportState } =
|
||||
useExploreDataExport({
|
||||
latestQueryFormData,
|
||||
canDownloadCSV: canDownload,
|
||||
slice,
|
||||
ownState,
|
||||
});
|
||||
|
||||
const downloadMenuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'export_csv',
|
||||
label: t('Export to .CSV'),
|
||||
onClick: exportCSV,
|
||||
},
|
||||
{
|
||||
key: 'export_json',
|
||||
label: t('Export to .JSON'),
|
||||
onClick: exportJson,
|
||||
},
|
||||
{
|
||||
key: 'export_excel',
|
||||
label: t('Export to Excel'),
|
||||
onClick: exportExcel,
|
||||
},
|
||||
],
|
||||
[exportCSV, exportExcel, exportJson],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
menu={{
|
||||
selectable: false,
|
||||
items: downloadMenuItems,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
aria-label={t('Download')}
|
||||
data-test="standalone-download-button"
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: ${theme.sizeUnit * 2}px;
|
||||
right: ${theme.sizeUnit * 2}px;
|
||||
z-index: 1;
|
||||
`}
|
||||
>
|
||||
<Icons.DownloadOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
|
||||
<StreamingExportModal
|
||||
visible={streamingExportState.isVisible}
|
||||
onCancel={streamingExportState.onCancel}
|
||||
onRetry={streamingExportState.onRetry}
|
||||
onDownload={streamingExportState.onDownload}
|
||||
progress={streamingExportState.progress}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default StandaloneDownloadControl;
|
||||
@@ -32,8 +32,6 @@ import {
|
||||
getExtensionsRegistry,
|
||||
} from '@superset-ui/core';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import { css, styled, useTheme } from '@apache-superset/core/theme';
|
||||
import ChartContainer from 'src/components/Chart/ChartContainer';
|
||||
import { updateExploreChartState } from 'src/explore/actions/exploreActions';
|
||||
@@ -58,7 +56,6 @@ import { DataTablesPane } from '../DataTablesPane';
|
||||
import { ChartPills } from '../ChartPills';
|
||||
import { ExploreAlert } from '../ExploreAlert';
|
||||
import useResizeDetectorByObserver from './useResizeDetectorByObserver';
|
||||
import StandaloneDownloadControl from './StandaloneDownloadControl';
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
const DefaultHeader: React.FC<{ children?: React.ReactNode }> = ({
|
||||
@@ -180,7 +177,6 @@ const ExploreChartPanel = ({
|
||||
}: ExploreChartPanelProps) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const showDownload = getUrlParam(URL_PARAMS.showDownload);
|
||||
const gutterMargin = theme.sizeUnit * GUTTER_SIZE_FACTOR;
|
||||
const gutterHeight = theme.sizeUnit * GUTTER_SIZE_FACTOR;
|
||||
|
||||
@@ -556,23 +552,7 @@ const ExploreChartPanel = ({
|
||||
document.body.className += ` ${standaloneClass}`;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
id="app"
|
||||
data-test="standalone-app"
|
||||
css={css`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
{showDownload && canDownload && (
|
||||
<StandaloneDownloadControl
|
||||
latestQueryFormData={chart.latestQueryFormData}
|
||||
canDownload={canDownload}
|
||||
slice={slice}
|
||||
ownState={mergedOwnState}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div id="app" data-test="standalone-app">
|
||||
{standaloneChartBody}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from '@superset-ui/core/components';
|
||||
import { Menu, MenuProps } from '@superset-ui/core/components/Menu';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { DEFAULT_CSV_STREAMING_ROW_THRESHOLD } from 'src/constants';
|
||||
import { exportChart, getChartKey } from 'src/explore/exploreUtils';
|
||||
import downloadAsImage from 'src/utils/downloadAsImage';
|
||||
import downloadAsPdf from 'src/utils/downloadAsPdf';
|
||||
@@ -64,14 +65,16 @@ import {
|
||||
LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS,
|
||||
} from 'src/logger/LogUtils';
|
||||
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
|
||||
import { StreamingProgress } from 'src/components/StreamingExportModal';
|
||||
import {
|
||||
useStreamingExport,
|
||||
StreamingProgress,
|
||||
} from 'src/components/StreamingExportModal';
|
||||
import { Slice } from 'src/types/Chart';
|
||||
import { ChartState, ExplorePageInitialData } from 'src/explore/types';
|
||||
import { ReportObject } from 'src/features/reports/types';
|
||||
import ViewQueryModal from '../controls/ViewQueryModal';
|
||||
import EmbedCodeContent from '../EmbedCodeContent';
|
||||
import { useDashboardsMenuItems } from './DashboardsSubMenu';
|
||||
import { useExploreDataExport } from './useExploreDataExport';
|
||||
|
||||
export const SEARCH_THRESHOLD = 10;
|
||||
|
||||
@@ -314,6 +317,11 @@ export const useExploreAdditionalActionsMenu = (
|
||||
const chart = useSelector<ExploreState, ChartState | undefined>(state =>
|
||||
state.explore ? state.charts?.[getChartKey(state.explore)] : undefined,
|
||||
);
|
||||
const streamingThreshold = useSelector<ExploreState, number>(
|
||||
state =>
|
||||
state.common?.conf?.CSV_STREAMING_ROW_THRESHOLD ||
|
||||
DEFAULT_CSV_STREAMING_ROW_THRESHOLD,
|
||||
);
|
||||
const exploreChartState = useSelector<ExploreState, JsonObject | undefined>(
|
||||
state => {
|
||||
const chartKey = state.explore ? getChartKey(state.explore) : undefined;
|
||||
@@ -354,20 +362,32 @@ export const useExploreAdditionalActionsMenu = (
|
||||
);
|
||||
|
||||
// Streaming export state and handlers
|
||||
const [isStreamingModalVisible, setIsStreamingModalVisible] = useState(false);
|
||||
const {
|
||||
exportCSV,
|
||||
exportCSVPivoted,
|
||||
exportJson,
|
||||
exportExcel,
|
||||
handleExportError,
|
||||
streamingExportState,
|
||||
} = useExploreDataExport({
|
||||
latestQueryFormData,
|
||||
canDownloadCSV,
|
||||
slice,
|
||||
ownState,
|
||||
progress,
|
||||
isExporting: _isExporting,
|
||||
startExport,
|
||||
cancelExport: _cancelExport,
|
||||
resetExport,
|
||||
retryExport,
|
||||
} = useStreamingExport({
|
||||
onComplete: () => {
|
||||
// Don't show toast here - wait for user to click Download button
|
||||
},
|
||||
onError: () => {
|
||||
addDangerToast(t('Export failed - please try again'));
|
||||
},
|
||||
});
|
||||
|
||||
const handleCloseStreamingModal = useCallback(() => {
|
||||
setIsStreamingModalVisible(false);
|
||||
resetExport();
|
||||
}, [resetExport]);
|
||||
|
||||
const handleDownloadComplete = useCallback(() => {
|
||||
addSuccessToast(t('CSV file downloaded successfully'));
|
||||
}, [addSuccessToast]);
|
||||
|
||||
// Use the updated report menu items hook
|
||||
const reportMenuItem = useHeaderReportMenuItems({
|
||||
chart,
|
||||
@@ -419,6 +439,159 @@ export const useExploreAdditionalActionsMenu = (
|
||||
}
|
||||
}, [addDangerToast, latestQueryFormData, permalinkChartState]);
|
||||
|
||||
const handleExportError = useCallback(
|
||||
(error: unknown) => {
|
||||
const exportError = error as Error & {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
response?: { status?: number };
|
||||
};
|
||||
const status = exportError.status || exportError.response?.status;
|
||||
if (status === 413) {
|
||||
addDangerToast(
|
||||
t(
|
||||
'The chart data is too large to download. Please try reducing the date range, limiting rows, or using fewer columns.',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const errorMessage =
|
||||
exportError.message ||
|
||||
exportError.statusText ||
|
||||
t(
|
||||
'Failed to export chart data. Please try again or contact your administrator.',
|
||||
);
|
||||
addDangerToast(errorMessage);
|
||||
}
|
||||
},
|
||||
[addDangerToast],
|
||||
);
|
||||
|
||||
const exportCSV = useCallback(async () => {
|
||||
if (!canDownloadCSV) return null;
|
||||
|
||||
// Determine row count for streaming threshold check
|
||||
let actualRowCount;
|
||||
const isTableViz = latestQueryFormData?.viz_type === 'table';
|
||||
const queriesResponse = chart?.queriesResponse;
|
||||
|
||||
if (
|
||||
isTableViz &&
|
||||
queriesResponse &&
|
||||
queriesResponse.length > 1 &&
|
||||
queriesResponse[1]?.data?.[0]?.rowcount
|
||||
) {
|
||||
actualRowCount = queriesResponse[1].data[0].rowcount;
|
||||
} else if (queriesResponse && queriesResponse[0]?.sql_rowcount != null) {
|
||||
actualRowCount = queriesResponse[0].sql_rowcount;
|
||||
} else if (queriesResponse && queriesResponse[0]?.rowcount != null) {
|
||||
actualRowCount = queriesResponse[0].rowcount;
|
||||
} else {
|
||||
actualRowCount = latestQueryFormData?.row_limit;
|
||||
}
|
||||
|
||||
// Check if streaming should be used
|
||||
const shouldUseStreaming =
|
||||
actualRowCount && actualRowCount >= streamingThreshold;
|
||||
|
||||
let filename: string | undefined;
|
||||
if (shouldUseStreaming) {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().slice(0, 10);
|
||||
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
||||
const timestamp = `_${date}_${time}`;
|
||||
const chartName =
|
||||
slice?.slice_name || latestQueryFormData.viz_type || 'chart';
|
||||
const safeChartName = chartName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
filename = `${safeChartName}${timestamp}.csv`;
|
||||
}
|
||||
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'full',
|
||||
resultFormat: 'csv',
|
||||
onStartStreamingExport: shouldUseStreaming
|
||||
? exportParams => {
|
||||
if (exportParams.url) {
|
||||
setIsStreamingModalVisible(true);
|
||||
startExport({
|
||||
...exportParams,
|
||||
url: exportParams.url,
|
||||
filename,
|
||||
expectedRows: actualRowCount,
|
||||
exportType: exportParams.exportType as 'csv' | 'xlsx',
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
canDownloadCSV,
|
||||
latestQueryFormData,
|
||||
ownState,
|
||||
chart,
|
||||
streamingThreshold,
|
||||
slice,
|
||||
startExport,
|
||||
handleExportError,
|
||||
]);
|
||||
|
||||
const exportCSVPivoted = useCallback(async () => {
|
||||
if (!canDownloadCSV) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'post_processed',
|
||||
resultFormat: 'csv',
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
return null;
|
||||
}, [canDownloadCSV, latestQueryFormData, ownState, handleExportError]);
|
||||
|
||||
const exportJson = useCallback(async () => {
|
||||
if (!canDownloadCSV) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'results',
|
||||
resultFormat: 'json',
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
return null;
|
||||
}, [canDownloadCSV, latestQueryFormData, ownState, handleExportError]);
|
||||
|
||||
const exportExcel = useCallback(async () => {
|
||||
if (!canDownloadCSV) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'results',
|
||||
resultFormat: 'xlsx',
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
return null;
|
||||
}, [canDownloadCSV, latestQueryFormData, ownState, handleExportError]);
|
||||
|
||||
const copyLink = useCallback(async () => {
|
||||
try {
|
||||
if (!latestQueryFormData?.datasource) {
|
||||
@@ -1073,5 +1246,14 @@ export const useExploreAdditionalActionsMenu = (
|
||||
canExportImage,
|
||||
]);
|
||||
|
||||
// Return streaming modal state and handlers for parent to render
|
||||
const streamingExportState = {
|
||||
isVisible: isStreamingModalVisible,
|
||||
progress,
|
||||
onCancel: handleCloseStreamingModal,
|
||||
onRetry: retryExport,
|
||||
onDownload: handleDownloadComplete,
|
||||
};
|
||||
|
||||
return [menu, isDropdownVisible, setIsDropdownVisible, streamingExportState];
|
||||
};
|
||||
|
||||
@@ -1,223 +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 { act, renderHook } from '@testing-library/react';
|
||||
import * as exploreUtils from 'src/explore/exploreUtils';
|
||||
import { useExploreDataExport } from './useExploreDataExport';
|
||||
|
||||
const mockAddDangerToast = jest.fn();
|
||||
const mockAddSuccessToast = jest.fn();
|
||||
const mockStartExport = jest.fn();
|
||||
const mockResetExport = jest.fn();
|
||||
const mockRetryExport = jest.fn();
|
||||
|
||||
jest.mock('src/components/MessageToasts/withToasts', () => ({
|
||||
useToasts: () => ({
|
||||
addDangerToast: mockAddDangerToast,
|
||||
addSuccessToast: mockAddSuccessToast,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('src/components/StreamingExportModal', () => ({
|
||||
useStreamingExport: jest.fn(() => ({
|
||||
progress: {
|
||||
rowsProcessed: 0,
|
||||
totalRows: undefined,
|
||||
totalSize: 0,
|
||||
speed: 0,
|
||||
mbPerSecond: 0,
|
||||
elapsedTime: 0,
|
||||
status: 'streaming',
|
||||
},
|
||||
startExport: mockStartExport,
|
||||
resetExport: mockResetExport,
|
||||
retryExport: mockRetryExport,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('src/explore/exploreUtils', () => ({
|
||||
__esModule: true,
|
||||
...jest.requireActual('src/explore/exploreUtils'),
|
||||
exportChart: jest.fn(),
|
||||
getChartKey: jest.fn(() => 1),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(callback =>
|
||||
callback({
|
||||
charts: {
|
||||
1: {
|
||||
queriesResponse: [{ sql_rowcount: 10 }],
|
||||
},
|
||||
},
|
||||
explore: {
|
||||
slice: {
|
||||
slice_id: 1,
|
||||
slice_name: 'Test Chart',
|
||||
},
|
||||
},
|
||||
common: {
|
||||
conf: {
|
||||
CSV_STREAMING_ROW_THRESHOLD: 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
const mockExportChart = exploreUtils.exportChart as jest.Mock;
|
||||
|
||||
const defaultProps = {
|
||||
latestQueryFormData: {
|
||||
datasource: '1__table',
|
||||
viz_type: 'table',
|
||||
row_limit: 10000,
|
||||
} as any,
|
||||
canDownloadCSV: true,
|
||||
slice: {
|
||||
slice_id: 1,
|
||||
slice_name: 'Test Chart',
|
||||
} as any,
|
||||
ownState: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockExportChart.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
test('exports CSV with full result type', async () => {
|
||||
const { result } = renderHook(() => useExploreDataExport(defaultProps));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCSV();
|
||||
});
|
||||
|
||||
expect(mockExportChart).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
formData: defaultProps.latestQueryFormData,
|
||||
ownState: defaultProps.ownState,
|
||||
resultType: 'full',
|
||||
resultFormat: 'csv',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('exports pivoted CSV with post processed result type', async () => {
|
||||
const { result } = renderHook(() => useExploreDataExport(defaultProps));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCSVPivoted();
|
||||
});
|
||||
|
||||
expect(mockExportChart).toHaveBeenCalledWith({
|
||||
formData: defaultProps.latestQueryFormData,
|
||||
ownState: defaultProps.ownState,
|
||||
resultType: 'post_processed',
|
||||
resultFormat: 'csv',
|
||||
});
|
||||
});
|
||||
|
||||
test('exports JSON with results result type', async () => {
|
||||
const { result } = renderHook(() => useExploreDataExport(defaultProps));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportJson();
|
||||
});
|
||||
|
||||
expect(mockExportChart).toHaveBeenCalledWith({
|
||||
formData: defaultProps.latestQueryFormData,
|
||||
ownState: defaultProps.ownState,
|
||||
resultType: 'results',
|
||||
resultFormat: 'json',
|
||||
});
|
||||
});
|
||||
|
||||
test('exports Excel with results result type', async () => {
|
||||
const { result } = renderHook(() => useExploreDataExport(defaultProps));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportExcel();
|
||||
});
|
||||
|
||||
expect(mockExportChart).toHaveBeenCalledWith({
|
||||
formData: defaultProps.latestQueryFormData,
|
||||
ownState: defaultProps.ownState,
|
||||
resultType: 'results',
|
||||
resultFormat: 'xlsx',
|
||||
});
|
||||
});
|
||||
|
||||
test('does not export when downloads are disabled', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useExploreDataExport({
|
||||
...defaultProps,
|
||||
canDownloadCSV: false,
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCSV();
|
||||
await result.current.exportCSVPivoted();
|
||||
await result.current.exportJson();
|
||||
await result.current.exportExcel();
|
||||
});
|
||||
|
||||
expect(mockExportChart).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('shows large data error toast when export fails with status 413', async () => {
|
||||
mockExportChart.mockRejectedValue({
|
||||
status: 413,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useExploreDataExport(defaultProps));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportCSV();
|
||||
});
|
||||
|
||||
expect(mockAddDangerToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining('too large to download'),
|
||||
);
|
||||
});
|
||||
|
||||
test('shows export error message when export fails', async () => {
|
||||
mockExportChart.mockRejectedValue(new Error('Export exploded'));
|
||||
|
||||
const { result } = renderHook(() => useExploreDataExport(defaultProps));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportJson();
|
||||
});
|
||||
|
||||
expect(mockAddDangerToast).toHaveBeenCalledWith('Export exploded');
|
||||
});
|
||||
|
||||
test('exposes streaming export modal state and handlers', () => {
|
||||
const { result } = renderHook(() => useExploreDataExport(defaultProps));
|
||||
|
||||
expect(result.current.streamingExportState).toEqual(
|
||||
expect.objectContaining({
|
||||
isVisible: false,
|
||||
onRetry: mockRetryExport,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1,275 +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 { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
JsonObject,
|
||||
LatestQueryFormData,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import {
|
||||
StreamingProgress,
|
||||
useStreamingExport,
|
||||
} from 'src/components/StreamingExportModal';
|
||||
import { DEFAULT_CSV_STREAMING_ROW_THRESHOLD } from 'src/constants';
|
||||
import { exportChart, getChartKey } from 'src/explore/exploreUtils';
|
||||
import { ChartState } from 'src/explore/types';
|
||||
import { Slice } from 'src/types/Chart';
|
||||
|
||||
interface ExploreSlice {
|
||||
slice?: Slice | null;
|
||||
}
|
||||
|
||||
interface ExploreState {
|
||||
charts?: Record<number, ChartState>;
|
||||
explore?: ExploreSlice;
|
||||
common?: {
|
||||
conf?: {
|
||||
CSV_STREAMING_ROW_THRESHOLD?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface StreamingExportState {
|
||||
isVisible: boolean;
|
||||
progress: StreamingProgress;
|
||||
onCancel: () => void;
|
||||
onRetry: () => void;
|
||||
onDownload: () => void;
|
||||
}
|
||||
|
||||
interface UseExploreDataExportProps {
|
||||
latestQueryFormData: LatestQueryFormData;
|
||||
canDownloadCSV: boolean;
|
||||
slice?: Slice | null;
|
||||
ownState?: JsonObject;
|
||||
}
|
||||
|
||||
export const useExploreDataExport = ({
|
||||
latestQueryFormData,
|
||||
canDownloadCSV,
|
||||
slice,
|
||||
ownState,
|
||||
}: UseExploreDataExportProps) => {
|
||||
const { addDangerToast, addSuccessToast } = useToasts();
|
||||
|
||||
const chart = useSelector<ExploreState, ChartState | undefined>(state =>
|
||||
state.explore ? state.charts?.[getChartKey(state.explore)] : undefined,
|
||||
);
|
||||
|
||||
const streamingThreshold = useSelector<ExploreState, number>(
|
||||
state =>
|
||||
state.common?.conf?.CSV_STREAMING_ROW_THRESHOLD ||
|
||||
DEFAULT_CSV_STREAMING_ROW_THRESHOLD,
|
||||
);
|
||||
|
||||
const [isStreamingModalVisible, setIsStreamingModalVisible] = useState(false);
|
||||
|
||||
const { progress, startExport, resetExport, retryExport } =
|
||||
useStreamingExport({
|
||||
onComplete: () => {
|
||||
// Wait for the user to click Download before showing a success toast.
|
||||
},
|
||||
onError: () => {
|
||||
addDangerToast(t('Export failed - please try again'));
|
||||
},
|
||||
});
|
||||
|
||||
const handleCloseStreamingModal = useCallback(() => {
|
||||
setIsStreamingModalVisible(false);
|
||||
resetExport();
|
||||
}, [resetExport]);
|
||||
|
||||
const handleDownloadComplete = useCallback(() => {
|
||||
addSuccessToast(t('CSV file downloaded successfully'));
|
||||
}, [addSuccessToast]);
|
||||
|
||||
const handleExportError = useCallback(
|
||||
(error: unknown) => {
|
||||
const exportError = error as Error & {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
response?: { status?: number };
|
||||
};
|
||||
const status = exportError.status || exportError.response?.status;
|
||||
|
||||
if (status === 413) {
|
||||
addDangerToast(
|
||||
t(
|
||||
'The chart data is too large to download. Please try reducing the date range, limiting rows, or using fewer columns.',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const errorMessage =
|
||||
exportError.message ||
|
||||
exportError.statusText ||
|
||||
t(
|
||||
'Failed to export chart data. Please try again or contact your administrator.',
|
||||
);
|
||||
addDangerToast(errorMessage);
|
||||
}
|
||||
},
|
||||
[addDangerToast],
|
||||
);
|
||||
|
||||
const exportCSV = useCallback(async () => {
|
||||
if (!canDownloadCSV) return null;
|
||||
|
||||
let actualRowCount;
|
||||
const isTableViz = latestQueryFormData?.viz_type === 'table';
|
||||
const queriesResponse = chart?.queriesResponse;
|
||||
|
||||
if (
|
||||
isTableViz &&
|
||||
queriesResponse &&
|
||||
queriesResponse.length > 1 &&
|
||||
queriesResponse[1]?.data?.[0]?.rowcount
|
||||
) {
|
||||
actualRowCount = queriesResponse[1].data[0].rowcount;
|
||||
} else if (queriesResponse && queriesResponse[0]?.sql_rowcount != null) {
|
||||
actualRowCount = queriesResponse[0].sql_rowcount;
|
||||
} else if (queriesResponse && queriesResponse[0]?.rowcount != null) {
|
||||
actualRowCount = queriesResponse[0].rowcount;
|
||||
} else {
|
||||
actualRowCount = latestQueryFormData?.row_limit;
|
||||
}
|
||||
|
||||
const shouldUseStreaming =
|
||||
actualRowCount && actualRowCount >= streamingThreshold;
|
||||
|
||||
let filename: string | undefined;
|
||||
if (shouldUseStreaming) {
|
||||
const now = new Date();
|
||||
const date = now.toISOString().slice(0, 10);
|
||||
const time = now.toISOString().slice(11, 19).replace(/:/g, '');
|
||||
const timestamp = `_${date}_${time}`;
|
||||
const chartName =
|
||||
slice?.slice_name || latestQueryFormData.viz_type || 'chart';
|
||||
const safeChartName = chartName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
filename = `${safeChartName}${timestamp}.csv`;
|
||||
}
|
||||
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'full',
|
||||
resultFormat: 'csv',
|
||||
onStartStreamingExport: shouldUseStreaming
|
||||
? exportParams => {
|
||||
if (exportParams.url) {
|
||||
setIsStreamingModalVisible(true);
|
||||
startExport({
|
||||
...exportParams,
|
||||
url: exportParams.url,
|
||||
filename,
|
||||
expectedRows: actualRowCount,
|
||||
exportType: exportParams.exportType as 'csv' | 'xlsx',
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [
|
||||
canDownloadCSV,
|
||||
latestQueryFormData,
|
||||
ownState,
|
||||
chart,
|
||||
streamingThreshold,
|
||||
slice,
|
||||
startExport,
|
||||
handleExportError,
|
||||
]);
|
||||
|
||||
const exportCSVPivoted = useCallback(async () => {
|
||||
if (!canDownloadCSV) return null;
|
||||
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'post_processed',
|
||||
resultFormat: 'csv',
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [canDownloadCSV, latestQueryFormData, ownState, handleExportError]);
|
||||
|
||||
const exportJson = useCallback(async () => {
|
||||
if (!canDownloadCSV) return null;
|
||||
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'results',
|
||||
resultFormat: 'json',
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [canDownloadCSV, latestQueryFormData, ownState, handleExportError]);
|
||||
|
||||
const exportExcel = useCallback(async () => {
|
||||
if (!canDownloadCSV) return null;
|
||||
|
||||
try {
|
||||
await exportChart({
|
||||
formData: latestQueryFormData as QueryFormData,
|
||||
ownState,
|
||||
resultType: 'results',
|
||||
resultFormat: 'xlsx',
|
||||
});
|
||||
} catch (error) {
|
||||
handleExportError(error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [canDownloadCSV, latestQueryFormData, ownState, handleExportError]);
|
||||
|
||||
const streamingExportState: StreamingExportState = {
|
||||
isVisible: isStreamingModalVisible,
|
||||
progress,
|
||||
onCancel: handleCloseStreamingModal,
|
||||
onRetry: retryExport,
|
||||
onDownload: handleDownloadComplete,
|
||||
};
|
||||
|
||||
return {
|
||||
exportCSV,
|
||||
exportCSVPivoted,
|
||||
exportJson,
|
||||
exportExcel,
|
||||
handleExportError,
|
||||
streamingExportState,
|
||||
};
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { AppSection, type ChartProps } from '@superset-ui/core';
|
||||
import { AppSection } from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
@@ -443,35 +443,6 @@ describe('RangeFilterPlugin', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('clears a filter still sitting at its default value', () => {
|
||||
const renderWith = (value: [number | null, number | null]) => (
|
||||
<RangeFilterPlugin
|
||||
{...(transformProps({
|
||||
...rangeProps,
|
||||
filterState: { value },
|
||||
} as unknown as ChartProps) as PluginFilterRangeProps)}
|
||||
setDataMask={setDataMask}
|
||||
/>
|
||||
);
|
||||
|
||||
// The filter loads at its default and the user never touches it.
|
||||
const { rerender } = render(renderWith([10, 70]));
|
||||
setDataMask.mockClear();
|
||||
|
||||
// Clear All stages [null, null] for range filters.
|
||||
rerender(renderWith([null, null]));
|
||||
|
||||
expect(setDataMask).toHaveBeenCalledWith({
|
||||
extraFormData: {},
|
||||
filterState: {
|
||||
value: [null, null],
|
||||
label: '',
|
||||
validateStatus: undefined,
|
||||
validateMessage: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('calculateStep returns ~100 steps for integer ranges', () => {
|
||||
|
||||
@@ -363,14 +363,8 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear all case. The filter bar stages [null, null] for range filters, so
|
||||
// matching only undefined let a filter still sitting at its default fall
|
||||
// through to the default-restoring branch below.
|
||||
if (
|
||||
(filterState.value === undefined ||
|
||||
isEqualArray(filterState.value, [null, null])) &&
|
||||
!filterState.validateStatus
|
||||
) {
|
||||
// Clear all case
|
||||
if (filterState.value === undefined && !filterState.validateStatus) {
|
||||
setInputValue([null, null]);
|
||||
updateDataMaskValue([null, null]);
|
||||
return;
|
||||
|
||||
@@ -134,44 +134,6 @@ describe('asyncEvent middleware', () => {
|
||||
expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('rejects with an AbortError and cancels the job when the signal aborts', async () => {
|
||||
const CANCEL_ENDPOINT = 'glob:*/api/v1/async_event/*/cancel';
|
||||
fetchMock.post(CANCEL_ENDPOINT, { status: 200, body: {} });
|
||||
|
||||
const controller = new AbortController();
|
||||
const promise = asyncEvent.waitForAsyncData(
|
||||
asyncPendingEvent,
|
||||
controller.signal,
|
||||
);
|
||||
controller.abort();
|
||||
|
||||
let error: any = null;
|
||||
try {
|
||||
await promise;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
expect(error?.name).toBe('AbortError');
|
||||
// The cancel POST is fire-and-forget; let its microtask flush.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
expect(fetchMock.callHistory.calls(CANCEL_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('rejects immediately when given an already-aborted signal', async () => {
|
||||
const CANCEL_ENDPOINT = 'glob:*/api/v1/async_event/*/cancel';
|
||||
fetchMock.post(CANCEL_ENDPOINT, { status: 200, body: {} });
|
||||
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
|
||||
await expect(
|
||||
asyncEvent.waitForAsyncData(asyncPendingEvent, controller.signal),
|
||||
).rejects.toMatchObject({ name: 'AbortError' });
|
||||
// The cancel POST is fire-and-forget; let its microtask flush.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
expect(fetchMock.callHistory.calls(CANCEL_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('rejects on event error status', async () => {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get(EVENTS_ENDPOINT, {
|
||||
|
||||
@@ -104,17 +104,6 @@ const fetchCachedData = async (
|
||||
return { status, data };
|
||||
};
|
||||
|
||||
const cancelAsyncJob = (jobId: string) => {
|
||||
// Best-effort server-side cancel; the request stops the running Celery task
|
||||
// so it no longer consumes warehouse resources. Failures are non-fatal: the
|
||||
// client has already stopped waiting on the job.
|
||||
SupersetClient.post({
|
||||
endpoint: `/api/v1/async_event/${jobId}/cancel`,
|
||||
}).catch(error => {
|
||||
logging.warn('Failed to cancel async job', jobId, error);
|
||||
});
|
||||
};
|
||||
|
||||
export const waitForAsyncData = async (
|
||||
asyncResponse: AsyncEvent,
|
||||
signal?: AbortSignal,
|
||||
@@ -133,7 +122,6 @@ export const waitForAsyncData = async (
|
||||
// Bail immediately if the caller has already aborted (e.g. the chart was
|
||||
// unmounted before the job started), avoiding a leaked listener.
|
||||
if (signal?.aborted) {
|
||||
cancelAsyncJob(jobId);
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
return;
|
||||
}
|
||||
@@ -173,13 +161,11 @@ export const waitForAsyncData = async (
|
||||
}
|
||||
};
|
||||
|
||||
// When the caller aborts (Stop pressed, chart superseded/unmounted), stop
|
||||
// listening so the listener and its retained closure don't leak, and ask the
|
||||
// server to cancel the job so it stops consuming warehouse resources.
|
||||
// When the caller aborts (chart superseded/unmounted), stop listening so the
|
||||
// listener and its retained closure don't leak and keep the poller busy.
|
||||
if (signal) {
|
||||
onAbort = () => {
|
||||
cleanup();
|
||||
cancelAsyncJob(jobId);
|
||||
reject(new DOMException('Aborted', 'AbortError'));
|
||||
};
|
||||
signal.addEventListener('abort', onAbort, { once: true });
|
||||
|
||||
679
superset-websocket/package-lock.json
generated
679
superset-websocket/package-lock.json
generated
@@ -24,15 +24,15 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^10.7.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier": "^3.9.5",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
@@ -402,6 +402,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -419,6 +422,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -436,6 +442,9 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -453,6 +462,9 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -470,6 +482,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -487,6 +502,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -723,17 +741,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
|
||||
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/type-utils": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/type-utils": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -746,176 +764,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
|
||||
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
|
||||
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
|
||||
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
|
||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -930,145 +794,15 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
|
||||
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
||||
"@typescript-eslint/types": "^8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1083,14 +817,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
|
||||
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1101,9 +835,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1118,15 +852,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -1142,164 +876,10 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
|
||||
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
|
||||
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1311,16 +891,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
|
||||
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"@typescript-eslint/project-service": "8.64.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -1339,16 +919,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
|
||||
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
|
||||
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0"
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1363,13 +943,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
|
||||
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2495,6 +2075,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2516,6 +2099,9 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2537,6 +2123,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2558,6 +2147,9 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2919,9 +2511,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||
"version": "3.9.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz",
|
||||
"integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -3269,95 +2861,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz",
|
||||
"integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.65.0",
|
||||
"@typescript-eslint/parser": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
|
||||
"integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/type-utils": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.64.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
|
||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.64.0",
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.64.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.64.0",
|
||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
||||
"@typescript-eslint/utils": "8.64.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
"@typescript-eslint/utils": "8.64.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@@ -33,15 +33,15 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.65.0",
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^10.7.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.9.6",
|
||||
"prettier": "^3.9.5",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -15,19 +15,14 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from flask import request, Response
|
||||
from flask_appbuilder import expose
|
||||
from flask_appbuilder.api import safe
|
||||
from flask_appbuilder.security.decorators import permission_name, protect
|
||||
|
||||
from superset.async_events.async_query_manager import (
|
||||
AsyncQueryJobException,
|
||||
AsyncQueryTokenException,
|
||||
)
|
||||
from superset.async_events.async_query_manager import AsyncQueryTokenException
|
||||
from superset.extensions import async_query_manager, event_logger
|
||||
from superset.utils.core import get_user_id
|
||||
from superset.views.base_api import BaseSupersetApi, statsd_metrics
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -104,76 +99,3 @@ class AsyncEventsRestApi(BaseSupersetApi):
|
||||
return self.response_401()
|
||||
|
||||
return self.response(200, result=events)
|
||||
|
||||
@expose("/<job_id>/cancel", methods=("POST",))
|
||||
@event_logger.log_this
|
||||
@protect()
|
||||
@safe
|
||||
@statsd_metrics
|
||||
@permission_name("cancel")
|
||||
def cancel(self, job_id: str) -> Response:
|
||||
"""Cancel a running async query job.
|
||||
---
|
||||
post:
|
||||
summary: Cancel a running async query job
|
||||
description: >-
|
||||
Revokes the Celery task backing an in-flight async query. The
|
||||
caller is authorized against the job's original owner (channel and
|
||||
user), both resolved server-side from the request, so a client
|
||||
cannot cancel a job it did not submit.
|
||||
parameters:
|
||||
- in: path
|
||||
name: job_id
|
||||
required: true
|
||||
description: The job ID returned when the async query was submitted
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Job cancelled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: object
|
||||
properties:
|
||||
job_id:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
400:
|
||||
$ref: '#/components/responses/400'
|
||||
401:
|
||||
$ref: '#/components/responses/401'
|
||||
403:
|
||||
$ref: '#/components/responses/403'
|
||||
404:
|
||||
$ref: '#/components/responses/404'
|
||||
500:
|
||||
$ref: '#/components/responses/500'
|
||||
"""
|
||||
try:
|
||||
uuid.UUID(job_id)
|
||||
except ValueError:
|
||||
return self.response_400(message="Invalid job ID")
|
||||
|
||||
try:
|
||||
async_channel_id = async_query_manager.parse_channel_id_from_request(
|
||||
request
|
||||
)
|
||||
except AsyncQueryTokenException:
|
||||
return self.response_401()
|
||||
|
||||
try:
|
||||
async_query_manager.cancel_job(job_id, async_channel_id, get_user_id())
|
||||
except AsyncQueryTokenException:
|
||||
return self.response_403()
|
||||
except AsyncQueryJobException:
|
||||
return self.response_404()
|
||||
|
||||
return self.response(
|
||||
200,
|
||||
result={"job_id": job_id, "status": async_query_manager.STATUS_CANCELLED},
|
||||
)
|
||||
|
||||
@@ -106,10 +106,6 @@ class AsyncQueryManager:
|
||||
STATUS_RUNNING = "running"
|
||||
STATUS_ERROR = "error"
|
||||
STATUS_DONE = "done"
|
||||
STATUS_CANCELLED = "cancelled"
|
||||
# Redis key prefix (within the GAQ stream namespace) for the per-job record
|
||||
# that authorizes cancellation and flags a job as cancelled for the worker.
|
||||
_JOB_REGISTRY_PREFIX = "job-cancel:"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -285,32 +281,10 @@ class AsyncQueryManager:
|
||||
|
||||
def init_job(self, channel_id: str, user_id: Optional[int]) -> dict[str, Any]:
|
||||
job_id = str(uuid.uuid4())
|
||||
self._register_cancellable_job(job_id, channel_id, user_id)
|
||||
return build_job_metadata(
|
||||
channel_id, job_id, user_id, status=self.STATUS_PENDING
|
||||
)
|
||||
|
||||
def _job_registry_key(self, job_id: str) -> str:
|
||||
return f"{self._stream_prefix}{self._JOB_REGISTRY_PREFIX}{job_id}"
|
||||
|
||||
def _register_cancellable_job(
|
||||
self, job_id: str, channel_id: str, user_id: Optional[int]
|
||||
) -> None:
|
||||
"""
|
||||
Persist the identity a later cancel request must match. Keyed by
|
||||
``job_id`` (also the Celery task id — see ``submit_chart_data_job``) so
|
||||
the cancel endpoint can authorize the caller against the job's original
|
||||
owner without trusting the client-supplied id. Expires with the JWT so
|
||||
it never outlives the job it guards.
|
||||
"""
|
||||
if not self._cache:
|
||||
return
|
||||
self._cache.set(
|
||||
self._job_registry_key(job_id),
|
||||
json.dumps({"channel_id": channel_id, "user_id": user_id}),
|
||||
ex=self._jwt_expiration_seconds or None,
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def submit_explore_json_job(
|
||||
self,
|
||||
@@ -333,9 +307,6 @@ class AsyncQueryManager:
|
||||
response_type,
|
||||
force,
|
||||
],
|
||||
# Use job_id as the Celery task id so the cancel endpoint can revoke
|
||||
# the running task by the id the client already holds.
|
||||
task_id=job_metadata["job_id"],
|
||||
expires=self._jwt_expiration_seconds,
|
||||
)
|
||||
return job_metadata
|
||||
@@ -361,9 +332,6 @@ class AsyncQueryManager:
|
||||
else job_metadata,
|
||||
form_data,
|
||||
],
|
||||
# Use job_id as the Celery task id so the cancel endpoint can revoke
|
||||
# the running task by the id the client already holds.
|
||||
task_id=job_metadata["job_id"],
|
||||
expires=self._jwt_expiration_seconds,
|
||||
)
|
||||
return job_metadata
|
||||
@@ -415,88 +383,5 @@ class AsyncQueryManager:
|
||||
logger.debug("********** logging event data to stream %s", scoped_stream_name)
|
||||
logger.debug(event_data)
|
||||
|
||||
# Drop the cancel record before announcing the result, so a cancel that
|
||||
# arrives once the job is done finds nothing to flag and is reported as
|
||||
# such instead of contradicting the event below. A cancelled job keeps
|
||||
# its record until the TTL: the worker still has to recognize the
|
||||
# SIGUSR1 it is about to receive as a cancellation.
|
||||
if status in (self.STATUS_DONE, self.STATUS_ERROR):
|
||||
if job_id := job_metadata.get("job_id"):
|
||||
self._cache.delete(self._job_registry_key(job_id))
|
||||
|
||||
self._cache.xadd(scoped_stream_name, event_data, "*", self._stream_limit)
|
||||
self._cache.xadd(full_stream_name, event_data, "*", self._stream_limit_firehose)
|
||||
|
||||
def is_job_cancelled(self, job_id: str) -> bool:
|
||||
"""
|
||||
Whether ``cancel_job`` has flagged this job for cancellation.
|
||||
|
||||
Called from the worker's exception handler, so any cache failure is
|
||||
swallowed and treated as "not cancelled" — a Redis blip must never mask
|
||||
the original error (e.g. a genuine timeout) with a connection error.
|
||||
"""
|
||||
if not self._cache:
|
||||
return False
|
||||
try:
|
||||
raw = self._cache.get(self._job_registry_key(job_id))
|
||||
if raw is None:
|
||||
return False
|
||||
return bool(json.loads(raw).get("cancelled"))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.warning(
|
||||
"Failed to read cancellation flag for job %s", job_id, exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
def cancel_job(self, job_id: str, channel_id: str, user_id: Optional[int]) -> None:
|
||||
"""
|
||||
Authorize and cancel a running async job.
|
||||
|
||||
The caller's ``channel_id`` and ``user_id`` (resolved server-side from
|
||||
the request, never taken from the client) must match the job's original
|
||||
owner. The terminal ``STATUS_CANCELLED`` event is emitted here rather
|
||||
than by the worker, which never runs for a task revoked while it was
|
||||
still queued; the worker only logs the cancellation it is told about
|
||||
through the flag, so a job still gets exactly one terminal event.
|
||||
|
||||
:raises AsyncQueryJobException: the job is unknown or already terminal
|
||||
:raises AsyncQueryTokenException: the caller does not own the job
|
||||
"""
|
||||
if not self._cache:
|
||||
raise CacheBackendNotInitialized("Cache backend not initialized")
|
||||
|
||||
key = self._job_registry_key(job_id)
|
||||
raw = self._cache.get(key)
|
||||
if raw is None:
|
||||
raise AsyncQueryJobException("Job not found or already completed")
|
||||
|
||||
record = json.loads(raw)
|
||||
if record.get("channel_id") != channel_id or record.get("user_id") != user_id:
|
||||
raise AsyncQueryTokenException("Not authorized to cancel this job")
|
||||
|
||||
# Flag before revoking so the worker's timeout handler, which may fire
|
||||
# almost immediately, reliably sees the cancellation. Write only if the
|
||||
# key still exists (``xx``): if the job finished and cleared its record
|
||||
# between the read above and here, don't recreate a stale record or
|
||||
# revoke a task that is already gone — report it as not found instead.
|
||||
flagged = self._cache.set(
|
||||
key,
|
||||
json.dumps({**record, "cancelled": True}),
|
||||
ex=self._jwt_expiration_seconds or None,
|
||||
xx=True,
|
||||
)
|
||||
if not flagged:
|
||||
raise AsyncQueryJobException("Job not found or already completed")
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from superset.extensions import celery_app
|
||||
|
||||
# SIGUSR1 raises SoftTimeLimitExceeded inside the running task rather
|
||||
# than hard-killing the process, so it unwinds through the task's
|
||||
# exception handling instead of dying mid-query.
|
||||
celery_app.control.revoke(job_id, terminate=True, signal="SIGUSR1")
|
||||
|
||||
self.update_job(
|
||||
build_job_metadata(channel_id, job_id, user_id),
|
||||
self.STATUS_CANCELLED,
|
||||
)
|
||||
|
||||
@@ -99,15 +99,6 @@ class RedisCacheBackend(RedisCache):
|
||||
"""
|
||||
return self._cache.set(name, value, ex=ex, px=px, nx=nx, xx=xx)
|
||||
|
||||
def get(self, name: str) -> Any:
|
||||
"""
|
||||
Get the raw value at key ``name``.
|
||||
|
||||
:param name: Key name
|
||||
:returns: The stored value (bytes), or None if the key is absent
|
||||
"""
|
||||
return self._cache.get(name)
|
||||
|
||||
def delete(self, *names: str) -> int:
|
||||
"""
|
||||
Delete one or more keys.
|
||||
@@ -292,15 +283,6 @@ class RedisSentinelCacheBackend(RedisSentinelCache):
|
||||
"""
|
||||
return self._cache.set(name, value, ex=ex, px=px, nx=nx, xx=xx)
|
||||
|
||||
def get(self, name: str) -> Any:
|
||||
"""
|
||||
Get the raw value at key ``name``.
|
||||
|
||||
:param name: Key name
|
||||
:returns: The stored value (bytes), or None if the key is absent
|
||||
"""
|
||||
return self._cache.get(name)
|
||||
|
||||
def delete(self, *names: str) -> int:
|
||||
"""
|
||||
Delete one or more keys.
|
||||
|
||||
@@ -261,42 +261,6 @@ class LoggingMiddleware(Middleware):
|
||||
dataset_id = params.get("dataset_id")
|
||||
return agent_id, user_id, dashboard_id, slice_id, dataset_id, params
|
||||
|
||||
def _extract_output_ids(self, result: ToolResult) -> tuple[int | None, int | None]:
|
||||
"""Extract dashboard/chart IDs created by the tool from its response.
|
||||
|
||||
Create-style tools (generate_chart, generate_dashboard) don't take
|
||||
chart_id/dashboard_id as input, so _extract_context_info never sees
|
||||
them and every retry logs slice_id/dashboard_id=None even on the
|
||||
attempt that actually persisted the object. Look at the response
|
||||
body instead, since that's the only place the new ID appears.
|
||||
Supports both flat ("chart_id"/"dashboard_id") and nested
|
||||
("chart"/"dashboard" objects with an "id" field) response shapes.
|
||||
"""
|
||||
from superset.utils.json import loads as json_loads
|
||||
|
||||
try:
|
||||
data = json_loads(result.content[0].text)
|
||||
except (AttributeError, IndexError, ValueError, TypeError):
|
||||
return None, None
|
||||
if not isinstance(data, dict):
|
||||
return None, None
|
||||
|
||||
slice_id = None
|
||||
chart = data.get("chart")
|
||||
if isinstance(chart, dict):
|
||||
slice_id = chart.get("id")
|
||||
if slice_id is None:
|
||||
slice_id = data.get("chart_id")
|
||||
|
||||
dashboard_id = None
|
||||
dashboard = data.get("dashboard")
|
||||
if isinstance(dashboard, dict):
|
||||
dashboard_id = dashboard.get("id")
|
||||
if dashboard_id is None:
|
||||
dashboard_id = data.get("dashboard_id")
|
||||
|
||||
return dashboard_id, slice_id
|
||||
|
||||
@staticmethod
|
||||
def _resolve_tool_name(tool_name: str | None, params: Any) -> str | None:
|
||||
"""Resolve the underlying tool name from call_tool proxy arguments.
|
||||
@@ -319,129 +283,6 @@ class LoggingMiddleware(Middleware):
|
||||
return params["name"]
|
||||
return None
|
||||
|
||||
def _backfill_output_ids(
|
||||
self,
|
||||
success: bool,
|
||||
result: Any,
|
||||
dashboard_id: int | None,
|
||||
slice_id: int | None,
|
||||
) -> tuple[int | None, int | None]:
|
||||
"""Fill in missing ids from a create tool's response on success.
|
||||
|
||||
Create-style tools (generate_chart, generate_dashboard) don't take
|
||||
the new object's ID as input, so it's missing from params. On a
|
||||
successful call, pull it from the response instead so retried
|
||||
creates are distinguishable.
|
||||
"""
|
||||
if not success or not isinstance(result, ToolResult):
|
||||
return dashboard_id, slice_id
|
||||
output_dashboard_id, output_slice_id = self._extract_output_ids(result)
|
||||
if dashboard_id is None:
|
||||
dashboard_id = output_dashboard_id
|
||||
if slice_id is None:
|
||||
slice_id = output_slice_id
|
||||
return dashboard_id, slice_id
|
||||
|
||||
@staticmethod
|
||||
def _build_call_tool_payload(
|
||||
*,
|
||||
mcp_call_id: str,
|
||||
tool_name: str | None,
|
||||
agent_id: str | None,
|
||||
params: Any,
|
||||
method: str,
|
||||
dashboard_id: int | None,
|
||||
slice_id: int | None,
|
||||
dataset_id: int | None,
|
||||
success: bool,
|
||||
mcp_tool: str | None,
|
||||
error_type: str | None,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"mcp_call_id": mcp_call_id,
|
||||
"tool": tool_name,
|
||||
"agent_id": agent_id,
|
||||
"params": _sanitize_params(params),
|
||||
"method": method,
|
||||
"dashboard_id": dashboard_id,
|
||||
"slice_id": slice_id,
|
||||
"dataset_id": dataset_id,
|
||||
"success": success,
|
||||
}
|
||||
if mcp_tool is not None:
|
||||
payload["mcp_tool"] = mcp_tool
|
||||
if error_type is not None:
|
||||
payload["error_type"] = error_type
|
||||
return payload
|
||||
|
||||
def _log_call_tool_result(
|
||||
self,
|
||||
*,
|
||||
context: MiddlewareContext,
|
||||
tool_name: str | None,
|
||||
mcp_tool: str | None,
|
||||
mcp_call_id: str,
|
||||
agent_id: str | None,
|
||||
user_id: int | None,
|
||||
dashboard_id: int | None,
|
||||
slice_id: int | None,
|
||||
dataset_id: int | None,
|
||||
params: Any,
|
||||
success: bool,
|
||||
error_type: str | None,
|
||||
result: Any,
|
||||
start_time: float,
|
||||
) -> None:
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
dashboard_id, slice_id = self._backfill_output_ids(
|
||||
success, result, dashboard_id, slice_id
|
||||
)
|
||||
payload = self._build_call_tool_payload(
|
||||
mcp_call_id=mcp_call_id,
|
||||
tool_name=tool_name,
|
||||
agent_id=agent_id,
|
||||
params=params,
|
||||
method=context.method,
|
||||
dashboard_id=dashboard_id,
|
||||
slice_id=slice_id,
|
||||
dataset_id=dataset_id,
|
||||
success=success,
|
||||
mcp_tool=mcp_tool,
|
||||
error_type=error_type,
|
||||
)
|
||||
if has_app_context():
|
||||
event_logger.log(
|
||||
user_id=user_id,
|
||||
action="mcp_tool_call",
|
||||
dashboard_id=dashboard_id,
|
||||
duration_ms=duration_ms,
|
||||
slice_id=slice_id,
|
||||
referrer=None,
|
||||
curated_payload=payload,
|
||||
)
|
||||
extra_parts = []
|
||||
if mcp_tool is not None:
|
||||
extra_parts.append(f"mcp_tool={mcp_tool}")
|
||||
if error_type is not None:
|
||||
extra_parts.append(f"error_type={error_type}")
|
||||
extra = (", " + ", ".join(extra_parts)) if extra_parts else ""
|
||||
logger.info(
|
||||
"MCP tool call: tool=%s, agent_id=%s, user_id=%s, method=%s, "
|
||||
"dashboard_id=%s, slice_id=%s, dataset_id=%s, duration_ms=%s, "
|
||||
"success=%s, mcp_call_id=%s%s",
|
||||
tool_name,
|
||||
agent_id,
|
||||
user_id,
|
||||
context.method,
|
||||
dashboard_id,
|
||||
slice_id,
|
||||
dataset_id,
|
||||
duration_ms,
|
||||
success,
|
||||
mcp_call_id,
|
||||
extra,
|
||||
)
|
||||
|
||||
async def on_call_tool(
|
||||
self,
|
||||
context: MiddlewareContext,
|
||||
@@ -459,7 +300,6 @@ class LoggingMiddleware(Middleware):
|
||||
start_time = time.time()
|
||||
success = False
|
||||
error_type: str | None = None
|
||||
result: Any = None
|
||||
try:
|
||||
result = await call_next(context)
|
||||
success = not self._is_error_response(result)
|
||||
@@ -476,21 +316,53 @@ class LoggingMiddleware(Middleware):
|
||||
success = False
|
||||
raise
|
||||
finally:
|
||||
self._log_call_tool_result(
|
||||
context=context,
|
||||
tool_name=tool_name,
|
||||
mcp_tool=mcp_tool,
|
||||
mcp_call_id=mcp_call_id,
|
||||
agent_id=agent_id,
|
||||
user_id=user_id,
|
||||
dashboard_id=dashboard_id,
|
||||
slice_id=slice_id,
|
||||
dataset_id=dataset_id,
|
||||
params=params,
|
||||
success=success,
|
||||
error_type=error_type,
|
||||
result=result,
|
||||
start_time=start_time,
|
||||
duration_ms = int((time.time() - start_time) * 1000)
|
||||
payload: dict[str, Any] = {
|
||||
"mcp_call_id": mcp_call_id,
|
||||
"tool": tool_name,
|
||||
"agent_id": agent_id,
|
||||
"params": _sanitize_params(params),
|
||||
"method": context.method,
|
||||
"dashboard_id": dashboard_id,
|
||||
"slice_id": slice_id,
|
||||
"dataset_id": dataset_id,
|
||||
"success": success,
|
||||
}
|
||||
if mcp_tool is not None:
|
||||
payload["mcp_tool"] = mcp_tool
|
||||
if error_type is not None:
|
||||
payload["error_type"] = error_type
|
||||
if has_app_context():
|
||||
event_logger.log(
|
||||
user_id=user_id,
|
||||
action="mcp_tool_call",
|
||||
dashboard_id=dashboard_id,
|
||||
duration_ms=duration_ms,
|
||||
slice_id=slice_id,
|
||||
referrer=None,
|
||||
curated_payload=payload,
|
||||
)
|
||||
extra_parts = []
|
||||
if mcp_tool is not None:
|
||||
extra_parts.append(f"mcp_tool={mcp_tool}")
|
||||
if error_type is not None:
|
||||
extra_parts.append(f"error_type={error_type}")
|
||||
extra = (", " + ", ".join(extra_parts)) if extra_parts else ""
|
||||
logger.info(
|
||||
"MCP tool call: tool=%s, agent_id=%s, user_id=%s, method=%s, "
|
||||
"dashboard_id=%s, slice_id=%s, dataset_id=%s, duration_ms=%s, "
|
||||
"success=%s, mcp_call_id=%s%s",
|
||||
tool_name,
|
||||
agent_id,
|
||||
user_id,
|
||||
context.method,
|
||||
dashboard_id,
|
||||
slice_id,
|
||||
dataset_id,
|
||||
duration_ms,
|
||||
success,
|
||||
mcp_call_id,
|
||||
extra,
|
||||
)
|
||||
|
||||
async def on_message(
|
||||
|
||||
@@ -84,28 +84,6 @@ def _load_user_from_job_metadata(job_metadata: dict[str, Any]) -> User:
|
||||
return user
|
||||
|
||||
|
||||
def _handle_soft_time_limit(
|
||||
job_metadata: dict[str, Any], ex: Exception, activity: str
|
||||
) -> None:
|
||||
"""
|
||||
SoftTimeLimitExceeded is raised both by a genuine timeout and by a
|
||||
user-initiated cancel (revoke sends SIGUSR1). The cancel endpoint has
|
||||
already emitted the terminal event for the latter - it has to, since a task
|
||||
revoked while still queued never reaches this handler - so only a timeout
|
||||
is reported here, and without one the client would wait forever.
|
||||
"""
|
||||
if async_query_manager.is_job_cancelled(job_metadata["job_id"]):
|
||||
logger.info("Cancelled by the user while %s", activity)
|
||||
return
|
||||
|
||||
logger.warning("A timeout occurred while %s, error: %s", activity, ex)
|
||||
async_query_manager.update_job(
|
||||
job_metadata,
|
||||
async_query_manager.STATUS_ERROR,
|
||||
errors=[{"message": f"A timeout occurred while {activity}"}],
|
||||
)
|
||||
|
||||
|
||||
@celery_app.task(name="load_chart_data_into_cache", soft_time_limit=query_timeout)
|
||||
def load_chart_data_into_cache(
|
||||
job_metadata: dict[str, Any],
|
||||
@@ -128,7 +106,7 @@ def load_chart_data_into_cache(
|
||||
result_url=result_url,
|
||||
)
|
||||
except SoftTimeLimitExceeded as ex:
|
||||
_handle_soft_time_limit(job_metadata, ex, "loading chart data")
|
||||
logger.warning("A timeout occurred while loading chart data, error: %s", ex)
|
||||
raise
|
||||
except Exception as ex:
|
||||
# Extract SIP-40 style errors when available
|
||||
@@ -198,7 +176,9 @@ def load_explore_json_into_cache( # pylint: disable=too-many-locals
|
||||
result_url=result_url,
|
||||
)
|
||||
except SoftTimeLimitExceeded as ex:
|
||||
_handle_soft_time_limit(job_metadata, ex, "loading explore json")
|
||||
logger.warning(
|
||||
"A timeout occurred while loading explore json, error: %s", ex
|
||||
)
|
||||
raise
|
||||
except Exception as ex:
|
||||
if isinstance(ex, SupersetVizException):
|
||||
|
||||
@@ -19,7 +19,7 @@ import logging
|
||||
from collections.abc import Iterator
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from croniter import croniter, CroniterBadDateError
|
||||
from croniter import croniter
|
||||
from flask import current_app
|
||||
from pytz import timezone as pytz_timezone, UnknownTimeZoneError
|
||||
|
||||
@@ -42,15 +42,8 @@ def cron_schedule_window(
|
||||
start_at = time_now - timedelta(seconds=window_size / 2)
|
||||
stop_at = time_now + timedelta(seconds=window_size / 2)
|
||||
crons = croniter(cron, start_at)
|
||||
try:
|
||||
for schedule in crons.all_next(datetime):
|
||||
if schedule >= stop_at:
|
||||
break
|
||||
# convert schedule back to utc
|
||||
yield schedule.astimezone(utc).replace(tzinfo=None)
|
||||
except CroniterBadDateError:
|
||||
logger.error(
|
||||
"Cron schedule %s can never match a valid date; "
|
||||
"it will not produce any executions",
|
||||
cron,
|
||||
)
|
||||
for schedule in crons.all_next(datetime):
|
||||
if schedule >= stop_at:
|
||||
break
|
||||
# convert schedule back to utc
|
||||
yield schedule.astimezone(utc).replace(tzinfo=None)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -114,7 +114,7 @@ msgid " expression which needs to adhere to the "
|
||||
msgstr "準拠すべき式"
|
||||
|
||||
msgid " for details."
|
||||
msgstr "をご覧ください。"
|
||||
msgstr "(詳細)"
|
||||
|
||||
#, python-format
|
||||
msgid " near '%(highlight)s'"
|
||||
@@ -196,7 +196,7 @@ msgstr "合計に対する %"
|
||||
|
||||
#, python-format
|
||||
msgid "%(alertType)s \"%(alertName)s\" triggered successfully"
|
||||
msgstr "%(alertType)s 「%(alertName)s」が正常に実行されました"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "%(dialect)s cannot be used as a data source for security reasons."
|
||||
@@ -208,7 +208,7 @@ msgstr "%(label)s ファイル"
|
||||
|
||||
#, python-format
|
||||
msgid "%(name)s.%(extension)s"
|
||||
msgstr "%(name)s.%(extension)s"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "%(name)s.csv"
|
||||
@@ -258,7 +258,7 @@ msgid ""
|
||||
"Please recheck your query.\n"
|
||||
"Exception: %(ex)s"
|
||||
msgstr ""
|
||||
"%(validator)s はクエリを検証できませんでした。\n"
|
||||
"%(validator)s はクエリをチェックできませんでした。\n"
|
||||
"クエリを再確認してください。\n"
|
||||
"例外: %(ex)s"
|
||||
|
||||
@@ -504,7 +504,7 @@ msgid "+ %s more"
|
||||
msgstr "他 %s 件"
|
||||
|
||||
msgid ", then paste the JSON below. See our"
|
||||
msgstr "で作成し、以下に JSON を貼り付けてください。詳細については、当社の"
|
||||
msgstr "、次に以下のJSONを貼り付けてください。詳細は当社の"
|
||||
|
||||
msgid ""
|
||||
"-- Note: Unless you save your query, these tabs will NOT persist if you "
|
||||
@@ -848,10 +848,10 @@ msgstr "国別の値を表示可能な世界地図です。"
|
||||
msgid ""
|
||||
"A map that takes rendering circles with a variable radius at "
|
||||
"latitude/longitude coordinates"
|
||||
msgstr "緯度・経度座標上に、可変半径の円を描画する地図です。"
|
||||
msgstr "緯度・経度座標に、半径が可変の円をレンダリングする地図です"
|
||||
|
||||
msgid "A metric to use for color"
|
||||
msgstr "色付けに使用するメトリック"
|
||||
msgstr "色に使用するメトリック"
|
||||
|
||||
msgid ""
|
||||
"A multiplier applied to the point radius. Use this to uniformly scale all"
|
||||
@@ -897,7 +897,7 @@ msgid ""
|
||||
"Restore it via POST /api/v1/dataset/%(uuid)s/restore before creating a "
|
||||
"new dataset over this table, or use a different table name."
|
||||
msgstr ""
|
||||
"論理削除されたデータセット (uuid %(uuid)s) "
|
||||
"ソフト削除されたデータセット (uuid %(uuid)s) "
|
||||
"が、すでにこのテーブルを参照しています。このテーブル上に新しいデータセットを作成する前に、POST "
|
||||
"/api/v1/dataset/%(uuid)s/restore を使用して復元するか、別のテーブル名を使用してください。"
|
||||
|
||||
@@ -907,7 +907,7 @@ msgid ""
|
||||
"Restore it via POST /api/v1/dataset/%(uuid)s/restore before uploading, or"
|
||||
" upload to a different table name."
|
||||
msgstr ""
|
||||
"論理削除されたデータセット(uuid %(uuid)s)が、すでにこのテーブルを参照しています。アップロードする前に、POST "
|
||||
"ソフト削除されたデータセット(uuid %(uuid)s)が、すでにこのテーブルを参照しています。アップロードする前に、POST "
|
||||
"/api/v1/dataset/%(uuid)s/restore を使用して復元するか、別のテーブル名にアップロードしてください。"
|
||||
|
||||
msgid "A timeout occurred while executing the query."
|
||||
@@ -920,7 +920,7 @@ msgid "A timeout occurred while generating a dataframe."
|
||||
msgstr "データフレームの生成中にタイムアウトが発生しました。"
|
||||
|
||||
msgid "A timeout occurred while generating an Excel file."
|
||||
msgstr "Excel ファイルの生成中にタイムアウトが発生しました。"
|
||||
msgstr ""
|
||||
|
||||
msgid "A timeout occurred while taking a screenshot."
|
||||
msgstr "スクリーンショットの撮影中にタイムアウトが発生しました。"
|
||||
@@ -975,13 +975,13 @@ msgid "AUG"
|
||||
msgstr "8月"
|
||||
|
||||
msgid "Aborted"
|
||||
msgstr "中止済み"
|
||||
msgstr "中止"
|
||||
|
||||
msgid "Aborting"
|
||||
msgstr "中止中"
|
||||
|
||||
msgid "About"
|
||||
msgstr "概要"
|
||||
msgstr "詳細"
|
||||
|
||||
msgid "Access"
|
||||
msgstr "アクセス"
|
||||
@@ -993,16 +993,16 @@ msgid "Account"
|
||||
msgstr "アカウント"
|
||||
|
||||
msgid "Action"
|
||||
msgstr "操作"
|
||||
msgstr "アクション"
|
||||
|
||||
msgid "Action Log"
|
||||
msgstr "操作ログ"
|
||||
msgstr "操作履歴"
|
||||
|
||||
msgid "Action Logs"
|
||||
msgstr "操作ログ"
|
||||
msgstr "操作履歴"
|
||||
|
||||
msgid "Actions"
|
||||
msgstr "操作"
|
||||
msgstr "アクション"
|
||||
|
||||
msgid "Active"
|
||||
msgstr "有効"
|
||||
@@ -1131,7 +1131,7 @@ msgid "Add dataset columns here to group the pivot table columns."
|
||||
msgstr "ここにデータセットの列を追加して、ピボットテーブルの列をグループ化します。"
|
||||
|
||||
msgid "Add delivery method"
|
||||
msgstr "配信方法を追加"
|
||||
msgstr "送信方法を追加"
|
||||
|
||||
msgid "Add description of your tag"
|
||||
msgstr "タグの説明を追加"
|
||||
@@ -1201,7 +1201,7 @@ msgid "Add required control values to save chart"
|
||||
msgstr "チャートを保存するために必要なコントロール値を追加してください"
|
||||
|
||||
msgid "Add service credentials"
|
||||
msgstr "サービスの認証情報を追加する"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add sheet"
|
||||
msgstr "シートを追加"
|
||||
@@ -1267,10 +1267,10 @@ msgid ""
|
||||
msgstr "比較値からの増減(正負の変化)に基づいて、チャートのシンボルに色を付けます。"
|
||||
|
||||
msgid "Adhoc metric SQL expression is invalid"
|
||||
msgstr "アドホックなメトリックのSQL式が無効です"
|
||||
msgstr ""
|
||||
|
||||
msgid "Adhoc metric aggregate is invalid"
|
||||
msgstr "アドホックなメトリックの集計が無効です"
|
||||
msgstr ""
|
||||
|
||||
msgid "Adjust how this database will interact with SQL Lab."
|
||||
msgstr "このデータベースとSQL Labとの連携設定を調整します。"
|
||||
@@ -1704,7 +1704,7 @@ msgstr "関数名の取得中にエラーが発生しました。"
|
||||
|
||||
#, python-format
|
||||
msgid "An error occurred while fetching row level security subject values: %s"
|
||||
msgstr "行レベルのセキュリティ対象値の取得中にエラーが発生しました: %s"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "An error occurred while fetching schema values: %s"
|
||||
@@ -1805,7 +1805,7 @@ msgid "An error occurred while syncing permissions for %s: %s"
|
||||
msgstr "%s の権限同期中にエラーが発生しました: %s"
|
||||
|
||||
msgid "An error occurred while triggering the report"
|
||||
msgstr "レポートの実行中にエラーが発生しました"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred while updating the extension."
|
||||
msgstr "拡張機能の更新中にエラーが発生しました。"
|
||||
@@ -1832,8 +1832,6 @@ msgid ""
|
||||
"Angle at which the first slice begins, in degrees. 90° starts at the top,"
|
||||
" 0°/360° at the right, 270° at the bottom, and 180° at the left."
|
||||
msgstr ""
|
||||
"最初のスライスを開始する角度(度)。90°は上端から始まり、"
|
||||
"0°/360°は右端から、270°は下端から、180°は左端から始まります。"
|
||||
|
||||
msgid "Angle at which to end progress axis"
|
||||
msgstr "進行軸を終了する角度"
|
||||
@@ -2069,7 +2067,7 @@ msgid "Are you sure you want to delete the selected layers?"
|
||||
msgstr "選択したレイヤーを削除してもよろしいですか?"
|
||||
|
||||
msgid "Are you sure you want to delete the selected queries?"
|
||||
msgstr "選択したクエリを削除してもよろしいですか?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete the selected roles?"
|
||||
msgstr "選択したロールを削除してもよろしいですか?"
|
||||
@@ -2660,7 +2658,7 @@ msgid ""
|
||||
"permanently once a purge capability ships, or remove the underlying rows "
|
||||
"out-of-band, before deleting the database."
|
||||
msgstr ""
|
||||
"残っているデータセットがすべて論理削除されているデータベースは削除できません。それらを復元(POST "
|
||||
"残っているデータセットがすべてソフト削除されているデータベースは削除できません。それらを復元(POST "
|
||||
"/api/v1/dataset/<uuid>/restore)し、パージ機能が提供されたら完全に削除するか、データベースを削除する前に、別の手順で基となる行を削除してください。"
|
||||
|
||||
msgid "Cannot delete system themes"
|
||||
@@ -2735,7 +2733,7 @@ msgid "Category, Value and Percentage"
|
||||
msgstr "カテゴリ、値、割合 (%)"
|
||||
|
||||
msgid "Cell Padding"
|
||||
msgstr "セルの余白"
|
||||
msgstr "セルのパディング"
|
||||
|
||||
msgid "Cell Radius"
|
||||
msgstr "セルの角の丸み"
|
||||
@@ -2996,7 +2994,7 @@ msgid "Check out this dashboard: "
|
||||
msgstr "このダッシュボードを確認: "
|
||||
|
||||
msgid "Check to force date partitions to have the same height"
|
||||
msgstr "日付パーティションを強制的に同じ高さにする場合はチェックを入れます"
|
||||
msgstr "日付パーティションを強制的に同じ高さにする場合はオンにしてください"
|
||||
|
||||
msgid "Child label position"
|
||||
msgstr "子ラベルの位置"
|
||||
@@ -3015,7 +3013,7 @@ msgid "Choose a chart for displaying on the map"
|
||||
msgstr "マップ上に表示するチャートを選択"
|
||||
|
||||
msgid "Choose a chart or dashboard not both"
|
||||
msgstr "チャートまたはダッシュボードのどちらか一方のみを選択してください"
|
||||
msgstr "チャートまたはダッシュボードのどちらか一方を選択してください"
|
||||
|
||||
msgid "Choose a database..."
|
||||
msgstr "データベースを選択..."
|
||||
@@ -3165,7 +3163,7 @@ msgid "Clear local theme"
|
||||
msgstr "ローカルテーマをクリア"
|
||||
|
||||
msgid "Clear search"
|
||||
msgstr "検索をクリア"
|
||||
msgstr ""
|
||||
|
||||
msgid "Clear the selection to revert to the system default theme"
|
||||
msgstr "選択を解除して、システムの既定テーマに戻します"
|
||||
@@ -3645,7 +3643,7 @@ msgid "Connection failed, please check your connection settings."
|
||||
msgstr "接続に失敗しました。接続設定を確認してください。"
|
||||
|
||||
msgid "Connection looks good!"
|
||||
msgstr "正常に接続できました!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Contains"
|
||||
msgstr "含む"
|
||||
@@ -3722,13 +3720,13 @@ msgid "Copy permalink to clipboard"
|
||||
msgstr "固定リンクをクリップボードにコピー"
|
||||
|
||||
msgid "Copy query"
|
||||
msgstr "クエリをコピー"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy query URL"
|
||||
msgstr "クエリのURLをコピー"
|
||||
msgstr ""
|
||||
|
||||
msgid "Copy query link to your clipboard"
|
||||
msgstr "クエリリンクをクリップボードにコピー"
|
||||
msgstr "クエリのリンクをクリップボードにコピー"
|
||||
|
||||
msgid "Copy the current data"
|
||||
msgstr "現在のデータをコピー"
|
||||
@@ -3846,7 +3844,7 @@ msgstr "新しいチャートを作成"
|
||||
msgid ""
|
||||
"Create a new tag and assign it to existing entities like charts or "
|
||||
"dashboards"
|
||||
msgstr "新しいタグを作成し、チャートやダッシュボードなどの既存のエンティティに割り当てる"
|
||||
msgstr ""
|
||||
|
||||
msgid "Create and explore dataset"
|
||||
msgstr "データセットを作成して探索"
|
||||
@@ -4020,7 +4018,7 @@ msgid "Customize"
|
||||
msgstr "カスタマイズ"
|
||||
|
||||
msgid "Customize Metrics"
|
||||
msgstr "メトリックのカスタマイズ"
|
||||
msgstr "メトリックをカスタマイズ"
|
||||
|
||||
msgid ""
|
||||
"Customize cell titles using Handlebars template syntax. Available "
|
||||
@@ -4028,7 +4026,7 @@ msgid ""
|
||||
msgstr "Handlebarsテンプレート構文を使用して、セルの見出しをカスタマイズします。利用可能な変数:{{rowLabel}}、{{colLabel}}"
|
||||
|
||||
msgid "Customize columns"
|
||||
msgstr "列のカスタマイズ"
|
||||
msgstr "列をカスタマイズ"
|
||||
|
||||
msgid "Customize data source, filters, and layout."
|
||||
msgstr "データソース、フィルター、およびレイアウトのカスタマイズ。"
|
||||
@@ -4049,10 +4047,10 @@ msgid ""
|
||||
msgstr "合計値に対して、ツールチップ、凡例、および軸に表示するラベルをカスタマイズします。"
|
||||
|
||||
msgid "Customize tooltips template"
|
||||
msgstr "ツールチップテンプレートのカスタマイズ"
|
||||
msgstr "ツールチップテンプレートをカスタマイズ"
|
||||
|
||||
msgid "Cyclic dependency detected"
|
||||
msgstr "循環依存(循環参照)が検出されました"
|
||||
msgstr "循環参照(依存関係)が検出されました"
|
||||
|
||||
msgid "D3 format"
|
||||
msgstr "D3形式"
|
||||
@@ -4133,8 +4131,6 @@ msgid ""
|
||||
"Dashboard cannot be restored because its slug is now used by another "
|
||||
"active dashboard. Rename one of the dashboards and retry."
|
||||
msgstr ""
|
||||
"このダッシュボードは、そのスラグが現在別のアクティブなダッシュボードで使用されているため、復元できません。"
|
||||
"いずれかのダッシュボードの名前を変更してから、再度お試しください。"
|
||||
|
||||
msgid "Dashboard cannot be unfavorited."
|
||||
msgstr "ダッシュボードのお気に入りを解除できませんでした。"
|
||||
@@ -4270,7 +4266,7 @@ msgid "Data refreshed"
|
||||
msgstr "データを更新しました"
|
||||
|
||||
msgid "Data type"
|
||||
msgstr "データ型"
|
||||
msgstr "データ種類"
|
||||
|
||||
msgid "DataFrame include at least one series"
|
||||
msgstr "データフレームには少なくとも1つの系列が必要です"
|
||||
@@ -4312,8 +4308,8 @@ msgid ""
|
||||
"Database driver for importing maybe not installed. Visit the Superset "
|
||||
"documentation page for installation instructions: "
|
||||
msgstr ""
|
||||
"インポートに必要なデータベースドライバーがインストールされていない可能性があります。"
|
||||
"インストール手順についてはSupersetのドキュメントを確認してください:"
|
||||
"インポートに必要なデータベースドライバーがインストールされていない可能性があります。インストール手順についてはSupersetのドキュメントを確認してください:"
|
||||
" "
|
||||
|
||||
msgid "Database error"
|
||||
msgstr "データベースエラー"
|
||||
@@ -4401,7 +4397,7 @@ msgid "Dataset does not exist"
|
||||
msgstr "データセットが存在しません"
|
||||
|
||||
msgid "Dataset is required"
|
||||
msgstr "データセットは必須です"
|
||||
msgstr "データセットが必要です"
|
||||
|
||||
msgid "Dataset metric delete failed."
|
||||
msgstr "データセットメトリックの削除に失敗しました。"
|
||||
@@ -4443,7 +4439,7 @@ msgid "Datasource does not exist"
|
||||
msgstr "データソースが存在しません"
|
||||
|
||||
msgid "Datasource is required"
|
||||
msgstr "データソースは必須です"
|
||||
msgstr "データソースが必要です"
|
||||
|
||||
msgid "Datasource is required for validation"
|
||||
msgstr "検証にはデータソースが必要です"
|
||||
@@ -4500,7 +4496,7 @@ msgid "Decides which column or measure to sort the base axis by."
|
||||
msgstr "基準軸の並べ替えに使用する列またはメジャーを選択します。"
|
||||
|
||||
msgid "Decimal character"
|
||||
msgstr "小数点記号"
|
||||
msgstr "小数点"
|
||||
|
||||
msgid "Deck.gl - 3D Grid"
|
||||
msgstr "Deck.gl - 3Dグリッド"
|
||||
@@ -4745,7 +4741,7 @@ msgid "Delete item"
|
||||
msgstr "項目を削除"
|
||||
|
||||
msgid "Delete query"
|
||||
msgstr "クエリを削除"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete role"
|
||||
msgstr "ロールを削除"
|
||||
@@ -4878,7 +4874,7 @@ msgid "Delimiter"
|
||||
msgstr "区切り文字"
|
||||
|
||||
msgid "Delivery method"
|
||||
msgstr "配信方法"
|
||||
msgstr "送信方法"
|
||||
|
||||
msgid "Density"
|
||||
msgstr "密度"
|
||||
@@ -4902,7 +4898,7 @@ msgid "Deselect all"
|
||||
msgstr "すべての選択を解除"
|
||||
|
||||
msgid "Design with"
|
||||
msgstr "デザインを"
|
||||
msgstr "デザイン"
|
||||
|
||||
msgid "Details"
|
||||
msgstr "詳細"
|
||||
@@ -4938,7 +4934,7 @@ msgid "Difference"
|
||||
msgstr "差分"
|
||||
|
||||
msgid "Dim Gray"
|
||||
msgstr "薄灰色"
|
||||
msgstr "ディム・グレー"
|
||||
|
||||
msgid "Dimension"
|
||||
msgstr "ディメンション"
|
||||
@@ -5022,8 +5018,6 @@ msgid ""
|
||||
"Display charts on a map. For using this plugin, users first have to "
|
||||
"create any other chart that can then be placed on the map."
|
||||
msgstr ""
|
||||
"地図上にグラフを表示します。"
|
||||
"このプラグインを使用するには、まず地図上に配置できる任意のグラフを作成する必要があります。"
|
||||
|
||||
msgid "Display column in the chart"
|
||||
msgstr "チャートに列を表示"
|
||||
@@ -5122,7 +5116,7 @@ msgid ""
|
||||
msgstr "ディメンションの値に基づいて、各カテゴリをさらにサブカテゴリに分割します。共通部分の除外などに利用できます。"
|
||||
|
||||
msgid "Do you want a donut or a pie?"
|
||||
msgstr "ドーナツ型と円型のどちらにしますか?"
|
||||
msgstr "ドーナツ型か円型のどちらにしますか?"
|
||||
|
||||
msgid "Documentation"
|
||||
msgstr "ドキュメント"
|
||||
@@ -5152,7 +5146,7 @@ msgid "Download is on the way"
|
||||
msgstr "ダウンロードを準備しています"
|
||||
|
||||
msgid "Download to CSV"
|
||||
msgstr "CSV形式でダウンロード"
|
||||
msgstr "CSVとしてダウンロード"
|
||||
|
||||
msgid "Download to client"
|
||||
msgstr "クライアントへダウンロード"
|
||||
@@ -5359,7 +5353,7 @@ msgstr "エラー"
|
||||
|
||||
#, python-format
|
||||
msgid "ERROR: %s"
|
||||
msgstr "エラー: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edge length"
|
||||
msgstr "エッジの長さ"
|
||||
@@ -5451,7 +5445,7 @@ msgid "Edit properties"
|
||||
msgstr "プロパティを編集"
|
||||
|
||||
msgid "Edit query"
|
||||
msgstr "クエリを編集"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit report"
|
||||
msgstr "レポートを編集"
|
||||
@@ -5535,7 +5529,7 @@ msgid "Email link"
|
||||
msgstr "メールリンク"
|
||||
|
||||
msgid "Email recipients"
|
||||
msgstr "メールの受信者"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email reports active"
|
||||
msgstr "メールレポート有効"
|
||||
@@ -5738,7 +5732,7 @@ msgid "Enter the required %(dbModelName)s credentials"
|
||||
msgstr "%(dbModelName)s に必要な認証情報を入力してください"
|
||||
|
||||
msgid "Enter the unique project id for your database."
|
||||
msgstr "データベースの一意なプロジェクトIDを入力してください。"
|
||||
msgstr "データベースのユニークなプロジェクトIDを入力してください。"
|
||||
|
||||
msgid "Enter the user's email"
|
||||
msgstr "ユーザーのメールアドレスを入力"
|
||||
@@ -5765,7 +5759,7 @@ msgid "Entity"
|
||||
msgstr "エンティティ"
|
||||
|
||||
msgid "Entries per page"
|
||||
msgstr "1ページあたりの表示件数"
|
||||
msgstr ""
|
||||
|
||||
msgid "Equal Date Sizes"
|
||||
msgstr "日付サイズを均等にする"
|
||||
@@ -5888,7 +5882,7 @@ msgid "Error: %s"
|
||||
msgstr "エラー: %s"
|
||||
|
||||
msgid "Error: permalink state not found"
|
||||
msgstr "エラー: 固定リンクの状態が見つかりません"
|
||||
msgstr "エラー: パーマリンクの状態が見つかりません"
|
||||
|
||||
msgid "Estimate cost"
|
||||
msgstr "コストを見積もる"
|
||||
@@ -6021,7 +6015,7 @@ msgid "Export as Example"
|
||||
msgstr "構成をエクスポート"
|
||||
|
||||
msgid "Export as PDF"
|
||||
msgstr "PDFとしてエクスポート"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export cancelled"
|
||||
msgstr "エクスポートがキャンセルされました"
|
||||
@@ -6040,13 +6034,13 @@ msgid "Export failed: %s"
|
||||
msgstr "エクスポート失敗: %s"
|
||||
|
||||
msgid "Export query"
|
||||
msgstr "クエリのエクスポート"
|
||||
msgstr ""
|
||||
|
||||
msgid "Export screenshot (jpeg)"
|
||||
msgstr "スクリーンショットをエクスポート (JPEG)"
|
||||
|
||||
msgid "Export screenshot (png)"
|
||||
msgstr "スクリーンショットをエクスポート (PNG)"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Export successful: %s"
|
||||
@@ -6090,15 +6084,11 @@ msgid ""
|
||||
"Exporting semantic views is not supported yet — %s semantic-view row(s) "
|
||||
"were skipped."
|
||||
msgstr ""
|
||||
"セマンティックビューのエクスポートは現時点ではサポートされていません。"
|
||||
"%s 個のセマンティックビュー行がスキップされました。"
|
||||
|
||||
msgid ""
|
||||
"Exporting semantic views is not supported yet. Deselect the semantic-view"
|
||||
" rows and try again."
|
||||
msgstr ""
|
||||
"セマンティックビューのエクスポートは、現時点ではサポートされていません。"
|
||||
"セマンティックビューの行の選択を解除して、もう一度お試しください。"
|
||||
|
||||
msgid "Expose database in SQL Lab"
|
||||
msgstr "SQL Labでデータベースを公開する"
|
||||
@@ -6202,8 +6192,6 @@ msgid ""
|
||||
"Failed to export chart data. Please try again or contact your "
|
||||
"administrator."
|
||||
msgstr ""
|
||||
"チャートデータのエクスポートに失敗しました。"
|
||||
"もう一度お試しいただくか、管理者にお問い合わせください。"
|
||||
|
||||
msgid "Failed to fetch API keys"
|
||||
msgstr "APIキーの取得に失敗しました"
|
||||
@@ -6279,7 +6267,7 @@ msgstr "項目へのタグ付けに失敗しました"
|
||||
|
||||
#, python-format
|
||||
msgid "Failed to trigger %(alertType)s \"%(alertName)s\": %(error)s"
|
||||
msgstr "%(alertType)s 「%(alertName)s」のトリガーに失敗しました:%(error)s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to update report"
|
||||
msgstr "レポートの更新に失敗しました"
|
||||
@@ -6479,7 +6467,7 @@ msgid "Fit columns dynamically"
|
||||
msgstr "列を動的にフィットさせる"
|
||||
|
||||
msgid "Fit data"
|
||||
msgstr "フィットデータ"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Fix the trend line to the full time range specified in case filtered "
|
||||
@@ -6591,7 +6579,7 @@ msgid "Forecast periods"
|
||||
msgstr "予測期間"
|
||||
|
||||
msgid "Forecast requires at least 2 data points"
|
||||
msgstr "予測には、少なくとも2つのデータポイントが必要です"
|
||||
msgstr ""
|
||||
|
||||
msgid "Foreign key"
|
||||
msgstr "外部キー"
|
||||
@@ -6637,7 +6625,7 @@ msgid "Formatted CSV attached in email"
|
||||
msgstr "整形済みCSVをメールに添付"
|
||||
|
||||
msgid "Formatted Excel attached in email"
|
||||
msgstr "メールに書式設定済みのExcelファイルを添付しました"
|
||||
msgstr ""
|
||||
|
||||
msgid "Formatted value"
|
||||
msgstr "整形済みの値"
|
||||
@@ -7299,7 +7287,7 @@ msgid "Invalid options for %(rolling_type)s: %(options)s"
|
||||
msgstr "%(rolling_type)s のオプションが無効です: %(options)s"
|
||||
|
||||
msgid "Invalid permalink key"
|
||||
msgstr "固定リンクキーが無効です"
|
||||
msgstr "パーマリンクキーが無効です"
|
||||
|
||||
#, python-format
|
||||
msgid "Invalid reference to column: \"%(column)s\""
|
||||
@@ -7510,7 +7498,7 @@ msgid "Label for your query"
|
||||
msgstr "クエリのラベル"
|
||||
|
||||
msgid "Label must not be empty."
|
||||
msgstr "ラベルを空にしないでください。"
|
||||
msgstr ""
|
||||
|
||||
msgid "Label position"
|
||||
msgstr "ラベルの位置"
|
||||
@@ -7792,7 +7780,7 @@ msgid "Lines encoding"
|
||||
msgstr "線のエンコーディング"
|
||||
|
||||
msgid "Link Copied!"
|
||||
msgstr "リンクがコピーされました!"
|
||||
msgstr ""
|
||||
|
||||
msgid "List"
|
||||
msgstr "一覧"
|
||||
@@ -8653,7 +8641,7 @@ msgid "No data after filtering or data is NULL for the latest time record"
|
||||
msgstr "フィルターの結果、または最新レコードのデータがNULLのため、データがありません"
|
||||
|
||||
msgid "No data found"
|
||||
msgstr "データが見つかりませんでした"
|
||||
msgstr ""
|
||||
|
||||
msgid "No data in file"
|
||||
msgstr "ファイル内にデータがありません"
|
||||
@@ -9035,7 +9023,7 @@ msgstr "1つ以上のメトリックが存在しません"
|
||||
|
||||
#, python-format
|
||||
msgid "One or more parameters are missing: %(missing)s"
|
||||
msgstr "1つ以上のパラメータが不足しています:%(missing)s"
|
||||
msgstr ""
|
||||
|
||||
msgid "One or more parameters needed to configure a database are missing."
|
||||
msgstr "データベース設定に必要な1つ以上のパラメータが不足しています。"
|
||||
@@ -9639,9 +9627,6 @@ msgid ""
|
||||
"period (e.g. today so far) against complete prior periods (e.g. all of "
|
||||
"yesterday)."
|
||||
msgstr ""
|
||||
"各時間シフトされた系列を、主系列に合わせて切り詰めるのではなく、その全時間範囲にわたってプロットします。"
|
||||
"これは、現在の期間の一部(例:今日のこれまでの時間)を、"
|
||||
"過去の期間全体(例:昨日の全期間)と比較する際に役立ちます。"
|
||||
|
||||
msgid "Plot the distance (like flight paths) between origin and destination."
|
||||
msgstr "出発地と目的地間の距離(飛行経路など)をプロットします。"
|
||||
@@ -10078,9 +10063,6 @@ msgid ""
|
||||
"except the subjects defined in the filter, and can be used to define what"
|
||||
" users can see if no RLS filters within a filter group apply to them."
|
||||
msgstr ""
|
||||
"通常のフィルターは、ユーザーがフィルターで参照されている対象と一致する場合、クエリにWHERE句を追加します。"
|
||||
"ベースフィルターは、フィルターで定義された対象を除くすべてのクエリにフィルターを適用し、"
|
||||
"フィルターグループ内のRLSフィルターがユーザーに適用されない場合に、そのユーザーに表示できる内容を定義するために使用できます。"
|
||||
|
||||
msgid "Relational"
|
||||
msgstr "関連した"
|
||||
@@ -10119,7 +10101,7 @@ msgid "Remove customization"
|
||||
msgstr "カスタマイズを削除"
|
||||
|
||||
msgid "Remove dependency"
|
||||
msgstr "依存関係を削除"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove filter"
|
||||
msgstr "フィルターを削除"
|
||||
@@ -10128,13 +10110,13 @@ msgid "Remove item"
|
||||
msgstr "項目を削除"
|
||||
|
||||
msgid "Remove notification method"
|
||||
msgstr "通知方法を削除"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove query from log"
|
||||
msgstr "ログからクエリを削除"
|
||||
|
||||
msgid "Remove sheet"
|
||||
msgstr "シートを削除"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Removed 1 column from the virtual dataset"
|
||||
@@ -10174,7 +10156,7 @@ msgid "Report Schedule delete failed."
|
||||
msgstr "レポートスケジュールの削除に失敗しました。"
|
||||
|
||||
msgid "Report Schedule execute now failed."
|
||||
msgstr "レポートスケジュールの即時実行に失敗しました。"
|
||||
msgstr ""
|
||||
|
||||
msgid "Report Schedule execution failed when generating a csv."
|
||||
msgstr "CSV生成時にレポートスケジュールの実行が失敗しました。"
|
||||
@@ -10189,7 +10171,7 @@ msgid "Report Schedule execution failed when generating a screenshot."
|
||||
msgstr "スクリーンショット生成時にレポートスケジュールの実行が失敗しました。"
|
||||
|
||||
msgid "Report Schedule execution failed when generating an Excel file."
|
||||
msgstr "Excel ファイルの生成中に、レポートスケジュールの実行に失敗しました。"
|
||||
msgstr ""
|
||||
|
||||
msgid "Report Schedule execution got an unexpected error."
|
||||
msgstr "レポートスケジュールの実行中に予期しないエラーが発生しました。"
|
||||
@@ -10199,12 +10181,10 @@ msgid ""
|
||||
"Please configure a Celery broker (Redis or RabbitMQ) and worker "
|
||||
"processes."
|
||||
msgstr ""
|
||||
"レポートスケジュールの実行には、Celeryバックエンドの設定が必要です。"
|
||||
"Celeryブローカー(RedisまたはRabbitMQ)とワーカープロセスを設定してください。"
|
||||
|
||||
#, python-format
|
||||
msgid "Report Schedule executor user %(username)s was not found."
|
||||
msgstr "レポートスケジュールの実行者ユーザー %(username)s が見つかりませんでした。"
|
||||
msgstr ""
|
||||
|
||||
msgid "Report Schedule is still working, refusing to re-compute."
|
||||
msgstr "レポートスケジュールはまだ処理中のため、再計算を拒否しました。"
|
||||
@@ -10853,7 +10833,7 @@ msgid "Search Metrics & Columns"
|
||||
msgstr "メトリックと列を検索"
|
||||
|
||||
msgid "Search a channel by name, or paste a channel ID"
|
||||
msgstr "チャンネル名を検索するか、チャンネルIDを貼り付けてください"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search all charts"
|
||||
msgstr "すべてのチャートを検索"
|
||||
@@ -10895,7 +10875,7 @@ msgid "Search metrics by key or label"
|
||||
msgstr "メトリックをキーまたはラベルで検索"
|
||||
|
||||
msgid "Search records"
|
||||
msgstr "記録を検索"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search tags"
|
||||
msgstr "タグを検索"
|
||||
@@ -10907,7 +10887,7 @@ msgid "Search..."
|
||||
msgstr "検索…"
|
||||
|
||||
msgid "Searches all text fields: Name, Description, Database & Schema"
|
||||
msgstr "すべてのテキストフィールド(「名前」、「説明」、「データベース」、「スキーマ」)を検索します"
|
||||
msgstr ""
|
||||
|
||||
msgid "Second"
|
||||
msgstr "秒"
|
||||
@@ -10948,7 +10928,7 @@ msgid "See all %(tableName)s"
|
||||
msgstr "すべての %(tableName)s を表示"
|
||||
|
||||
msgid "See all dashboards"
|
||||
msgstr "すべてのダッシュボードを表示する"
|
||||
msgstr ""
|
||||
|
||||
msgid "See less"
|
||||
msgstr "表示を減らす"
|
||||
@@ -11210,10 +11190,10 @@ msgid "Select operator"
|
||||
msgstr "演算子を選択"
|
||||
|
||||
msgid "Select or type BCC recipients"
|
||||
msgstr "BCCの受信者を選択するか、入力してください"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select or type CC recipients"
|
||||
msgstr "CCの受信者を選択するか、入力してください"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select or type a custom value..."
|
||||
msgstr "カスタム値を選択または入力..."
|
||||
@@ -11225,7 +11205,7 @@ msgid "Select or type dataset name"
|
||||
msgstr "データセット名を選択または入力"
|
||||
|
||||
msgid "Select or type email recipients"
|
||||
msgstr "メールの受信者を選択するか、入力してください"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select page size"
|
||||
msgstr "ページサイズを選択"
|
||||
@@ -11412,7 +11392,7 @@ msgid "Send as CSV"
|
||||
msgstr "CSVとして送信"
|
||||
|
||||
msgid "Send as Excel"
|
||||
msgstr "Excelとして送信"
|
||||
msgstr ""
|
||||
|
||||
msgid "Send as PDF"
|
||||
msgstr "PDFとして送信"
|
||||
@@ -11619,7 +11599,7 @@ msgid "Show Metric Names"
|
||||
msgstr "メトリック名を表示"
|
||||
|
||||
msgid "Show Null Values"
|
||||
msgstr "NULL値を表示する"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show Range Filter"
|
||||
msgstr "範囲フィルターを表示"
|
||||
@@ -11657,16 +11637,13 @@ msgid ""
|
||||
msgstr "スパークラインにY軸を表示します。手動設定されている場合はその最小/最大値を、そうでない場合はデータの最小/最大値を表示します。"
|
||||
|
||||
msgid "Show a draggable slider to control the visible range of the Y-axis."
|
||||
msgstr "Y軸の表示範囲を制御するための、ドラッグ可能なスライダーを表示します。"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Show a summary row of total aggregations: the selected metrics in "
|
||||
"aggregate mode, or the sum of numeric columns in raw records mode. Note "
|
||||
"that row limit does not apply to the result."
|
||||
msgstr ""
|
||||
"集計結果の要約行を表示します。"
|
||||
"集計モードでは選択されたメトリクス、未加工レコードモードでは数値列の合計が表示されます。"
|
||||
"なお、結果には行数の制限は適用されません。"
|
||||
|
||||
msgid "Show all columns"
|
||||
msgstr "すべての列を表示"
|
||||
@@ -11705,7 +11682,7 @@ msgid "Show entries per page"
|
||||
msgstr "1ページあたりの表示件数"
|
||||
|
||||
msgid "Show full range for time shift"
|
||||
msgstr "タイムシフトの全範囲を表示する"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Show hierarchical relationships of data, with the value represented by "
|
||||
@@ -11786,7 +11763,7 @@ msgstr "値を表示"
|
||||
msgid ""
|
||||
"Showcases a metric along with a comparison of value, change, and percent "
|
||||
"change for a selected time period."
|
||||
msgstr "選択した期間における指標と、その値、変化量、および変化率の比較を表示します。"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Showcases a single metric front-and-center. Big number is best used to "
|
||||
@@ -11911,7 +11888,7 @@ msgid "Solid"
|
||||
msgstr "実線"
|
||||
|
||||
msgid "Solid background"
|
||||
msgstr "単色の背景"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some groups could not be resolved and are shown as IDs."
|
||||
msgstr "一部のグループを解決できなかったため、IDとして表示されています。"
|
||||
@@ -11923,13 +11900,12 @@ msgid "Some required filters on other tabs have values and will not be cleared"
|
||||
msgstr "他のタブの一部の必須フィルターには値が設定されており、クリアされません"
|
||||
|
||||
msgid "Some tables are not shown. Refine your search."
|
||||
msgstr "一部のテーブルは表示されていません。検索条件を絞り込んでください。"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Something went wrong loading the dashboard. Check the dev console for "
|
||||
"details."
|
||||
msgstr ""
|
||||
"ダッシュボードの読み込み中に問題が発生しました。詳細については開発者コンソールを確認してください。"
|
||||
|
||||
msgid "Something went wrong while saving the user info"
|
||||
msgstr "ユーザー情報の保存中に問題が発生しました"
|
||||
@@ -12307,7 +12283,7 @@ msgid "Success"
|
||||
msgstr "成功"
|
||||
|
||||
msgid "Success message"
|
||||
msgstr "成功メッセージ"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "Successfully changed %s!"
|
||||
@@ -12375,7 +12351,7 @@ msgid "Swap rows and columns"
|
||||
msgstr "行と列を入れ替え"
|
||||
|
||||
msgid "Sweep angle"
|
||||
msgstr "スイープ角度"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Swiss army knife for visualizing data. Choose between step, line, "
|
||||
@@ -12530,7 +12506,7 @@ msgid "Tag created"
|
||||
msgstr "タグを作成しました"
|
||||
|
||||
msgid "Tag description"
|
||||
msgstr "タグの説明"
|
||||
msgstr ""
|
||||
|
||||
msgid "Tag name"
|
||||
msgstr "タグ名"
|
||||
@@ -12652,7 +12628,7 @@ msgstr "メールに埋め込まれるテキスト"
|
||||
|
||||
#, python-format
|
||||
msgid "The %(key)s in metadata_cache_timeout must be a non-negative integer."
|
||||
msgstr "metadata_cache_timeout 内の %(key)s は、非負の整数でなければなりません。"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "The %s"
|
||||
@@ -12750,8 +12726,6 @@ msgid ""
|
||||
"The chart data is too large to download. Please try reducing the date "
|
||||
"range, limiting rows, or using fewer columns."
|
||||
msgstr ""
|
||||
"チャートのデータ量が大きすぎてダウンロードできません。"
|
||||
"日付範囲を狭めたり、行数を減らしたり、表示する列の数を減らしたりしてみてください。"
|
||||
|
||||
msgid "The chart is still loading. Please wait a moment and try again."
|
||||
msgstr "チャートはまだ読み込み中です。しばらく待ってから再試行してください。"
|
||||
@@ -12762,7 +12736,7 @@ msgid ""
|
||||
msgstr "このレポートの対象であるチャートは削除されました。チャートを復元するか、レポートを更新して有効なチャートを指定してください。"
|
||||
|
||||
msgid "The chat failed to load."
|
||||
msgstr "チャットが読み込めませんでした。"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"The classic. Great for showing how much of a company each investor gets, "
|
||||
@@ -12845,7 +12819,7 @@ msgid ""
|
||||
msgstr "このレポートの対象であるダッシュボードは削除されました。ダッシュボードを復元するか、レポートを更新して有効なダッシュボードを指定してください。"
|
||||
|
||||
msgid "The dashboard you are looking for may have been deleted or moved."
|
||||
msgstr "探しているダッシュボードは、削除されたか、移動された可能性があります。"
|
||||
msgstr ""
|
||||
|
||||
msgid "The data source seems to have been deleted"
|
||||
msgstr "データソースが削除されたようです"
|
||||
@@ -13061,7 +13035,7 @@ msgstr "メトリックの最大値。設定は任意です。"
|
||||
msgid ""
|
||||
"The metadata_cache_timeout must be a mapping from string keys to non-"
|
||||
"negative integer values."
|
||||
msgstr "「metadata_cache_timeout」 は、文字列キーから非負の整数値へのマッピングでなければなりません。"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
@@ -13238,7 +13212,7 @@ msgid "The query contains one or more malformed template parameters."
|
||||
msgstr "クエリに1つ以上の不正なテンプレートパラメータが含まれています。"
|
||||
|
||||
msgid "The query context datasource does not match the chart datasource"
|
||||
msgstr "クエリのコンテキストのデータソースが、チャートのデータソースと一致しません"
|
||||
msgstr ""
|
||||
|
||||
msgid "The query couldn't be loaded"
|
||||
msgstr "クエリを読み込めませんでした"
|
||||
@@ -13592,7 +13566,7 @@ msgid "There was an error fetching the filtered charts and dashboards:"
|
||||
msgstr "フィルタリングされたチャートおよびダッシュボードの取得中にエラーが発生しました:"
|
||||
|
||||
msgid "There was an error generating the permalink."
|
||||
msgstr "固定リンクの生成中にエラーが発生しました。"
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error loading groups."
|
||||
msgstr "グループの読み込み中にエラーが発生しました。"
|
||||
@@ -13688,7 +13662,7 @@ msgstr "選択したレイヤーの削除中に問題が発生しました: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue deleting the selected queries: %s"
|
||||
msgstr "選択したクエリの削除中に問題が発生しました:%s"
|
||||
msgstr ""
|
||||
|
||||
#, python-format
|
||||
msgid "There was an issue deleting the selected templates: %s"
|
||||
@@ -13725,7 +13699,7 @@ msgid "There was an issue exporting the selected dashboards"
|
||||
msgstr "選択したダッシュボードのエクスポート中に問題が発生しました"
|
||||
|
||||
msgid "There was an issue exporting the selected queries"
|
||||
msgstr "選択したクエリのエクスポート中に問題が発生しました"
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an issue exporting the selected themes"
|
||||
msgstr "選択したテーマのエクスポート中に問題が発生しました"
|
||||
@@ -13866,7 +13840,7 @@ msgstr ""
|
||||
"注釈データを含むチャートに引き継ぐかどうかを制御します。"
|
||||
|
||||
msgid "This dashboard does not exist"
|
||||
msgstr "このダッシュボードは存在しません"
|
||||
msgstr ""
|
||||
|
||||
msgid "This dashboard is managed externally, and can't be edited in Superset"
|
||||
msgstr "このダッシュボードは外部で管理されているため、Superset では編集できません"
|
||||
@@ -14159,7 +14133,7 @@ msgid "Time Grain"
|
||||
msgstr "時間粒度"
|
||||
|
||||
msgid "Time Grain must be specified when using Time Comparison."
|
||||
msgstr "「時間比較」を使用する場合は、「時間粒度」を指定する必要があります。"
|
||||
msgstr ""
|
||||
|
||||
msgid "Time Granularity"
|
||||
msgstr "時間粒度"
|
||||
@@ -14428,9 +14402,6 @@ msgid ""
|
||||
" angle is a multiple of 90°, the chart is automatically re-centered to "
|
||||
"make use of the empty space."
|
||||
msgstr ""
|
||||
"チャートがカバーする総角度(度単位)。360°では完全な円が描かれ、180°では半円が描かれます。"
|
||||
"スイープ角度が180°以下で、開始角度が90°の倍数である場合、空きスペースを活用するために、"
|
||||
"チャートの中心位置が自動的に再設定されます。"
|
||||
|
||||
msgid "Total color"
|
||||
msgstr "合計の色"
|
||||
@@ -14455,7 +14426,7 @@ msgid "Transparent"
|
||||
msgstr "透明"
|
||||
|
||||
msgid "Transparent background"
|
||||
msgstr "透明な背景"
|
||||
msgstr ""
|
||||
|
||||
msgid "Transpose pivot"
|
||||
msgstr "ピボットを転置"
|
||||
@@ -14485,7 +14456,7 @@ msgid "Trigger Alert If..."
|
||||
msgstr "アラートの発生条件..."
|
||||
|
||||
msgid "Trigger now"
|
||||
msgstr "今すぐ実行"
|
||||
msgstr ""
|
||||
|
||||
msgid "True"
|
||||
msgstr "真 (True)"
|
||||
@@ -14590,7 +14561,7 @@ msgid "URL parameters"
|
||||
msgstr "URLパラメータ"
|
||||
|
||||
msgid "UUID to track the execution status"
|
||||
msgstr "実行状況を追跡するためのUUID"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to calculate such a date delta"
|
||||
msgstr "指定された期間差を計算できません"
|
||||
@@ -14649,7 +14620,7 @@ msgstr "ダウンロード用ペイロードを生成できません"
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to generate forecast: %(error)s"
|
||||
msgstr "予測を生成できませんでした:%(error)s"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Unable to identify temporal column for date range time comparison.Please "
|
||||
@@ -14660,7 +14631,7 @@ msgstr "期間比較のための時間列を特定できません。データセ
|
||||
msgid ""
|
||||
"Unable to interpret the time offset: %(offset)s. Use a relative time such"
|
||||
" as \"1 month ago\"."
|
||||
msgstr "時間オフセット %(offset)s を解釈できません。「1ヶ月前」などの相対的な時間を使用してください。"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Unable to load columns for the selected table. Please select a different "
|
||||
@@ -15530,7 +15501,7 @@ msgid "Whether to display bubbles on top of countries"
|
||||
msgstr "国の上にバブルを表示するかどうか"
|
||||
|
||||
msgid "Whether to display entries with null values in the hierarchy"
|
||||
msgstr "階層内でNULL値を持つエントリを表示するかどうか"
|
||||
msgstr ""
|
||||
|
||||
msgid "Whether to display in the chart"
|
||||
msgstr "チャートに表示するかどうか"
|
||||
@@ -15655,8 +15626,6 @@ msgid ""
|
||||
"Whether to sort tooltip by the selected metric in descending order. On "
|
||||
"stacked charts, values are shown in ascending order."
|
||||
msgstr ""
|
||||
"ツールチップを選択した指標に基づいて降順で並べ替えるかどうか。"
|
||||
"積み上げグラフでは、値は昇順で表示されます。"
|
||||
|
||||
msgid "Whether to truncate metrics"
|
||||
msgstr "メトリックを切り詰めるかどうか"
|
||||
@@ -15816,7 +15785,7 @@ msgid "Y-axis bounds"
|
||||
msgstr "Y軸の境界"
|
||||
|
||||
msgid "Y-axis range slider"
|
||||
msgstr "Y軸の範囲スライダー"
|
||||
msgstr ""
|
||||
|
||||
msgid "Y-scale interval"
|
||||
msgstr "Y軸の間隔"
|
||||
@@ -16052,36 +16021,26 @@ msgid ""
|
||||
"You must be a chart editor in order to delete. Please reach out to a "
|
||||
"chart editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"削除を行うには、チャート編集者である必要があります。"
|
||||
"変更依頼や編集権限の付与については、チャート編集者にお問い合わせください。"
|
||||
|
||||
msgid ""
|
||||
"You must be a chart editor in order to edit. Please reach out to a chart "
|
||||
"editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"編集を行うには、チャート編集者である必要があります。"
|
||||
"変更依頼や編集権限の付与については、チャート編集者にお問い合わせください。"
|
||||
|
||||
msgid ""
|
||||
"You must be a dashboard editor in order to delete. Please reach out to a "
|
||||
"dashboard editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"削除を行うには、ダッシュボード編集者である必要があります。"
|
||||
"変更や編集権限の付与をご希望の場合は、ダッシュボード編集者にお問い合わせください。"
|
||||
|
||||
msgid ""
|
||||
"You must be a dashboard editor in order to edit. Please reach out to a "
|
||||
"dashboard editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"編集を行うには、ダッシュボード編集者である必要があります。"
|
||||
"変更依頼や編集権限の付与については、ダッシュボード編集者にお問い合わせください。"
|
||||
|
||||
msgid ""
|
||||
"You must be a dataset editor in order to delete. Please reach out to a "
|
||||
"dataset editor to request modifications or edit access."
|
||||
msgstr ""
|
||||
"削除を行うには、データセット編集者である必要があります。"
|
||||
"変更の依頼や編集権限の付与については、データセット編集者にお問い合わせください。"
|
||||
|
||||
msgid ""
|
||||
"You must be a dataset editor in order to edit. Please reach out to a "
|
||||
@@ -16145,9 +16104,6 @@ msgid ""
|
||||
"into multiple dashboards) or raise the "
|
||||
"SUPERSET_DASHBOARD_POSITION_DATA_LIMIT config setting."
|
||||
msgstr ""
|
||||
"ダッシュボードが大きすぎて保存できません。シリアル化されたレイアウトの長さは %s ですが、制限値は %s です。"
|
||||
"ダッシュボードのサイズを縮小するか(たとえば複数のダッシュボードに分割するなど)"
|
||||
"SUPERSET_DASHBOARD_POSITION_DATA_LIMIT 設定値を上げてください。"
|
||||
|
||||
msgid "Your query could not be saved"
|
||||
msgstr "クエリを保存できませんでした"
|
||||
@@ -16921,7 +16877,7 @@ msgid ""
|
||||
msgstr "パーセンタイルは、1つ目が2つ目より小さい2つの数値のリストまたはタプルである必要があります"
|
||||
|
||||
msgid "permalink state not found"
|
||||
msgstr "固定リンクの状態が見つかりません"
|
||||
msgstr "パーマリンクの状態が見つかりません"
|
||||
|
||||
#. do-not-translate
|
||||
msgid "pivoted_xlsx"
|
||||
@@ -16950,7 +16906,7 @@ msgid "quarter"
|
||||
msgstr "四半期"
|
||||
|
||||
msgid "queries"
|
||||
msgstr "クエリ"
|
||||
msgstr ""
|
||||
|
||||
msgid "query"
|
||||
msgstr "クエリ"
|
||||
@@ -16996,7 +16952,7 @@ msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "semantic layer"
|
||||
msgstr "セマンティックレイヤー"
|
||||
msgstr ""
|
||||
|
||||
msgid "series"
|
||||
msgstr "系列"
|
||||
|
||||
@@ -2017,26 +2017,13 @@ def _process_datetime_column(
|
||||
|
||||
# Parse with or without format (suppress warning if no format)
|
||||
if format_to_use:
|
||||
converted = pd.to_datetime(
|
||||
df[col.col_label] = pd.to_datetime(
|
||||
df[col.col_label],
|
||||
utc=False,
|
||||
format=format_to_use,
|
||||
errors="coerce",
|
||||
exact=False,
|
||||
)
|
||||
# A format that coerces every non-null value to NaT is a mismatch
|
||||
# (e.g. an epoch-millis column that inherited a '%Y' string format
|
||||
# when used as a chart's granularity). Assigning it would silently
|
||||
# blank the whole column, so keep the original values instead.
|
||||
if df[col.col_label].notna().any() and not converted.notna().any():
|
||||
logger.warning(
|
||||
"Datetime format %s coerced every value of column %s to NaT; "
|
||||
"keeping the original values",
|
||||
format_to_use,
|
||||
col.col_label,
|
||||
)
|
||||
else:
|
||||
df[col.col_label] = converted
|
||||
else:
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message=".*Could not infer format.*")
|
||||
|
||||
@@ -35,7 +35,6 @@ from flask_appbuilder.security.sqla import models as ab_models
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
from flask_babel import _
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from superset import appbuilder, dataframe, db, result_set, viz
|
||||
from superset.common.db_query_status import QueryStatus
|
||||
@@ -198,27 +197,6 @@ def loads_request_json(request_json_data: str) -> dict[Any, Any]:
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
|
||||
|
||||
def get_request_json_body() -> dict[Any, Any]:
|
||||
"""Parse the request body as JSON, coercing failures to ``{}``.
|
||||
|
||||
``request.is_json`` only inspects the Content-Type header, not whether the
|
||||
body is actually parseable JSON. Callers reaching ``get_form_data`` from a
|
||||
non-HTTP-chart-data context (e.g. an MCP tool call rendering
|
||||
``filter_values()``) can have a request context whose Content-Type claims
|
||||
JSON but whose body isn't a JSON chart-data payload, which makes Werkzeug
|
||||
raise ``BadRequest`` from ``request.get_json()``. A well-formed but
|
||||
non-object JSON body (e.g. ``null``, a scalar, or an array) is coerced to
|
||||
``{}`` too, since callers treat the result as a mapping.
|
||||
"""
|
||||
if not request.is_json:
|
||||
return {}
|
||||
try:
|
||||
data = request.get_json(cache=True)
|
||||
except BadRequest:
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
|
||||
#: Parameter names `url_for` interprets itself rather than appending to the
|
||||
#: query string. Request-supplied query keys matching these must never be
|
||||
#: forwarded as `url_for` kwargs.
|
||||
@@ -367,7 +345,7 @@ def get_form_data(
|
||||
form_data: dict[str, Any] = initial_form_data or {}
|
||||
|
||||
if has_request_context():
|
||||
json_data = get_request_json_body()
|
||||
json_data = request.get_json(cache=True) if request.is_json else {}
|
||||
|
||||
# chart data API requests are JSON
|
||||
first_query = (
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
from typing import Any, Optional, Type
|
||||
from unittest import mock
|
||||
|
||||
from superset.async_events.async_query_manager import (
|
||||
AsyncQueryJobException,
|
||||
AsyncQueryTokenException,
|
||||
)
|
||||
from superset.async_events.cache_backend import (
|
||||
RedisCacheBackend,
|
||||
RedisSentinelCacheBackend,
|
||||
@@ -34,16 +30,12 @@ from tests.integration_tests.test_app import app
|
||||
|
||||
class TestAsyncEventApi(SupersetTestCase):
|
||||
UUID = "943c920-32a5-412a-977d-b8e47d36f5a4"
|
||||
JOB_ID = "10a0bd9a-03c8-4737-9345-f4234ba86512"
|
||||
|
||||
def fetch_events(self, last_id: Optional[str] = None):
|
||||
base_uri = "api/v1/async_event/"
|
||||
uri = f"{base_uri}?last_id={last_id}" if last_id else base_uri
|
||||
return self.client.get(uri)
|
||||
|
||||
def cancel_event(self, job_id: str):
|
||||
return self.client.post(f"api/v1/async_event/{job_id}/cancel")
|
||||
|
||||
def run_test_with_cache_backend(self, cache_backend_cls: Type[Any], test_func):
|
||||
app._got_first_request = False
|
||||
async_query_manager_factory.init_app(app)
|
||||
@@ -144,67 +136,3 @@ class TestAsyncEventApi(SupersetTestCase):
|
||||
self.client.set_cookie(app.config["GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME"], "")
|
||||
rv = self.fetch_events()
|
||||
assert rv.status_code == 401
|
||||
|
||||
def _test_cancel_logic(self, mock_cache):
|
||||
with mock.patch.object(async_query_manager, "cancel_job") as mock_cancel:
|
||||
rv = self.cancel_event(self.JOB_ID)
|
||||
|
||||
assert rv.status_code == 200
|
||||
mock_cancel.assert_called_once()
|
||||
assert mock_cancel.call_args.args[0] == self.JOB_ID
|
||||
response = json.loads(rv.data.decode("utf-8"))
|
||||
assert response["result"] == {"job_id": self.JOB_ID, "status": "cancelled"}
|
||||
|
||||
def _test_cancel_invalid_job_id_logic(self, mock_cache):
|
||||
# job_id lands in a Redis key, so reject anything that isn't a UUID
|
||||
# before it reaches the cache backend.
|
||||
with mock.patch.object(async_query_manager, "cancel_job") as mock_cancel:
|
||||
rv = self.cancel_event("not-a-uuid")
|
||||
assert rv.status_code == 400
|
||||
mock_cancel.assert_not_called()
|
||||
|
||||
def _test_cancel_forbidden_logic(self, mock_cache):
|
||||
with mock.patch.object(
|
||||
async_query_manager,
|
||||
"cancel_job",
|
||||
side_effect=AsyncQueryTokenException("nope"),
|
||||
):
|
||||
rv = self.cancel_event(self.JOB_ID)
|
||||
assert rv.status_code == 403
|
||||
|
||||
def _test_cancel_not_found_logic(self, mock_cache):
|
||||
with mock.patch.object(
|
||||
async_query_manager,
|
||||
"cancel_job",
|
||||
side_effect=AsyncQueryJobException("gone"),
|
||||
):
|
||||
rv = self.cancel_event(self.JOB_ID)
|
||||
assert rv.status_code == 404
|
||||
|
||||
@mock.patch("uuid.uuid4", return_value=UUID)
|
||||
def test_cancel_redis_cache_backend(self, mock_uuid4):
|
||||
self.run_test_with_cache_backend(RedisCacheBackend, self._test_cancel_logic)
|
||||
|
||||
@mock.patch("uuid.uuid4", return_value=UUID)
|
||||
def test_cancel_invalid_job_id(self, mock_uuid4):
|
||||
self.run_test_with_cache_backend(
|
||||
RedisCacheBackend, self._test_cancel_invalid_job_id_logic
|
||||
)
|
||||
|
||||
@mock.patch("uuid.uuid4", return_value=UUID)
|
||||
def test_cancel_forbidden(self, mock_uuid4):
|
||||
self.run_test_with_cache_backend(
|
||||
RedisCacheBackend, self._test_cancel_forbidden_logic
|
||||
)
|
||||
|
||||
@mock.patch("uuid.uuid4", return_value=UUID)
|
||||
def test_cancel_not_found(self, mock_uuid4):
|
||||
self.run_test_with_cache_backend(
|
||||
RedisCacheBackend, self._test_cancel_not_found_logic
|
||||
)
|
||||
|
||||
def test_cancel_no_login(self):
|
||||
app._got_first_request = False
|
||||
async_query_manager_factory.init_app(app)
|
||||
rv = self.cancel_event(self.JOB_ID)
|
||||
assert rv.status_code == 401
|
||||
|
||||
@@ -24,7 +24,6 @@ from pytest import fixture, mark, raises # noqa: PT013
|
||||
|
||||
from superset import security_manager
|
||||
from superset.async_events.async_query_manager import (
|
||||
AsyncQueryJobException,
|
||||
AsyncQueryManager,
|
||||
AsyncQueryTokenException,
|
||||
)
|
||||
@@ -32,7 +31,6 @@ from superset.async_events.cache_backend import (
|
||||
RedisCacheBackend,
|
||||
RedisSentinelCacheBackend,
|
||||
)
|
||||
from superset.utils import json
|
||||
|
||||
JWT_TOKEN_SECRET = "some_secret" # noqa: S105
|
||||
JWT_TOKEN_COOKIE_NAME = "superset_async_jwt" # noqa: S105
|
||||
@@ -308,7 +306,6 @@ def test_submit_chart_data_job_as_guest_user(
|
||||
},
|
||||
{},
|
||||
],
|
||||
task_id=ANY,
|
||||
expires=3600,
|
||||
)
|
||||
|
||||
@@ -417,174 +414,7 @@ def test_submit_explore_json_job_as_guest_user(
|
||||
"json",
|
||||
False,
|
||||
],
|
||||
task_id=ANY,
|
||||
expires=3600,
|
||||
)
|
||||
|
||||
assert "guest_token" not in job_meta
|
||||
|
||||
|
||||
@fixture
|
||||
def cancellable_manager():
|
||||
"""A manager wired to a mock Redis backend for cancellation tests."""
|
||||
manager = AsyncQueryManager()
|
||||
manager._jwt_expiration_seconds = 3600
|
||||
manager._stream_prefix = "async-events-"
|
||||
manager._cache = mock.Mock(spec=RedisCacheBackend)
|
||||
return manager
|
||||
|
||||
|
||||
def test_init_job_registers_cancellable_record(cancellable_manager):
|
||||
"""init_job persists the owner identity a later cancel must match."""
|
||||
cancellable_manager.init_job("chan-1", 7)
|
||||
|
||||
cancellable_manager._cache.set.assert_called_once()
|
||||
key, value = cancellable_manager._cache.set.call_args.args
|
||||
assert key.startswith("async-events-job-cancel:")
|
||||
assert json.loads(value) == {"channel_id": "chan-1", "user_id": 7}
|
||||
|
||||
|
||||
def test_cancel_job_authorized_revokes_task(cancellable_manager):
|
||||
cancellable_manager._stream_limit = 100
|
||||
cancellable_manager._stream_limit_firehose = 1000
|
||||
cancellable_manager._cache.get.return_value = json.dumps(
|
||||
{"channel_id": "chan-1", "user_id": 7}
|
||||
)
|
||||
cancellable_manager._cache.set.return_value = True
|
||||
|
||||
with mock.patch("superset.extensions.celery_app") as celery_app:
|
||||
cancellable_manager.cancel_job("job-1", "chan-1", 7)
|
||||
|
||||
celery_app.control.revoke.assert_called_once_with(
|
||||
"job-1", terminate=True, signal="SIGUSR1"
|
||||
)
|
||||
# The job is flagged cancelled (conditionally, xx=True) so the worker knows
|
||||
# what the signal it is about to receive means.
|
||||
assert cancellable_manager._cache.set.call_args.kwargs["xx"] is True
|
||||
flagged = json.loads(cancellable_manager._cache.set.call_args.args[1])
|
||||
assert flagged["cancelled"] is True
|
||||
|
||||
|
||||
def test_cancel_job_emits_the_terminal_event(cancellable_manager):
|
||||
"""A task revoked before a worker picks it up never reports on itself."""
|
||||
cancellable_manager._stream_limit = 100
|
||||
cancellable_manager._stream_limit_firehose = 1000
|
||||
cancellable_manager._cache.get.return_value = json.dumps(
|
||||
{"channel_id": "chan-1", "user_id": 7}
|
||||
)
|
||||
cancellable_manager._cache.set.return_value = True
|
||||
|
||||
with mock.patch("superset.extensions.celery_app"):
|
||||
cancellable_manager.cancel_job("job-1", "chan-1", 7)
|
||||
|
||||
scoped_stream, event_data = cancellable_manager._cache.xadd.call_args_list[0].args[
|
||||
:2
|
||||
]
|
||||
assert scoped_stream == "async-events-chan-1"
|
||||
assert json.loads(event_data["data"]) == {
|
||||
"channel_id": "chan-1",
|
||||
"job_id": "job-1",
|
||||
"user_id": 7,
|
||||
"status": AsyncQueryManager.STATUS_CANCELLED,
|
||||
"errors": [],
|
||||
"result_url": None,
|
||||
}
|
||||
# the record has to outlive the event: the worker still needs to recognize
|
||||
# the signal on its way as a cancellation
|
||||
cancellable_manager._cache.delete.assert_not_called()
|
||||
|
||||
|
||||
def test_cancel_job_completed_between_read_and_flag(cancellable_manager):
|
||||
"""If the job's record is cleared after the auth read, don't revoke."""
|
||||
cancellable_manager._cache.get.return_value = json.dumps(
|
||||
{"channel_id": "chan-1", "user_id": 7}
|
||||
)
|
||||
# Conditional (xx) write finds no key: the job finished and cleaned up.
|
||||
cancellable_manager._cache.set.return_value = None
|
||||
|
||||
with (
|
||||
mock.patch("superset.extensions.celery_app") as celery_app,
|
||||
raises(AsyncQueryJobException),
|
||||
):
|
||||
cancellable_manager.cancel_job("job-1", "chan-1", 7)
|
||||
|
||||
celery_app.control.revoke.assert_not_called()
|
||||
|
||||
|
||||
def test_cancel_job_wrong_user_is_rejected(cancellable_manager):
|
||||
cancellable_manager._cache.get.return_value = json.dumps(
|
||||
{"channel_id": "chan-1", "user_id": 7}
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch("superset.extensions.celery_app") as celery_app,
|
||||
raises(AsyncQueryTokenException),
|
||||
):
|
||||
cancellable_manager.cancel_job("job-1", "chan-1", 999)
|
||||
|
||||
celery_app.control.revoke.assert_not_called()
|
||||
|
||||
|
||||
def test_cancel_job_wrong_channel_is_rejected(cancellable_manager):
|
||||
"""A matching user on a different channel still cannot cancel the job."""
|
||||
cancellable_manager._cache.get.return_value = json.dumps(
|
||||
{"channel_id": "chan-1", "user_id": 7}
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch("superset.extensions.celery_app") as celery_app,
|
||||
raises(AsyncQueryTokenException),
|
||||
):
|
||||
cancellable_manager.cancel_job("job-1", "other-chan", 7)
|
||||
|
||||
celery_app.control.revoke.assert_not_called()
|
||||
|
||||
|
||||
def test_cancel_job_unknown_raises(cancellable_manager):
|
||||
cancellable_manager._cache.get.return_value = None
|
||||
|
||||
with (
|
||||
mock.patch("superset.extensions.celery_app") as celery_app,
|
||||
raises(AsyncQueryJobException),
|
||||
):
|
||||
cancellable_manager.cancel_job("job-1", "chan-1", 7)
|
||||
|
||||
celery_app.control.revoke.assert_not_called()
|
||||
|
||||
|
||||
def test_is_job_cancelled(cancellable_manager):
|
||||
cancellable_manager._cache.get.return_value = json.dumps(
|
||||
{"channel_id": "chan-1", "user_id": 7, "cancelled": True}
|
||||
)
|
||||
assert cancellable_manager.is_job_cancelled("job-1") is True
|
||||
|
||||
cancellable_manager._cache.get.return_value = json.dumps(
|
||||
{"channel_id": "chan-1", "user_id": 7}
|
||||
)
|
||||
assert cancellable_manager.is_job_cancelled("job-1") is False
|
||||
|
||||
cancellable_manager._cache.get.return_value = None
|
||||
assert cancellable_manager.is_job_cancelled("job-1") is False
|
||||
|
||||
|
||||
def test_is_job_cancelled_swallows_cache_errors(cancellable_manager):
|
||||
"""A cache failure must not escape and mask the worker's original error."""
|
||||
cancellable_manager._cache.get.side_effect = RuntimeError("redis down")
|
||||
assert cancellable_manager.is_job_cancelled("job-1") is False
|
||||
|
||||
|
||||
def test_update_job_clears_registry_before_terminal_event(cancellable_manager):
|
||||
"""Clearing first is what makes a cancel that lost the race a 404."""
|
||||
calls = []
|
||||
cancellable_manager._stream_limit = 100
|
||||
cancellable_manager._stream_limit_firehose = 1000
|
||||
cancellable_manager._cache.delete.side_effect = lambda *_: calls.append("delete")
|
||||
cancellable_manager._cache.xadd.side_effect = lambda *_: calls.append("xadd")
|
||||
job_metadata = {"channel_id": "chan-1", "job_id": "job-1", "user_id": 7}
|
||||
|
||||
cancellable_manager.update_job(job_metadata, AsyncQueryManager.STATUS_DONE)
|
||||
|
||||
cancellable_manager._cache.delete.assert_called_once_with(
|
||||
"async-events-job-cancel:job-1"
|
||||
)
|
||||
assert calls == ["delete", "xadd", "xadd"]
|
||||
|
||||
@@ -1,46 +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.
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def test_image_tag_services_use_bundled_packages() -> None:
|
||||
repository_root = Path(__file__).resolve().parents[2]
|
||||
compose = yaml.safe_load(
|
||||
(repository_root / "docker-compose-image-tag.yml").read_text(),
|
||||
)
|
||||
image = compose["x-superset-image"]
|
||||
image_services = [
|
||||
service
|
||||
for service in compose["services"].values()
|
||||
if service.get("image") == image
|
||||
]
|
||||
|
||||
assert image_services
|
||||
assert all(
|
||||
service.get("environment", {}).get("DEV_MODE") == "false"
|
||||
for service in image_services
|
||||
)
|
||||
assert all(
|
||||
all(
|
||||
not isinstance(volume, str) or ":/app/superset-core" not in volume
|
||||
for volume in service.get("volumes", [])
|
||||
)
|
||||
for service in image_services
|
||||
)
|
||||
@@ -216,102 +216,6 @@ class TestLoggingMiddlewareOnCallTool:
|
||||
assert call_kwargs["slice_id"] == 20
|
||||
assert call_kwargs["curated_payload"]["dataset_id"] == 30
|
||||
|
||||
@patch("superset.mcp_service.middleware.event_logger")
|
||||
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_call_tool_extracts_chart_id_from_response(
|
||||
self, mock_get_user_id, mock_event_logger
|
||||
) -> None:
|
||||
"""generate_chart takes no chart_id as input, so on a successful
|
||||
create the new chart's ID must be pulled from the response body
|
||||
instead -- otherwise every retry logs slice_id=None and a
|
||||
successful attempt can't be told apart from the failed ones.
|
||||
"""
|
||||
middleware = LoggingMiddleware()
|
||||
ctx = _make_context(name="generate_chart", params={"dataset_id": 5})
|
||||
response_text = (
|
||||
'{"success": true, "chart": {"id": 123, "slice_name": "My Chart"}}'
|
||||
)
|
||||
original_result = ToolResult(
|
||||
content=[mt.TextContent(type="text", text=response_text)]
|
||||
)
|
||||
call_next = AsyncMock(return_value=original_result)
|
||||
|
||||
await middleware.on_call_tool(ctx, call_next)
|
||||
|
||||
call_kwargs = mock_event_logger.log.call_args[1]
|
||||
assert call_kwargs["slice_id"] == 123
|
||||
assert call_kwargs["curated_payload"]["slice_id"] == 123
|
||||
assert call_kwargs["curated_payload"]["success"] is True
|
||||
|
||||
@patch("superset.mcp_service.middleware.event_logger")
|
||||
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_call_tool_extracts_dashboard_id_from_response(
|
||||
self, mock_get_user_id, mock_event_logger
|
||||
) -> None:
|
||||
"""generate_dashboard likewise creates an ID that only appears in
|
||||
the response, not the input params."""
|
||||
middleware = LoggingMiddleware()
|
||||
ctx = _make_context(name="generate_dashboard", params={"chart_ids": [1, 2]})
|
||||
response_text = '{"success": true, "dashboard": {"id": 456}}'
|
||||
original_result = ToolResult(
|
||||
content=[mt.TextContent(type="text", text=response_text)]
|
||||
)
|
||||
call_next = AsyncMock(return_value=original_result)
|
||||
|
||||
await middleware.on_call_tool(ctx, call_next)
|
||||
|
||||
call_kwargs = mock_event_logger.log.call_args[1]
|
||||
assert call_kwargs["dashboard_id"] == 456
|
||||
assert call_kwargs["curated_payload"]["dashboard_id"] == 456
|
||||
|
||||
@patch("superset.mcp_service.middleware.event_logger")
|
||||
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_call_tool_does_not_extract_id_on_failed_response(
|
||||
self, mock_get_user_id, mock_event_logger
|
||||
) -> None:
|
||||
"""A failed create (error schema response, no exception raised)
|
||||
must not report a chart_id -- nothing was actually persisted."""
|
||||
middleware = LoggingMiddleware()
|
||||
ctx = _make_context(name="generate_chart", params={"dataset_id": 5})
|
||||
response_text = (
|
||||
'{"success": false, "chart": null, '
|
||||
'"error": {"error_type": "validation_error"}}'
|
||||
)
|
||||
original_result = ToolResult(
|
||||
content=[mt.TextContent(type="text", text=response_text)]
|
||||
)
|
||||
call_next = AsyncMock(return_value=original_result)
|
||||
|
||||
await middleware.on_call_tool(ctx, call_next)
|
||||
|
||||
call_kwargs = mock_event_logger.log.call_args[1]
|
||||
assert call_kwargs["curated_payload"]["success"] is False
|
||||
assert call_kwargs["slice_id"] is None
|
||||
|
||||
@patch("superset.mcp_service.middleware.event_logger")
|
||||
@patch("superset.mcp_service.middleware.get_user_id", return_value=42)
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_call_tool_prefers_input_slice_id_over_response(
|
||||
self, mock_get_user_id, mock_event_logger
|
||||
) -> None:
|
||||
"""When chart_id is already known from input params (e.g.
|
||||
update_chart), the response body must not override it."""
|
||||
middleware = LoggingMiddleware()
|
||||
ctx = _make_context(name="update_chart", params={"chart_id": 111})
|
||||
response_text = '{"success": true, "chart": {"id": 999}}'
|
||||
original_result = ToolResult(
|
||||
content=[mt.TextContent(type="text", text=response_text)]
|
||||
)
|
||||
call_next = AsyncMock(return_value=original_result)
|
||||
|
||||
await middleware.on_call_tool(ctx, call_next)
|
||||
|
||||
call_kwargs = mock_event_logger.log.call_args[1]
|
||||
assert call_kwargs["slice_id"] == 111
|
||||
|
||||
|
||||
class TestLoggingMiddlewareOnMessage:
|
||||
"""Tests for LoggingMiddleware.on_message()."""
|
||||
@@ -564,40 +468,6 @@ class TestIsErrorResponse:
|
||||
assert call_kwargs["curated_payload"]["tool"] == "get_chart_info"
|
||||
|
||||
|
||||
class TestExtractOutputIds:
|
||||
"""Tests for LoggingMiddleware._extract_output_ids()."""
|
||||
|
||||
def test_returns_none_for_non_json_body(self) -> None:
|
||||
"""A malformed/non-JSON response body must not raise -- the
|
||||
defensive try/except should fall back to (None, None)."""
|
||||
middleware = LoggingMiddleware()
|
||||
result = ToolResult(
|
||||
content=[mt.TextContent(type="text", text="not valid json {{{")]
|
||||
)
|
||||
assert middleware._extract_output_ids(result) == (None, None)
|
||||
|
||||
def test_returns_none_for_empty_content(self) -> None:
|
||||
"""A ToolResult with no content items must not raise."""
|
||||
middleware = LoggingMiddleware()
|
||||
assert middleware._extract_output_ids(ToolResult(content=[])) == (
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
def test_returns_none_for_non_dict_json_body(self) -> None:
|
||||
"""A JSON body that parses but isn't an object (e.g. a bare
|
||||
list) must not raise and must yield no IDs."""
|
||||
middleware = LoggingMiddleware()
|
||||
result = ToolResult(content=[mt.TextContent(type="text", text="[1, 2, 3]")])
|
||||
assert middleware._extract_output_ids(result) == (None, None)
|
||||
|
||||
def test_extracts_both_ids_from_flat_response(self) -> None:
|
||||
middleware = LoggingMiddleware()
|
||||
response_text = '{"chart_id": 123, "dashboard_id": 456}'
|
||||
result = ToolResult(content=[mt.TextContent(type="text", text=response_text)])
|
||||
assert middleware._extract_output_ids(result) == (456, 123)
|
||||
|
||||
|
||||
class TestMiddlewareChainOrder:
|
||||
"""Test that the middleware order from server.py logs failures correctly.
|
||||
|
||||
|
||||
@@ -94,10 +94,3 @@ def test_fetch_gives_up_after_max_retries() -> None:
|
||||
change_detector.fetch_files_github_api("http://api")
|
||||
|
||||
assert urlopen_mock.call_count == change_detector.MAX_RETRIES
|
||||
|
||||
|
||||
def test_image_tag_compose_changes_trigger_python_tests() -> None:
|
||||
assert change_detector.detect_changes(
|
||||
["docker-compose-image-tag.yml"],
|
||||
change_detector.PATTERNS["python"],
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ from typing import Any
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from celery.exceptions import SoftTimeLimitExceeded
|
||||
from flask_babel import lazy_gettext as _
|
||||
|
||||
from superset.commands.chart.exceptions import ChartDataQueryFailedError
|
||||
@@ -64,62 +63,6 @@ def test_load_chart_data_into_cache_with_error(
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("superset.tasks.async_queries.security_manager")
|
||||
@mock.patch("superset.tasks.async_queries.async_query_manager")
|
||||
@mock.patch("superset.tasks.async_queries.ChartDataQueryContextSchema")
|
||||
def test_load_chart_data_into_cache_cancelled_emits_no_event(
|
||||
mock_query_context_schema_cls, mock_async_query_manager, mock_security_manager
|
||||
):
|
||||
"""A revoke leaves the terminal event to the cancel request that sent it."""
|
||||
from superset.tasks.async_queries import load_chart_data_into_cache
|
||||
|
||||
job_metadata = {"user_id": 1, "job_id": "job-1"}
|
||||
form_data: dict[str, Any] = {}
|
||||
|
||||
mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
|
||||
# Sync Mock: is_job_cancelled is a plain method, but patching the manager
|
||||
# yields an AsyncMock whose calls would otherwise return truthy coroutines.
|
||||
mock_async_query_manager.is_job_cancelled = mock.Mock(return_value=True)
|
||||
mock_query_context_schema_cls.return_value.load.side_effect = (
|
||||
SoftTimeLimitExceeded()
|
||||
)
|
||||
|
||||
with pytest.raises(SoftTimeLimitExceeded):
|
||||
load_chart_data_into_cache(job_metadata, form_data)
|
||||
|
||||
mock_async_query_manager.is_job_cancelled.assert_called_once_with("job-1")
|
||||
mock_async_query_manager.update_job.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("superset.tasks.async_queries.security_manager")
|
||||
@mock.patch("superset.tasks.async_queries.async_query_manager")
|
||||
@mock.patch("superset.tasks.async_queries.ChartDataQueryContextSchema")
|
||||
def test_load_chart_data_into_cache_timeout_emits_error(
|
||||
mock_query_context_schema_cls, mock_async_query_manager, mock_security_manager
|
||||
):
|
||||
"""A genuine timeout reports an error, or the client waits forever."""
|
||||
from superset.tasks.async_queries import load_chart_data_into_cache
|
||||
|
||||
job_metadata = {"user_id": 1, "job_id": "job-1"}
|
||||
form_data: dict[str, Any] = {}
|
||||
|
||||
mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
|
||||
mock_async_query_manager.STATUS_ERROR = "error"
|
||||
mock_async_query_manager.is_job_cancelled = mock.Mock(return_value=False)
|
||||
mock_query_context_schema_cls.return_value.load.side_effect = (
|
||||
SoftTimeLimitExceeded()
|
||||
)
|
||||
|
||||
with pytest.raises(SoftTimeLimitExceeded):
|
||||
load_chart_data_into_cache(job_metadata, form_data)
|
||||
|
||||
mock_async_query_manager.update_job.assert_called_once_with(
|
||||
job_metadata,
|
||||
"error",
|
||||
errors=[{"message": "A timeout occurred while loading chart data"}],
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("superset.tasks.async_queries.security_manager")
|
||||
@mock.patch("superset.tasks.async_queries.async_query_manager")
|
||||
@mock.patch("superset.tasks.async_queries.ChartDataQueryContextSchema")
|
||||
@@ -430,71 +373,3 @@ def test_load_explore_json_into_cache_falls_back_to_string_for_generic_exception
|
||||
|
||||
errors = mock_async_query_manager.update_job.call_args[1]["errors"]
|
||||
assert errors == ["boom"]
|
||||
|
||||
|
||||
@mock.patch("superset.tasks.async_queries.security_manager")
|
||||
@mock.patch("superset.tasks.async_queries.async_query_manager")
|
||||
@mock.patch("superset.tasks.async_queries.get_viz")
|
||||
@mock.patch("superset.tasks.async_queries.get_datasource_info")
|
||||
def test_load_explore_json_into_cache_cancelled_emits_no_event(
|
||||
mock_get_datasource_info,
|
||||
mock_get_viz,
|
||||
mock_async_query_manager,
|
||||
mock_security_manager,
|
||||
):
|
||||
"""A revoke leaves the terminal event to the cancel request that sent it."""
|
||||
from superset.tasks.async_queries import load_explore_json_into_cache
|
||||
|
||||
job_metadata = {"user_id": 1, "job_id": "job-1"}
|
||||
form_data: dict = {}
|
||||
|
||||
mock_get_datasource_info.return_value = (1, "table")
|
||||
mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
|
||||
# Sync Mock: is_job_cancelled is a plain method, but patching the manager
|
||||
# yields an AsyncMock whose calls would otherwise return truthy coroutines.
|
||||
mock_async_query_manager.is_job_cancelled = mock.Mock(return_value=True)
|
||||
|
||||
viz_obj = mock.MagicMock()
|
||||
viz_obj.get_payload.side_effect = SoftTimeLimitExceeded()
|
||||
mock_get_viz.return_value = viz_obj
|
||||
|
||||
with pytest.raises(SoftTimeLimitExceeded):
|
||||
load_explore_json_into_cache(job_metadata, form_data)
|
||||
|
||||
mock_async_query_manager.is_job_cancelled.assert_called_once_with("job-1")
|
||||
mock_async_query_manager.update_job.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch("superset.tasks.async_queries.security_manager")
|
||||
@mock.patch("superset.tasks.async_queries.async_query_manager")
|
||||
@mock.patch("superset.tasks.async_queries.get_viz")
|
||||
@mock.patch("superset.tasks.async_queries.get_datasource_info")
|
||||
def test_load_explore_json_into_cache_timeout_emits_error(
|
||||
mock_get_datasource_info,
|
||||
mock_get_viz,
|
||||
mock_async_query_manager,
|
||||
mock_security_manager,
|
||||
):
|
||||
"""A genuine timeout reports an error, or the client waits forever."""
|
||||
from superset.tasks.async_queries import load_explore_json_into_cache
|
||||
|
||||
job_metadata = {"user_id": 1, "job_id": "job-1"}
|
||||
form_data: dict = {}
|
||||
|
||||
mock_get_datasource_info.return_value = (1, "table")
|
||||
mock_security_manager.get_user_by_id.return_value = mock.MagicMock()
|
||||
mock_async_query_manager.STATUS_ERROR = "error"
|
||||
mock_async_query_manager.is_job_cancelled = mock.Mock(return_value=False)
|
||||
|
||||
viz_obj = mock.MagicMock()
|
||||
viz_obj.get_payload.side_effect = SoftTimeLimitExceeded()
|
||||
mock_get_viz.return_value = viz_obj
|
||||
|
||||
with pytest.raises(SoftTimeLimitExceeded):
|
||||
load_explore_json_into_cache(job_metadata, form_data)
|
||||
|
||||
mock_async_query_manager.update_job.assert_called_once_with(
|
||||
job_metadata,
|
||||
"error",
|
||||
errors=[{"message": "A timeout occurred while loading explore json"}],
|
||||
)
|
||||
|
||||
@@ -235,24 +235,3 @@ def test_cron_schedule_window_chicago_daylight(
|
||||
assert (
|
||||
list(cron.strftime("%A, %d %B %Y, %H:%M:%S") for cron in datetimes) == expected # noqa: C400
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"current_dttm, cron, expected",
|
||||
[
|
||||
("2020-01-01T08:59:01+00:00", "0 0 30 2 *", []),
|
||||
("2020-01-01T08:59:01+00:00", "0 0 31 4 *", []),
|
||||
],
|
||||
)
|
||||
def test_cron_schedule_window_invalid_cron_date(
|
||||
current_dttm: str, cron: str, expected: list[FakeDatetime]
|
||||
) -> None:
|
||||
"""
|
||||
Reports scheduler: Test cron schedule window for a cron that is
|
||||
syntactically valid but can never match a real calendar date
|
||||
"""
|
||||
|
||||
datetimes = cron_schedule_window(datetime.fromisoformat(current_dttm), cron, "UTC")
|
||||
assert (
|
||||
list(cron.strftime("%A, %d %B %Y, %H:%M:%S") for cron in datetimes) == expected # noqa: C400
|
||||
)
|
||||
|
||||
@@ -273,22 +273,6 @@ def test_normalize_dttm_col() -> None:
|
||||
assert df["__time"].astype(str).tolist() == ["2017-07-01"]
|
||||
|
||||
|
||||
def test_normalize_dttm_col_mismatched_format_keeps_values() -> None:
|
||||
"""A datetime format that coerces every value to NaT is a mismatch (e.g. an
|
||||
epoch-millis column that inherited a ``%Y`` string format when used as a
|
||||
chart's granularity); applying it would silently blank the whole column, so
|
||||
the original values are kept instead of being nulled. Regression for the
|
||||
Samples pane showing N/A for such columns."""
|
||||
df = pd.DataFrame({"year": [1136073600000, 473385600000]}) # epoch ms
|
||||
before = df["year"].tolist()
|
||||
|
||||
normalize_dttm_col(df, (DateColumn(col_label="year", timestamp_format="%Y"),))
|
||||
|
||||
# not blanked to NaT/None
|
||||
assert df["year"].notna().all()
|
||||
assert df["year"].tolist() == before
|
||||
|
||||
|
||||
def test_normalize_dttm_col_epoch_seconds() -> None:
|
||||
"""Test conversion of epoch seconds."""
|
||||
df = pd.DataFrame(
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Tests for superset.views.utils module"""
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from superset.views.utils import get_form_data
|
||||
|
||||
|
||||
def test_get_form_data_handles_non_json_body_with_json_content_type() -> None:
|
||||
"""get_form_data returns gracefully when Content-Type claims JSON but the
|
||||
body isn't parseable JSON, instead of letting Werkzeug's BadRequest escape.
|
||||
|
||||
This is the shape of the request context an MCP tool call runs in when a
|
||||
chart/dataset SQL template calls the ``filter_values()`` Jinja macro: the
|
||||
Content-Type header says ``application/json`` but the body is not a JSON
|
||||
chart-data payload.
|
||||
"""
|
||||
with current_app.test_request_context(
|
||||
data="not-json-at-all", content_type="application/json"
|
||||
):
|
||||
form_data, slc = get_form_data()
|
||||
|
||||
assert form_data == {}
|
||||
assert slc is None
|
||||
|
||||
|
||||
def test_get_form_data_handles_non_dict_json_body() -> None:
|
||||
"""get_form_data coerces a well-formed but non-object JSON body to {}.
|
||||
|
||||
``request.get_json()`` happily returns a scalar or list for valid JSON
|
||||
that isn't a JSON object (e.g. ``null`` or ``42``). Downstream code treats
|
||||
the parsed body as a mapping, so a non-dict result must not leak through.
|
||||
"""
|
||||
with current_app.test_request_context(data="42", content_type="application/json"):
|
||||
form_data, slc = get_form_data()
|
||||
|
||||
assert form_data == {}
|
||||
assert slc is None
|
||||
Reference in New Issue
Block a user