Compare commits

...

18 Commits

Author SHA1 Message Date
Beto Dealmeida
05182f95a0 Address comments 2026-06-23 12:00:54 -04:00
Beto Dealmeida
ace7a65716 Small fixes 2026-06-23 11:50:57 -04:00
Beto Dealmeida
0a40a94ab2 fix: enforce time filter on adhoc SQL 2026-06-23 10:02:28 -04:00
Beto Dealmeida
5d06438a07 fix(docker): restore working docker compose up for the dev stack (#41077) 2026-06-23 10:01:57 -04:00
dependabot[bot]
eb0d4dd601 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.61.0 to 8.61.1 in /superset-websocket (#41315)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 05:55:14 -07:00
dependabot[bot]
92109f0f99 chore(deps-dev): bump eslint-plugin-storybook from 10.4.4 to 10.4.5 in /superset-frontend (#41316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 05:55:11 -07:00
dependabot[bot]
9431381c3e chore(deps-dev): bump @storybook/addon-docs from 10.4.4 to 10.4.5 in /superset-frontend (#41326)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 05:55:07 -07:00
dependabot[bot]
b94f90e39e chore(deps-dev): bump @formatjs/intl-durationformat from 0.10.14 to 0.10.15 in /superset-frontend (#41332)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:51:20 -07:00
dependabot[bot]
714c5cd075 chore(deps-dev): bump oxlint from 1.69.0 to 1.70.0 in /superset-frontend (#41331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:46 -07:00
dependabot[bot]
c65c0951cf chore(deps-dev): bump storybook from 10.4.4 to 10.4.5 in /superset-frontend (#41330)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:42 -07:00
dependabot[bot]
ae5c08b993 chore(deps-dev): bump @playwright/test from 1.60.0 to 1.61.0 in /superset-frontend (#41327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:37 -07:00
dependabot[bot]
b9c61a079d chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 1.0.0 to 1.0.1 in /superset-frontend (#41322)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:27 -07:00
dependabot[bot]
2599bea0c2 chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#41321)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:07 -07:00
dependabot[bot]
6c70f3d275 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.61.0 to 8.61.1 in /superset-frontend (#41320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:02 -07:00
dependabot[bot]
da893462b8 chore(deps-dev): bump typescript-eslint from 8.61.0 to 8.61.1 in /docs (#41319)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:49:58 -07:00
dependabot[bot]
18853c6ecf chore(deps): bump actions/setup-java from 5.2.0 to 5.3.0 (#41317)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:49:53 -07:00
dependabot[bot]
8768e5be0f chore(deps-dev): bump @typescript-eslint/parser from 8.61.0 to 8.61.1 in /superset-websocket (#41312)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:49:46 -07:00
yousoph
133473d0f4 fix(explore): pre-populate SaveModal dashboard from chart metadata (#41181)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 00:54:49 -07:00
19 changed files with 2139 additions and 334 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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": {

View File

@@ -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"

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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 = (

View File

@@ -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 = {

View File

@@ -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,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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:

View File

@@ -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(

View File

@@ -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)

View File

@@ -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