mirror of
https://github.com/apache/superset.git
synced 2026-07-16 19:55:39 +00:00
Compare commits
18 Commits
codex/refa
...
dashboard-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c38c10f5fe | ||
|
|
47a566b91f | ||
|
|
6251fdfc3d | ||
|
|
d5e75c4813 | ||
|
|
cc32d16e0c | ||
|
|
258f4c035b | ||
|
|
d49365757c | ||
|
|
68881a60ca | ||
|
|
41efdb3082 | ||
|
|
e86dcf7b82 | ||
|
|
35d05cc278 | ||
|
|
115ce12ff7 | ||
|
|
ccfccdf237 | ||
|
|
42523f8cc4 | ||
|
|
e165762bb7 | ||
|
|
8afad27c84 | ||
|
|
e420812eb7 | ||
|
|
993a43396a |
2
.github/workflows/generate-FOSSA-report.yml
vendored
2
.github/workflows/generate-FOSSA-report.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "11"
|
||||
|
||||
2
.github/workflows/license-check.yml
vendored
2
.github/workflows/license-check.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "11"
|
||||
|
||||
2
.github/workflows/superset-docs-deploy.yml
vendored
2
.github/workflows/superset-docs-deploy.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "21"
|
||||
|
||||
4
.github/workflows/tag-release.yml
vendored
4
.github/workflows/tag-release.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- name: Use Node.js 20
|
||||
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
|
||||
- name: Use Node.js 20
|
||||
# zizmor: ignore[cache-poisoning] - node only runs the supersetbot CLI; no dependency cache is enabled
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"@swc/core": "^1.15.43",
|
||||
"antd": "^6.5.0",
|
||||
"baseline-browser-mapping": "^2.10.40",
|
||||
"caniuse-lite": "^1.0.30001802",
|
||||
"caniuse-lite": "^1.0.30001803",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.0",
|
||||
"js-yaml": "^5.2.0",
|
||||
@@ -90,7 +90,7 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.3",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
|
||||
@@ -5518,7 +5518,7 @@
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.62.1", "@typescript-eslint/parser@^8.61.0":
|
||||
"@typescript-eslint/parser@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.62.1.tgz#d3f7ba18f1bf78bfb7256fea021d1927b48e7080"
|
||||
integrity sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==
|
||||
@@ -5529,6 +5529,17 @@
|
||||
"@typescript-eslint/visitor-keys" "8.62.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/parser@^8.63.0":
|
||||
version "8.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.63.0.tgz#2993338c379903e6afc72c3532ae6e15b97334d4"
|
||||
integrity sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.63.0"
|
||||
"@typescript-eslint/types" "8.63.0"
|
||||
"@typescript-eslint/typescript-estree" "8.63.0"
|
||||
"@typescript-eslint/visitor-keys" "8.63.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.62.1.tgz#78d880eb1cf6859b5ec263d04f95403e9f90ae47"
|
||||
@@ -5538,6 +5549,15 @@
|
||||
"@typescript-eslint/types" "^8.62.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.63.0":
|
||||
version "8.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.63.0.tgz#01a3d0550a860127444a9939749ab434591cd71a"
|
||||
integrity sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.63.0"
|
||||
"@typescript-eslint/types" "^8.63.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz#7ee65e9a6eb3ccdc4816593a4ff38840306de88a"
|
||||
@@ -5546,11 +5566,29 @@
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
"@typescript-eslint/visitor-keys" "8.62.1"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.63.0":
|
||||
version "8.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz#c9cf7ecd234f7ec346f62e5c7d28dfaf4d507b4d"
|
||||
integrity sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.63.0"
|
||||
"@typescript-eslint/visitor-keys" "8.63.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.62.1", "@typescript-eslint/tsconfig-utils@^8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz#e2b5f24fe721044189cb7e81117c96d75979d627"
|
||||
integrity sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.63.0":
|
||||
version "8.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz#f7e1cf9a029bb71f4027ffa4194ba82afb564cd3"
|
||||
integrity sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@^8.63.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.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz#ebd30b13bacb13070917259a23309cf644121f9a"
|
||||
@@ -5562,11 +5600,21 @@
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.62.1", "@typescript-eslint/types@^8.62.1":
|
||||
"@typescript-eslint/types@8.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.1.tgz#c58be954e483b2fc98275374d5bcb40b99842dc1"
|
||||
integrity sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==
|
||||
|
||||
"@typescript-eslint/types@8.63.0":
|
||||
version "8.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.63.0.tgz#6b32b0a5913520554d81a986acfffba2d32b6963"
|
||||
integrity sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==
|
||||
|
||||
"@typescript-eslint/types@^8.62.1", "@typescript-eslint/types@^8.63.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.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz#98c1bb17635d5b026b24193a8d29188ac64380ff"
|
||||
@@ -5582,6 +5630,21 @@
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.63.0":
|
||||
version "8.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz#a6f9c9cd290e98203ad29850b0d72529dc83be73"
|
||||
integrity sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.63.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.63.0"
|
||||
"@typescript-eslint/types" "8.63.0"
|
||||
"@typescript-eslint/visitor-keys" "8.63.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.62.1":
|
||||
version "8.62.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.1.tgz#1622b75c7e6df308181dd0b44855dc4228da0457"
|
||||
@@ -5600,6 +5663,14 @@
|
||||
"@typescript-eslint/types" "8.62.1"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.63.0":
|
||||
version "8.63.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz#2853a8e33b52f23570338f96cc89cb223daabd44"
|
||||
integrity sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.63.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.2.tgz#a03ad82cd5676414d068ba86f880c5681194aadf"
|
||||
@@ -6567,10 +6638,10 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001802:
|
||||
version "1.0.30001802"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001802.tgz#2671fb13d468930586c56ffa80feb1c51e18ec69"
|
||||
integrity sha512-vmv8ub2xwTNmljSKf82mtCk5JH7hC+YgzLj3P5zotvA0tPQ9016tdNNOG8WRca1IxOnhSsivB+J0z5FeE5LOUw==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001803:
|
||||
version "1.0.30001803"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz#b2a5d696e042bc8304dcd4942c39fe330fbbcb24"
|
||||
integrity sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
|
||||
@@ -37,6 +37,8 @@ filterwarnings =
|
||||
# error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:The ``declarative_base\(\)`` function is now available:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The Engine.execute\(\) method is considered legacy:sqlalchemy.exc.RemovedIn20Warning
|
||||
|
||||
387
superset-frontend/package-lock.json
generated
387
superset-frontend/package-lock.json
generated
@@ -188,7 +188,7 @@
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.43",
|
||||
"@swc/plugin-emotion": "^14.14.1",
|
||||
"@swc/plugin-emotion": "^14.15.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
@@ -216,7 +216,7 @@
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
@@ -260,7 +260,7 @@
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.6.1",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.72.0",
|
||||
"oxlint": "^1.73.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"prettier": "3.9.4",
|
||||
@@ -289,7 +289,7 @@
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
"webpack-manifest-plugin": "^6.0.1",
|
||||
"webpack-sources": "^3.5.0",
|
||||
"webpack-sources": "^3.5.1",
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8443,9 +8443,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm-eabi": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.72.0.tgz",
|
||||
"integrity": "sha512-zhCmvn+1Mj3UchAc/90i99S0t7jJUsHmFVSPg4UWrjO8b8eaSGwscgO6QAUtvHBstkjQwBttQNswEnAF1mIQdA==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz",
|
||||
"integrity": "sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8460,9 +8460,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm64": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.72.0.tgz",
|
||||
"integrity": "sha512-mtH+aY/ozv1eZoCUC2owjFAtyNBKHpJHygKeEu9zXXnQGW1Q2/qOpvx+I+Lf23+TvTz66F4iiXUbl2cGvoLPCQ==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.73.0.tgz",
|
||||
"integrity": "sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8477,9 +8477,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-arm64": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.72.0.tgz",
|
||||
"integrity": "sha512-EvnajNPDtfknB3ZieeOOyDTwJn9QXDiwfnF4ZDQqART6RG6hjY4WigQcZdGoK2dkB3e1vrmEzN9aYbQCUkh/gQ==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.73.0.tgz",
|
||||
"integrity": "sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8494,9 +8494,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-darwin-x64": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.72.0.tgz",
|
||||
"integrity": "sha512-ZkCdEa/G80A7vEHfeCDz/+L3m33DE73v32mDKhgOIgz8Uwf0DFcK7+uu6qC+7LEhmz5fpOe1osWKyjSNMydFIQ==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.73.0.tgz",
|
||||
"integrity": "sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8511,9 +8511,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-freebsd-x64": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.72.0.tgz",
|
||||
"integrity": "sha512-NroXv2vh+sxVY1uya/rM5pjhx1hm8BzlYpx9q67QP0Xhw5MH2bf5GJylpvLEC+781p1Xli/317EoV9AlGwViag==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.73.0.tgz",
|
||||
"integrity": "sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -8528,9 +8528,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.72.0.tgz",
|
||||
"integrity": "sha512-0NDywYgfj279Ou/BcQuCYSj7NJwBfmWn5qc5uGO/Ny7fUWmXyIpvawqX/8acQlWG6IXelJsJhj+JAy6sjsKj0A==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.73.0.tgz",
|
||||
"integrity": "sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8545,9 +8545,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm-musleabihf": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.72.0.tgz",
|
||||
"integrity": "sha512-4vpXB06h65Ezsy4hRyrGjGrfa1SkVPii09yaajiYhmVpgsFiLD+KNxIx/BNAY+XiO+i1yqp9HHdwqM8VTqa5XQ==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.73.0.tgz",
|
||||
"integrity": "sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -8562,13 +8562,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-gnu": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.72.0.tgz",
|
||||
"integrity": "sha512-immaN4g2ZGFiOkKrvRX9LvzZdd2GkQM5wR+UyzYyUuyhUTXGQ4HKUJH18xp4G8OfhCVaVAJfKZxwE1r8+4hhaQ==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.73.0.tgz",
|
||||
"integrity": "sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8579,13 +8582,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-arm64-musl": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.72.0.tgz",
|
||||
"integrity": "sha512-JGHS9Mnr7iWyyLDxgCv1MhzVpAckgptg00F2gnxt/GD7lQ2SW1BRcxHqhSTaSdDpjWRrBkBxMMh4+Hn3aVtExg==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.73.0.tgz",
|
||||
"integrity": "sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8596,13 +8602,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-ppc64-gnu": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.72.0.tgz",
|
||||
"integrity": "sha512-AOYgBZqxNshrg83P9v0RYv+m8s10Cqkj4/PxXFDhcS3k7FqsIG5+CxErshZCIN7G8iy4Y+VGfAsuEdar8AcbBg==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.73.0.tgz",
|
||||
"integrity": "sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8613,13 +8622,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-gnu": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.72.0.tgz",
|
||||
"integrity": "sha512-QMybPS5ij3/vrKG67mqzHwW++91sYxK/PPUVi6SBtNCEzW4niS52fVBdXbQ6nou0wWbUPEpx8Sl/ZjtgE3clXA==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.73.0.tgz",
|
||||
"integrity": "sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8630,13 +8642,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-riscv64-musl": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.72.0.tgz",
|
||||
"integrity": "sha512-gOc3W7JV0PXRpIL7stUlLe3Wa9Gp0Kdlup87IT3gHDvPKck2xNgMIl/Gs2lldYY2lyXZDC4rWi3hmoLUobkgbQ==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.73.0.tgz",
|
||||
"integrity": "sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8647,13 +8662,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-s390x-gnu": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.72.0.tgz",
|
||||
"integrity": "sha512-rpGxph+FjjHcYI5q6uxB3Az+tnfmEnDbSA8+PK9ZE/VzyUAkvBOMeuY7ZQMhu5mpZH7YQDsTdW6Cx4kV/msc6w==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.73.0.tgz",
|
||||
"integrity": "sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8664,13 +8682,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-gnu": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.72.0.tgz",
|
||||
"integrity": "sha512-WND+uhf/Ko13SLqQMWQUgsZuLvYYEvL0ZKgg0tgGYfLqxG7l8Ju123fHDMJyYSDl5E3bUbpFUuii/OvMreFQzw==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.73.0.tgz",
|
||||
"integrity": "sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8681,13 +8702,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-linux-x64-musl": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.72.0.tgz",
|
||||
"integrity": "sha512-SrpbrUL70nG9vh6zP4/oKHWgLuHquwsr7MW9XOn0olBVgh10Uqr8qscKhQoBGEn6olK/IUpn5GSKcdQ5AjUhGA==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.73.0.tgz",
|
||||
"integrity": "sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8698,9 +8722,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-openharmony-arm64": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.72.0.tgz",
|
||||
"integrity": "sha512-qkrsEn6NmgFKr7U/QnezQMb+q/vzAy0Dd9Y95gQGQTyjzDLN+HRZMuM5u70iyH4nBLCfKBzhjMsYCehKay2jyg==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.73.0.tgz",
|
||||
"integrity": "sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8715,9 +8739,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-arm64-msvc": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.72.0.tgz",
|
||||
"integrity": "sha512-LWR6ZlFZph+KPjXv8opgZsXRDCdrdQe8VL8Cg9zxCoBS73h6znzZpydVgmdnwj8mB9AuSM5jxEgDJDpQkjboeg==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.73.0.tgz",
|
||||
"integrity": "sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -8732,9 +8756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-ia32-msvc": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.72.0.tgz",
|
||||
"integrity": "sha512-yt6HEh7IsHvtjRWtmeZRX134eaXKHq5Gnqlf1xBJdJl1JtdoRUEJw3nAxpZoUDS860cX/foKbztO441anVBtVQ==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.73.0.tgz",
|
||||
"integrity": "sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -8749,9 +8773,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-win32-x64-msvc": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.72.0.tgz",
|
||||
"integrity": "sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.73.0.tgz",
|
||||
"integrity": "sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -11326,9 +11350,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/plugin-emotion": {
|
||||
"version": "14.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/plugin-emotion/-/plugin-emotion-14.14.1.tgz",
|
||||
"integrity": "sha512-2EgdUStcBO6Zwpept8JVEKmP4AInL6UoYbcWq9G5blc5MSjRW4bn3KUTVEBrxCAklGq/79YBjsV/sqKqgqg4zA==",
|
||||
"version": "14.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@swc/plugin-emotion/-/plugin-emotion-14.15.0.tgz",
|
||||
"integrity": "sha512-nCsTO7mOOPz2UnT3N6YWb014uI0CVxeKg53A/KM/CvuSIE6H3KPkhaziJQ3q2jI3u3LfFuDKEnU5ZmB1330Dqg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -12741,16 +12765,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
|
||||
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
|
||||
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -12765,6 +12789,175 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
|
||||
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.63.0",
|
||||
"@typescript-eslint/types": "^8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
|
||||
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
|
||||
"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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
|
||||
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
|
||||
"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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
|
||||
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
|
||||
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.63.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/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/parser/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/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/parser/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/project-service": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
|
||||
@@ -32150,9 +32343,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/oxlint": {
|
||||
"version": "1.72.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.72.0.tgz",
|
||||
"integrity": "sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==",
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz",
|
||||
"integrity": "sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -32165,28 +32358,28 @@
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxlint/binding-android-arm-eabi": "1.72.0",
|
||||
"@oxlint/binding-android-arm64": "1.72.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.72.0",
|
||||
"@oxlint/binding-darwin-x64": "1.72.0",
|
||||
"@oxlint/binding-freebsd-x64": "1.72.0",
|
||||
"@oxlint/binding-linux-arm-gnueabihf": "1.72.0",
|
||||
"@oxlint/binding-linux-arm-musleabihf": "1.72.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "1.72.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "1.72.0",
|
||||
"@oxlint/binding-linux-ppc64-gnu": "1.72.0",
|
||||
"@oxlint/binding-linux-riscv64-gnu": "1.72.0",
|
||||
"@oxlint/binding-linux-riscv64-musl": "1.72.0",
|
||||
"@oxlint/binding-linux-s390x-gnu": "1.72.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "1.72.0",
|
||||
"@oxlint/binding-linux-x64-musl": "1.72.0",
|
||||
"@oxlint/binding-openharmony-arm64": "1.72.0",
|
||||
"@oxlint/binding-win32-arm64-msvc": "1.72.0",
|
||||
"@oxlint/binding-win32-ia32-msvc": "1.72.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "1.72.0"
|
||||
"@oxlint/binding-android-arm-eabi": "1.73.0",
|
||||
"@oxlint/binding-android-arm64": "1.73.0",
|
||||
"@oxlint/binding-darwin-arm64": "1.73.0",
|
||||
"@oxlint/binding-darwin-x64": "1.73.0",
|
||||
"@oxlint/binding-freebsd-x64": "1.73.0",
|
||||
"@oxlint/binding-linux-arm-gnueabihf": "1.73.0",
|
||||
"@oxlint/binding-linux-arm-musleabihf": "1.73.0",
|
||||
"@oxlint/binding-linux-arm64-gnu": "1.73.0",
|
||||
"@oxlint/binding-linux-arm64-musl": "1.73.0",
|
||||
"@oxlint/binding-linux-ppc64-gnu": "1.73.0",
|
||||
"@oxlint/binding-linux-riscv64-gnu": "1.73.0",
|
||||
"@oxlint/binding-linux-riscv64-musl": "1.73.0",
|
||||
"@oxlint/binding-linux-s390x-gnu": "1.73.0",
|
||||
"@oxlint/binding-linux-x64-gnu": "1.73.0",
|
||||
"@oxlint/binding-linux-x64-musl": "1.73.0",
|
||||
"@oxlint/binding-openharmony-arm64": "1.73.0",
|
||||
"@oxlint/binding-win32-arm64-msvc": "1.73.0",
|
||||
"@oxlint/binding-win32-ia32-msvc": "1.73.0",
|
||||
"@oxlint/binding-win32-x64-msvc": "1.73.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"oxlint-tsgolint": ">=0.22.1",
|
||||
"oxlint-tsgolint": ">=0.24.0",
|
||||
"vite-plus": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -42210,9 +42403,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz",
|
||||
"integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==",
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz",
|
||||
"integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.43",
|
||||
"@swc/plugin-emotion": "^14.14.1",
|
||||
"@swc/plugin-emotion": "^14.15.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
@@ -301,7 +301,7 @@
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
@@ -345,7 +345,7 @@
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.6.1",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.72.0",
|
||||
"oxlint": "^1.73.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"prettier": "3.9.4",
|
||||
@@ -374,7 +374,7 @@
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
"webpack-manifest-plugin": "^6.0.1",
|
||||
"webpack-sources": "^3.5.0",
|
||||
"webpack-sources": "^3.5.1",
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { Page, Download, Locator } from '@playwright/test';
|
||||
import { Menu } from '../components/core';
|
||||
import { Button, Menu } from '../components/core';
|
||||
import { NativeFiltersConfigModal } from '../components/modals';
|
||||
import { gotoWithRetry } from '../helpers/navigation';
|
||||
import { TIMEOUT } from '../utils/constants';
|
||||
@@ -176,4 +176,121 @@ export class DashboardPage {
|
||||
await menu.selectSubmenuItem('Download', optionText);
|
||||
return downloadPromise;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Drill to detail
|
||||
//
|
||||
// Charts that implement the DRILL_TO_DETAIL behavior expose two entry points:
|
||||
// the chart's "More Options" header menu, and a right-click context menu on
|
||||
// the chart body (a cell, the big-number value, or a canvas data point). Both
|
||||
// open the same DrillDetailModal, which renders the underlying sample rows for
|
||||
// the (optionally filtered) chart by calling the `/datasource/samples` API.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Locator for a chart card on the dashboard by its visualization type, e.g.
|
||||
* `table`, `pie`, `big_number_total`.
|
||||
*/
|
||||
chartByVizType(vizType: string): Locator {
|
||||
return this.page.locator(`[data-test-viz-type="${vizType}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the "Drill to detail" item from a chart's "More Options" header menu.
|
||||
* This is the whole-chart entry point (no row-level filters applied).
|
||||
*/
|
||||
async openDrillToDetailFromMenu(vizType: string): Promise<void> {
|
||||
const moreOptions = new Button(
|
||||
this.page,
|
||||
this.chartByVizType(vizType).getByLabel('More Options', { exact: true }),
|
||||
);
|
||||
await moreOptions.click();
|
||||
await this.page
|
||||
.getByRole('menuitem', { name: 'Drill to detail', exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The DrillDetailModal dialog (titled "Drill to detail: <chart name>").
|
||||
*/
|
||||
drillModal(): Locator {
|
||||
return this.page.getByRole('dialog', { name: /^Drill to detail:/ });
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the drill-to-detail modal if it is open (idempotent).
|
||||
*/
|
||||
async closeDrillModal(): Promise<void> {
|
||||
const close = this.page.locator('[data-test="close-drilltodetail-modal"]');
|
||||
if (await close.count()) {
|
||||
await close.first().click();
|
||||
await this.drillModal().waitFor({ state: 'hidden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The applied-filter value tags inside the drill modal (`<col>=<val>`).
|
||||
*/
|
||||
drillFilterValues(): Locator {
|
||||
return this.page.locator('[data-test="filter-val"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the plain "Drill to detail" item in an open chart context menu
|
||||
* (whole chart, no row-level filter).
|
||||
*/
|
||||
async contextMenuDrillToDetail(): Promise<void> {
|
||||
await this.page
|
||||
.getByRole('menuitem', { name: 'Drill to detail', exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* The "Drill to detail by" submenu parent (title) in an open context menu.
|
||||
* Targeted by its submenu-title element rather than role+name because antd
|
||||
* appends the arrow-icon name ("right") to the accessible name, and the leaf
|
||||
* items ("Drill to detail by boy") would otherwise match a role+name lookup.
|
||||
*/
|
||||
private drillBySubmenuTitle(): Locator {
|
||||
return this.page.locator('.ant-dropdown-menu-submenu-title', {
|
||||
hasText: 'Drill to detail by',
|
||||
});
|
||||
}
|
||||
|
||||
/** The "Drill to detail by" submenu popup (its leaf value items live here). */
|
||||
private drillBySubmenu(): Locator {
|
||||
return this.page.locator('.chart-context-submenu');
|
||||
}
|
||||
|
||||
/**
|
||||
* From an open chart context menu, hover the "Drill to detail by" submenu and
|
||||
* click the entry for a specific value (e.g. "boy", "1965", "all").
|
||||
*/
|
||||
async contextMenuDrillToDetailBy(value: string): Promise<void> {
|
||||
await this.drillBySubmenuTitle().hover();
|
||||
await this.drillBySubmenu()
|
||||
.getByRole('menuitem', {
|
||||
name: `Drill to detail by ${value}`,
|
||||
exact: true,
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* From an open chart context menu, hover "Drill to detail by" and return the
|
||||
* concrete values offered by the submenu (e.g. ["1965", "boy"]), skipping the
|
||||
* aggregate "all" entry. Used by canvas charts where the value under the
|
||||
* cursor is data-dependent: the test drills by whatever the menu actually
|
||||
* offers and asserts that same value round-trips into the modal, which keeps
|
||||
* the assertion independent of exact pixel/slice geometry.
|
||||
*/
|
||||
async drillByOfferedValues(): Promise<string[]> {
|
||||
await this.drillBySubmenuTitle().hover();
|
||||
const items = this.drillBySubmenu().locator('[role="menuitem"]');
|
||||
await items.first().waitFor();
|
||||
const labels = await items.allInnerTexts();
|
||||
return labels
|
||||
.map(l => l.replace(/^Drill to detail by\s*/i, '').trim())
|
||||
.filter(v => v.length > 0 && v.toLowerCase() !== 'all');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,850 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* E2E migration of the Cypress "Drill to detail modal" suite
|
||||
* (dashboard/drilltodetail.test.ts).
|
||||
*
|
||||
* Drill to detail lets a viewer open a modal of the underlying sample rows for a
|
||||
* chart — optionally filtered to a single data point — by either the chart's
|
||||
* "More Options" header menu or a right-click context menu on the chart body.
|
||||
* The modal calls the real `/datasource/samples` API, so this is genuinely
|
||||
* end-to-end: each test API-builds a hermetic dashboard from the `birth_names`
|
||||
* dataset, renders it in the browser, drives the real menus, and asserts the
|
||||
* resulting backend round-trip (the samples POST and the filter the modal
|
||||
* applies).
|
||||
*
|
||||
* Why the original suite was fully `describe.skip`:
|
||||
* "it has issues with autoscrolling and the locked title flakes intricately
|
||||
* when the rightClick is obstructed by the title."
|
||||
* That failure mode is Cypress-specific — Cypress auto-scrolls the target under
|
||||
* the sticky chart header before every action. Playwright scrolls once and the
|
||||
* target stays put, so the entry points are portable here.
|
||||
*
|
||||
* What is migrated, and how it is kept deterministic:
|
||||
* - Modal mechanics (open from header menu, pagination, reload-resets-page)
|
||||
* and the no-filter big-number drill use stable DOM elements.
|
||||
* - Table and Pivot drills right-click real DOM cells (no canvas pixels).
|
||||
* - Canvas (echarts) charts — Pie, Line, Scatter, generic/smooth/step
|
||||
* time-series, Mixed, Box plot, Funnel, Gauge, Treemap — DID rely on
|
||||
* hard-coded pixel coordinates in Cypress to land on a specific slice/point.
|
||||
* Instead of reproducing those brittle pixels, these tests scan a stable
|
||||
* region of the canvas (see `rightClickCanvasDatum`), read whichever value
|
||||
* the drill submenu actually offers for the point under the cursor, drill by
|
||||
* that value, and assert the SAME value round-trips into the modal filter.
|
||||
* This exercises the full canvas → contextmenu → datum → samples pipeline
|
||||
* while staying independent of exact geometry. `Big Number with Trendline`
|
||||
* drills the whole chart (no datum filter), like `Big Number`.
|
||||
*
|
||||
* Excluded (kept out, matching the original's own `describe.skip`s): Bar, Area,
|
||||
* World Map, Radar — skipped upstream for chart-specific reasons.
|
||||
*/
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { apiPost, apiPut } from '../../helpers/api/requests';
|
||||
import { apiPostDashboard } from '../../helpers/api/dashboard';
|
||||
import { getDatasetByName } from '../../helpers/api/dataset';
|
||||
import { TIMEOUT } from '../../utils/constants';
|
||||
import { DashboardPage } from '../../pages/DashboardPage';
|
||||
|
||||
const DATASET_NAME = 'birth_names';
|
||||
|
||||
/**
|
||||
* Parse a RowCountLabel value ("75.7k rows", "1,234 rows") into a number so
|
||||
* tests can assert the *invariant* (filtered < unfiltered) without hard-coding
|
||||
* the dataset-specific totals the original Cypress suite baked in.
|
||||
*/
|
||||
function parseRowCount(text: string): number {
|
||||
const m = text.match(/([\d.,]+)\s*([kKmM]?)/);
|
||||
if (!m) return NaN;
|
||||
let n = parseFloat(m[1].replace(/,/g, ''));
|
||||
const suffix = m[2].toLowerCase();
|
||||
if (suffix === 'k') n *= 1e3;
|
||||
if (suffix === 'm') n *= 1e6;
|
||||
return n;
|
||||
}
|
||||
|
||||
interface ChartSpec {
|
||||
vizType: string;
|
||||
sliceName: string;
|
||||
params: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* API-build a hermetic single-chart dashboard from birth_names and return its
|
||||
* id. Mirrors the build pattern used by the other migrated dashboard specs
|
||||
* (create chart → build positionJson → create dashboard → link chart).
|
||||
*/
|
||||
async function buildSingleChartDashboard(
|
||||
page: Page,
|
||||
testAssets: {
|
||||
trackChart(id: number): void;
|
||||
trackDashboard(id: number): void;
|
||||
},
|
||||
spec: ChartSpec,
|
||||
): Promise<number> {
|
||||
const dataset = await getDatasetByName(page, DATASET_NAME);
|
||||
if (!dataset) {
|
||||
throw new Error(`Dataset ${DATASET_NAME} not found`);
|
||||
}
|
||||
const datasetId = dataset.id;
|
||||
const datasource = `${datasetId}__table`;
|
||||
|
||||
const chartResp = await apiPost(page, 'api/v1/chart/', {
|
||||
slice_name: `${spec.sliceName}_${Date.now()}`,
|
||||
viz_type: spec.vizType,
|
||||
datasource_id: datasetId,
|
||||
datasource_type: 'table',
|
||||
params: JSON.stringify({
|
||||
datasource,
|
||||
viz_type: spec.vizType,
|
||||
...spec.params,
|
||||
}),
|
||||
});
|
||||
expect(chartResp.ok()).toBe(true);
|
||||
const chart = await chartResp.json();
|
||||
const chartId: number = chart.id ?? chart.result?.id;
|
||||
testAssets.trackChart(chartId);
|
||||
|
||||
const chartLayoutKey = `CHART-${chartId}`;
|
||||
const positionJson = {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-1'],
|
||||
parents: ['ROOT_ID'],
|
||||
},
|
||||
'ROW-1': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-1',
|
||||
children: [chartLayoutKey],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
[chartLayoutKey]: {
|
||||
type: 'CHART',
|
||||
id: chartLayoutKey,
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
||||
meta: { chartId, width: 12, height: 60, sliceName: spec.sliceName },
|
||||
},
|
||||
};
|
||||
|
||||
const dashResp = await apiPostDashboard(page, {
|
||||
dashboard_title: `${spec.sliceName}_dash_${Date.now()}`,
|
||||
published: true,
|
||||
position_json: JSON.stringify(positionJson),
|
||||
});
|
||||
expect(dashResp.ok()).toBe(true);
|
||||
const dashBody = await dashResp.json();
|
||||
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
|
||||
testAssets.trackDashboard(dashboardId);
|
||||
|
||||
const linkResp = await apiPut(page, `api/v1/chart/${chartId}`, {
|
||||
dashboards: [dashboardId],
|
||||
});
|
||||
expect(linkResp.ok()).toBe(true);
|
||||
|
||||
return dashboardId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-click an echarts canvas until a data point is hit — i.e. until the
|
||||
* context menu offers an *enabled* "Drill to detail by" submenu (a miss renders
|
||||
* that item disabled, as a plain menu item rather than a submenu title).
|
||||
*
|
||||
* echarts renders to a single canvas, so there is no per-datum DOM element to
|
||||
* target and the exact pixel of a mark depends on chart geometry (donut hole,
|
||||
* legend size, axis padding). Rather than hard-code Cypress's brittle pixel
|
||||
* coordinates, this scans a small set of candidate points — a radial ring for
|
||||
* pie/radial charts, a grid for cartesian charts — and stops at the first that
|
||||
* lands on a mark. The drill value is then whatever that mark represents, so the
|
||||
* caller asserts a value round-trip rather than a specific geometry.
|
||||
*/
|
||||
async function rightClickCanvasDatum(
|
||||
page: Page,
|
||||
canvas: ReturnType<Page['locator']>,
|
||||
pattern: 'ring' | 'grid' | 'dense',
|
||||
): Promise<void> {
|
||||
const box = await canvas.boundingBox();
|
||||
if (!box) throw new Error('canvas has no bounding box');
|
||||
|
||||
const ringPoints = (): Array<{ x: number; y: number }> => {
|
||||
const pts: Array<{ x: number; y: number }> = [];
|
||||
const cx = box.width / 2;
|
||||
const cy = box.height / 2;
|
||||
const minSide = Math.min(box.width, box.height);
|
||||
for (const rf of [0.3, 0.22, 0.38]) {
|
||||
for (let a = 0; a < 360; a += 45) {
|
||||
const rad = (a * Math.PI) / 180;
|
||||
pts.push({
|
||||
x: cx + Math.cos(rad) * minSide * rf,
|
||||
y: cy + Math.sin(rad) * minSide * rf,
|
||||
});
|
||||
}
|
||||
}
|
||||
return pts;
|
||||
};
|
||||
const gridPoints = (): Array<{ x: number; y: number }> => {
|
||||
const pts: Array<{ x: number; y: number }> = [];
|
||||
for (const yf of [0.5, 0.4, 0.6, 0.3, 0.7]) {
|
||||
for (const xf of [0.3, 0.45, 0.6, 0.2, 0.75]) {
|
||||
pts.push({ x: box.width * xf, y: box.height * yf });
|
||||
}
|
||||
}
|
||||
return pts;
|
||||
};
|
||||
|
||||
// 'dense' merges both scans for radial/stacked shapes (gauge, funnel, box
|
||||
// plot) whose drillable marks don't fall neatly on a single ring or grid.
|
||||
let candidates: Array<{ x: number; y: number }>;
|
||||
if (pattern === 'ring') candidates = ringPoints();
|
||||
else if (pattern === 'grid') candidates = gridPoints();
|
||||
else candidates = [...gridPoints(), ...ringPoints()];
|
||||
|
||||
// The submenu *title* element only exists when "Drill to detail by" is an
|
||||
// enabled submenu (a real datum was hit); a miss renders a disabled item.
|
||||
const enabledDrillBy = page
|
||||
.locator('.ant-dropdown-menu-submenu-title')
|
||||
.filter({ hasText: 'Drill to detail by' });
|
||||
|
||||
for (const pt of candidates) {
|
||||
await canvas.click({ button: 'right', position: pt });
|
||||
const hit = await enabledDrillBy
|
||||
.waitFor({ state: 'visible', timeout: 400 })
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
if (hit) return;
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
throw new Error(
|
||||
`no drillable datum found on canvas after scanning ${candidates.length} points`,
|
||||
);
|
||||
}
|
||||
|
||||
/** A samples POST fired (proves the modal hit the real backend). */
|
||||
function expectSamplesPost(page: Page) {
|
||||
return page.waitForResponse(
|
||||
r =>
|
||||
r.url().includes('/datasource/samples') &&
|
||||
r.request().method() === 'POST',
|
||||
{ timeout: TIMEOUT.API_RESPONSE },
|
||||
);
|
||||
}
|
||||
|
||||
async function loadDashboardWithChart(
|
||||
dashboard: DashboardPage,
|
||||
dashboardId: number,
|
||||
vizType: string,
|
||||
): Promise<void> {
|
||||
await dashboard.gotoById(dashboardId);
|
||||
await dashboard.waitForLoad();
|
||||
await dashboard
|
||||
.chartByVizType(vizType)
|
||||
.locator('[data-test="chart-container"]')
|
||||
.first()
|
||||
.waitFor({ state: 'visible', timeout: TIMEOUT.QUERY_EXECUTION });
|
||||
await dashboard.waitForChartsToLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Full canvas-drill round-trip for an echarts (canvas-rendered) chart: build a
|
||||
* hermetic single-chart dashboard, render it, right-click a real datum, drill by
|
||||
* whatever value the submenu offers under the cursor, and assert that same value
|
||||
* lands in the modal filter. Geometry-independent — see rightClickCanvasDatum.
|
||||
* Reused across every canvas viz type so each migrated chart is a thin caller.
|
||||
*/
|
||||
async function expectCanvasDrillByValueRoundTrips(
|
||||
page: Page,
|
||||
testAssets: {
|
||||
trackChart(id: number): void;
|
||||
trackDashboard(id: number): void;
|
||||
},
|
||||
spec: ChartSpec,
|
||||
pattern: 'ring' | 'grid' | 'dense',
|
||||
): Promise<void> {
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, spec);
|
||||
await loadDashboardWithChart(dashboard, dashboardId, spec.vizType);
|
||||
|
||||
const canvas = dashboard
|
||||
.chartByVizType(spec.vizType)
|
||||
.locator('canvas')
|
||||
.first();
|
||||
await expect(canvas).toBeVisible();
|
||||
await rightClickCanvasDatum(page, canvas, pattern);
|
||||
|
||||
const offered = await dashboard.drillByOfferedValues();
|
||||
expect(offered.length).toBeGreaterThan(0);
|
||||
const value = offered[0];
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard.contextMenuDrillToDetailBy(value);
|
||||
await samples;
|
||||
|
||||
await expect(dashboard.drillModal()).toBeVisible();
|
||||
await expect(dashboard.drillFilterValues().first()).toContainText(value);
|
||||
}
|
||||
|
||||
// Shared form-data fragment for the echarts time-series family (line/scatter/
|
||||
// generic/smooth/step): one temporal axis, one metric, split by gender series.
|
||||
const TIMESERIES_PARAMS = {
|
||||
x_axis: 'ds',
|
||||
time_grain_sqla: 'P1Y',
|
||||
metrics: ['count'],
|
||||
groupby: ['gender'],
|
||||
row_limit: 1000,
|
||||
};
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: opens from the header menu, paginates, and reload resets to page 1',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'big_number_total',
|
||||
sliceName: 'drill_bignum',
|
||||
params: { metric: 'count', adhoc_filters: [] },
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(dashboard, dashboardId, 'big_number_total');
|
||||
|
||||
// Open the modal from the chart's "More Options" header menu.
|
||||
const samplesOnOpen = expectSamplesPost(page);
|
||||
await dashboard.openDrillToDetailFromMenu('big_number_total');
|
||||
await samplesOnOpen;
|
||||
|
||||
const modal = dashboard.drillModal();
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(modal).toContainText('Drill to detail:');
|
||||
// The metadata bar and a real row count prove the modal loaded backend data.
|
||||
await expect(modal.locator('[data-test="metadata-bar"]')).toBeVisible();
|
||||
await expect(modal.locator('[data-test="row-count-label"]')).toContainText(
|
||||
'rows',
|
||||
);
|
||||
// No drill filter was applied (whole-chart drill).
|
||||
await expect(dashboard.drillFilterValues()).toHaveCount(0);
|
||||
|
||||
// The full dataset spans multiple pages, and the grid has rendered rows.
|
||||
const pageItems = modal.locator('.ant-pagination-item');
|
||||
expect(await pageItems.count()).toBeGreaterThan(1);
|
||||
await expect(modal.locator('.virtual-table-cell').first()).toBeVisible();
|
||||
await expect(modal.locator('.ant-pagination-item-active')).toContainText(
|
||||
'1',
|
||||
);
|
||||
|
||||
// Paginate forward: clicking page 2 fires a real samples fetch and moves the
|
||||
// active page to 2.
|
||||
const samplesOnPage2 = expectSamplesPost(page);
|
||||
await modal.locator('.ant-pagination-item').nth(1).click();
|
||||
await samplesOnPage2;
|
||||
await expect(modal.locator('.ant-pagination-item-active')).toContainText(
|
||||
'2',
|
||||
);
|
||||
|
||||
// Reload re-fetches and resets back to the first page.
|
||||
const samplesOnReload = expectSamplesPost(page);
|
||||
await modal.getByRole('button', { name: 'Reload' }).click();
|
||||
await samplesOnReload;
|
||||
await expect(modal.locator('.ant-pagination-item-active')).toContainText(
|
||||
'1',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: big number value right-click drills the whole chart (no filter)',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'big_number_total',
|
||||
sliceName: 'drill_bignum_rc',
|
||||
params: { metric: 'count', adhoc_filters: [] },
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(dashboard, dashboardId, 'big_number_total');
|
||||
|
||||
// Right-click the rendered number itself opens the context menu.
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard
|
||||
.chartByVizType('big_number_total')
|
||||
.locator('.header-line')
|
||||
.click({ button: 'right' });
|
||||
await dashboard.contextMenuDrillToDetail();
|
||||
await samples;
|
||||
|
||||
await expect(dashboard.drillModal()).toBeVisible();
|
||||
// Whole-chart drill: no per-value filter tag.
|
||||
await expect(dashboard.drillFilterValues()).toHaveCount(0);
|
||||
await expect(
|
||||
dashboard.drillModal().locator('[data-test="row-count-label"]'),
|
||||
).toContainText('rows');
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: table cell right-click drills by that value and clearing the filter restores the full set',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'table',
|
||||
sliceName: 'drill_table',
|
||||
params: {
|
||||
query_mode: 'aggregate',
|
||||
groupby: ['gender'],
|
||||
metrics: ['count'],
|
||||
row_limit: 100,
|
||||
server_pagination: false,
|
||||
},
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(dashboard, dashboardId, 'table');
|
||||
|
||||
// Right-click the "boy" dimension cell and drill by it.
|
||||
const samplesOnDrill = expectSamplesPost(page);
|
||||
await dashboard
|
||||
.chartByVizType('table')
|
||||
.getByText('boy', { exact: true })
|
||||
.first()
|
||||
.click({ button: 'right' });
|
||||
await dashboard.contextMenuDrillToDetailBy('boy');
|
||||
await samplesOnDrill;
|
||||
|
||||
const modal = dashboard.drillModal();
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(dashboard.drillFilterValues().first()).toContainText('boy');
|
||||
|
||||
const filteredCount = parseRowCount(
|
||||
await modal.locator('[data-test="row-count-label"]').innerText(),
|
||||
);
|
||||
expect(filteredCount).toBeGreaterThan(0);
|
||||
|
||||
// Clearing the filter reloads the samples and restores the larger, unfiltered total.
|
||||
const samplesOnClear = expectSamplesPost(page);
|
||||
await modal.locator('[data-test="filter-col"]').getByLabel('Close').click();
|
||||
await samplesOnClear;
|
||||
await expect(dashboard.drillFilterValues()).toHaveCount(0);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
parseRowCount(
|
||||
await modal.locator('[data-test="row-count-label"]').innerText(),
|
||||
),
|
||||
)
|
||||
.toBeGreaterThan(filteredCount);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: pivot table cell right-click drills by the cell value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'pivot_table_v2',
|
||||
sliceName: 'drill_pivot',
|
||||
params: {
|
||||
groupbyRows: ['gender'],
|
||||
groupbyColumns: [],
|
||||
metrics: ['count'],
|
||||
aggregateFunction: 'Sum',
|
||||
rowTotals: false,
|
||||
colTotals: false,
|
||||
},
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(dashboard, dashboardId, 'pivot_table_v2');
|
||||
|
||||
await dashboard
|
||||
.chartByVizType('pivot_table_v2')
|
||||
.locator('[role="gridcell"]')
|
||||
.first()
|
||||
.click({ button: 'right' });
|
||||
|
||||
// The cell's row dimension determines the offered value; drill by it and
|
||||
// assert the same value lands in the modal filter.
|
||||
const offered = await dashboard.drillByOfferedValues();
|
||||
expect(offered.length).toBeGreaterThan(0);
|
||||
const value = offered[0];
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard.contextMenuDrillToDetailBy(value);
|
||||
await samples;
|
||||
|
||||
await expect(dashboard.drillModal()).toBeVisible();
|
||||
await expect(dashboard.drillFilterValues().first()).toContainText(value);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: pie slice right-click (canvas) drills by the slice value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'pie',
|
||||
sliceName: 'drill_pie',
|
||||
params: { groupby: ['gender'], metric: 'count' },
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(dashboard, dashboardId, 'pie');
|
||||
|
||||
const canvas = dashboard.chartByVizType('pie').locator('canvas').first();
|
||||
await expect(canvas).toBeVisible();
|
||||
|
||||
// Pie is a donut by default (center is a hole), so scan the ring for a slice.
|
||||
await rightClickCanvasDatum(page, canvas, 'ring');
|
||||
|
||||
const offered = await dashboard.drillByOfferedValues();
|
||||
expect(offered.length).toBeGreaterThan(0);
|
||||
const value = offered[0];
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard.contextMenuDrillToDetailBy(value);
|
||||
await samples;
|
||||
|
||||
await expect(dashboard.drillModal()).toBeVisible();
|
||||
await expect(dashboard.drillFilterValues().first()).toContainText(value);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: line chart point right-click (canvas) drills by the point value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'echarts_timeseries_line',
|
||||
sliceName: 'drill_line',
|
||||
params: {
|
||||
x_axis: 'ds',
|
||||
time_grain_sqla: 'P1Y',
|
||||
metrics: ['count'],
|
||||
groupby: ['gender'],
|
||||
row_limit: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(
|
||||
dashboard,
|
||||
dashboardId,
|
||||
'echarts_timeseries_line',
|
||||
);
|
||||
|
||||
const canvas = dashboard
|
||||
.chartByVizType('echarts_timeseries_line')
|
||||
.locator('canvas')
|
||||
.first();
|
||||
await expect(canvas).toBeVisible();
|
||||
|
||||
// Scan the plot grid for a point on one of the series lines.
|
||||
await rightClickCanvasDatum(page, canvas, 'grid');
|
||||
|
||||
const offered = await dashboard.drillByOfferedValues();
|
||||
expect(offered.length).toBeGreaterThan(0);
|
||||
const value = offered[0];
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard.contextMenuDrillToDetailBy(value);
|
||||
await samples;
|
||||
|
||||
await expect(dashboard.drillModal()).toBeVisible();
|
||||
await expect(dashboard.drillFilterValues().first()).toContainText(value);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: big number with trendline right-click drills the whole chart (no filter)',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'big_number',
|
||||
sliceName: 'drill_bignum_trend',
|
||||
params: {
|
||||
metric: 'count',
|
||||
x_axis: 'ds',
|
||||
time_grain_sqla: 'P1Y',
|
||||
adhoc_filters: [],
|
||||
},
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(dashboard, dashboardId, 'big_number');
|
||||
|
||||
// Right-click the rendered number opens the context menu; whole-chart drill.
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard
|
||||
.chartByVizType('big_number')
|
||||
.locator('.header-line')
|
||||
.click({ button: 'right' });
|
||||
await dashboard.contextMenuDrillToDetail();
|
||||
await samples;
|
||||
|
||||
await expect(dashboard.drillModal()).toBeVisible();
|
||||
await expect(dashboard.drillFilterValues()).toHaveCount(0);
|
||||
await expect(
|
||||
dashboard.drillModal().locator('[data-test="row-count-label"]'),
|
||||
).toContainText('rows');
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: scatter chart point right-click (canvas) drills by the point value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'echarts_timeseries_scatter',
|
||||
sliceName: 'drill_scatter',
|
||||
// Enlarge the markers so a region scan reliably lands on a point;
|
||||
// scatter's default dots are a few pixels wide and a sparse grid misses
|
||||
// them.
|
||||
params: { ...TIMESERIES_PARAMS, markerSize: 20 },
|
||||
},
|
||||
'dense',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: generic time-series point right-click (canvas) drills by the point value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'echarts_timeseries',
|
||||
sliceName: 'drill_generic',
|
||||
params: TIMESERIES_PARAMS,
|
||||
},
|
||||
'grid',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: smooth line point right-click (canvas) drills by the point value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'echarts_timeseries_smooth',
|
||||
sliceName: 'drill_smooth',
|
||||
params: TIMESERIES_PARAMS,
|
||||
},
|
||||
'grid',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: step line point right-click (canvas) drills by the point value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'echarts_timeseries_step',
|
||||
sliceName: 'drill_step',
|
||||
params: TIMESERIES_PARAMS,
|
||||
},
|
||||
'grid',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: mixed time-series point right-click (canvas) drills by the point value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'mixed_timeseries',
|
||||
sliceName: 'drill_mixed',
|
||||
params: {
|
||||
x_axis: 'ds',
|
||||
time_grain_sqla: 'P1Y',
|
||||
metrics: ['count'],
|
||||
groupby: ['gender'],
|
||||
metrics_b: ['count'],
|
||||
groupby_b: ['gender'],
|
||||
row_limit: 1000,
|
||||
},
|
||||
},
|
||||
'grid',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: box plot right-click (canvas) drills by the box value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'box_plot',
|
||||
sliceName: 'drill_boxplot',
|
||||
params: {
|
||||
groupby: ['gender'],
|
||||
metrics: ['count'],
|
||||
columns: ['ds'],
|
||||
},
|
||||
},
|
||||
'dense',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: funnel segment right-click (canvas) drills by the segment value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'funnel',
|
||||
sliceName: 'drill_funnel',
|
||||
params: { groupby: ['gender'], metric: 'count' },
|
||||
},
|
||||
'dense',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: gauge right-click (canvas) drills by the gauge value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'gauge_chart',
|
||||
sliceName: 'drill_gauge',
|
||||
params: { groupby: ['gender'], metric: 'count' },
|
||||
},
|
||||
'dense',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: treemap tile right-click (canvas) drills by the tile value',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
await expectCanvasDrillByValueRoundTrips(
|
||||
page,
|
||||
testAssets,
|
||||
{
|
||||
vizType: 'treemap_v2',
|
||||
sliceName: 'drill_treemap',
|
||||
params: { metric: 'count', groupby: ['gender'] },
|
||||
},
|
||||
'dense',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: drilling a time-series point "by all" applies every dimension of that point',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'echarts_timeseries_line',
|
||||
sliceName: 'drill_all',
|
||||
// Two groupby dimensions so each point genuinely carries more than one
|
||||
// drillable value — the whole point of "Drill to detail by all".
|
||||
params: { ...TIMESERIES_PARAMS, groupby: ['gender', 'state'] },
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(
|
||||
dashboard,
|
||||
dashboardId,
|
||||
'echarts_timeseries_line',
|
||||
);
|
||||
|
||||
const canvas = dashboard
|
||||
.chartByVizType('echarts_timeseries_line')
|
||||
.locator('canvas')
|
||||
.first();
|
||||
await expect(canvas).toBeVisible();
|
||||
await rightClickCanvasDatum(page, canvas, 'grid');
|
||||
|
||||
// A line point carries two dimensions (the temporal value and the gender
|
||||
// series), so "Drill to detail by all" must apply both as filters.
|
||||
const offered = await dashboard.drillByOfferedValues();
|
||||
expect(offered.length).toBeGreaterThanOrEqual(2);
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard.contextMenuDrillToDetailBy('all');
|
||||
await samples;
|
||||
|
||||
await expect(dashboard.drillModal()).toBeVisible();
|
||||
expect(await dashboard.drillFilterValues().count()).toBeGreaterThanOrEqual(
|
||||
2,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWithAssets(
|
||||
'drill-to-detail modal: table drills correctly by each of multiple dimension values',
|
||||
async ({ page, testAssets }) => {
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
const dashboard = new DashboardPage(page);
|
||||
const dashboardId = await buildSingleChartDashboard(page, testAssets, {
|
||||
vizType: 'table',
|
||||
sliceName: 'drill_table_multi',
|
||||
params: {
|
||||
query_mode: 'aggregate',
|
||||
groupby: ['gender'],
|
||||
metrics: ['count'],
|
||||
row_limit: 100,
|
||||
server_pagination: false,
|
||||
},
|
||||
});
|
||||
|
||||
await loadDashboardWithChart(dashboard, dashboardId, 'table');
|
||||
|
||||
for (const value of ['boy', 'girl']) {
|
||||
const samples = expectSamplesPost(page);
|
||||
await dashboard
|
||||
.chartByVizType('table')
|
||||
.getByText(value, { exact: true })
|
||||
.first()
|
||||
.click({ button: 'right' });
|
||||
await dashboard.contextMenuDrillToDetailBy(value);
|
||||
await samples;
|
||||
|
||||
const modal = dashboard.drillModal();
|
||||
await expect(modal).toBeVisible();
|
||||
await expect(dashboard.drillFilterValues().first()).toContainText(value);
|
||||
await dashboard.closeDrillModal();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -177,6 +177,28 @@ describe('Filter utils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Regression for #27510: formats large BIGINT column values as exact strings without precision loss', () => {
|
||||
// A BIGINT column value with 16+ digits arrives as a native `bigint`
|
||||
// (decoded from the `json-bigint` parse of the chart data response).
|
||||
// Historically the filter UI rendered it as a raw BigNumber object
|
||||
// ({ s, e, c }), throwing React error #31, or coerced it through a JS
|
||||
// number and silently corrupted the trailing digits. The label
|
||||
// formatter must turn it into its exact decimal string so the Select
|
||||
// filter option renders correctly and the value keeps full precision.
|
||||
const formatter = getDataRecordFormatter();
|
||||
const largeBigInt = 1234567890125123456n;
|
||||
|
||||
expect(formatter(largeBigInt, GenericDataType.Numeric)).toEqual(
|
||||
'1234567890125123456',
|
||||
);
|
||||
expect(formatter(largeBigInt, GenericDataType.String)).toEqual(
|
||||
'1234567890125123456',
|
||||
);
|
||||
// Guard: routing the same value through a JS number (the pre-fix
|
||||
// behavior) loses precision, which is exactly what broke the filter.
|
||||
expect(String(Number(largeBigInt))).not.toEqual('1234567890125123456');
|
||||
});
|
||||
|
||||
test('formatter with defined formatters returns expected values', () => {
|
||||
const formatter = getDataRecordFormatter({
|
||||
timeFormatter: getTimeFormatter(TimeFormats.DATABASE_DATETIME),
|
||||
|
||||
@@ -173,6 +173,237 @@ describe('asyncEvent middleware', () => {
|
||||
expect(fetchMock.callHistory.calls(CACHED_DATA_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('backs off exponentially when polling requests keep failing', async () => {
|
||||
// stop the real-timer polling loop started by beforeEach before
|
||||
// switching to fake timers, so all polls run on the fake clock
|
||||
mockedIsFeatureEnabled.mockReturnValueOnce(false);
|
||||
asyncEvent.init(config);
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get(EVENTS_ENDPOINT, { status: 403 });
|
||||
asyncEvent.init(config);
|
||||
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
|
||||
|
||||
// first poll fires after the configured delay and fails
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
// next poll is delayed by 2x the configured delay, so nothing yet
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
|
||||
|
||||
// after the second failure the delay grows to 4x
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
3 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(3);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
test('resumes the configured polling delay after a successful poll', async () => {
|
||||
// stop the real-timer polling loop started by beforeEach before
|
||||
// switching to fake timers, so all polls run on the fake clock
|
||||
mockedIsFeatureEnabled.mockReturnValueOnce(false);
|
||||
asyncEvent.init(config);
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get(EVENTS_ENDPOINT, { status: 403 });
|
||||
asyncEvent.init(config);
|
||||
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
|
||||
|
||||
// two failed polls: 1x delay, then 2x delay
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
3 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
|
||||
|
||||
// subsequent polls succeed, resetting the backoff
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get(EVENTS_ENDPOINT, {
|
||||
status: 200,
|
||||
body: { result: [] },
|
||||
});
|
||||
|
||||
// third poll fires 4x delay after the second failure and succeeds
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
4 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
// polling is back to the configured delay
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
test('caps the polling backoff delay at 60 seconds', async () => {
|
||||
const MAX_ERROR_POLLING_DELAY_MS = 60000;
|
||||
// stop the real-timer polling loop started by beforeEach before
|
||||
// switching to fake timers, so all polls run on the fake clock
|
||||
mockedIsFeatureEnabled.mockReturnValueOnce(false);
|
||||
asyncEvent.init(config);
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get(EVENTS_ENDPOINT, { status: 403 });
|
||||
asyncEvent.init(config);
|
||||
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
|
||||
|
||||
// first poll fires after the configured delay and fails
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
// walk the uncapped backoff: after failure N the next delay is
|
||||
// 2^N times the configured delay, which stays below the cap through
|
||||
// failure 10 (50ms * 2^10 = 51.2s)
|
||||
for (let failures = 1; failures <= 10; failures += 1) {
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY * 2 ** failures,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(
|
||||
failures + 1,
|
||||
);
|
||||
}
|
||||
|
||||
// after failure 11 the uncapped delay would be 102.4s, so the cap
|
||||
// takes over: no poll just before the 60s mark...
|
||||
await jest.advanceTimersByTimeAsync(MAX_ERROR_POLLING_DELAY_MS - 1);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(11);
|
||||
|
||||
// ...and the next poll fires exactly at 60s
|
||||
await jest.advanceTimersByTimeAsync(1);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(12);
|
||||
|
||||
// additional failures remain capped at 60s
|
||||
await jest.advanceTimersByTimeAsync(MAX_ERROR_POLLING_DELAY_MS);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(13);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
test('does not start a second loop when re-initialized during an in-flight poll', async () => {
|
||||
// stop the real-timer polling loop started by beforeEach before
|
||||
// switching to fake timers, so all polls run on the fake clock
|
||||
mockedIsFeatureEnabled.mockReturnValueOnce(false);
|
||||
asyncEvent.init(config);
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
let resolveFetch: (response: any) => void = () => {};
|
||||
fetchMock.get(
|
||||
EVENTS_ENDPOINT,
|
||||
new Promise(resolve => {
|
||||
resolveFetch = resolve;
|
||||
}),
|
||||
);
|
||||
asyncEvent.init(config);
|
||||
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
|
||||
|
||||
// first poll fires and stays in-flight
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
// re-init while that fetch is pending, then let it resolve; the
|
||||
// stale invocation must not schedule a second loop
|
||||
asyncEvent.init(config);
|
||||
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
|
||||
resolveFetch({ status: 200, body: { result: [] } });
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get(EVENTS_ENDPOINT, {
|
||||
status: 200,
|
||||
body: { result: [] },
|
||||
});
|
||||
|
||||
// exactly one poll per delay from here on
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(2);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
test('does not resume polling when re-initialized with the feature disabled during an in-flight poll', async () => {
|
||||
// stop the real-timer polling loop started by beforeEach before
|
||||
// switching to fake timers, so all polls run on the fake clock
|
||||
mockedIsFeatureEnabled.mockReturnValueOnce(false);
|
||||
asyncEvent.init(config);
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
let resolveFetch: (response: any) => void = () => {};
|
||||
fetchMock.get(
|
||||
EVENTS_ENDPOINT,
|
||||
new Promise(resolve => {
|
||||
resolveFetch = resolve;
|
||||
}),
|
||||
);
|
||||
asyncEvent.init(config);
|
||||
asyncEvent.waitForAsyncData(asyncPendingEvent).catch(() => {});
|
||||
|
||||
// first poll fires and stays in-flight
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
// disable the feature and re-init while the fetch is pending; the
|
||||
// stale invocation must not restart the stopped loop when it resumes
|
||||
mockedIsFeatureEnabled.mockReturnValueOnce(false);
|
||||
asyncEvent.init(config);
|
||||
resolveFetch({ status: 200, body: { result: [] } });
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
fetchMock.get(EVENTS_ENDPOINT, {
|
||||
status: 200,
|
||||
body: { result: [] },
|
||||
});
|
||||
|
||||
await jest.advanceTimersByTimeAsync(
|
||||
10 * config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY,
|
||||
);
|
||||
expect(fetchMock.callHistory.calls(EVENTS_ENDPOINT)).toHaveLength(0);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
// Regression guard for the motivating CodeQL case: a job_id that collides
|
||||
// with a built-in Object property (e.g. "__proto__"/"constructor") must be
|
||||
// routed through the Map-based registries without triggering prototype
|
||||
|
||||
@@ -58,6 +58,9 @@ const LOCALSTORAGE_KEY = 'last_async_event_id';
|
||||
const POLLING_URL = '/api/v1/async_event/';
|
||||
const MAX_RETRIES = 6;
|
||||
const RETRY_DELAY = 100;
|
||||
// Cap for the exponential backoff applied when polling requests fail
|
||||
// repeatedly (e.g. expired session, server or network errors)
|
||||
const MAX_ERROR_POLLING_DELAY_MS = 60000;
|
||||
|
||||
let config: AppConfig;
|
||||
let transport: string;
|
||||
@@ -66,6 +69,11 @@ let pollingTimeoutId: number;
|
||||
let listenersByJobId: Map<string, ListenerFn>;
|
||||
let retriesByJobId: Map<string, number>;
|
||||
let lastReceivedEventId: string | null | undefined;
|
||||
let consecutivePollingErrorCount = 0;
|
||||
// Incremented on every init() so polling invocations that are already
|
||||
// awaiting a fetch when re-init happens can detect they are stale and
|
||||
// stop, instead of mutating fresh state or scheduling a second loop
|
||||
let pollingGeneration = 0;
|
||||
|
||||
const addListener = (id: string, fn: ListenerFn) => {
|
||||
listenersByJobId.set(id, fn);
|
||||
@@ -170,19 +178,34 @@ export const processEvents = async (events: AsyncEvent[]) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getPollingDelay = () => {
|
||||
if (!consecutivePollingErrorCount) return pollingDelayMs;
|
||||
const backoffDelayMs = pollingDelayMs * 2 ** consecutivePollingErrorCount;
|
||||
return Math.max(
|
||||
pollingDelayMs,
|
||||
Math.min(backoffDelayMs, MAX_ERROR_POLLING_DELAY_MS),
|
||||
);
|
||||
};
|
||||
|
||||
const loadEventsFromApi = async () => {
|
||||
const generation = pollingGeneration;
|
||||
const eventArgs = lastReceivedEventId ? { last_id: lastReceivedEventId } : {};
|
||||
if (listenersByJobId.size) {
|
||||
try {
|
||||
const { result: events } = await fetchEvents(eventArgs);
|
||||
if (generation !== pollingGeneration) return;
|
||||
consecutivePollingErrorCount = 0;
|
||||
if (events?.length) await processEvents(events);
|
||||
} catch (err) {
|
||||
if (generation !== pollingGeneration) return;
|
||||
consecutivePollingErrorCount += 1;
|
||||
logging.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (generation !== pollingGeneration) return;
|
||||
if (transport === TRANSPORT_POLLING) {
|
||||
pollingTimeoutId = window.setTimeout(loadEventsFromApi, pollingDelayMs);
|
||||
pollingTimeoutId = window.setTimeout(loadEventsFromApi, getPollingDelay());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -232,12 +255,14 @@ const wsConnect = (): void => {
|
||||
};
|
||||
|
||||
export const init = (appConfig?: AppConfig) => {
|
||||
if (!isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) return;
|
||||
pollingGeneration += 1;
|
||||
if (pollingTimeoutId) clearTimeout(pollingTimeoutId);
|
||||
if (!isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) return;
|
||||
|
||||
listenersByJobId = new Map();
|
||||
retriesByJobId = new Map();
|
||||
lastReceivedEventId = null;
|
||||
consecutivePollingErrorCount = 0;
|
||||
|
||||
config = appConfig || getBootstrapData().common.conf;
|
||||
transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING;
|
||||
|
||||
281
superset-websocket/package-lock.json
generated
281
superset-websocket/package-lock.json
generated
@@ -25,14 +25,14 @@
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
||||
"@typescript-eslint/parser": "^8.62.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^10.5.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.9.4",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.1",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
@@ -741,17 +741,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
|
||||
"integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.63.0.tgz",
|
||||
"integrity": "sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/type-utils": "8.62.1",
|
||||
"@typescript-eslint/utils": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/type-utils": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
@@ -764,22 +764,22 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
|
||||
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.63.0.tgz",
|
||||
"integrity": "sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -794,15 +794,145 @@
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
|
||||
"integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
|
||||
"node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
|
||||
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.62.1",
|
||||
"@typescript-eslint/types": "^8.62.1",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.63.0",
|
||||
"@typescript-eslint/types": "^8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
|
||||
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
|
||||
"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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
|
||||
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
|
||||
"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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
|
||||
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
|
||||
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.63.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.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.63.0.tgz",
|
||||
"integrity": "sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.63.0",
|
||||
"@typescript-eslint/types": "^8.63.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -817,14 +947,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
|
||||
"integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.63.0.tgz",
|
||||
"integrity": "sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1"
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -835,9 +965,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
|
||||
"integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -852,15 +982,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
|
||||
"integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.63.0.tgz",
|
||||
"integrity": "sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||
"@typescript-eslint/utils": "8.62.1",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
@@ -877,9 +1007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
|
||||
"integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.63.0.tgz",
|
||||
"integrity": "sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -891,16 +1021,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
|
||||
"integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.63.0.tgz",
|
||||
"integrity": "sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.62.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"@typescript-eslint/project-service": "8.63.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/visitor-keys": "8.63.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
@@ -919,16 +1049,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
|
||||
"integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.63.0.tgz",
|
||||
"integrity": "sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1"
|
||||
"@typescript-eslint/scope-manager": "8.63.0",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -943,13 +1073,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
|
||||
"integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.63.0.tgz",
|
||||
"integrity": "sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/types": "8.63.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2861,16 +2991,41 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
|
||||
"integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
|
||||
"version": "8.63.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.63.0.tgz",
|
||||
"integrity": "sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.62.1",
|
||||
"@typescript-eslint/parser": "8.62.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.63.0",
|
||||
"@typescript-eslint/parser": "8.63.0",
|
||||
"@typescript-eslint/typescript-estree": "8.63.0",
|
||||
"@typescript-eslint/utils": "8.63.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/parser": {
|
||||
"version": "8.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
|
||||
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.62.1",
|
||||
"@typescript-eslint/types": "8.62.1",
|
||||
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||
"@typescript-eslint/utils": "8.62.1"
|
||||
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
||||
"@typescript-eslint/parser": "^8.62.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"eslint": "^10.5.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.9.4",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.62.1",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -417,12 +417,12 @@ class SupersetShillelaghAdapter(Adapter):
|
||||
query = self._build_sql(bounds, order, limit, offset)
|
||||
|
||||
with self.engine_context() as engine:
|
||||
connection = engine.connect()
|
||||
rows = connection.execute(query)
|
||||
for i, row in enumerate(rows):
|
||||
data = dict(zip(self.columns, row, strict=False))
|
||||
data["rowid"] = data[self._rowid] if self._rowid else i
|
||||
yield data
|
||||
with engine.connect() as connection:
|
||||
rows = connection.execute(query)
|
||||
for i, row in enumerate(rows):
|
||||
data = dict(zip(self.columns, row, strict=False))
|
||||
data["rowid"] = data[self._rowid] if self._rowid else i
|
||||
yield data
|
||||
|
||||
@check_dml
|
||||
def insert_row(self, row: Row) -> int:
|
||||
@@ -445,15 +445,16 @@ class SupersetShillelaghAdapter(Adapter):
|
||||
query = self._table.insert().values(**row)
|
||||
|
||||
with self.engine_context() as engine:
|
||||
connection = engine.connect()
|
||||
result = connection.execute(query)
|
||||
with engine.begin() as connection:
|
||||
result = connection.execute(query)
|
||||
|
||||
# return rowid
|
||||
if self._rowid:
|
||||
return result.inserted_primary_key[0]
|
||||
# return rowid
|
||||
if self._rowid:
|
||||
return result.inserted_primary_key[0]
|
||||
|
||||
query = select(func.count()).select_from(self._table)
|
||||
return connection.execute(query).scalar()
|
||||
with engine.connect() as connection:
|
||||
return connection.execute(query).scalar()
|
||||
|
||||
@check_dml
|
||||
@has_rowid
|
||||
@@ -464,8 +465,8 @@ class SupersetShillelaghAdapter(Adapter):
|
||||
query = self._table.delete().where(self._table.c[self._rowid] == row_id)
|
||||
|
||||
with self.engine_context() as engine:
|
||||
connection = engine.connect()
|
||||
connection.execute(query)
|
||||
with engine.begin() as connection:
|
||||
connection.execute(query)
|
||||
|
||||
@check_dml
|
||||
@has_rowid
|
||||
@@ -488,5 +489,5 @@ class SupersetShillelaghAdapter(Adapter):
|
||||
)
|
||||
|
||||
with self.engine_context() as engine:
|
||||
connection = engine.connect()
|
||||
connection.execute(query)
|
||||
with engine.begin() as connection:
|
||||
connection.execute(query)
|
||||
|
||||
@@ -55,6 +55,52 @@ from superset.sql.parse import SQLScript
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _validate_non_destructive_sql(
|
||||
request: ExecuteSqlRequest,
|
||||
ctx: Context,
|
||||
database: Any,
|
||||
sql_preview: str,
|
||||
) -> ExecuteSqlResponse | None:
|
||||
"""Return an error response when SQL cannot safely be executed."""
|
||||
with event_logger.log_context(action="mcp.execute_sql.ddl_check"):
|
||||
try:
|
||||
sql_to_check: str = request.sql
|
||||
if request.template_params:
|
||||
from superset.jinja_context import get_template_processor
|
||||
|
||||
tp = get_template_processor(database=database)
|
||||
sql_to_check = tp.process_template(
|
||||
request.sql, **request.template_params
|
||||
)
|
||||
|
||||
script = SQLScript(sql_to_check, database.db_engine_spec.engine)
|
||||
if script.has_destructive():
|
||||
await ctx.error("Destructive DDL blocked: sql_preview=%r" % sql_preview)
|
||||
return ExecuteSqlResponse(
|
||||
success=False,
|
||||
error=(
|
||||
"Destructive DDL statements (DROP, TRUNCATE, ALTER) "
|
||||
"are not allowed through MCP. Use the Superset SQL "
|
||||
"Lab UI for administrative database operations."
|
||||
),
|
||||
error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR.value,
|
||||
)
|
||||
except Exception as parse_err:
|
||||
await ctx.error(
|
||||
"DDL pre-check failed to parse SQL, blocking query: %s" % str(parse_err)
|
||||
)
|
||||
return ExecuteSqlResponse(
|
||||
success=False,
|
||||
error=(
|
||||
"SQL could not be parsed for security validation. "
|
||||
"Please check your SQL syntax and try again."
|
||||
),
|
||||
error_type=SupersetErrorType.INVALID_SQL_ERROR.value,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@tool(
|
||||
tags=["mutate"],
|
||||
class_permission_name="SQLLab",
|
||||
@@ -122,44 +168,11 @@ async def execute_sql(request: ExecuteSqlRequest, ctx: Context) -> ExecuteSqlRes
|
||||
# Fail-closed: if parsing fails, block the query rather than
|
||||
# allowing potentially destructive SQL to bypass the check.
|
||||
# Render Jinja2 templates first so templated SQL can be parsed.
|
||||
with event_logger.log_context(action="mcp.execute_sql.ddl_check"):
|
||||
try:
|
||||
sql_to_check = request.sql
|
||||
if request.template_params:
|
||||
from superset.jinja_context import get_template_processor
|
||||
|
||||
tp = get_template_processor(database=database)
|
||||
sql_to_check = tp.process_template(
|
||||
request.sql, **request.template_params
|
||||
)
|
||||
|
||||
script = SQLScript(sql_to_check, database.db_engine_spec.engine)
|
||||
if script.has_destructive():
|
||||
await ctx.error(
|
||||
"Destructive DDL blocked: sql_preview=%r" % sql_preview
|
||||
)
|
||||
return ExecuteSqlResponse(
|
||||
success=False,
|
||||
error=(
|
||||
"Destructive DDL statements (DROP, TRUNCATE, ALTER) "
|
||||
"are not allowed through MCP. Use the Superset SQL "
|
||||
"Lab UI for administrative database operations."
|
||||
),
|
||||
error_type=SupersetErrorType.DML_NOT_ALLOWED_ERROR.value,
|
||||
)
|
||||
except Exception as parse_err:
|
||||
await ctx.error(
|
||||
"DDL pre-check failed to parse SQL, blocking query: %s"
|
||||
% str(parse_err)
|
||||
)
|
||||
return ExecuteSqlResponse(
|
||||
success=False,
|
||||
error=(
|
||||
"SQL could not be parsed for security validation. "
|
||||
"Please check your SQL syntax and try again."
|
||||
),
|
||||
error_type=SupersetErrorType.INVALID_SQL_ERROR.value,
|
||||
)
|
||||
validation_error: (
|
||||
ExecuteSqlResponse | None
|
||||
) = await _validate_non_destructive_sql(request, ctx, database, sql_preview)
|
||||
if validation_error is not None:
|
||||
return validation_error
|
||||
|
||||
# 3. Build QueryOptions and execute query
|
||||
cache_opts = CacheOptions(force_refresh=True) if request.force_refresh else None
|
||||
|
||||
@@ -121,12 +121,13 @@ def test_time_grain_expressions(dttm: str, grain: str, expected: str) -> None:
|
||||
from superset.db_engine_specs.sqlite import SqliteEngineSpec
|
||||
|
||||
engine = create_engine("sqlite://")
|
||||
connection = engine.connect()
|
||||
connection.execute(text("CREATE TABLE t (dttm DATETIME)"))
|
||||
connection.execute(text("INSERT INTO t VALUES (:dttm)"), {"dttm": dttm})
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text("CREATE TABLE t (dttm DATETIME)"))
|
||||
connection.execute(text("INSERT INTO t VALUES (:dttm)"), {"dttm": dttm})
|
||||
|
||||
# pylint: disable=protected-access
|
||||
expression = SqliteEngineSpec._time_grain_expressions[grain].format(col="dttm")
|
||||
sql = f"SELECT {expression} FROM t" # noqa: S608
|
||||
result = connection.execute(text(sql)).scalar()
|
||||
with engine.connect() as connection:
|
||||
result = connection.execute(text(sql)).scalar()
|
||||
assert result == expected
|
||||
|
||||
@@ -63,16 +63,17 @@ def database1(session: Session) -> Iterator["Database"]:
|
||||
@pytest.fixture
|
||||
def table1(session: Session, database1: "Database") -> Iterator[None]:
|
||||
with database1.get_sqla_engine() as engine:
|
||||
conn = engine.connect()
|
||||
conn.execute(
|
||||
text("CREATE TABLE table1 (a INTEGER NOT NULL PRIMARY KEY, b INTEGER)")
|
||||
)
|
||||
conn.execute(text("INSERT INTO table1 (a, b) VALUES (1, 10), (2, 20)"))
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text("CREATE TABLE table1 (a INTEGER NOT NULL PRIMARY KEY, b INTEGER)")
|
||||
)
|
||||
conn.execute(text("INSERT INTO table1 (a, b) VALUES (1, 10), (2, 20)"))
|
||||
db.session.commit()
|
||||
|
||||
yield
|
||||
|
||||
conn.execute(text("DROP TABLE table1"))
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("DROP TABLE table1"))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -99,16 +100,19 @@ def database2(session: Session) -> Iterator["Database"]:
|
||||
@pytest.fixture
|
||||
def table2(session: Session, database2: "Database") -> Iterator[None]:
|
||||
with database2.get_sqla_engine() as engine:
|
||||
conn = engine.connect()
|
||||
conn.execute(
|
||||
text("CREATE TABLE table2 (a INTEGER NOT NULL PRIMARY KEY, b TEXT)")
|
||||
)
|
||||
conn.execute(text("INSERT INTO table2 (a, b) VALUES (1, 'ten'), (2, 'twenty')"))
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text("CREATE TABLE table2 (a INTEGER NOT NULL PRIMARY KEY, b TEXT)")
|
||||
)
|
||||
conn.execute(
|
||||
text("INSERT INTO table2 (a, b) VALUES (1, 'ten'), (2, 'twenty')")
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
yield
|
||||
|
||||
conn.execute(text("DROP TABLE table2"))
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("DROP TABLE table2"))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@@ -137,9 +141,9 @@ def test_superset(mocker: MockerFixture, app_context: None, table1: None) -> Non
|
||||
# Skip test if superset:// dialect can't be loaded (common in Docker)
|
||||
pytest.skip(f"Superset dialect not available: {e}")
|
||||
|
||||
conn = engine.connect()
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20)]
|
||||
with engine.connect() as conn:
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20)]
|
||||
|
||||
|
||||
@with_config(
|
||||
@@ -178,9 +182,9 @@ def test_superset_limit(mocker: MockerFixture, app_context: None, table1: None)
|
||||
# Skip test if superset:// dialect can't be loaded (common in Docker)
|
||||
pytest.skip(f"Superset dialect not available: {e}")
|
||||
|
||||
conn = engine.connect()
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10)]
|
||||
with engine.connect() as conn:
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10)]
|
||||
|
||||
|
||||
@with_feature_flags(ENABLE_SUPERSET_META_DB=True)
|
||||
@@ -213,16 +217,16 @@ def test_superset_joins(
|
||||
# Skip test if superset:// dialect can't be loaded (common in Docker)
|
||||
pytest.skip(f"Superset dialect not available: {e}")
|
||||
|
||||
conn = engine.connect()
|
||||
results = conn.execute(
|
||||
text("""
|
||||
SELECT t1.b, t2.b
|
||||
FROM "database1.table1" AS t1
|
||||
JOIN "database2.table2" AS t2
|
||||
ON t1.a = t2.a
|
||||
""")
|
||||
)
|
||||
assert list(results) == [(10, "ten"), (20, "twenty")]
|
||||
with engine.connect() as conn:
|
||||
results = conn.execute(
|
||||
text("""
|
||||
SELECT t1.b, t2.b
|
||||
FROM "database1.table1" AS t1
|
||||
JOIN "database2.table2" AS t2
|
||||
ON t1.a = t2.a
|
||||
""")
|
||||
)
|
||||
assert list(results) == [(10, "ten"), (20, "twenty")]
|
||||
|
||||
|
||||
@with_feature_flags(ENABLE_SUPERSET_META_DB=True)
|
||||
@@ -257,22 +261,27 @@ def test_dml(
|
||||
# Skip test if superset:// dialect can't be loaded (common in Docker)
|
||||
pytest.skip(f"Superset dialect not available: {e}")
|
||||
|
||||
conn = engine.connect()
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text('INSERT INTO "database1.table1" (a, b) VALUES (3, 30)'))
|
||||
with engine.connect() as conn:
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20), (3, 30)]
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text('UPDATE "database1.table1" SET b=35 WHERE a=3'))
|
||||
with engine.connect() as conn:
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20), (3, 35)]
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text('DELETE FROM "database1.table1" WHERE b>20'))
|
||||
with engine.connect() as conn:
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20)]
|
||||
|
||||
conn.execute(text('INSERT INTO "database1.table1" (a, b) VALUES (3, 30)'))
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20), (3, 30)]
|
||||
conn.execute(text('UPDATE "database1.table1" SET b=35 WHERE a=3'))
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20), (3, 35)]
|
||||
conn.execute(text('DELETE FROM "database1.table1" WHERE b>20'))
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20)]
|
||||
|
||||
with pytest.raises(ProgrammingError) as excinfo:
|
||||
conn.execute(
|
||||
text("""INSERT INTO "database2.table2" (a, b) VALUES (3, 'thirty')""")
|
||||
)
|
||||
with engine.begin() as conn:
|
||||
with pytest.raises(ProgrammingError) as excinfo:
|
||||
conn.execute(
|
||||
text("""INSERT INTO "database2.table2" (a, b) VALUES (3, 'thirty')""")
|
||||
)
|
||||
assert str(excinfo.value).strip() == (
|
||||
"(shillelagh.exceptions.ProgrammingError) DML not enabled in database "
|
||||
'"database2"\n[SQL: INSERT INTO "database2.table2" (a, b) '
|
||||
@@ -324,9 +333,9 @@ def test_security_manager(
|
||||
# Skip test if superset:// dialect can't be loaded (common in Docker)
|
||||
pytest.skip(f"Superset dialect not available: {e}")
|
||||
|
||||
conn = engine.connect()
|
||||
with pytest.raises(SupersetSecurityException) as excinfo:
|
||||
conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
with engine.connect() as conn:
|
||||
with pytest.raises(SupersetSecurityException) as excinfo:
|
||||
conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert str(excinfo.value) == (
|
||||
"You need access to the following tables: `table1`,\n "
|
||||
"`all_database_access` or `all_datasource_access` permission"
|
||||
@@ -358,13 +367,13 @@ def test_allowed_dbs(mocker: MockerFixture, app_context: None, table1: None) ->
|
||||
# Skip test if superset:// dialect can't be loaded (common in Docker)
|
||||
pytest.skip(f"Superset dialect not available: {e}")
|
||||
|
||||
conn = engine.connect()
|
||||
with engine.connect() as conn:
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20)]
|
||||
|
||||
results = conn.execute(text('SELECT * FROM "database1.table1"'))
|
||||
assert list(results) == [(1, 10), (2, 20)]
|
||||
|
||||
with pytest.raises(ProgrammingError) as excinfo:
|
||||
conn.execute(text('SELECT * FROM "database2.table2"'))
|
||||
with engine.connect() as conn:
|
||||
with pytest.raises(ProgrammingError) as excinfo:
|
||||
conn.execute(text('SELECT * FROM "database2.table2"'))
|
||||
assert str(excinfo.value) == (
|
||||
"""
|
||||
(shillelagh.exceptions.ProgrammingError) Unsupported table: database2.table2
|
||||
|
||||
@@ -103,7 +103,7 @@ def _indexes(engine: Engine) -> set[str]:
|
||||
|
||||
|
||||
def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -115,7 +115,7 @@ def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
|
||||
|
||||
def test_downgrade_drops_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -138,7 +138,7 @@ def test_downgrade_preserves_soft_deleted_row_data(engine: Engine) -> None:
|
||||
hard-delete, restore, or rename BEFORE downgrading. See the
|
||||
"Rollback note" in ``UPDATING.md``.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -198,7 +198,7 @@ def test_sqlite_slug_constraint_swap_is_a_no_op(engine: Engine) -> None:
|
||||
"pre-condition: legacy slug constraint must exist before the migration runs"
|
||||
)
|
||||
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -207,7 +207,7 @@ def test_sqlite_slug_constraint_swap_is_a_no_op(engine: Engine) -> None:
|
||||
"upgrade() must keep the legacy slug constraint on SQLite (no-op branch)"
|
||||
)
|
||||
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.downgrade()
|
||||
@@ -222,7 +222,7 @@ def test_upgrade_is_idempotent(engine: Engine) -> None:
|
||||
idempotent skip-if-exists; running ``upgrade()`` twice must not
|
||||
raise.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -233,7 +233,7 @@ def test_downgrade_is_idempotent(engine: Engine) -> None:
|
||||
"""``drop_columns`` / ``drop_index`` are skip-if-not-exists; running
|
||||
``downgrade()`` twice must not raise.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
|
||||
@@ -88,7 +88,7 @@ def _indexes(engine: Engine) -> set[str]:
|
||||
|
||||
def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
"""upgrade() adds the nullable ``deleted_at`` column and its index."""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -101,7 +101,7 @@ def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
|
||||
def test_downgrade_drops_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
"""downgrade() removes both schema artifacts (column and index)."""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -124,7 +124,7 @@ def test_downgrade_preserves_soft_deleted_row_data(engine: Engine) -> None:
|
||||
hard-delete, restore, or rename BEFORE downgrading. See the
|
||||
"Rollback note" in ``UPDATING.md``.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -171,7 +171,7 @@ def test_upgrade_is_idempotent(engine: Engine) -> None:
|
||||
idempotent skip-if-exists; running ``upgrade()`` twice must not
|
||||
raise.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -182,7 +182,7 @@ def test_downgrade_is_idempotent(engine: Engine) -> None:
|
||||
"""``drop_columns`` / ``drop_index`` are skip-if-not-exists; running
|
||||
``downgrade()`` twice must not raise.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
|
||||
@@ -85,7 +85,7 @@ def _indexes(engine: Engine) -> set[str]:
|
||||
|
||||
|
||||
def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -97,7 +97,7 @@ def test_upgrade_adds_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
|
||||
|
||||
def test_downgrade_drops_deleted_at_column_and_index(engine: Engine) -> None:
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -120,7 +120,7 @@ def test_downgrade_preserves_soft_deleted_row_data(engine: Engine) -> None:
|
||||
hard-delete, restore, or rename BEFORE downgrading. See the
|
||||
"Rollback note" in ``UPDATING.md``.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -170,7 +170,7 @@ def test_upgrade_is_idempotent(engine: Engine) -> None:
|
||||
idempotent skip-if-exists; running ``upgrade()`` twice must not
|
||||
raise.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
@@ -181,7 +181,7 @@ def test_downgrade_is_idempotent(engine: Engine) -> None:
|
||||
"""``drop_columns`` / ``drop_index`` are skip-if-not-exists; running
|
||||
``downgrade()`` twice must not raise.
|
||||
"""
|
||||
with engine.connect() as conn:
|
||||
with engine.begin() as conn:
|
||||
ctx = MigrationContext.configure(conn)
|
||||
with Operations.context(ctx):
|
||||
migration.upgrade()
|
||||
|
||||
@@ -3491,6 +3491,7 @@ def test_map_query_object_shifts_time_offset_via_temporal_range_filter(
|
||||
main_query, offset_query = queries[0], queries[1]
|
||||
|
||||
def time_filter_bounds(query: SemanticQuery) -> tuple[datetime, datetime]:
|
||||
assert query.filters is not None
|
||||
gte = next(
|
||||
f.value
|
||||
for f in query.filters
|
||||
@@ -3505,6 +3506,8 @@ def test_map_query_object_shifts_time_offset_via_temporal_range_filter(
|
||||
and f.column.name == "order_date"
|
||||
and f.operator == Operator.LESS_THAN
|
||||
)
|
||||
assert isinstance(gte, datetime)
|
||||
assert isinstance(lt, datetime)
|
||||
return gte, lt
|
||||
|
||||
main_gte, main_lt = time_filter_bounds(main_query)
|
||||
|
||||
Reference in New Issue
Block a user