mirror of
https://github.com/apache/superset.git
synced 2026-08-03 04:22:35 +00:00
Compare commits
8 Commits
feat/remov
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d64eaf9cdb | ||
|
|
4915a3e8c7 | ||
|
|
7ce56d369a | ||
|
|
b6d8c830e7 | ||
|
|
4e3ff37137 | ||
|
|
120b4420b9 | ||
|
|
0628b0b813 | ||
|
|
378634cceb |
5
.github/workflows/check-python-deps.yml
vendored
5
.github/workflows/check-python-deps.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -21,6 +22,10 @@ jobs:
|
||||
check-python-deps:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
5
.github/workflows/dependency-review.yml
vendored
5
.github/workflows/dependency-review.yml
vendored
@@ -20,6 +20,7 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
@@ -49,6 +50,10 @@ jobs:
|
||||
# You cannot use a liccheck.ini file in this workflow.
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
11
.github/workflows/docker.yml
vendored
11
.github/workflows/docker.yml
vendored
@@ -213,3 +213,14 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
docker compose -f docker-compose-image-tag.yml up superset-init --exit-code-from superset-init
|
||||
|
||||
actions-timeline:
|
||||
needs: [docker-build, docker-compose-image-tag]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
10
.github/workflows/pre-commit.yml
vendored
10
.github/workflows/pre-commit.yml
vendored
@@ -15,6 +15,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -188,3 +189,12 @@ jobs:
|
||||
echo "📖 More details here: https://superset.apache.org/docs/contributing/development#git-hooks"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actions-timeline:
|
||||
needs: pre-commit
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
5
.github/workflows/superset-app-cli.yml
vendored
5
.github/workflows/superset-app-cli.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -40,6 +41,10 @@ jobs:
|
||||
ports:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
5
.github/workflows/superset-docs-deploy.yml
vendored
5
.github/workflows/superset-docs-deploy.yml
vendored
@@ -30,6 +30,7 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
config:
|
||||
@@ -59,6 +60,10 @@ jobs:
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
11
.github/workflows/superset-e2e.yml
vendored
11
.github/workflows/superset-e2e.yml
vendored
@@ -340,3 +340,14 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "playwright-tests result: $RESULT (changes: $CHANGES)"
|
||||
|
||||
actions-timeline:
|
||||
needs: [cypress-matrix, playwright-tests, cypress-matrix-required, playwright-tests-required]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
10
.github/workflows/superset-extensions-cli.yml
vendored
10
.github/workflows/superset-extensions-cli.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -69,3 +70,12 @@ jobs:
|
||||
with:
|
||||
name: superset-extensions-cli-coverage-html
|
||||
path: htmlcov/
|
||||
|
||||
actions-timeline:
|
||||
needs: test-superset-extensions-cli-package
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
11
.github/workflows/superset-frontend.yml
vendored
11
.github/workflows/superset-frontend.yml
vendored
@@ -201,3 +201,14 @@ jobs:
|
||||
run: |
|
||||
docker run --rm $TAG bash -c \
|
||||
"npm run build-storybook && npx playwright install-deps && npx playwright install chromium && npm run test-storybook:ci"
|
||||
|
||||
actions-timeline:
|
||||
needs: [report-coverage, lint-frontend, validate-frontend, test-storybook]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
# Serialize runs per PR without cancelling: when a first-time contributor's
|
||||
# queued runs are approved together, cancel-in-progress lets an older run
|
||||
@@ -21,6 +22,10 @@ jobs:
|
||||
lint-test:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
11
.github/workflows/superset-playwright.yml
vendored
11
.github/workflows/superset-playwright.yml
vendored
@@ -170,3 +170,14 @@ jobs:
|
||||
${{ github.workspace }}/superset-frontend/playwright-results/
|
||||
${{ github.workspace }}/superset-frontend/test-results/
|
||||
name: playwright-experimental-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
||||
|
||||
actions-timeline:
|
||||
needs: playwright-tests-experimental
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -255,3 +255,14 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "test-postgres result: $RESULT"
|
||||
|
||||
actions-timeline:
|
||||
needs: [test-mysql, test-postgres, test-sqlite, test-postgres-required]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -158,3 +158,14 @@ jobs:
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
actions-timeline:
|
||||
needs: [test-postgres-presto, test-postgres-hive]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -101,7 +101,14 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: Check unit-tests result
|
||||
env:
|
||||
RESULT: ${{ needs.unit-tests.result }}
|
||||
|
||||
11
.github/workflows/superset-translations.yml
vendored
11
.github/workflows/superset-translations.yml
vendored
@@ -153,3 +153,14 @@ jobs:
|
||||
- name: Fail if regression detected
|
||||
if: steps.regression.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
actions-timeline:
|
||||
needs: [frontend-check-translations, babel-extract]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 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
|
||||
@@ -15,8 +17,6 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Function to determine Python command
|
||||
get_python_command() {
|
||||
if command -v python3 &>/dev/null; then
|
||||
|
||||
@@ -38,7 +38,7 @@ RESET='\033[0m'
|
||||
echo -e "${GREEN}Updating package lists...${RESET}"
|
||||
apt-get update -qq
|
||||
|
||||
echo -e "${GREEN}Installing packages: $@${RESET}"
|
||||
echo -e "${GREEN}Installing packages: $*${RESET}"
|
||||
apt-get install -yqq --no-install-recommends "$@"
|
||||
|
||||
echo -e "${GREEN}Autoremoving unnecessary packages...${RESET}"
|
||||
|
||||
@@ -163,10 +163,10 @@ do
|
||||
# Iterate through the components of the version strings
|
||||
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
|
||||
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="greater"
|
||||
break
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="lesser"
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -122,7 +122,7 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
athena = ["pyathena[pandas]>=2, <4"]
|
||||
athena = ["pyathena[pandas]>=3.35.2, <4"]
|
||||
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
|
||||
bigquery = [
|
||||
"pandas-gbq>=0.35.0",
|
||||
@@ -173,7 +173,7 @@ hana = ["hdbcli==2.29.25", "sqlalchemy_hana==3.0.3"]
|
||||
hive = [
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
"tableschema",
|
||||
"thrift>=0.23.0, <1.0.0",
|
||||
"thrift>=0.24.0, <1.0.0",
|
||||
"thrift_sasl>=0.4.3, < 1.0.0",
|
||||
]
|
||||
impala = ["impyla>=0.24.0, <0.25"]
|
||||
@@ -206,7 +206,7 @@ sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
|
||||
spark = [
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
"tableschema",
|
||||
"thrift>=0.23.0, <1",
|
||||
"thrift>=0.24.0, <1",
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.8.9",
|
||||
|
||||
@@ -35,7 +35,7 @@ acquire_rat_jar () {
|
||||
wget --quiet ${URL} -O "$JAR_DL" && mv "$JAR_DL" "$JAR"
|
||||
else
|
||||
printf "You do not have curl or wget installed, please install rat manually.\n"
|
||||
exit -1
|
||||
exit 255
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -44,7 +44,7 @@ acquire_rat_jar () {
|
||||
# We failed to download
|
||||
rm "$JAR"
|
||||
printf "Our attempt to download rat locally to ${JAR} failed. Please install rat manually.\n"
|
||||
exit -1
|
||||
exit 255
|
||||
fi
|
||||
printf "Done downloading.\n"
|
||||
}
|
||||
|
||||
@@ -163,10 +163,10 @@ do
|
||||
# Iterate through the components of the version strings
|
||||
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
|
||||
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="greater"
|
||||
break
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="lesser"
|
||||
break
|
||||
fi
|
||||
|
||||
210
superset-frontend/package-lock.json
generated
210
superset-frontend/package-lock.json
generated
@@ -190,9 +190,9 @@
|
||||
"@swc/core": "^1.15.46",
|
||||
"@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",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
@@ -11433,28 +11433,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "9.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
|
||||
"integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.1.3",
|
||||
"chalk": "^4.1.0",
|
||||
"aria-query": "5.3.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"picocolors": "1.1.1",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
||||
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-7.0.0.tgz",
|
||||
"integrity": "sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": "^4.4.0",
|
||||
@@ -11465,9 +11465,12 @@
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14",
|
||||
"node": ">=22",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@testing-library/dom": ">=10 <11"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
|
||||
@@ -11477,21 +11480,27 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/react": {
|
||||
"version": "14.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz",
|
||||
"integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==",
|
||||
"version": "15.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-15.0.7.tgz",
|
||||
"integrity": "sha512-cg0RvEdD1TIhhkm1IeYMQxrzy0MtUNfa3minv4MjbgcYzJAZ7yD0i0lwoPOTPr+INtiXFezt2o8xMSnyHhEn2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/dom": "^10.0.0",
|
||||
"@types/react-dom": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/user-event": {
|
||||
@@ -14434,6 +14443,7 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -14601,12 +14611,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
|
||||
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"deep-equal": "^2.0.5"
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/arr-union": {
|
||||
@@ -16177,6 +16187,7 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
@@ -18172,38 +18183,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-equal": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
|
||||
"integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"array-buffer-byte-length": "^1.0.0",
|
||||
"call-bind": "^1.0.5",
|
||||
"es-get-iterator": "^1.1.3",
|
||||
"get-intrinsic": "^1.2.2",
|
||||
"is-arguments": "^1.1.1",
|
||||
"is-array-buffer": "^3.0.2",
|
||||
"is-date-object": "^1.0.5",
|
||||
"is-regex": "^1.1.4",
|
||||
"is-shared-array-buffer": "^1.0.2",
|
||||
"isarray": "^2.0.5",
|
||||
"object-is": "^1.1.5",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.4",
|
||||
"regexp.prototype.flags": "^1.5.1",
|
||||
"side-channel": "^1.0.4",
|
||||
"which-boxed-primitive": "^1.0.2",
|
||||
"which-collection": "^1.0.1",
|
||||
"which-typed-array": "^1.1.13"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
@@ -19215,26 +19194,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-get-iterator": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
|
||||
"integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.2",
|
||||
"get-intrinsic": "^1.1.3",
|
||||
"has-symbols": "^1.0.3",
|
||||
"is-arguments": "^1.1.1",
|
||||
"is-map": "^2.0.2",
|
||||
"is-set": "^2.0.2",
|
||||
"is-string": "^1.0.7",
|
||||
"isarray": "^2.0.5",
|
||||
"stop-iteration-iterator": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
@@ -22777,6 +22736,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -23985,22 +23945,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"has-tostringtag": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||
@@ -31769,22 +31713,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-is": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind": "^1.0.7",
|
||||
"define-properties": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
@@ -38501,26 +38429,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/storybook/node_modules/@testing-library/dom": {
|
||||
"version": "10.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"node_modules/storybook/node_modules/@testing-library/jest-dom": {
|
||||
"version": "6.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
||||
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^5.0.1",
|
||||
"aria-query": "5.3.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.5.0",
|
||||
"picocolors": "1.1.1",
|
||||
"pretty-format": "^27.0.2"
|
||||
"@adobe/css-tools": "^4.4.0",
|
||||
"aria-query": "^5.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.6.3",
|
||||
"picocolors": "^1.1.1",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/storybook/node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
|
||||
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/storybook/node_modules/@testing-library/user-event": {
|
||||
"version": "14.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
|
||||
@@ -38535,16 +38470,6 @@
|
||||
"@testing-library/dom": ">=7.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/storybook/node_modules/aria-query": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/storybook/node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
@@ -39311,6 +39236,7 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
@@ -43686,9 +43612,9 @@
|
||||
"@babel/preset-react": "^7.29.7",
|
||||
"@babel/preset-typescript": "^7.29.7",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/react": "*",
|
||||
@@ -43728,9 +43654,9 @@
|
||||
"@ant-design/icons": "^5.6.1 || ^6.0.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
@@ -43813,9 +43739,9 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
@@ -44088,7 +44014,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
@@ -44184,9 +44110,9 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.3.0",
|
||||
@@ -44326,9 +44252,9 @@
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.7",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/jest": "^30.0.0",
|
||||
"jest": "^30.4.2"
|
||||
@@ -44384,9 +44310,9 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^8.2.0",
|
||||
|
||||
@@ -275,9 +275,9 @@
|
||||
"@swc/core": "^1.15.46",
|
||||
"@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",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
|
||||
@@ -93,9 +93,9 @@
|
||||
"typescript": "^5.0.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
"@ant-design/icons": "^5.6.1 || ^6.0.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.3.0",
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.7",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/jest": "^30.0.0",
|
||||
"jest": "^30.4.2"
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^8.2.0",
|
||||
|
||||
@@ -868,7 +868,7 @@ def check_chart_data_access(chart: Any) -> "DatasetValidationResult":
|
||||
"""
|
||||
from superset.mcp_service.chart.chart_utils import validate_chart_dataset
|
||||
|
||||
return validate_chart_dataset(chart, check_access=True)
|
||||
return validate_chart_dataset(chart.datasource_id, check_access=True)
|
||||
|
||||
|
||||
def _log_user_resolution_failure(exc: ValueError | PermissionError) -> None:
|
||||
|
||||
@@ -70,7 +70,7 @@ class DatasetValidationResult:
|
||||
|
||||
|
||||
def validate_chart_dataset(
|
||||
chart: Any,
|
||||
datasource_id: int | None,
|
||||
check_access: bool = True,
|
||||
) -> DatasetValidationResult:
|
||||
"""
|
||||
@@ -79,8 +79,12 @@ def validate_chart_dataset(
|
||||
This shared utility should be called by MCP tools after creating or retrieving
|
||||
charts to detect issues like missing or deleted datasets early.
|
||||
|
||||
Takes the datasource id rather than the chart so that callers holding an ORM
|
||||
instance read it while that instance is attached; reading it here can raise
|
||||
``DetachedInstanceError`` when a concurrent request has torn down the session.
|
||||
|
||||
Args:
|
||||
chart: A chart-like object with datasource_id, datasource_type attributes
|
||||
datasource_id: The chart's ``datasource_id``, or None if it has none
|
||||
check_access: Whether to also check user permissions (default True)
|
||||
|
||||
Returns:
|
||||
@@ -92,7 +96,6 @@ def validate_chart_dataset(
|
||||
from superset.mcp_service.auth import has_dataset_access
|
||||
|
||||
warnings: list[str] = []
|
||||
datasource_id = getattr(chart, "datasource_id", None)
|
||||
|
||||
# Check if chart has a datasource reference
|
||||
if datasource_id is None:
|
||||
@@ -1524,11 +1527,9 @@ def get_table_chart_type_label(viz_type: str | None) -> str | None:
|
||||
return TABLE_VIZ_TYPE_LABELS.get(viz_type) if viz_type is not None else None
|
||||
|
||||
|
||||
def analyze_chart_capabilities(chart: Any | None, config: Any) -> ChartCapabilities:
|
||||
def analyze_chart_capabilities(viz_type: str | None, config: Any) -> ChartCapabilities:
|
||||
"""Analyze chart capabilities based on type and configuration."""
|
||||
if chart:
|
||||
viz_type = getattr(chart, "viz_type", "unknown")
|
||||
else:
|
||||
if not viz_type:
|
||||
viz_type = _resolve_viz_type(config)
|
||||
|
||||
# Determine interaction capabilities based on chart type
|
||||
@@ -1574,11 +1575,9 @@ def analyze_chart_capabilities(chart: Any | None, config: Any) -> ChartCapabilit
|
||||
)
|
||||
|
||||
|
||||
def analyze_chart_semantics(chart: Any | None, config: Any) -> ChartSemantics:
|
||||
def analyze_chart_semantics(viz_type: str | None, config: Any) -> ChartSemantics:
|
||||
"""Generate semantic understanding of the chart."""
|
||||
if chart:
|
||||
viz_type = getattr(chart, "viz_type", "unknown")
|
||||
else:
|
||||
if not viz_type:
|
||||
viz_type = _resolve_viz_type(config)
|
||||
|
||||
# Generate primary insight based on chart type
|
||||
|
||||
@@ -317,6 +317,9 @@ async def generate_chart( # noqa: C901
|
||||
|
||||
chart = None
|
||||
chart_id = None
|
||||
chart_slice_name = None
|
||||
chart_viz_type = None
|
||||
chart_uuid = None
|
||||
explore_url = None
|
||||
form_data_key = None
|
||||
response_warnings: list[str] = form_data.pop("_mcp_warnings", [])
|
||||
@@ -424,7 +427,6 @@ async def generate_chart( # noqa: C901
|
||||
)
|
||||
|
||||
chart = command.run()
|
||||
chart_id = chart.id
|
||||
|
||||
# Ensure chart was created successfully before committing
|
||||
if not chart or not chart.id:
|
||||
@@ -432,6 +434,19 @@ async def generate_chart( # noqa: C901
|
||||
"Chart creation failed - no chart ID returned"
|
||||
)
|
||||
|
||||
# Snapshot the scalar fields now, while the instance is
|
||||
# known to be attached. The chart is already committed at
|
||||
# this point, and every read further down happens after an
|
||||
# await: under concurrency another in-flight request can
|
||||
# tear down the shared session in between, which detaches
|
||||
# this instance and turns any attribute access into a
|
||||
# DetachedInstanceError.
|
||||
chart_id = chart.id
|
||||
chart_slice_name = chart.slice_name
|
||||
chart_viz_type = chart.viz_type
|
||||
chart_uuid = str(chart.uuid) if chart.uuid else None
|
||||
chart_datasource_id = chart.datasource_id
|
||||
|
||||
# Reload server-generated timestamps (created_on,
|
||||
# changed_on) so the serializer sees real values.
|
||||
from superset import db
|
||||
@@ -442,20 +457,22 @@ async def generate_chart( # noqa: C901
|
||||
logger.warning(
|
||||
"Chart %s created but refresh failed; "
|
||||
"continuing with current values",
|
||||
chart.id,
|
||||
chart_id,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
await ctx.info(
|
||||
"Chart created successfully: chart_id=%s, chart_name=%s"
|
||||
% (
|
||||
chart.id,
|
||||
chart.slice_name,
|
||||
chart_id,
|
||||
chart_slice_name,
|
||||
)
|
||||
)
|
||||
|
||||
# Post-creation validation: verify the chart's dataset is accessible
|
||||
dataset_check = validate_chart_dataset(chart, check_access=True)
|
||||
dataset_check = validate_chart_dataset(
|
||||
chart_datasource_id, check_access=True
|
||||
)
|
||||
if not dataset_check.is_valid:
|
||||
# Dataset validation failed - warn but don't fail the operation
|
||||
await ctx.warning(
|
||||
@@ -464,7 +481,7 @@ async def generate_chart( # noqa: C901
|
||||
)
|
||||
logger.warning(
|
||||
"Chart %s created but dataset validation failed: %s",
|
||||
chart.id,
|
||||
chart_id,
|
||||
dataset_check.error,
|
||||
)
|
||||
if dataset_check.error:
|
||||
@@ -482,7 +499,7 @@ async def generate_chart( # noqa: C901
|
||||
# Query failed — delete the broken chart and return an error
|
||||
logger.warning(
|
||||
"Compile check failed for chart %s: %s",
|
||||
chart.id,
|
||||
chart_id,
|
||||
compile_result.error,
|
||||
)
|
||||
await ctx.warning(
|
||||
@@ -537,7 +554,7 @@ async def generate_chart( # noqa: C901
|
||||
await ctx.error("Chart creation failed: error=%s" % (str(e),))
|
||||
raise
|
||||
# Update explore URL to use saved chart
|
||||
explore_url = f"{get_superset_base_url()}/explore/?slice_id={chart.id}"
|
||||
explore_url = f"{get_superset_base_url()}/explore/?slice_id={chart_id}"
|
||||
|
||||
# Generate form_data_key for saved charts (needed for chatbot rendering)
|
||||
try:
|
||||
@@ -561,7 +578,7 @@ async def generate_chart( # noqa: C901
|
||||
cmd_params = CommandParameters(
|
||||
datasource_type=DatasourceType.TABLE,
|
||||
datasource_id=dataset.id,
|
||||
chart_id=chart.id,
|
||||
chart_id=chart_id,
|
||||
tab_id=None,
|
||||
form_data=json.dumps(form_data_with_datasource),
|
||||
)
|
||||
@@ -666,8 +683,8 @@ async def generate_chart( # noqa: C901
|
||||
response_warnings.extend(compile_result.warnings)
|
||||
|
||||
# Generate semantic analysis
|
||||
capabilities = analyze_chart_capabilities(chart, config)
|
||||
semantics = analyze_chart_semantics(chart, config)
|
||||
capabilities = analyze_chart_capabilities(chart_viz_type, config)
|
||||
semantics = analyze_chart_semantics(chart_viz_type, config)
|
||||
|
||||
# Create performance metadata
|
||||
execution_time = int((time.time() - start_time) * 1000)
|
||||
@@ -678,11 +695,7 @@ async def generate_chart( # noqa: C901
|
||||
)
|
||||
|
||||
# Create accessibility metadata
|
||||
chart_name = (
|
||||
chart.slice_name
|
||||
if chart and hasattr(chart, "slice_name")
|
||||
else generate_chart_name(config)
|
||||
)
|
||||
chart_name = chart_slice_name or generate_chart_name(config)
|
||||
accessibility = AccessibilityMetadata(
|
||||
color_blind_safe=True, # Would need actual analysis
|
||||
alt_text=f"Chart showing {chart_name}",
|
||||
@@ -775,7 +788,7 @@ async def generate_chart( # noqa: C901
|
||||
# Build chart info using serialize_chart_object for saved charts
|
||||
chart_info = None
|
||||
chart_data = None
|
||||
if request.save_chart and chart:
|
||||
if request.save_chart and chart_id:
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from superset import db
|
||||
@@ -793,7 +806,7 @@ async def generate_chart( # noqa: C901
|
||||
try:
|
||||
chart = (
|
||||
ChartDAO.find_by_id(
|
||||
chart.id,
|
||||
chart_id,
|
||||
query_options=[
|
||||
joinedload(Slice.editors),
|
||||
joinedload(Slice.tags),
|
||||
@@ -804,7 +817,7 @@ async def generate_chart( # noqa: C901
|
||||
except SQLAlchemyError:
|
||||
logger.warning(
|
||||
"Re-fetch of chart %s failed; returning minimal response",
|
||||
chart.id,
|
||||
chart_id,
|
||||
exc_info=True,
|
||||
)
|
||||
try:
|
||||
@@ -815,11 +828,11 @@ async def generate_chart( # noqa: C901
|
||||
exc_info=True,
|
||||
)
|
||||
chart_data = {
|
||||
"id": chart.id,
|
||||
"slice_name": chart.slice_name,
|
||||
"viz_type": chart.viz_type,
|
||||
"id": chart_id,
|
||||
"slice_name": chart_slice_name,
|
||||
"viz_type": chart_viz_type,
|
||||
"url": explore_url,
|
||||
"uuid": str(chart.uuid) if chart.uuid else None,
|
||||
"uuid": chart_uuid,
|
||||
}
|
||||
|
||||
if chart_data is None:
|
||||
@@ -849,14 +862,10 @@ async def generate_chart( # noqa: C901
|
||||
"form_data": _sanitize_generate_chart_form_data_for_llm_context(form_data),
|
||||
"form_data_key": form_data_key,
|
||||
"api_endpoints": {
|
||||
"data": f"{get_superset_base_url()}/api/v1/chart/{chart.id}/data/"
|
||||
if chart
|
||||
else None,
|
||||
"export": f"{get_superset_base_url()}/api/v1/chart/{chart.id}/export/"
|
||||
if chart
|
||||
else None,
|
||||
"data": f"{get_superset_base_url()}/api/v1/chart/{chart_id}/data/",
|
||||
"export": f"{get_superset_base_url()}/api/v1/chart/{chart_id}/export/",
|
||||
}
|
||||
if chart
|
||||
if chart_id
|
||||
else {},
|
||||
"performance": performance.model_dump() if performance else None,
|
||||
"accessibility": accessibility.model_dump() if accessibility else None,
|
||||
@@ -870,7 +879,7 @@ async def generate_chart( # noqa: C901
|
||||
await ctx.info(
|
||||
"Chart generation completed successfully: chart_id=%s, execution_time_ms=%s"
|
||||
% (
|
||||
chart.id if chart else None,
|
||||
chart_id,
|
||||
int((time.time() - start_time) * 1000),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -416,7 +416,9 @@ async def get_chart_data( # noqa: C901
|
||||
|
||||
# Skip the dataset RBAC pre-check for guests (see guest_scope.is_guest_read).
|
||||
if not guest_scope.is_guest_read():
|
||||
validation_result = validate_chart_dataset(chart, check_access=True)
|
||||
validation_result = validate_chart_dataset(
|
||||
chart.datasource_id, check_access=True
|
||||
)
|
||||
if not validation_result.is_valid:
|
||||
await ctx.warning(
|
||||
"Chart found but dataset is not accessible: %s"
|
||||
|
||||
@@ -116,7 +116,7 @@ async def _validate_chart_dataset_access(
|
||||
chart = ChartDAO.find_by_id(result.id)
|
||||
if not chart:
|
||||
return None
|
||||
validation_result = validate_chart_dataset(chart, check_access=True)
|
||||
validation_result = validate_chart_dataset(chart.datasource_id, check_access=True)
|
||||
if not validation_result.is_valid:
|
||||
await ctx.warning(
|
||||
"Chart found but dataset is not accessible: %s" % (validation_result.error,)
|
||||
|
||||
@@ -1262,7 +1262,9 @@ async def _get_chart_preview_internal( # noqa: C901
|
||||
from superset.mcp_service import guest_scope
|
||||
|
||||
if getattr(chart, "id", None) is not None and not guest_scope.is_guest_read():
|
||||
validation_result = validate_chart_dataset(chart, check_access=True)
|
||||
validation_result = validate_chart_dataset(
|
||||
chart.datasource_id, check_access=True
|
||||
)
|
||||
if not validation_result.is_valid:
|
||||
await ctx.warning(
|
||||
"Chart found but dataset is not accessible: %s"
|
||||
|
||||
@@ -421,7 +421,7 @@ async def _handle_chart_sql_request(
|
||||
)
|
||||
|
||||
# Validate the chart's dataset is accessible
|
||||
validation_result = validate_chart_dataset(chart, check_access=True)
|
||||
validation_result = validate_chart_dataset(chart.datasource_id, check_access=True)
|
||||
if not validation_result.is_valid:
|
||||
await ctx.warning(
|
||||
"Chart found but dataset is not accessible: %s" % (validation_result.error,)
|
||||
|
||||
@@ -579,8 +579,9 @@ async def update_chart( # noqa: C901
|
||||
)
|
||||
|
||||
chart_for_analysis = updated_chart if saved else chart
|
||||
capabilities = analyze_chart_capabilities(chart_for_analysis, parsed_config)
|
||||
semantics = analyze_chart_semantics(chart_for_analysis, parsed_config)
|
||||
viz_type_for_analysis = getattr(chart_for_analysis, "viz_type", None)
|
||||
capabilities = analyze_chart_capabilities(viz_type_for_analysis, parsed_config)
|
||||
semantics = analyze_chart_semantics(viz_type_for_analysis, parsed_config)
|
||||
|
||||
execution_time = int((time.time() - start_time) * 1000)
|
||||
performance = PerformanceMetadata(
|
||||
|
||||
@@ -224,6 +224,7 @@ class TestChartRestoreApi(SupersetTestCase):
|
||||
chart_id = chart.id
|
||||
chart_uuid = str(chart.uuid)
|
||||
entity_uuid = chart.uuid
|
||||
assert entity_uuid is not None
|
||||
original_name = chart.slice_name
|
||||
original_created_by = chart.created_by_fk
|
||||
before_changed_on = chart.changed_on
|
||||
@@ -329,6 +330,8 @@ class TestChartRestoreApi(SupersetTestCase):
|
||||
assert alpha not in chart.editors
|
||||
|
||||
ver_cls = version_class(Slice)
|
||||
entity_uuid = chart.uuid
|
||||
assert entity_uuid is not None
|
||||
first_tx = (
|
||||
db.session.query(ver_cls.transaction_id)
|
||||
.filter(ver_cls.id == chart.id)
|
||||
@@ -337,7 +340,7 @@ class TestChartRestoreApi(SupersetTestCase):
|
||||
.scalar()
|
||||
)
|
||||
assert first_tx is not None
|
||||
target_uuid = str(derive_version_uuid(chart.uuid, first_tx))
|
||||
target_uuid = str(derive_version_uuid(entity_uuid, first_tx))
|
||||
|
||||
self.login(ALPHA_USERNAME)
|
||||
rv = self._restore(str(chart.uuid), target_uuid)
|
||||
@@ -387,6 +390,8 @@ class TestChartRestoreApi(SupersetTestCase):
|
||||
assert boys is not None
|
||||
|
||||
ver_cls = version_class(Slice)
|
||||
boys_uuid = boys.uuid
|
||||
assert boys_uuid is not None
|
||||
boys_tx = (
|
||||
db.session.query(ver_cls.transaction_id)
|
||||
.filter(ver_cls.id == boys.id)
|
||||
@@ -395,7 +400,7 @@ class TestChartRestoreApi(SupersetTestCase):
|
||||
.scalar()
|
||||
)
|
||||
assert boys_tx is not None
|
||||
boys_version_uuid = str(derive_version_uuid(boys.uuid, boys_tx))
|
||||
boys_version_uuid = str(derive_version_uuid(boys_uuid, boys_tx))
|
||||
|
||||
self.login(ADMIN_USERNAME)
|
||||
rv = self._restore(str(girls.uuid), boys_version_uuid)
|
||||
@@ -421,6 +426,8 @@ class TestChartRestoreApi(SupersetTestCase):
|
||||
db.session.commit()
|
||||
|
||||
ver_cls = version_class(Slice)
|
||||
entity_uuid = chart.uuid
|
||||
assert entity_uuid is not None
|
||||
first_tx = (
|
||||
db.session.query(ver_cls.transaction_id)
|
||||
.filter(ver_cls.id == chart_id)
|
||||
@@ -428,7 +435,7 @@ class TestChartRestoreApi(SupersetTestCase):
|
||||
.limit(1)
|
||||
.scalar()
|
||||
)
|
||||
target_uuid = str(derive_version_uuid(chart.uuid, first_tx))
|
||||
target_uuid = str(derive_version_uuid(entity_uuid, first_tx))
|
||||
|
||||
self.login(ADMIN_USERNAME)
|
||||
rv = self._restore(str(chart.uuid), target_uuid)
|
||||
|
||||
@@ -90,6 +90,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
original_title = dashboard.dashboard_title
|
||||
dashboard_id = dashboard.id
|
||||
entity_uuid = dashboard.uuid
|
||||
assert entity_uuid is not None
|
||||
|
||||
# Make two more edits so we have a known non-trivial history to
|
||||
# navigate: [initial, v1, v2].
|
||||
@@ -151,6 +152,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
dashboard_uuid = str(dashboard.uuid)
|
||||
dashboard_id = dashboard.id
|
||||
entity_uuid = dashboard.uuid
|
||||
assert entity_uuid is not None
|
||||
|
||||
original_slice_ids = sorted(s.id for s in dashboard.slices)
|
||||
assert len(original_slice_ids) >= 2, (
|
||||
@@ -225,6 +227,8 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
db.session.commit()
|
||||
|
||||
ver_cls = version_class(Dashboard)
|
||||
entity_uuid = dashboard.uuid
|
||||
assert entity_uuid is not None
|
||||
target_tx = (
|
||||
db.session.query(ver_cls.transaction_id)
|
||||
.filter(ver_cls.id == dashboard_id)
|
||||
@@ -232,7 +236,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
.limit(1)
|
||||
.scalar()
|
||||
)
|
||||
target_uuid = str(derive_version_uuid(dashboard.uuid, target_tx))
|
||||
target_uuid = str(derive_version_uuid(entity_uuid, target_tx))
|
||||
|
||||
# Edit the member chart AFTER the snapshot.
|
||||
member = db.session.query(Slice).filter(Slice.id == member_id).one()
|
||||
@@ -282,6 +286,8 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
db.session.commit()
|
||||
|
||||
ver_cls = version_class(Dashboard)
|
||||
entity_uuid = dashboard.uuid
|
||||
assert entity_uuid is not None
|
||||
target_tx = (
|
||||
db.session.query(ver_cls.transaction_id)
|
||||
.filter(ver_cls.id == dashboard_id)
|
||||
@@ -289,7 +295,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
.limit(1)
|
||||
.scalar()
|
||||
)
|
||||
target_uuid = str(derive_version_uuid(dashboard.uuid, target_tx))
|
||||
target_uuid = str(derive_version_uuid(entity_uuid, target_tx))
|
||||
|
||||
# Detach, then hard-delete the victim via raw SQL so no live row
|
||||
# remains (bypasses the soft-delete listener deliberately — the
|
||||
@@ -340,6 +346,8 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
assert alpha not in dashboard.editors
|
||||
|
||||
ver_cls = version_class(Dashboard)
|
||||
entity_uuid = dashboard.uuid
|
||||
assert entity_uuid is not None
|
||||
first_tx = (
|
||||
db.session.query(ver_cls.transaction_id)
|
||||
.filter(ver_cls.id == dashboard.id)
|
||||
@@ -348,7 +356,7 @@ class TestDashboardRestoreApi(SupersetTestCase):
|
||||
.scalar()
|
||||
)
|
||||
assert first_tx is not None
|
||||
target_uuid = str(derive_version_uuid(dashboard.uuid, first_tx))
|
||||
target_uuid = str(derive_version_uuid(entity_uuid, first_tx))
|
||||
|
||||
self.login(ALPHA_USERNAME)
|
||||
rv = self._restore(str(dashboard.uuid), target_uuid)
|
||||
|
||||
@@ -109,6 +109,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
|
||||
assert table is not None
|
||||
table_uuid = str(table.uuid)
|
||||
entity_uuid = table.uuid
|
||||
assert entity_uuid is not None
|
||||
table_id = table.id
|
||||
original_description = table.description
|
||||
|
||||
@@ -164,6 +165,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
|
||||
assert table is not None
|
||||
table_uuid = str(table.uuid)
|
||||
entity_uuid = table.uuid
|
||||
assert entity_uuid is not None
|
||||
table_id = table.id
|
||||
|
||||
col = table.columns[0]
|
||||
@@ -219,6 +221,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
|
||||
table_id = table.id
|
||||
table_uuid = str(table.uuid)
|
||||
entity_uuid = table.uuid
|
||||
assert entity_uuid is not None
|
||||
|
||||
original_col_names = sorted(c.column_name for c in table.columns)
|
||||
removed_name = table.columns[0].column_name
|
||||
@@ -285,6 +288,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
|
||||
table_id = table.id
|
||||
table_uuid = str(table.uuid)
|
||||
entity_uuid = table.uuid
|
||||
assert entity_uuid is not None
|
||||
removed_name = table.columns[0].column_name
|
||||
added_name = "__restore_full_diff_test__"
|
||||
|
||||
@@ -399,6 +403,7 @@ class TestDatasetRestoreApi(SupersetTestCase):
|
||||
table_id = table.id
|
||||
table_uuid = str(table.uuid)
|
||||
entity_uuid = table.uuid
|
||||
assert entity_uuid is not None
|
||||
original_description = table.description
|
||||
original_col_names = sorted(c.column_name for c in table.columns)
|
||||
|
||||
|
||||
@@ -1945,9 +1945,8 @@ class TestValidateChartDataset:
|
||||
def test_validate_chart_dataset_no_datasource_id(
|
||||
self, mock_find: MagicMock, mock_access: MagicMock
|
||||
) -> None:
|
||||
"""Chart with no datasource_id returns invalid result."""
|
||||
chart = MagicMock(spec=[]) # no datasource_id attribute
|
||||
result = validate_chart_dataset(chart)
|
||||
"""A chart with no datasource_id returns invalid result."""
|
||||
result = validate_chart_dataset(None)
|
||||
assert not result.is_valid
|
||||
assert result.dataset_id is None
|
||||
assert "no dataset reference" in (result.error or "").lower()
|
||||
@@ -1959,9 +1958,7 @@ class TestValidateChartDataset:
|
||||
self, mock_find: MagicMock, mock_access: MagicMock
|
||||
) -> None:
|
||||
"""Chart whose dataset was deleted returns invalid result."""
|
||||
chart = MagicMock()
|
||||
chart.datasource_id = 42
|
||||
result = validate_chart_dataset(chart)
|
||||
result = validate_chart_dataset(42)
|
||||
assert not result.is_valid
|
||||
assert result.dataset_id == 42
|
||||
assert "deleted" in (result.error or "").lower()
|
||||
@@ -1976,9 +1973,7 @@ class TestValidateChartDataset:
|
||||
dataset.table_name = "my_table"
|
||||
dataset.sql = None
|
||||
mock_find.return_value = dataset
|
||||
chart = MagicMock()
|
||||
chart.datasource_id = 7
|
||||
result = validate_chart_dataset(chart)
|
||||
result = validate_chart_dataset(7)
|
||||
assert result.is_valid
|
||||
assert result.dataset_id == 7
|
||||
assert result.dataset_name == "my_table"
|
||||
@@ -1994,9 +1989,7 @@ class TestValidateChartDataset:
|
||||
dataset.table_name = "virt_ds"
|
||||
dataset.sql = "SELECT 1"
|
||||
mock_find.return_value = dataset
|
||||
chart = MagicMock()
|
||||
chart.datasource_id = 10
|
||||
result = validate_chart_dataset(chart)
|
||||
result = validate_chart_dataset(10)
|
||||
assert result.is_valid
|
||||
assert len(result.warnings) == 1
|
||||
assert "virtual" in result.warnings[0].lower()
|
||||
@@ -2010,9 +2003,7 @@ class TestValidateChartDataset:
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
mock_find.side_effect = SQLAlchemyError("connection lost")
|
||||
chart = MagicMock()
|
||||
chart.datasource_id = 99
|
||||
result = validate_chart_dataset(chart)
|
||||
result = validate_chart_dataset(99)
|
||||
assert not result.is_valid
|
||||
assert result.dataset_id == 99
|
||||
assert "error" in (result.error or "").lower()
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
Unit tests for MCP generate_chart tool
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm.exc import DetachedInstanceError
|
||||
|
||||
from superset.mcp_service.chart.schemas import (
|
||||
@@ -438,6 +440,154 @@ def _make_mock_chart(chart_id: int = 42) -> Mock:
|
||||
return chart
|
||||
|
||||
|
||||
class _DetachableSlice:
|
||||
"""A Slice stand-in that starts attached and can be detached at will.
|
||||
|
||||
Once detached, every attribute read raises ``DetachedInstanceError``, which
|
||||
is what SQLAlchemy does when a concurrent request tears down the session
|
||||
this instance was loaded in.
|
||||
"""
|
||||
|
||||
def __init__(self, chart_id: int = 42) -> None:
|
||||
self._values = {
|
||||
"id": chart_id,
|
||||
"slice_name": "Concurrent chart",
|
||||
"viz_type": "table",
|
||||
"uuid": "2a0e0e0e-0000-4000-8000-000000000042",
|
||||
"datasource_id": 1,
|
||||
}
|
||||
self._detached = False
|
||||
|
||||
def detach(self) -> None:
|
||||
self._detached = True
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if self._detached:
|
||||
raise DetachedInstanceError(
|
||||
f"Instance <Slice> is not bound to a Session; "
|
||||
f"attribute refresh operation cannot proceed ({name})"
|
||||
)
|
||||
try:
|
||||
return self._values[name]
|
||||
except KeyError as ex:
|
||||
raise AttributeError(name) from ex
|
||||
|
||||
|
||||
async def _generate_saved_chart(
|
||||
refetch: Any,
|
||||
) -> tuple[Any, _DetachableSlice]:
|
||||
"""Run generate_chart(save_chart=True) with a chart that detaches on commit.
|
||||
|
||||
``refetch`` is used as the ``ChartDAO.find_by_id`` behaviour of the
|
||||
serialization path.
|
||||
"""
|
||||
request = GenerateChartRequest(
|
||||
dataset_id="1",
|
||||
config=TableChartConfig(chart_type="table", columns=[ColumnRef(name="region")]),
|
||||
save_chart=True,
|
||||
generate_preview=False,
|
||||
)
|
||||
ctx = MagicMock()
|
||||
ctx.info = AsyncMock()
|
||||
ctx.debug = AsyncMock()
|
||||
ctx.warning = AsyncMock()
|
||||
ctx.error = AsyncMock()
|
||||
ctx.report_progress = AsyncMock()
|
||||
|
||||
chart = _DetachableSlice()
|
||||
dataset = Mock(
|
||||
id=1, datasource_name="test_table", table_name="test_table", sql=None
|
||||
)
|
||||
validation_result = Mock(is_valid=True, request=request, warnings={}, error=None)
|
||||
session = MagicMock()
|
||||
# The instance is detached right after the commit, before any of the reads
|
||||
# that build the response.
|
||||
session.refresh.side_effect = lambda _chart: chart.detach()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"superset.mcp_service.auth.get_user_from_request",
|
||||
return_value=Mock(id=1, username="admin", roles=[], groups=[]),
|
||||
),
|
||||
patch(
|
||||
"superset.mcp_service.chart.validation.ValidationPipeline."
|
||||
"validate_request_with_warnings",
|
||||
return_value=validation_result,
|
||||
),
|
||||
patch("superset.daos.dataset.DatasetDAO.find_by_id", return_value=dataset),
|
||||
patch(
|
||||
"superset.mcp_service.chart.tool.generate_chart.has_dataset_access",
|
||||
return_value=True,
|
||||
),
|
||||
# validate_chart_dataset is deliberately not mocked: it runs for real
|
||||
# against the detached instance, which is where it used to raise.
|
||||
patch("superset.mcp_service.auth.has_dataset_access", return_value=True),
|
||||
patch(
|
||||
"superset.commands.chart.create.CreateChartCommand",
|
||||
return_value=Mock(run=Mock(return_value=chart)),
|
||||
),
|
||||
patch("superset.db.session", session),
|
||||
patch(
|
||||
"superset.mcp_service.chart.tool.generate_chart._compile_chart",
|
||||
return_value=CompileResult(success=True, warnings=[]),
|
||||
),
|
||||
patch("superset.daos.chart.ChartDAO", Mock(find_by_id=refetch)),
|
||||
patch(
|
||||
"superset.mcp_service.commands.create_form_data.MCPCreateFormDataCommand",
|
||||
return_value=Mock(run=Mock(return_value="form-data-key")),
|
||||
),
|
||||
patch(
|
||||
"superset.mcp_service.chart.tool.generate_chart.get_superset_base_url",
|
||||
return_value="http://localhost:8088",
|
||||
),
|
||||
):
|
||||
result = await generate_chart(request, ctx=ctx)
|
||||
|
||||
return result, chart
|
||||
|
||||
|
||||
class TestGenerateChartDetachedInstance:
|
||||
"""The committed chart must be reported even if its instance is detached.
|
||||
|
||||
Regression tests for https://github.com/apache/superset/issues/42567: under
|
||||
concurrency the chart was written to the database and the tool still
|
||||
returned an error, because the response was built by reading attributes off
|
||||
an instance another request had detached.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detached_chart_is_reported_as_created(self) -> None:
|
||||
"""A detached instance no longer turns a committed chart into an error."""
|
||||
refetched = _make_mock_chart()
|
||||
|
||||
result, chart = await _generate_saved_chart(
|
||||
refetch=Mock(return_value=refetched)
|
||||
)
|
||||
|
||||
assert chart._detached is True
|
||||
assert result.success is True
|
||||
assert result.error is None
|
||||
assert result.chart is not None
|
||||
assert result.chart.id == 42
|
||||
assert result.explore_url == "http://localhost:8088/explore/?slice_id=42"
|
||||
assert result.api_endpoints["data"].endswith("/api/v1/chart/42/data/")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_detached_chart_falls_back_to_captured_scalars(self) -> None:
|
||||
"""The minimal fallback response never reads the detached instance."""
|
||||
result, chart = await _generate_saved_chart(
|
||||
refetch=Mock(side_effect=SQLAlchemyError("session is gone"))
|
||||
)
|
||||
|
||||
assert chart._detached is True
|
||||
assert result.success is True
|
||||
assert result.error is None
|
||||
assert result.chart is not None
|
||||
assert result.chart.id == 42
|
||||
assert result.chart.slice_name == "Concurrent chart"
|
||||
assert result.chart.viz_type == "table"
|
||||
|
||||
|
||||
class TestChartSerializationEagerLoading:
|
||||
"""Tests for eager loading fix in generate_chart serialization path."""
|
||||
|
||||
|
||||
@@ -2405,6 +2405,76 @@ def test_get_sqla_query_allows_jinja_templated_custom_sql_metric_with_columns(
|
||||
assert "{{" not in sql
|
||||
|
||||
|
||||
def test_get_sqla_query_virtual_dataset_filter_values_drill_to_detail(
|
||||
database: Database,
|
||||
) -> None:
|
||||
"""
|
||||
Regression for #35263: a Jinja-templated virtual dataset that calls
|
||||
``filter_values()`` in its own SQL must see filters sent in the native
|
||||
``{col, op, val}`` format that Drill to Detail/Drill by use, not just
|
||||
the ``adhoc_filters`` format used by ordinary chart/explore requests.
|
||||
Without this, Jinja-based datasets return zero rows when drilled into,
|
||||
even though the parent chart shows data for the selected value.
|
||||
"""
|
||||
from superset.connectors.sqla.models import SqlaTable, TableColumn
|
||||
|
||||
table = SqlaTable(
|
||||
database=database,
|
||||
schema=None,
|
||||
table_name="t",
|
||||
sql=(
|
||||
"SELECT a, b FROM t WHERE 1=1 "
|
||||
"{% if filter_values('b') %} "
|
||||
"AND b IN {{ filter_values('b') | where_in }} "
|
||||
"{% endif %}"
|
||||
),
|
||||
columns=[
|
||||
TableColumn(column_name="a", type="INTEGER"),
|
||||
TableColumn(column_name="b", type="TEXT"),
|
||||
],
|
||||
)
|
||||
|
||||
result = table.get_sqla_query(
|
||||
columns=["a", "b"],
|
||||
metrics=[],
|
||||
extras={},
|
||||
filter=[{"col": "b", "op": "IN", "val": ["Alice"]}],
|
||||
granularity=None,
|
||||
is_timeseries=False,
|
||||
)
|
||||
assert result is not None
|
||||
|
||||
with database.get_sqla_engine() as engine:
|
||||
sql = str(
|
||||
result.sqla_query.compile(
|
||||
dialect=engine.dialect,
|
||||
compile_kwargs={"literal_binds": True},
|
||||
)
|
||||
)
|
||||
|
||||
assert "'Alice'" in sql, (
|
||||
"filter_values() should resolve native drill-to-detail-style "
|
||||
f"filters inside a virtual dataset's own SQL. Generated SQL: {sql}"
|
||||
)
|
||||
|
||||
# The assertion above can pass even when filter_values() itself is
|
||||
# broken, because get_sqla_query() independently applies the native
|
||||
# filter as an outer WHERE predicate on top of whatever the virtual
|
||||
# dataset's own SQL renders to. Pull the virtual dataset's own rendered
|
||||
# SQL directly out of the compiled query (rather than re-rendering it
|
||||
# via a separately constructed template processor, which would not
|
||||
# catch get_sqla_query() failing to forward the filter to the template
|
||||
# processor it builds internally) to confirm filter_values() actually
|
||||
# resolved the native filter *inside* the templated subquery.
|
||||
virtual_table_from = result.sqla_query.get_final_froms()[0]
|
||||
rendered_inner_sql = virtual_table_from.element.element.text
|
||||
assert "'Alice'" in rendered_inner_sql, (
|
||||
"filter_values() should render the native drill-to-detail-style "
|
||||
"filter directly into the virtual dataset's own templated SQL, "
|
||||
f"not just the outer query. Rendered SQL: {rendered_inner_sql}"
|
||||
)
|
||||
|
||||
|
||||
def test_extras_where_is_parenthesized(
|
||||
database: Database,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user