mirror of
https://github.com/apache/superset.git
synced 2026-07-10 00:35:39 +00:00
Compare commits
18 Commits
fix/issue-
...
fix-clickh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05182f95a0 | ||
|
|
ace7a65716 | ||
|
|
0a40a94ab2 | ||
|
|
5d06438a07 | ||
|
|
eb0d4dd601 | ||
|
|
92109f0f99 | ||
|
|
9431381c3e | ||
|
|
b94f90e39e | ||
|
|
714c5cd075 | ||
|
|
c65c0951cf | ||
|
|
ae5c08b993 | ||
|
|
b9c61a079d | ||
|
|
2599bea0c2 | ||
|
|
6c70f3d275 | ||
|
|
da893462b8 | ||
|
|
18853c6ecf | ||
|
|
8768e5be0f | ||
|
|
133473d0f4 |
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@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.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@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.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@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
|
||||
with:
|
||||
distribution: "zulu"
|
||||
java-version: "21"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
# zizmor: ignore[artipacked] - required persisted credentials to push synced requirement changes back to remote
|
||||
- name: Checkout source code
|
||||
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: true
|
||||
|
||||
@@ -72,20 +72,23 @@ services:
|
||||
- -c
|
||||
- |
|
||||
url="http://host.docker.internal:9000/static/assets/manifest.json"
|
||||
max_attempts=150 # ~5 minutes at 2s intervals
|
||||
echo "Waiting for webpack dev server at $url..."
|
||||
max_attempts=300 # ~10 minutes at 2s intervals; first build can be slow
|
||||
echo "Waiting for webpack dev server at $$url..."
|
||||
attempt=0
|
||||
until curl -sf --max-time 5 -o /dev/null "$url"; do
|
||||
attempt=$((attempt + 1))
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
echo "ERROR: webpack dev server did not serve $url after $max_attempts attempts (~5 minutes)." >&2
|
||||
until curl -sf --max-time 5 -H "Host: localhost" -o /dev/null "$$url"; do
|
||||
attempt=$$((attempt + 1))
|
||||
if [ "$$attempt" -ge "$$max_attempts" ]; then
|
||||
echo "ERROR: webpack dev server did not serve $$url after $$max_attempts attempts." >&2
|
||||
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $$((attempt % 15)) -eq 0 ]; then
|
||||
echo "Still waiting for webpack dev server... ($$attempt/$$max_attempts)"
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "Webpack dev server is ready; starting nginx."
|
||||
exec nginx -g 'daemon off;'
|
||||
exec /docker-entrypoint.sh nginx -g 'daemon off;'
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
|
||||
@@ -81,17 +81,19 @@ case "${1}" in
|
||||
app)
|
||||
echo "Starting web app (using development server)..."
|
||||
|
||||
# Environment-based debugger control for security
|
||||
# Only enable Werkzeug interactive debugger when explicitly requested
|
||||
# Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
|
||||
# Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
|
||||
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
|
||||
# Default to Flask debug mode in this dev compose entrypoint so the Talisman
|
||||
# dev CSP (which permits 'unsafe-eval' required by React Refresh / HMR) is
|
||||
# served. Operators can still set FLASK_DEBUG=false in docker/.env-local
|
||||
# to exercise the production-like CSP and error handling.
|
||||
: "${FLASK_DEBUG:=1}"
|
||||
export FLASK_DEBUG
|
||||
|
||||
# Werkzeug's interactive debugger (/console) is a separate, security-sensitive
|
||||
# feature and must be opted into explicitly via SUPERSET_DEBUG_ENABLED=true.
|
||||
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
|
||||
export FLASK_DEBUG=1
|
||||
DEBUGGER_FLAG="--debugger"
|
||||
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
|
||||
else
|
||||
export FLASK_DEBUG=0
|
||||
DEBUGGER_FLAG="--no-debugger"
|
||||
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
|
||||
fi
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.4",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"typescript-eslint": "^8.61.1",
|
||||
"webpack": "^5.107.2"
|
||||
},
|
||||
"browserslist": {
|
||||
|
||||
150
docs/yarn.lock
150
docs/yarn.lock
@@ -4932,110 +4932,110 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
|
||||
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
|
||||
"@typescript-eslint/eslint-plugin@8.61.1", "@typescript-eslint/eslint-plugin@^8.59.3":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz#6e4b7fee21f1983308e9e9b634ecbaf702c86006"
|
||||
integrity sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.61.0"
|
||||
"@typescript-eslint/type-utils" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
"@typescript-eslint/scope-manager" "8.61.1"
|
||||
"@typescript-eslint/type-utils" "8.61.1"
|
||||
"@typescript-eslint/utils" "8.61.1"
|
||||
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/parser@8.61.0", "@typescript-eslint/parser@^8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
|
||||
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
|
||||
"@typescript-eslint/parser@8.61.1", "@typescript-eslint/parser@^8.61.0":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.1.tgz#881fba60b50636249cdeea2e547bf75715254c72"
|
||||
integrity sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
"@typescript-eslint/scope-manager" "8.61.1"
|
||||
"@typescript-eslint/types" "8.61.1"
|
||||
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
|
||||
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
|
||||
"@typescript-eslint/project-service@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.1.tgz#fcd9739964a40867eed55f1ac318d3909f24b4af"
|
||||
integrity sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.61.0"
|
||||
"@typescript-eslint/types" "^8.61.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.61.1"
|
||||
"@typescript-eslint/types" "^8.61.1"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
|
||||
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
|
||||
"@typescript-eslint/scope-manager@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz#2479921a40fdb0afa18f5838fae6167264b417b2"
|
||||
integrity sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.1"
|
||||
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
|
||||
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@^8.61.0":
|
||||
"@typescript-eslint/tsconfig-utils@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
|
||||
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
|
||||
|
||||
"@typescript-eslint/type-utils@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
|
||||
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
|
||||
"@typescript-eslint/tsconfig-utils@^8.61.1":
|
||||
version "8.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
|
||||
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
|
||||
|
||||
"@typescript-eslint/type-utils@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz#8fa18f453ee140893b47d339d1a6b64cac9b08a1"
|
||||
integrity sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.1"
|
||||
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||
"@typescript-eslint/utils" "8.61.1"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.5.0"
|
||||
|
||||
"@typescript-eslint/types@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
|
||||
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
|
||||
|
||||
"@typescript-eslint/types@^8.61.0":
|
||||
"@typescript-eslint/types@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
|
||||
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
|
||||
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
|
||||
"@typescript-eslint/types@^8.61.1":
|
||||
version "8.62.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
|
||||
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz#febbe70365ac0bf7611262b61b338fc8797965c7"
|
||||
integrity sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.61.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/visitor-keys" "8.61.0"
|
||||
"@typescript-eslint/project-service" "8.61.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.61.1"
|
||||
"@typescript-eslint/types" "8.61.1"
|
||||
"@typescript-eslint/visitor-keys" "8.61.1"
|
||||
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.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
|
||||
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
|
||||
"@typescript-eslint/utils@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.1.tgz#ffd1054de7dd33b7873cd6c6713ec6b0366316d3"
|
||||
integrity sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/scope-manager" "8.61.1"
|
||||
"@typescript-eslint/types" "8.61.1"
|
||||
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.61.0":
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
|
||||
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
|
||||
"@typescript-eslint/visitor-keys@8.61.1":
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz#546cf102b4efdb72a9a08e63a1b0d7d745eb66eb"
|
||||
integrity sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.61.0"
|
||||
"@typescript-eslint/types" "8.61.1"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -14502,15 +14502,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.61.0:
|
||||
version "8.61.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
|
||||
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
|
||||
typescript-eslint@^8.61.1:
|
||||
version "8.61.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.1.tgz#7c224a9a643b7f42d295c67a75c1e30fee8c3eaa"
|
||||
integrity sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.61.0"
|
||||
"@typescript-eslint/parser" "8.61.0"
|
||||
"@typescript-eslint/typescript-estree" "8.61.0"
|
||||
"@typescript-eslint/utils" "8.61.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.61.1"
|
||||
"@typescript-eslint/parser" "8.61.1"
|
||||
"@typescript-eslint/typescript-estree" "8.61.1"
|
||||
"@typescript-eslint/utils" "8.61.1"
|
||||
|
||||
typescript@~6.0.3:
|
||||
version "6.0.3"
|
||||
|
||||
727
superset-frontend/package-lock.json
generated
727
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -260,11 +260,11 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.14",
|
||||
"@formatjs/intl-durationformat": "^0.10.15",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@playwright/test": "^1.61.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.4",
|
||||
"@storybook/addon-docs": "10.4.5",
|
||||
"@storybook/addon-links": "10.4.4",
|
||||
"@storybook/react-webpack5": "10.4.4",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
@@ -296,7 +296,7 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.1",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
@@ -323,8 +323,8 @@
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.0",
|
||||
"eslint-plugin-storybook": "10.4.4",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.4.5",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -343,7 +343,7 @@
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.69.0",
|
||||
"oxlint": "^1.70.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.4",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
@@ -355,7 +355,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.4.4",
|
||||
"storybook": "10.4.5",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
QueryFormData,
|
||||
DatasourceType,
|
||||
isDefined,
|
||||
isAdhocColumn,
|
||||
JsonValue,
|
||||
NO_TIME_RANGE,
|
||||
usePrevious,
|
||||
@@ -70,6 +71,7 @@ import {
|
||||
import Tabs from '@superset-ui/core/components/Tabs';
|
||||
import { PluginContext } from 'src/components';
|
||||
import { useConfirmModal } from 'src/hooks/useConfirmModal';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
|
||||
import { getSectionsToRender } from 'src/explore/controlUtils';
|
||||
import { ExploreActions } from 'src/explore/actions/exploreActions';
|
||||
@@ -300,6 +302,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
const theme = useTheme();
|
||||
const pluginContext = useContext(PluginContext);
|
||||
const { showConfirm, ConfirmModal } = useConfirmModal();
|
||||
const { addWarningToast } = useToasts();
|
||||
|
||||
const prevState = usePrevious(props.exploreState);
|
||||
const prevDatasource = usePrevious(props.exploreState.datasource);
|
||||
@@ -321,7 +324,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
|
||||
const { form_data, actions } = props;
|
||||
const { setControlValue } = actions;
|
||||
const { x_axis, adhoc_filters } = form_data;
|
||||
const { x_axis, adhoc_filters, granularity_sqla } = form_data;
|
||||
|
||||
const previousXAxis = usePrevious(x_axis);
|
||||
|
||||
@@ -370,6 +373,24 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
||||
showConfirm,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
x_axis &&
|
||||
(previousXAxis === undefined || x_axis !== previousXAxis) &&
|
||||
isAdhocColumn(x_axis) &&
|
||||
!granularity_sqla
|
||||
) {
|
||||
addWarningToast(
|
||||
t(
|
||||
`The X-axis is a SQL expression, so time range filtering is not ` +
|
||||
`applied automatically. Without a time filter, queries may scan ` +
|
||||
`the entire table. Add a filter on your dataset's time column in ` +
|
||||
`the Filters section to limit the rows read.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [x_axis, previousXAxis, granularity_sqla, addWarningToast]);
|
||||
|
||||
useEffect(() => {
|
||||
let shouldUpdateControls = false;
|
||||
const removeDatasourceWarningFromControl = (
|
||||
|
||||
@@ -615,6 +615,172 @@ test('onTabChange correctly updates selectedTab via forceUpdate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const ownerUser = {
|
||||
userId: 1,
|
||||
username: 'testuser',
|
||||
firstName: 'Test',
|
||||
lastName: 'User',
|
||||
isActive: true,
|
||||
isAnonymous: false,
|
||||
permissions: {},
|
||||
roles: { Alpha: [['can_write', 'Dashboard']] as [string, string][] },
|
||||
groups: [],
|
||||
};
|
||||
|
||||
const makeMetadataDashboard = (id: number, title: string) => ({
|
||||
id,
|
||||
dashboard_title: title,
|
||||
owners: [{ id: 1, first_name: 'Test', last_name: 'User' }],
|
||||
extra_owners: [],
|
||||
roles: [],
|
||||
url: `/superset/dashboard/${id}/`,
|
||||
slug: null,
|
||||
thumbnail_url: null,
|
||||
published: true,
|
||||
changed_by_name: 'Test User',
|
||||
changed_by: { id: 1, first_name: 'Test', last_name: 'User' },
|
||||
changed_on: '2024-01-01',
|
||||
charts: [],
|
||||
});
|
||||
|
||||
test('pre-populates dashboard from metadata.dashboards when dashboardId prop is absent', async () => {
|
||||
const dashboardId = 5;
|
||||
const dashboardTitle = 'Chart Dashboard';
|
||||
|
||||
const myProps = {
|
||||
...defaultProps,
|
||||
dashboardId: null,
|
||||
metadata: {
|
||||
dashboards: [{ id: dashboardId, dashboard_title: dashboardTitle }],
|
||||
owners: ['Test User'],
|
||||
created_on_humanized: '2 days ago',
|
||||
changed_on_humanized: '1 day ago',
|
||||
},
|
||||
user: ownerUser,
|
||||
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
|
||||
dispatch: jest.fn(),
|
||||
addDangerToast: jest.fn(),
|
||||
};
|
||||
|
||||
const component = new TestSaveModal(myProps);
|
||||
const mockFull = makeMetadataDashboard(dashboardId, dashboardTitle);
|
||||
|
||||
component.loadDashboard = jest.fn().mockResolvedValue(mockFull);
|
||||
component.loadTabs = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const stateUpdates: any[] = [];
|
||||
component.setState = jest.fn((update: any) => {
|
||||
stateUpdates.push(update);
|
||||
});
|
||||
|
||||
try {
|
||||
sessionStorage.clear();
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
await component.componentDidMount();
|
||||
|
||||
expect(component.loadDashboard).toHaveBeenCalledWith(dashboardId);
|
||||
expect(stateUpdates).toContainEqual({
|
||||
dashboard: { label: dashboardTitle, value: dashboardId },
|
||||
});
|
||||
expect(component.loadTabs).toHaveBeenCalledWith(dashboardId);
|
||||
});
|
||||
|
||||
test('skips non-editable dashboards and picks the first editable one from metadata', async () => {
|
||||
const editableId = 7;
|
||||
const editableTitle = 'Editable Dashboard';
|
||||
|
||||
const myProps = {
|
||||
...defaultProps,
|
||||
dashboardId: null,
|
||||
metadata: {
|
||||
dashboards: [
|
||||
{ id: 6, dashboard_title: 'Not Mine' },
|
||||
{ id: editableId, dashboard_title: editableTitle },
|
||||
],
|
||||
owners: ['Test User'],
|
||||
created_on_humanized: '2 days ago',
|
||||
changed_on_humanized: '1 day ago',
|
||||
},
|
||||
user: ownerUser,
|
||||
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
|
||||
dispatch: jest.fn(),
|
||||
addDangerToast: jest.fn(),
|
||||
};
|
||||
|
||||
const component = new TestSaveModal(myProps);
|
||||
|
||||
const notMine = makeMetadataDashboard(6, 'Not Mine');
|
||||
notMine.owners = [{ id: 99, first_name: 'Other', last_name: 'Owner' }];
|
||||
const editable = makeMetadataDashboard(editableId, editableTitle);
|
||||
|
||||
component.loadDashboard = jest
|
||||
.fn()
|
||||
.mockImplementation((id: number) =>
|
||||
Promise.resolve(id === 6 ? notMine : editable),
|
||||
);
|
||||
component.loadTabs = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const stateUpdates: any[] = [];
|
||||
component.setState = jest.fn((update: any) => {
|
||||
stateUpdates.push(update);
|
||||
});
|
||||
|
||||
try {
|
||||
sessionStorage.clear();
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
await component.componentDidMount();
|
||||
|
||||
expect(stateUpdates).toContainEqual({
|
||||
dashboard: { label: editableTitle, value: editableId },
|
||||
});
|
||||
expect(component.loadTabs).toHaveBeenCalledWith(editableId);
|
||||
});
|
||||
|
||||
test('does not use metadata fallback when dashboardId prop is set', async () => {
|
||||
const propDashboardId = 3;
|
||||
const propDashboardTitle = 'Prop Dashboard';
|
||||
|
||||
const myProps = {
|
||||
...defaultProps,
|
||||
dashboardId: propDashboardId,
|
||||
metadata: {
|
||||
dashboards: [{ id: 99, dashboard_title: 'Should Not Be Used' }],
|
||||
owners: ['Test User'],
|
||||
created_on_humanized: '2 days ago',
|
||||
changed_on_humanized: '1 day ago',
|
||||
},
|
||||
user: ownerUser,
|
||||
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
|
||||
dispatch: jest.fn(),
|
||||
addDangerToast: jest.fn(),
|
||||
};
|
||||
|
||||
const component = new TestSaveModal(myProps);
|
||||
const mockFull = makeMetadataDashboard(propDashboardId, propDashboardTitle);
|
||||
|
||||
component.loadDashboard = jest.fn().mockResolvedValue(mockFull);
|
||||
component.loadTabs = jest.fn().mockResolvedValue([]);
|
||||
|
||||
const stateUpdates: any[] = [];
|
||||
component.setState = jest.fn((update: any) => {
|
||||
stateUpdates.push(update);
|
||||
});
|
||||
|
||||
await component.componentDidMount();
|
||||
|
||||
expect(component.loadDashboard).toHaveBeenCalledWith(propDashboardId);
|
||||
expect(component.loadDashboard).not.toHaveBeenCalledWith(99);
|
||||
expect(stateUpdates).toContainEqual({
|
||||
dashboard: { label: propDashboardTitle, value: propDashboardId },
|
||||
});
|
||||
});
|
||||
|
||||
test('chart placement logic finds row with available space', () => {
|
||||
// Test case 1: Row has space (8 + 4 = 12 <= 12)
|
||||
const positionJson1 = {
|
||||
|
||||
@@ -54,7 +54,11 @@ import {
|
||||
isUserAdmin,
|
||||
} from 'src/dashboard/util/permissionUtils';
|
||||
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
|
||||
import { SaveActionType, ChartStatusType } from 'src/explore/types';
|
||||
import {
|
||||
SaveActionType,
|
||||
ChartStatusType,
|
||||
ExplorePageInitialData,
|
||||
} from 'src/explore/types';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
import {
|
||||
removeChartState,
|
||||
@@ -81,6 +85,7 @@ interface SaveModalProps extends RouteComponentProps {
|
||||
isVisible: boolean;
|
||||
dispatch: Dispatch;
|
||||
theme: SupersetTheme;
|
||||
metadata?: ExplorePageInitialData['metadata'];
|
||||
}
|
||||
|
||||
type SaveModalState = {
|
||||
@@ -162,6 +167,35 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
|
||||
t('An error occurred while loading dashboard information.'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const metadataDashboards = this.props.metadata?.dashboards;
|
||||
if (metadataDashboards?.length) {
|
||||
// Fallback: the chart is already on one or more dashboards (from Explore API
|
||||
// metadata). Pre-populate with the first dashboard the user can edit so the
|
||||
// "Save & go to dashboard" button works out of the box.
|
||||
try {
|
||||
let editable: Dashboard | undefined;
|
||||
for (const { id } of metadataDashboards) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const result = await this.loadDashboard(id).catch(() => null);
|
||||
if (result && canUserEditDashboard(result, this.props.user)) {
|
||||
editable = result as Dashboard;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (editable) {
|
||||
this.setState({
|
||||
dashboard: {
|
||||
label: editable.dashboard_title,
|
||||
value: editable.id,
|
||||
},
|
||||
});
|
||||
await this.loadTabs(editable.id);
|
||||
}
|
||||
} catch (error) {
|
||||
logging.warn(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,6 +860,7 @@ interface StateProps {
|
||||
dashboards: any;
|
||||
alert: any;
|
||||
isVisible: boolean;
|
||||
metadata?: ExplorePageInitialData['metadata'];
|
||||
}
|
||||
|
||||
function mapStateToProps({
|
||||
@@ -841,6 +876,7 @@ function mapStateToProps({
|
||||
dashboards: saveModal.dashboards,
|
||||
alert: saveModal.saveModalAlert,
|
||||
isVisible: saveModal.isVisible,
|
||||
metadata: explore.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1081
superset-websocket/package-lock.json
generated
1081
superset-websocket/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,8 +33,8 @@
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^25.9.3",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.0",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.1",
|
||||
"@typescript-eslint/parser": "^8.61.1",
|
||||
"eslint": "^10.5.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
|
||||
@@ -308,6 +308,29 @@ class QueryContextFactory: # pylint: disable=too-few-public-methods
|
||||
if filter["col"] != filter_to_remove
|
||||
]
|
||||
|
||||
elif is_adhoc_column(x_axis) and ( # type: ignore
|
||||
query_object.from_dttm or query_object.to_dttm
|
||||
):
|
||||
# x-axis is a SQL expression (not a physical temporal column) and a
|
||||
# time range is configured, but no time column (granularity) was set.
|
||||
# Fall back to the dataset's main datetime column so helpers.py adds a
|
||||
# WHERE time filter — without it engines like ClickHouse perform a full
|
||||
# table scan and trigger max_rows_to_read errors.
|
||||
main_dttm_col = getattr(datasource, "main_dttm_col", None)
|
||||
if main_dttm_col and main_dttm_col in temporal_columns:
|
||||
query_object.granularity = main_dttm_col
|
||||
# Remove any TEMPORAL_RANGE filter already targeting main_dttm_col
|
||||
# so helpers.py doesn't add the same condition a second time via the
|
||||
# filter loop (granularity covers it via from_dttm/to_dttm).
|
||||
query_object.filter = [
|
||||
f
|
||||
for f in query_object.filter
|
||||
if not (
|
||||
f.get("op") == "TEMPORAL_RANGE"
|
||||
and f.get("col") == main_dttm_col
|
||||
)
|
||||
]
|
||||
|
||||
def _apply_filters(self, query_object: QueryObject) -> None:
|
||||
if query_object.time_range:
|
||||
for filter_object in query_object.filter:
|
||||
|
||||
@@ -123,7 +123,7 @@ class DatetimeFormatDetector:
|
||||
sql = database.apply_limit_to_sql(sql, limit=self.sample_size, force=True)
|
||||
|
||||
# Execute query and get results
|
||||
df = database.get_df(sql, dataset.schema)
|
||||
df = database.get_df(sql, dataset.catalog, dataset.schema)
|
||||
|
||||
if df.empty or column.column_name not in df.columns:
|
||||
logger.warning(
|
||||
|
||||
@@ -384,6 +384,203 @@ class TestQueryContextFactory:
|
||||
|
||||
assert query_object.columns == ["ds", "other_col"]
|
||||
|
||||
def test_apply_granularity_expression_xaxis_sets_main_dttm_col(self):
|
||||
"""SQL expression x-axis with time range falls back to main_dttm_col."""
|
||||
from datetime import datetime
|
||||
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.granularity = None
|
||||
query_object.from_dttm = datetime(2023, 1, 1)
|
||||
query_object.to_dttm = None
|
||||
query_object.columns = [
|
||||
{
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
"columnType": "BASE_AXIS",
|
||||
}
|
||||
]
|
||||
query_object.filter = []
|
||||
|
||||
form_data = {
|
||||
"x_axis": {
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
}
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.columns = [{"column_name": "ts", "is_dttm": True}]
|
||||
datasource.main_dttm_col = "ts"
|
||||
|
||||
self.factory._apply_granularity(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.granularity == "ts"
|
||||
|
||||
def test_apply_granularity_expression_xaxis_no_time_range_no_fallback(self):
|
||||
"""SQL expression x-axis without time range does not set granularity."""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.granularity = None
|
||||
query_object.from_dttm = None
|
||||
query_object.to_dttm = None
|
||||
query_object.columns = [
|
||||
{
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
"columnType": "BASE_AXIS",
|
||||
}
|
||||
]
|
||||
query_object.filter = []
|
||||
|
||||
form_data = {
|
||||
"x_axis": {
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
}
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.columns = [{"column_name": "ts", "is_dttm": True}]
|
||||
datasource.main_dttm_col = "ts"
|
||||
|
||||
self.factory._apply_granularity(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.granularity is None
|
||||
|
||||
def test_apply_granularity_expression_xaxis_no_main_dttm_col(self):
|
||||
"""SQL expression x-axis with time range but no main_dttm_col stays None."""
|
||||
from datetime import datetime
|
||||
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.granularity = None
|
||||
query_object.from_dttm = datetime(2023, 1, 1)
|
||||
query_object.to_dttm = None
|
||||
query_object.columns = [
|
||||
{
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
"columnType": "BASE_AXIS",
|
||||
}
|
||||
]
|
||||
query_object.filter = []
|
||||
|
||||
form_data = {
|
||||
"x_axis": {
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
}
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.columns = []
|
||||
datasource.main_dttm_col = None
|
||||
|
||||
self.factory._apply_granularity(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.granularity is None
|
||||
|
||||
def test_apply_granularity_expression_xaxis_main_dttm_not_temporal(self):
|
||||
"""SQL expression x-axis fallback skipped if main_dttm_col is not temporal."""
|
||||
from datetime import datetime
|
||||
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.granularity = None
|
||||
query_object.from_dttm = datetime(2023, 1, 1)
|
||||
query_object.to_dttm = None
|
||||
query_object.columns = [
|
||||
{
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
"columnType": "BASE_AXIS",
|
||||
}
|
||||
]
|
||||
query_object.filter = []
|
||||
|
||||
form_data = {
|
||||
"x_axis": {
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
}
|
||||
}
|
||||
datasource = Mock()
|
||||
# main_dttm_col points to a column not flagged as temporal
|
||||
datasource.columns = [{"column_name": "ts", "is_dttm": False}]
|
||||
datasource.main_dttm_col = "ts"
|
||||
|
||||
self.factory._apply_granularity(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.granularity is None
|
||||
|
||||
def test_apply_granularity_expression_xaxis_deduplicates_temporal_filter(self):
|
||||
"""Fallback removes existing TEMPORAL_RANGE filter for main_dttm_col."""
|
||||
from datetime import datetime
|
||||
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.granularity = None
|
||||
query_object.from_dttm = datetime(2023, 1, 1)
|
||||
query_object.to_dttm = None
|
||||
query_object.columns = [
|
||||
{
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
"columnType": "BASE_AXIS",
|
||||
}
|
||||
]
|
||||
# Pre-existing TEMPORAL_RANGE filter for the main dttm column (e.g.
|
||||
# left over from when 'ts' was the physical x-axis).
|
||||
query_object.filter = [
|
||||
{"col": "ts", "op": "TEMPORAL_RANGE", "val": "Last 7 days"},
|
||||
{"col": "other", "op": "==", "val": "foo"},
|
||||
]
|
||||
|
||||
form_data = {
|
||||
"x_axis": {
|
||||
"sqlExpression": "duration * 0.05",
|
||||
"expressionType": "SQL",
|
||||
"label": "duration * 0.05",
|
||||
}
|
||||
}
|
||||
datasource = Mock()
|
||||
datasource.columns = [{"column_name": "ts", "is_dttm": True}]
|
||||
datasource.main_dttm_col = "ts"
|
||||
|
||||
self.factory._apply_granularity(query_object, form_data, datasource)
|
||||
|
||||
assert query_object.granularity == "ts"
|
||||
# TEMPORAL_RANGE filter for 'ts' removed to prevent duplicate WHERE clause
|
||||
assert not any(
|
||||
f.get("col") == "ts" and f.get("op") == "TEMPORAL_RANGE"
|
||||
for f in query_object.filter
|
||||
)
|
||||
# Unrelated filter preserved
|
||||
assert any(f.get("col") == "other" for f in query_object.filter)
|
||||
|
||||
def test_apply_granularity_physical_string_xaxis_no_fallback(self):
|
||||
"""Physical column string x-axis without granularity: no fallback."""
|
||||
from datetime import datetime
|
||||
|
||||
query_object = Mock(spec=QueryObject)
|
||||
query_object.granularity = None
|
||||
query_object.from_dttm = datetime(2023, 1, 1)
|
||||
query_object.to_dttm = None
|
||||
query_object.columns = ["non_temporal_col"]
|
||||
query_object.filter = []
|
||||
|
||||
form_data = {"x_axis": "non_temporal_col"}
|
||||
datasource = Mock()
|
||||
datasource.columns = [{"column_name": "ts", "is_dttm": True}]
|
||||
datasource.main_dttm_col = "ts"
|
||||
|
||||
self.factory._apply_granularity(query_object, form_data, datasource)
|
||||
|
||||
# String x-axis is not an adhoc column so no fallback should trigger
|
||||
assert query_object.granularity is None
|
||||
|
||||
def test_apply_granularity_x_axis_not_temporal(self):
|
||||
"""Test _apply_granularity when x_axis is not a temporal column"""
|
||||
query_object = Mock(spec=QueryObject)
|
||||
|
||||
@@ -31,6 +31,7 @@ def mock_dataset() -> MagicMock:
|
||||
dataset = MagicMock(spec=SqlaTable)
|
||||
dataset.table_name = "test_table"
|
||||
dataset.schema = "test_schema"
|
||||
dataset.catalog = None
|
||||
dataset.is_virtual = False
|
||||
|
||||
# Mock the database engine and dialect for identifier quoting
|
||||
@@ -243,7 +244,7 @@ def test_detect_column_format_query_has_no_is_not_null(
|
||||
captured_sql: list[str] = []
|
||||
original_get_df = mock_dataset.database.get_df
|
||||
|
||||
def capture_sql(sql: str, schema: str) -> pd.DataFrame:
|
||||
def capture_sql(sql: str, catalog: str | None, schema: str | None) -> pd.DataFrame:
|
||||
captured_sql.append(sql)
|
||||
return original_get_df.return_value
|
||||
|
||||
|
||||
Reference in New Issue
Block a user