mirror of
https://github.com/apache/superset.git
synced 2026-06-23 16:39:22 +00:00
Compare commits
23 Commits
chore/sqla
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90fa31f305 | ||
|
|
5731d0874a | ||
|
|
66f5ab2d2f | ||
|
|
36b0ed023b | ||
|
|
3ff90bd532 | ||
|
|
5d06438a07 | ||
|
|
eb0d4dd601 | ||
|
|
92109f0f99 | ||
|
|
9431381c3e | ||
|
|
b94f90e39e | ||
|
|
714c5cd075 | ||
|
|
c65c0951cf | ||
|
|
ae5c08b993 | ||
|
|
b9c61a079d | ||
|
|
2599bea0c2 | ||
|
|
6c70f3d275 | ||
|
|
da893462b8 | ||
|
|
18853c6ecf | ||
|
|
8768e5be0f | ||
|
|
133473d0f4 | ||
|
|
5916ec4876 | ||
|
|
36781fbf47 | ||
|
|
215b207ae4 |
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
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
#
|
||||
HYPHEN_SYMBOL='-'
|
||||
|
||||
gunicorn \
|
||||
exec gunicorn \
|
||||
--bind "${SUPERSET_BIND_ADDRESS:-0.0.0.0}:${SUPERSET_PORT:-8088}" \
|
||||
--access-logfile "${ACCESS_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
||||
--error-logfile "${ERROR_LOG_FILE:-$HYPHEN_SYMBOL}" \
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.17.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.17.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ supersetNode:
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; /usr/bin/run-server.sh"
|
||||
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec /usr/bin/run-server.sh"
|
||||
connections:
|
||||
# -- Change in case of bringing your own redis and then also set redis.enabled:false
|
||||
redis_host: "{{ .Release.Name }}-redis-headless"
|
||||
|
||||
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",
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ControlPanelConfig,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
@@ -145,7 +146,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
D3_FORMAT_DOCS,
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import OptionDescription from './OptionDescription';
|
||||
@@ -154,7 +155,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Date Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
sections,
|
||||
getStandardizedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -78,7 +79,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Date Time Format'),
|
||||
renderTrigger: true,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
D3_FORMAT_DOCS,
|
||||
D3_FORMAT_OPTIONS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
/*
|
||||
@@ -235,7 +236,7 @@ export const xAxisFormat: CustomControlItem = {
|
||||
label: t('X Axis Format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const { cols: groupby } = formData;
|
||||
const { cols: groupby, extra_form_data } = formData;
|
||||
|
||||
const queryContextA = buildQueryContext(formData, baseQueryObject => {
|
||||
const postProcessing: PostProcessingRule[] = [];
|
||||
@@ -58,14 +58,24 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
timeOffsets = timeOffsets.concat(['inherit']);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
extra_form_data?.time_compare &&
|
||||
!timeOffsets.includes(extra_form_data.time_compare)
|
||||
) {
|
||||
timeOffsets = [extra_form_data.time_compare];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
...baseQueryObject,
|
||||
groupby,
|
||||
post_processing: postProcessing,
|
||||
time_offsets: isTimeComparison(formData, baseQueryObject)
|
||||
? ensureIsArray(timeOffsets)
|
||||
: [],
|
||||
time_offsets:
|
||||
isTimeComparison(formData, baseQueryObject) ||
|
||||
extra_form_data?.time_compare
|
||||
? ensureIsArray(timeOffsets)
|
||||
: [],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
@@ -111,7 +111,11 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
const metrics = chartProps.datasource?.metrics || [];
|
||||
const originalLabel = getOriginalLabel(metric, metrics);
|
||||
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
|
||||
const timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||
|
||||
const dashboardTimeCompare = formData?.extraFormData?.time_compare;
|
||||
const timeComparison =
|
||||
dashboardTimeCompare ||
|
||||
ensureIsArray(chartProps.rawFormData?.time_compare)[0];
|
||||
const startDateOffset = chartProps.rawFormData?.start_date_offset;
|
||||
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
|
||||
(adhoc_filter: SimpleAdhocFilter) =>
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
ControlPanelState,
|
||||
getTemporalColumns,
|
||||
sharedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
@@ -153,7 +154,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
sharedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
import { legendSection } from '../controls';
|
||||
@@ -188,7 +189,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
sharedControls,
|
||||
ControlFormItemSpec,
|
||||
getStandardizedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
import { LabelPositionEnum } from '../types';
|
||||
@@ -181,7 +182,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
D3_FORMAT_OPTIONS,
|
||||
D3_TIME_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { DEFAULT_FORM_DATA } from './types';
|
||||
|
||||
@@ -132,7 +133,7 @@ const config: ControlPanelConfig = {
|
||||
label: t('Date format'),
|
||||
renderTrigger: true,
|
||||
choices: D3_TIME_FORMAT_OPTIONS,
|
||||
default: 'smart_date',
|
||||
default: DEFAULT_TIME_FORMAT,
|
||||
description: D3_FORMAT_DOCS,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -182,7 +182,6 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -396,3 +396,102 @@ test('does not emit cross-filter when no dimensions and time-based X-axis', asyn
|
||||
expect(setDataMaskMock).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
// Test for issue #41102: horizontal bar cross-filter must use the category
|
||||
// value, not the metric. For horizontal bars the data tuple is value-first
|
||||
// (e.g. [100, 'Product A']), so relying on data[0] emitted the metric value.
|
||||
test('emits cross-filter on the category value for a horizontal categorical bar', async () => {
|
||||
const setDataMaskMock = jest.fn();
|
||||
|
||||
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
|
||||
...defaultProps,
|
||||
emitCrossFilters: true,
|
||||
setDataMask: setDataMaskMock,
|
||||
groupby: [], // No dimensions
|
||||
xAxis: {
|
||||
label: 'category_column',
|
||||
type: AxisType.Category, // Categorical X-axis
|
||||
},
|
||||
};
|
||||
|
||||
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
|
||||
|
||||
const lastCall = mockEchart.mock.calls.at(-1);
|
||||
expect(lastCall).toBeDefined();
|
||||
const [props] = lastCall as [EchartsProps];
|
||||
|
||||
const clickHandler = props.eventHandlers?.click;
|
||||
if (clickHandler) {
|
||||
clickHandler({
|
||||
seriesName: 'Sales', // This is the metric name
|
||||
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||
name: 'Product A',
|
||||
dataIndex: 0,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(setDataMaskMock).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 500 },
|
||||
);
|
||||
|
||||
// Must filter on the category ('Product A'), not the metric value (100)
|
||||
const dataMaskCall = setDataMaskMock.mock.calls[0][0];
|
||||
expect(dataMaskCall.extraFormData.filters).toEqual([
|
||||
{
|
||||
col: 'category_column',
|
||||
op: 'IN',
|
||||
val: ['Product A'],
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Test for issue #41102: the context-menu ("Add cross-filter") path must also
|
||||
// use the category value, not the metric, for a horizontal categorical bar.
|
||||
test('context menu cross-filter uses the category value for a horizontal categorical bar', async () => {
|
||||
const onContextMenuMock = jest.fn();
|
||||
|
||||
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
|
||||
...defaultProps,
|
||||
emitCrossFilters: true,
|
||||
onContextMenu: onContextMenuMock,
|
||||
groupby: [], // No dimensions
|
||||
xAxis: {
|
||||
label: 'category_column',
|
||||
type: AxisType.Category, // Categorical X-axis
|
||||
},
|
||||
};
|
||||
|
||||
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
|
||||
|
||||
const lastCall = mockEchart.mock.calls.at(-1);
|
||||
expect(lastCall).toBeDefined();
|
||||
const [props] = lastCall as [EchartsProps];
|
||||
|
||||
const contextMenuHandler = props.eventHandlers?.contextmenu;
|
||||
expect(contextMenuHandler).toBeDefined();
|
||||
if (contextMenuHandler) {
|
||||
await contextMenuHandler({
|
||||
seriesName: 'Sales', // This is the metric name
|
||||
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||
name: 'Product A',
|
||||
event: { stop: jest.fn(), event: { clientX: 10, clientY: 20 } },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onContextMenuMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// The cross-filter must use the category ('Product A'), not the metric (100)
|
||||
const { crossFilter } = onContextMenuMock.mock.calls[0][2];
|
||||
expect(crossFilter.dataMask.extraFormData.filters).toEqual([
|
||||
{
|
||||
col: 'category_column',
|
||||
op: 'IN',
|
||||
val: ['Product A'],
|
||||
},
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -234,9 +234,12 @@ export default function EchartsTimeseries({
|
||||
// Cross-filter by dimension (original behavior)
|
||||
const { seriesName: name } = props;
|
||||
handleChange(name);
|
||||
} else if (canCrossFilterByXAxis && props.data?.[0] != null) {
|
||||
// Cross-filter by X-axis value when no dimensions (issue #25334)
|
||||
handleXAxisChange(props.data[0]);
|
||||
} else if (canCrossFilterByXAxis && props.name != null) {
|
||||
// Cross-filter by X-axis value when no dimensions (issue #25334).
|
||||
// Use `name` (the category-axis value) instead of `data[0]`: for
|
||||
// horizontal bars the data tuple is value-first, so `data[0]` would
|
||||
// be the metric value rather than the category (issue #41102).
|
||||
handleXAxisChange(props.name);
|
||||
}
|
||||
}, TIMER_DURATION);
|
||||
},
|
||||
@@ -318,8 +321,10 @@ export default function EchartsTimeseries({
|
||||
let crossFilter;
|
||||
if (hasDimensions) {
|
||||
crossFilter = getCrossFilterDataMask(seriesName);
|
||||
} else if (canCrossFilterByXAxis && data?.[0] != null) {
|
||||
crossFilter = getXAxisCrossFilterDataMask(data[0]);
|
||||
} else if (canCrossFilterByXAxis && eventParams.name != null) {
|
||||
// Use `name` (the category-axis value), not `data[0]`, so horizontal
|
||||
// bars cross-filter on the category and not the metric (issue #41102).
|
||||
crossFilter = getXAxisCrossFilterDataMask(eventParams.name);
|
||||
}
|
||||
|
||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||
|
||||
@@ -174,7 +174,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
(isXAxis ? isVertical(controls) : isHorizontal(controls)) &&
|
||||
|
||||
@@ -147,7 +147,6 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -113,7 +113,6 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -112,7 +112,6 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -164,7 +164,6 @@ const config: ControlPanelConfig = {
|
||||
name: 'x_axis_time_format',
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
default: 'smart_date',
|
||||
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
checkColumnType(
|
||||
|
||||
@@ -258,7 +258,6 @@ export const tooltipTimeFormatControl: ControlSetItem = {
|
||||
config: {
|
||||
...sharedControls.x_axis_time_format,
|
||||
label: t('Tooltip time format'),
|
||||
default: 'smart_date',
|
||||
clearable: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { QueryFormData } from '@superset-ui/core';
|
||||
import buildQuery from '../../../src/BigNumber/BigNumberPeriodOverPeriod/buildQuery';
|
||||
|
||||
describe('BigNumberPeriodOverPeriod buildQuery', () => {
|
||||
const baseFormData: QueryFormData = {
|
||||
datasource: '1__table',
|
||||
viz_type: 'pop_kpi',
|
||||
metric: 'count',
|
||||
cols: [],
|
||||
adhoc_filters: [
|
||||
{
|
||||
clause: 'WHERE',
|
||||
subject: 'order_date',
|
||||
operator: 'TEMPORAL_RANGE',
|
||||
comparator: '2003-07-01 : 2004-01-01',
|
||||
expressionType: 'SIMPLE',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('flows extra_form_data.time_compare override into time_offsets', () => {
|
||||
const queryContext = buildQuery({
|
||||
...baseFormData,
|
||||
extra_form_data: { time_compare: '1 year ago' },
|
||||
});
|
||||
|
||||
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
|
||||
});
|
||||
|
||||
test('requests offsets from the override even without the chart time_compare control', () => {
|
||||
const queryContext = buildQuery({
|
||||
...baseFormData,
|
||||
time_compare: undefined,
|
||||
extra_form_data: { time_compare: '1 year ago' },
|
||||
});
|
||||
|
||||
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
|
||||
});
|
||||
|
||||
test('does not duplicate the offset when it already matches time_compare', () => {
|
||||
const queryContext = buildQuery({
|
||||
...baseFormData,
|
||||
time_compare: ['1 year ago'],
|
||||
extra_form_data: { time_compare: '1 year ago' },
|
||||
});
|
||||
|
||||
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
|
||||
});
|
||||
|
||||
test('omits time_offsets when neither the control nor the override is set', () => {
|
||||
const queryContext = buildQuery(baseFormData);
|
||||
|
||||
expect(queryContext.queries[0].time_offsets).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
248
superset-websocket/package-lock.json
generated
248
superset-websocket/package-lock.json
generated
@@ -25,8 +25,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",
|
||||
@@ -37,7 +37,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.0"
|
||||
"typescript-eslint": "^8.61.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.16.0",
|
||||
@@ -1844,17 +1844,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
|
||||
"integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
|
||||
"integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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"
|
||||
@@ -1867,7 +1867,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"@typescript-eslint/parser": "^8.61.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
@@ -1883,16 +1883,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
|
||||
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
|
||||
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1908,14 +1908,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
|
||||
"integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
|
||||
"integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1930,14 +1930,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
|
||||
"integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
|
||||
"integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1948,9 +1948,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
|
||||
"integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1965,15 +1965,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
|
||||
"integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
@@ -1990,9 +1990,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
|
||||
"integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
|
||||
"integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2004,16 +2004,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
|
||||
"integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
|
||||
"integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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",
|
||||
@@ -2071,16 +2071,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
|
||||
"integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
|
||||
"integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -2095,13 +2095,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
|
||||
"integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
|
||||
"integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.61.0",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6191,16 +6191,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
|
||||
"integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
|
||||
"integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -7930,16 +7930,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
|
||||
"integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
|
||||
"integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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"
|
||||
@@ -7954,75 +7954,75 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
|
||||
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
|
||||
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
|
||||
"integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
|
||||
"integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
|
||||
"integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
|
||||
"integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
|
||||
"integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
|
||||
"integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
|
||||
"integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
|
||||
"integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
|
||||
"integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
|
||||
"integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
|
||||
"integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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",
|
||||
@@ -8057,24 +8057,24 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
|
||||
"integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
|
||||
"integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
|
||||
"integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
|
||||
"integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "8.61.0",
|
||||
"@typescript-eslint/types": "8.61.1",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -11024,15 +11024,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"typescript-eslint": {
|
||||
"version": "8.61.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
|
||||
"integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
|
||||
"version": "8.61.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
|
||||
"integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@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"
|
||||
}
|
||||
},
|
||||
"uglify-js": {
|
||||
|
||||
@@ -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",
|
||||
@@ -45,7 +45,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.61.0"
|
||||
"typescript-eslint": "^8.61.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.16.0",
|
||||
|
||||
@@ -25,7 +25,14 @@ from typing import TYPE_CHECKING
|
||||
import paramiko
|
||||
import sshtunnel
|
||||
from flask import Flask
|
||||
from paramiko import RSAKey
|
||||
from paramiko import (
|
||||
ECDSAKey,
|
||||
Ed25519Key,
|
||||
PasswordRequiredException,
|
||||
PKey,
|
||||
RSAKey,
|
||||
SSHException,
|
||||
)
|
||||
from paramiko.pkey import UnknownKeyType
|
||||
|
||||
from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
@@ -40,6 +47,39 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Order matters: paramiko's per-class loaders raise SSHException with vague
|
||||
# "unpack requires 4 bytes" messages on type mismatches, so we try the more
|
||||
# modern key types first (ed25519, ECDSA) and fall back to RSA, which is the
|
||||
# most permissive parser and the historical default in this codebase.
|
||||
_SSH_KEY_TYPES: tuple[type[PKey], ...] = (Ed25519Key, ECDSAKey, RSAKey)
|
||||
|
||||
|
||||
def _load_private_key(pem: str, password: str | None) -> PKey:
|
||||
"""Load a private key PEM regardless of algorithm (ed25519, ECDSA, RSA).
|
||||
|
||||
paramiko 3.2+ has ``PKey.from_path()`` for polymorphic loading, but it
|
||||
requires a filesystem path; writing private key material to disk would be a
|
||||
security regression. Each per-class loader only accepts its own format, so
|
||||
iterate over the supported types on the in-memory ``StringIO`` and return
|
||||
the first that parses cleanly.
|
||||
"""
|
||||
last_exc: SSHException | None = None
|
||||
for key_class in _SSH_KEY_TYPES:
|
||||
try:
|
||||
return key_class.from_private_key(StringIO(pem), password=password)
|
||||
except PasswordRequiredException:
|
||||
raise
|
||||
except SSHException as exc:
|
||||
last_exc = exc
|
||||
# NOTE: last_exc holds the error from the final attempt (RSAKey), not the
|
||||
# closest-matching type. For a corrupted ed25519 key, the appended message
|
||||
# reflects RSAKey's parse error; the full type list above still identifies
|
||||
# all types attempted.
|
||||
raise SSHException(
|
||||
"Unable to parse SSH private key as any of "
|
||||
f"{', '.join(k.__name__ for k in _SSH_KEY_TYPES)}: {last_exc}"
|
||||
) from last_exc
|
||||
|
||||
|
||||
def _parse_authorized_key(authorized_key: str) -> paramiko.PKey:
|
||||
"""
|
||||
@@ -171,6 +211,9 @@ class SSHManager:
|
||||
ssh_tunnel: "SSHTunnel",
|
||||
sqlalchemy_database_uri: str,
|
||||
) -> sshtunnel.SSHTunnelForwarder:
|
||||
# Deferred import to break a circular import:
|
||||
# superset.utils.ssh_tunnel -> superset.databases.ssh_tunnel.models
|
||||
# -> superset.extensions -> superset.extensions.ssh (this module).
|
||||
from superset.utils.ssh_tunnel import get_default_port
|
||||
|
||||
url = make_url_safe(sqlalchemy_database_uri)
|
||||
@@ -203,11 +246,9 @@ class SSHManager:
|
||||
if ssh_tunnel.password:
|
||||
params["ssh_password"] = ssh_tunnel.password
|
||||
elif ssh_tunnel.private_key:
|
||||
private_key_file = StringIO(ssh_tunnel.private_key)
|
||||
private_key = RSAKey.from_private_key(
|
||||
private_key_file, ssh_tunnel.private_key_password
|
||||
params["ssh_pkey"] = _load_private_key(
|
||||
ssh_tunnel.private_key, ssh_tunnel.private_key_password
|
||||
)
|
||||
params["ssh_pkey"] = private_key
|
||||
|
||||
return sshtunnel.open_tunnel(**params)
|
||||
|
||||
|
||||
@@ -1289,9 +1289,13 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||
:returns: The error message
|
||||
"""
|
||||
|
||||
quoted_tables = [f"`{table}`" for table in tables]
|
||||
return f"""You need access to the following tables: {", ".join(quoted_tables)},
|
||||
`all_database_access` or `all_datasource_access` permission"""
|
||||
quoted_tables = [f'"{table}"' for table in tables]
|
||||
return _(
|
||||
"You need access to the following tables: %(tables)s, "
|
||||
"'all_database_access' or 'all_datasource_access' permission"
|
||||
) % {
|
||||
"tables": ",".join(quoted_tables),
|
||||
}
|
||||
|
||||
def get_table_access_error_object(self, tables: set["Table"]) -> SupersetError:
|
||||
"""
|
||||
|
||||
@@ -1123,14 +1123,17 @@ class SQLStatement(BaseSQLStatement[exp.Expression]):
|
||||
"""
|
||||
Check if the statement has a subquery.
|
||||
|
||||
Covers explicit subqueries, set operations (``UNION``/``INTERSECT``/
|
||||
``EXCEPT``), and any nested ``SELECT`` regardless of the top-level node
|
||||
type (e.g. when wrapped in parentheses or a set operation).
|
||||
|
||||
:return: True if the statement has a subquery.
|
||||
"""
|
||||
return bool(self._parsed.find(exp.Subquery)) or (
|
||||
isinstance(self._parsed, exp.Select)
|
||||
and any(
|
||||
isinstance(expression, exp.Select)
|
||||
for expression in self._parsed.walk()
|
||||
if expression != self._parsed
|
||||
return (
|
||||
self.is_set_operation()
|
||||
or bool(self._parsed.find(exp.Subquery))
|
||||
or any(
|
||||
select != self._parsed for select in self._parsed.find_all(exp.Select)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,24 @@ from unittest.mock import Mock, patch
|
||||
import paramiko
|
||||
import pytest
|
||||
import sshtunnel
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import (
|
||||
generate_private_key as generate_rsa_key,
|
||||
)
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
BestAvailableEncryption,
|
||||
Encoding,
|
||||
NoEncryption,
|
||||
PrivateFormat,
|
||||
)
|
||||
from paramiko import (
|
||||
ECDSAKey,
|
||||
Ed25519Key,
|
||||
PasswordRequiredException,
|
||||
RSAKey,
|
||||
SSHException,
|
||||
)
|
||||
|
||||
from superset.commands.database.ssh_tunnel.exceptions import (
|
||||
SSHTunnelHostKeyVerificationError,
|
||||
@@ -40,6 +58,20 @@ def _make_manager(strict: bool = False) -> SSHManager:
|
||||
return SSHManager(app)
|
||||
|
||||
|
||||
def _make_ssh_tunnel(private_key: str, private_key_password: str | None = None) -> Mock:
|
||||
ssh_tunnel = Mock()
|
||||
ssh_tunnel.server_address = "ssh.example.com"
|
||||
ssh_tunnel.server_port = 22
|
||||
ssh_tunnel.username = "tunneluser"
|
||||
ssh_tunnel.password = None
|
||||
ssh_tunnel.private_key = private_key
|
||||
ssh_tunnel.private_key_password = private_key_password
|
||||
# No expected host key: keeps create_tunnel on the key-parsing path without
|
||||
# triggering host-key verification (exercised separately below).
|
||||
ssh_tunnel.server_host_key = None
|
||||
return ssh_tunnel
|
||||
|
||||
|
||||
def _authorized_key(key: paramiko.PKey) -> str:
|
||||
"""Render a paramiko key in authorized-key (``"<type> <base64>"``) form."""
|
||||
return f"{key.get_name()} {key.get_base64()}"
|
||||
@@ -69,6 +101,154 @@ def test_ssh_tunnel_timeout_setting() -> None:
|
||||
assert sshtunnel.SSH_TIMEOUT == 321.0
|
||||
|
||||
|
||||
def _make_ed25519_pem() -> str:
|
||||
"""Generate a fresh OpenSSH-format ed25519 private key PEM."""
|
||||
key = Ed25519PrivateKey.generate()
|
||||
return key.private_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
format=PrivateFormat.OpenSSH,
|
||||
encryption_algorithm=NoEncryption(),
|
||||
).decode()
|
||||
|
||||
|
||||
def test_create_tunnel_accepts_ed25519_private_key() -> None:
|
||||
"""
|
||||
Regression for #24180: ed25519 SSH keys must be loadable for tunnel
|
||||
setup. The bug surfaces as ``unpack requires 4 bytes`` because the
|
||||
code unconditionally calls ``RSAKey.from_private_key`` regardless of
|
||||
key type, which mis-parses the ed25519 byte stream.
|
||||
|
||||
The Superset UI accepts any key the user pastes, so the fix is to
|
||||
detect the key type (or use ``paramiko.PKey.from_private_key`` once
|
||||
paramiko exposes a polymorphic loader) rather than hard-coding RSA.
|
||||
|
||||
This test exercises ``create_tunnel`` end-to-end with a freshly
|
||||
generated ed25519 key. It mocks ``sshtunnel.open_tunnel`` so the
|
||||
test does not actually open a network connection — only the key
|
||||
parsing path is exercised.
|
||||
"""
|
||||
manager = _make_manager()
|
||||
ssh_tunnel = _make_ssh_tunnel(_make_ed25519_pem())
|
||||
|
||||
with patch("superset.extensions.ssh.sshtunnel.open_tunnel") as mock_open:
|
||||
manager.create_tunnel(
|
||||
ssh_tunnel, "postgresql://user:pass@db.example.com:5432/x"
|
||||
)
|
||||
|
||||
# Key-type-agnostic loader must produce a paramiko PKey usable as ssh_pkey.
|
||||
assert mock_open.called, "open_tunnel was never invoked — key parsing aborted"
|
||||
forwarded_pkey = mock_open.call_args.kwargs["ssh_pkey"]
|
||||
assert isinstance(forwarded_pkey, Ed25519Key)
|
||||
|
||||
|
||||
def test_create_tunnel_accepts_rsa_private_key_unchanged() -> None:
|
||||
"""
|
||||
Companion to test_create_tunnel_accepts_ed25519_private_key: pin the
|
||||
backward-compatible RSA path so a fix for ed25519 doesn't regress the
|
||||
historically-supported RSA case. Uses a freshly generated RSA key in
|
||||
OpenSSH format.
|
||||
"""
|
||||
rsa_pem = (
|
||||
generate_rsa_key(public_exponent=65537, key_size=2048)
|
||||
.private_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
format=PrivateFormat.OpenSSH,
|
||||
encryption_algorithm=NoEncryption(),
|
||||
)
|
||||
.decode()
|
||||
)
|
||||
|
||||
manager = _make_manager()
|
||||
ssh_tunnel = _make_ssh_tunnel(rsa_pem)
|
||||
|
||||
with patch("superset.extensions.ssh.sshtunnel.open_tunnel") as mock_open:
|
||||
manager.create_tunnel(
|
||||
ssh_tunnel, "postgresql://user:pass@db.example.com:5432/x"
|
||||
)
|
||||
|
||||
assert mock_open.called, "open_tunnel was never invoked — RSA key parsing aborted"
|
||||
assert isinstance(mock_open.call_args.kwargs["ssh_pkey"], RSAKey)
|
||||
|
||||
|
||||
def test_create_tunnel_accepts_ecdsa_private_key() -> None:
|
||||
"""
|
||||
Companion to the ed25519 and RSA tests: verify ECDSAKey (the third type
|
||||
in _SSH_KEY_TYPES) is reachable by _load_private_key. Uses NIST P-256,
|
||||
the most common ECDSA curve in practice.
|
||||
"""
|
||||
ecdsa_pem = (
|
||||
ec.generate_private_key(ec.SECP256R1())
|
||||
.private_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
format=PrivateFormat.OpenSSH,
|
||||
encryption_algorithm=NoEncryption(),
|
||||
)
|
||||
.decode()
|
||||
)
|
||||
|
||||
manager = _make_manager()
|
||||
ssh_tunnel = _make_ssh_tunnel(ecdsa_pem)
|
||||
|
||||
with patch("superset.extensions.ssh.sshtunnel.open_tunnel") as mock_open:
|
||||
manager.create_tunnel(
|
||||
ssh_tunnel, "postgresql://user:pass@db.example.com:5432/x"
|
||||
)
|
||||
|
||||
assert mock_open.called, "open_tunnel was never invoked — ECDSA key parsing aborted"
|
||||
assert isinstance(mock_open.call_args.kwargs["ssh_pkey"], ECDSAKey)
|
||||
|
||||
|
||||
def test_create_tunnel_passphrase_protected_key_without_password() -> None:
|
||||
"""
|
||||
A passphrase-protected key supplied without a passphrase must surface as
|
||||
``PasswordRequiredException`` (an actionable "key requires passphrase"
|
||||
signal) rather than being absorbed by the per-type loop and reported as a
|
||||
generic "Unable to parse" error.
|
||||
"""
|
||||
encrypted_pem = (
|
||||
Ed25519PrivateKey.generate()
|
||||
.private_bytes(
|
||||
encoding=Encoding.PEM,
|
||||
format=PrivateFormat.OpenSSH,
|
||||
encryption_algorithm=BestAvailableEncryption(b"correct horse"),
|
||||
)
|
||||
.decode()
|
||||
)
|
||||
|
||||
manager = _make_manager()
|
||||
ssh_tunnel = _make_ssh_tunnel(encrypted_pem, private_key_password=None)
|
||||
|
||||
with patch("superset.extensions.ssh.sshtunnel.open_tunnel") as mock_open:
|
||||
with pytest.raises(PasswordRequiredException):
|
||||
manager.create_tunnel(
|
||||
ssh_tunnel, "postgresql://user:pass@db.example.com:5432/x"
|
||||
)
|
||||
|
||||
assert not mock_open.called
|
||||
|
||||
|
||||
def test_create_tunnel_invalid_key_raises_combined_error() -> None:
|
||||
"""
|
||||
When a key parses as none of the supported types, ``_load_private_key``
|
||||
raises ``SSHException`` whose message lists every type that was attempted,
|
||||
so the failure clearly communicates that all loaders were tried.
|
||||
"""
|
||||
manager = _make_manager()
|
||||
ssh_tunnel = _make_ssh_tunnel("not a valid private key")
|
||||
|
||||
with patch("superset.extensions.ssh.sshtunnel.open_tunnel") as mock_open:
|
||||
with pytest.raises(SSHException) as exc_info:
|
||||
manager.create_tunnel(
|
||||
ssh_tunnel, "postgresql://user:pass@db.example.com:5432/x"
|
||||
)
|
||||
|
||||
message = str(exc_info.value)
|
||||
assert "Ed25519Key" in message
|
||||
assert "ECDSAKey" in message
|
||||
assert "RSAKey" in message
|
||||
assert not mock_open.called
|
||||
|
||||
|
||||
@patch("superset.extensions.ssh.socket.create_connection")
|
||||
@patch("superset.extensions.ssh.paramiko.Transport")
|
||||
def test_verify_host_key_match(
|
||||
|
||||
@@ -402,8 +402,8 @@ def test_raise_for_access_query_default_schema(
|
||||
)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== """You need access to the following tables: `public.ab_user`,
|
||||
`all_database_access` or `all_datasource_access` permission"""
|
||||
== 'You need access to the following tables: "public.ab_user", '
|
||||
"'all_database_access' or 'all_datasource_access' permission"
|
||||
)
|
||||
|
||||
|
||||
@@ -1454,8 +1454,8 @@ def test_raise_for_access_catalog(
|
||||
sm.raise_for_access(query=query)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== """You need access to the following tables: `db1.public.ab_user`,
|
||||
`all_database_access` or `all_datasource_access` permission"""
|
||||
== 'You need access to the following tables: "db1.public.ab_user", '
|
||||
"'all_database_access' or 'all_datasource_access' permission"
|
||||
)
|
||||
|
||||
query.sql = "SELECT * FROM db2.public.ab_user"
|
||||
@@ -1463,8 +1463,8 @@ def test_raise_for_access_catalog(
|
||||
sm.raise_for_access(query=query)
|
||||
assert (
|
||||
str(excinfo.value)
|
||||
== """You need access to the following tables: `db2.public.ab_user`,
|
||||
`all_database_access` or `all_datasource_access` permission"""
|
||||
== 'You need access to the following tables: "db2.public.ab_user", '
|
||||
"'all_database_access' or 'all_datasource_access' permission"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3549,6 +3549,17 @@ def test_tokenize_kql(kql: str, expected: list[tuple[KQLTokenType, str]]) -> Non
|
||||
"postgresql",
|
||||
True,
|
||||
),
|
||||
# Set operations: a top-level UNION/INTERSECT/EXCEPT is not an
|
||||
# exp.Subquery, so it must be detected explicitly. A predicate fragment
|
||||
# that introduces one (e.g. supplied through a chart filter) must be
|
||||
# flagged.
|
||||
("true UNION SELECT name FROM other_table", "postgresql", True),
|
||||
("1 = 1 UNION ALL SELECT password FROM users", "postgresql", True),
|
||||
("SELECT 1 INTERSECT SELECT 2", "postgresql", True),
|
||||
("SELECT 1 EXCEPT SELECT 2", "postgresql", True),
|
||||
# Nested SELECT under non-Select top-level nodes (e.g. extra
|
||||
# parentheses) must still be detected.
|
||||
("name IN (((SELECT secret FROM s)))", "postgresql", True),
|
||||
],
|
||||
)
|
||||
def test_has_subquery(sql: str, engine: str, expected: bool) -> None:
|
||||
|
||||
Reference in New Issue
Block a user