Compare commits
178 Commits
fix/issue-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3da949f61 | ||
|
|
27d5c3655d | ||
|
|
ee7b7fdc90 | ||
|
|
fc754d0e81 | ||
|
|
fc1eeb066a | ||
|
|
f88b2ef259 | ||
|
|
ac3fc943dd | ||
|
|
715dc017df | ||
|
|
794b243c54 | ||
|
|
55e39fc5f7 | ||
|
|
5a2f4c8e80 | ||
|
|
17f4c215b3 | ||
|
|
1473d48134 | ||
|
|
fcee985241 | ||
|
|
7330ca4c37 | ||
|
|
3cfc745867 | ||
|
|
3413a50af0 | ||
|
|
d6c871a57f | ||
|
|
000297a6f0 | ||
|
|
854d3d59e4 | ||
|
|
a5ffac58f4 | ||
|
|
457007b2f8 | ||
|
|
9d348facce | ||
|
|
9e18d415d9 | ||
|
|
f9e9ece86f | ||
|
|
7b3e3ad6f7 | ||
|
|
4578ab31ad | ||
|
|
518bfbc511 | ||
|
|
bc1abf498d | ||
|
|
7b3ce2cf36 | ||
|
|
6aebcd2452 | ||
|
|
e0ff107487 | ||
|
|
7b463ddee0 | ||
|
|
9a345ad011 | ||
|
|
c30ae9fd12 | ||
|
|
71ee07e2e2 | ||
|
|
31e3c9a194 | ||
|
|
8ff884059f | ||
|
|
05a3d4b880 | ||
|
|
13493a3e47 | ||
|
|
ce39d221aa | ||
|
|
2fdf756cf1 | ||
|
|
0c6373cd9f | ||
|
|
3e50c2c128 | ||
|
|
895bc20360 | ||
|
|
13861568aa | ||
|
|
0616ba68af | ||
|
|
48ac1bf8c6 | ||
|
|
09015bb4bf | ||
|
|
7fecceb690 | ||
|
|
31d2d06bba | ||
|
|
83ea1d9e6f | ||
|
|
303a68b8e2 | ||
|
|
c00f6a7aab | ||
|
|
d62fc92273 | ||
|
|
343dcb0935 | ||
|
|
fee8e1b9e5 | ||
|
|
bc69daa232 | ||
|
|
7a62b3a653 | ||
|
|
3ac856ceb1 | ||
|
|
46b9ba9587 | ||
|
|
7a2913e2db | ||
|
|
b5b8a9f3bd | ||
|
|
68f19947a8 | ||
|
|
ccae778498 | ||
|
|
cdafffeacf | ||
|
|
5d158e7e79 | ||
|
|
e3bdb3894c | ||
|
|
e72d63b20d | ||
|
|
acdd390b35 | ||
|
|
3fccf29946 | ||
|
|
f2ff458acd | ||
|
|
549508b340 | ||
|
|
98be2d17da | ||
|
|
291d098917 | ||
|
|
8ba2667967 | ||
|
|
dbd3e28aa3 | ||
|
|
3960407334 | ||
|
|
a65fe06c51 | ||
|
|
3a9becd5cb | ||
|
|
afa98f3391 | ||
|
|
f1ae8909d8 | ||
|
|
ea79c63e87 | ||
|
|
81466987cb | ||
|
|
6966c1f6cc | ||
|
|
10356c30ed | ||
|
|
d668928394 | ||
|
|
87bb752814 | ||
|
|
743a27f7ed | ||
|
|
62ab4cae23 | ||
|
|
2220f53d9d | ||
|
|
ba453d3cb4 | ||
|
|
646de3a46b | ||
|
|
7260916b90 | ||
|
|
8d026b4a56 | ||
|
|
11bdbc91f3 | ||
|
|
15bde2a301 | ||
|
|
52d8b405c9 | ||
|
|
3fa06db85b | ||
|
|
396030d87e | ||
|
|
f9d67e1a11 | ||
|
|
ef2c05c090 | ||
|
|
47ba8fc801 | ||
|
|
ece8b8bd7b | ||
|
|
3f7d01aaaf | ||
|
|
7fea33761b | ||
|
|
2872d16ee6 | ||
|
|
bc3698b5d6 | ||
|
|
240a4267f8 | ||
|
|
f6155b9e9e | ||
|
|
e32493c9d9 | ||
|
|
03b61cc2d8 | ||
|
|
8849542bda | ||
|
|
8d25777b99 | ||
|
|
b0ac9ef61f | ||
|
|
ef64bacbaa | ||
|
|
e606cadff7 | ||
|
|
c1bb129c21 | ||
|
|
e048389872 | ||
|
|
0fdfd6660e | ||
|
|
10d06859b8 | ||
|
|
005edd7423 | ||
|
|
7bb2964941 | ||
|
|
1b0a37cf22 | ||
|
|
ba3500e0e9 | ||
|
|
25956d6c44 | ||
|
|
27c9213cca | ||
|
|
95faf3a6a0 | ||
|
|
c43770e12d | ||
|
|
dfccc2e1e9 | ||
|
|
6559b483f2 | ||
|
|
4a19948659 | ||
|
|
338e640df9 | ||
|
|
edc91b86e5 | ||
|
|
9c7c96fe05 | ||
|
|
b94f88ffbb | ||
|
|
6d97cde59a | ||
|
|
21b937b59d | ||
|
|
342a8f1e4e | ||
|
|
db56168a86 | ||
|
|
f7badeb095 | ||
|
|
e770202034 | ||
|
|
6a832cb210 | ||
|
|
3f6d4cd689 | ||
|
|
abdf2e6c70 | ||
|
|
c82f97b69d | ||
|
|
217e4c0223 | ||
|
|
446853004a | ||
|
|
2c72d4d4fe | ||
|
|
4ae8fe7122 | ||
|
|
33292f249d | ||
|
|
0a0a1974c5 | ||
|
|
a12d1b8e12 | ||
|
|
e5ec22ab3e | ||
|
|
ee39954adb | ||
|
|
8ab7a85a3d | ||
|
|
74d885233d | ||
|
|
b98e19f6eb | ||
|
|
8b643519ca | ||
|
|
6418473be1 | ||
|
|
4f9c22db6d | ||
|
|
3109cfaf6d | ||
|
|
4911dbe00b | ||
|
|
4cdd680f39 | ||
|
|
d2fb52ac83 | ||
|
|
924343e3ee | ||
|
|
e91ce19a60 | ||
|
|
90fc473583 | ||
|
|
bbf00956d4 | ||
|
|
855bb4e21c | ||
|
|
5cf8213354 | ||
|
|
8baa9c1f2e | ||
|
|
dc0e306a24 | ||
|
|
d2101c8dba | ||
|
|
fd524f4b21 | ||
|
|
f02f53df23 | ||
|
|
d80fcfc02a | ||
|
|
bbf0fa5f42 |
@@ -77,7 +77,7 @@ github:
|
||||
# combination here.
|
||||
contexts:
|
||||
- lint-check
|
||||
- cypress-matrix-required
|
||||
- babel-extract
|
||||
- dependency-review
|
||||
- enforce-single-migration-head
|
||||
- frontend-build
|
||||
|
||||
@@ -20,8 +20,8 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
|
||||
|
||||
### Testing Strategy Migration
|
||||
- **Prefer unit tests** over integration tests
|
||||
- **Prefer integration tests** over Cypress end-to-end tests
|
||||
- **Cypress is last resort** - Actively moving away from Cypress
|
||||
- **Prefer integration tests** over end-to-end tests
|
||||
- **Use Playwright for E2E tests** - Cypress has been removed
|
||||
- **Use Jest + React Testing Library** for component testing
|
||||
|
||||
### Backend Type Safety
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
tests/
|
||||
docs/
|
||||
install/
|
||||
superset-frontend/cypress-base/
|
||||
superset-frontend/coverage/
|
||||
superset-frontend/.temp_cache/
|
||||
superset/static/assets/
|
||||
|
||||
@@ -32,7 +32,6 @@ _p=80; while ! _is_free $_p; do ((_p++)); done; export NGINX_PORT=$_p
|
||||
_p=8088; while ! _is_free $_p; do ((_p++)); done; export SUPERSET_PORT=$_p
|
||||
_p=9000; while ! _is_free $_p; do ((_p++)); done; export NODE_PORT=$_p
|
||||
_p=8080; while ! _is_free $_p || [ $_p -eq $NGINX_PORT ]; do ((_p++)); done; export WEBSOCKET_PORT=$_p
|
||||
_p=8081; while ! _is_free $_p || [ $_p -eq $WEBSOCKET_PORT ]; do ((_p++)); done; export CYPRESS_PORT=$_p
|
||||
_p=5432; while ! _is_free $_p; do ((_p++)); done; export DATABASE_PORT=$_p
|
||||
_p=6379; while ! _is_free $_p; do ((_p++)); done; export REDIS_PORT=$_p
|
||||
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -9,7 +9,7 @@
|
||||
|
||||
# Notify PMC members of changes to required GitHub Actions
|
||||
|
||||
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @Antonio-RiveroMartnez @msyavuz
|
||||
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @sadpandajoe @Antonio-RiveroMartnez @msyavuz
|
||||
|
||||
# Maps are fragile and political. GeoJson edits MUST be made in the Jupyter notebook or they'll be overwritten.
|
||||
|
||||
|
||||
5
.github/actions/change-detector/action.yml
vendored
5
.github/actions/change-detector/action.yml
vendored
@@ -20,6 +20,11 @@ outputs:
|
||||
superset-extensions-cli:
|
||||
description: Whether superset-extensions-cli package-related files were changed
|
||||
value: ${{ steps.change-detector.outputs.superset-extensions-cli }}
|
||||
languages:
|
||||
description: >-
|
||||
JSON array of CodeQL languages touched, classified by file extension
|
||||
regardless of directory (e.g. ["javascript", "python"])
|
||||
value: ${{ steps.change-detector.outputs.languages }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
||||
1
.github/actions/chart-testing-action
vendored
1
.github/actions/chart-testing-action
vendored
Submodule .github/actions/chart-testing-action deleted from afea100a51
2
.github/labeler.yml
vendored
2
.github/labeler.yml
vendored
@@ -54,8 +54,6 @@
|
||||
- 'superset-embedded-sdk/package-lock.json'
|
||||
- 'superset-websocket/package.json'
|
||||
- 'superset-websocket/package-lock.json'
|
||||
- 'superset-frontend/cypress-base/package.json'
|
||||
- 'superset-frontend/cypress-base/package-lock.json'
|
||||
- 'superset-frontend/packages/**/package.json'
|
||||
- 'superset-frontend/plugins/**/package.json'
|
||||
|
||||
|
||||
279
.github/workflows/bashlib.sh
vendored
279
.github/workflows/bashlib.sh
vendored
@@ -161,108 +161,6 @@ celery-worker() {
|
||||
say "::endgroup::"
|
||||
}
|
||||
|
||||
cypress-install() {
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
|
||||
|
||||
cache-restore cypress
|
||||
|
||||
say "::group::Install Cypress"
|
||||
npm ci
|
||||
say "::endgroup::"
|
||||
|
||||
cache-save cypress
|
||||
}
|
||||
|
||||
cypress-run-all() {
|
||||
local USE_DASHBOARD=$1
|
||||
local APP_ROOT=$2
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
|
||||
|
||||
# Start the Superset backend via gunicorn (not `flask run`). The Flask
|
||||
# development server is single-threaded and has no crash-recovery, so
|
||||
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
|
||||
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
|
||||
# / `Missing CSRF token` cascades. Gunicorn gives us multiple workers,
|
||||
# a request timeout, and worker-recycling under load.
|
||||
local serverlog="${HOME}/superset-cypress.log"
|
||||
local port=8081
|
||||
CYPRESS_BASE_URL="http://localhost:${port}"
|
||||
if [ -n "$APP_ROOT" ]; then
|
||||
export SUPERSET_APP_ROOT=$APP_ROOT
|
||||
CYPRESS_BASE_URL=${CYPRESS_BASE_URL}${APP_ROOT}
|
||||
fi
|
||||
export CYPRESS_BASE_URL
|
||||
|
||||
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
|
||||
# gthread threads) to keep parity with production. Multi-worker
|
||||
# configurations expose timing-sensitive races in the SQL Lab → Explore
|
||||
# navigation flow under E2E. We diverge from the entrypoint on:
|
||||
# --timeout 120: heavy dashboard import/export specs exceed the 60s
|
||||
# default
|
||||
# superset.app:create_app(): explicit factory so we don't depend on
|
||||
# FLASK_APP being exported
|
||||
#
|
||||
# No --max-requests, matching the entrypoint's default of 0 (recycling
|
||||
# off). With a single worker a recycle takes the whole backend offline for
|
||||
# the graceful-timeout drain — browser keep-alive connections hold it open
|
||||
# for the full 30s — plus ~5s of app boot. A run issues ~3800 requests in
|
||||
# ~8 minutes, so recycling every 500 produced seven ~35s outages per run
|
||||
# and flaked whichever specs happened to navigate into one. Lowering
|
||||
# --graceful-timeout is not enough: a dashboard load plus chart render
|
||||
# needs 6-10s, which still lands inside the window.
|
||||
nohup gunicorn \
|
||||
--bind "127.0.0.1:$port" \
|
||||
--workers 1 \
|
||||
--worker-class gthread \
|
||||
--threads 20 \
|
||||
--timeout 120 \
|
||||
--access-logfile - \
|
||||
--error-logfile - \
|
||||
"superset.app:create_app()" \
|
||||
>"$serverlog" 2>&1 </dev/null &
|
||||
local serverPid=$!
|
||||
|
||||
# Ensure the backend is cleaned up and its log is emitted even when the
|
||||
# test runner fails under `set -e`.
|
||||
trap '
|
||||
echo "::group::gunicorn log for Cypress run"
|
||||
cat "'"$serverlog"'" || true
|
||||
echo "::endgroup::"
|
||||
kill '"$serverPid"' 2>/dev/null || true
|
||||
' EXIT
|
||||
|
||||
# Wait for the backend to be ready before launching Cypress; otherwise
|
||||
# the first spec can race the server bind and see connection errors.
|
||||
local timeout=60
|
||||
say "Waiting for gunicorn server to start on port $port..."
|
||||
while [ $timeout -gt 0 ]; do
|
||||
if curl -f "http://localhost:${port}${APP_ROOT}/health" >/dev/null 2>&1; then
|
||||
say "gunicorn server is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "::error::gunicorn server failed to start within 60 seconds"
|
||||
echo "::group::Server startup log"
|
||||
cat "$serverlog"
|
||||
echo "::endgroup::"
|
||||
return 1
|
||||
fi
|
||||
|
||||
USE_DASHBOARD_FLAG=''
|
||||
if [ "$USE_DASHBOARD" = "true" ]; then
|
||||
USE_DASHBOARD_FLAG='--use-dashboard'
|
||||
fi
|
||||
|
||||
# UNCOMMENT the next few commands to monitor memory usage
|
||||
# monitor_memory & # Start memory monitoring in the background
|
||||
# memoryMonitorPid=$!
|
||||
python ../../scripts/cypress_run.py --retries 5 $USE_DASHBOARD_FLAG
|
||||
# kill $memoryMonitorPid
|
||||
}
|
||||
|
||||
playwright-install() {
|
||||
cd "$GITHUB_WORKSPACE/superset-frontend"
|
||||
|
||||
@@ -276,11 +174,20 @@ playwright-install() {
|
||||
|
||||
playwright-run() {
|
||||
local APP_ROOT=$1
|
||||
local TEST_PATH=$2
|
||||
shift || true
|
||||
# Remaining arguments are test paths, relative to playwright/tests. Passing
|
||||
# more than one lets a caller run a suite that spans directories. With none,
|
||||
# selection comes from PLAYWRIGHT_EXTRA_ARGS instead -- that is how
|
||||
# playwright-run-gaq picks its suite, via --project.
|
||||
local TEST_PATHS=("$@")
|
||||
local TEST_PATH=${TEST_PATHS[0]:-}
|
||||
|
||||
# Start the Superset backend via gunicorn from the project root.
|
||||
# See cypress-run-all() above for the rationale — the Flask dev server
|
||||
# cannot survive the dashboard import/export tests under load.
|
||||
# Start the Superset backend via gunicorn (not `flask run`). The Flask
|
||||
# development server is single-threaded and has no crash-recovery, so
|
||||
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
|
||||
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
|
||||
# / `Missing CSRF token` cascades. Gunicorn gives us a request timeout
|
||||
# and a multi-threaded worker.
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
local serverlog="${HOME}/superset-playwright.log"
|
||||
local port=8081
|
||||
@@ -296,9 +203,22 @@ playwright-run() {
|
||||
fi
|
||||
export PLAYWRIGHT_BASE_URL
|
||||
|
||||
# See cypress-run-all() above for the args rationale (1 worker × 20
|
||||
# gthread threads matching docker/entrypoints/run-server.sh, a 120s
|
||||
# timeout for heavy E2E load, and why worker recycling is off).
|
||||
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
|
||||
# gthread threads) to keep parity with production. Multi-worker
|
||||
# configurations expose timing-sensitive races in the SQL Lab → Explore
|
||||
# navigation flow under E2E. We diverge from the entrypoint on:
|
||||
# --timeout 120: heavy dashboard import/export specs exceed the 60s
|
||||
# default
|
||||
# superset.app:create_app(): explicit factory so we don't depend on
|
||||
# FLASK_APP being exported
|
||||
#
|
||||
# No --max-requests, matching the entrypoint's default of 0 (recycling
|
||||
# off). With a single worker a recycle takes the whole backend offline for
|
||||
# the graceful-timeout drain — browser keep-alive connections hold it open
|
||||
# for the full 30s — plus ~5s of app boot, which flakes whichever specs
|
||||
# happen to navigate into the outage. Lowering --graceful-timeout is not
|
||||
# enough: a dashboard load plus chart render needs 6-10s, which still
|
||||
# lands inside the window.
|
||||
nohup gunicorn \
|
||||
--bind "127.0.0.1:$port" \
|
||||
--workers 1 \
|
||||
@@ -345,18 +265,29 @@ playwright-run() {
|
||||
say "::group::Run Playwright tests"
|
||||
echo "Running Playwright with baseURL: ${PLAYWRIGHT_BASE_URL}"
|
||||
if [ -n "$TEST_PATH" ]; then
|
||||
# Check if there are any test files in the specified path
|
||||
if ! find "playwright/tests/${TEST_PATH}" -name "*.spec.ts" -type f 2>/dev/null | grep -q .; then
|
||||
echo "No test files found in ${TEST_PATH} - skipping test run"
|
||||
# Check if there are any test files in the specified paths
|
||||
local found=0
|
||||
local candidate
|
||||
for candidate in "${TEST_PATHS[@]}"; do
|
||||
if find "playwright/tests/${candidate}" -name "*.spec.ts" -type f 2>/dev/null | grep -q .; then
|
||||
found=1
|
||||
elif [ -f "playwright/tests/${candidate}" ]; then
|
||||
found=1
|
||||
fi
|
||||
done
|
||||
if [ "$found" -eq 0 ]; then
|
||||
echo "No test files found in ${TEST_PATHS[*]} - skipping test run"
|
||||
say "::endgroup::"
|
||||
return 0
|
||||
fi
|
||||
echo "Running tests: ${TEST_PATH}"
|
||||
npx playwright test "${TEST_PATH}" --output=playwright-results
|
||||
echo "Running tests: ${TEST_PATHS[*]}"
|
||||
# shellcheck disable=SC2086
|
||||
npx playwright test "${TEST_PATHS[@]}" --output=playwright-results ${PLAYWRIGHT_EXTRA_ARGS:-}
|
||||
local status=$?
|
||||
else
|
||||
echo "Running all default-project tests"
|
||||
npx playwright test --output=playwright-results
|
||||
echo "Running with no explicit paths (project selection comes from PLAYWRIGHT_EXTRA_ARGS, if set)"
|
||||
# shellcheck disable=SC2086
|
||||
npx playwright test --output=playwright-results ${PLAYWRIGHT_EXTRA_ARGS:-}
|
||||
local status=$?
|
||||
fi
|
||||
say "::endgroup::"
|
||||
@@ -364,6 +295,122 @@ playwright-run() {
|
||||
return $status
|
||||
}
|
||||
|
||||
playwright-run-gaq() {
|
||||
# Global Async Queries needs more than a feature flag: submissions are handed
|
||||
# to Celery, so without a worker consuming the queue the API returns 202 and
|
||||
# no job ever runs -- the specs would time out rather than fail usefully.
|
||||
# `playwright-run` boots gunicorn with this step's environment, so the flag
|
||||
# set on the step reaches both the web server and the worker started here.
|
||||
local APP_ROOT=$1
|
||||
|
||||
cd "$GITHUB_WORKSPACE"
|
||||
|
||||
if [ "${SUPERSET_FEATURE_GLOBAL_ASYNC_QUERIES:-}" != "true" ]; then
|
||||
echo "::error::SUPERSET_FEATURE_GLOBAL_ASYNC_QUERIES must be \"true\" for this step; the specs would skip themselves and report nothing."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local workerlog="${HOME}/superset-gaq-worker.log"
|
||||
say "::group::Start Celery worker for GAQ"
|
||||
# Mirrors docker/docker-bootstrap.sh's worker invocation.
|
||||
nohup celery --app=superset.tasks.celery_app:app worker \
|
||||
-O fair \
|
||||
--loglevel=INFO \
|
||||
--concurrency=2 \
|
||||
>"$workerlog" 2>&1 </dev/null &
|
||||
local workerPid=$!
|
||||
|
||||
# Fail fast on a worker that never comes up: a dead worker is
|
||||
# indistinguishable from a slow one once the specs start timing out.
|
||||
local timeout=60
|
||||
while [ $timeout -gt 0 ]; do
|
||||
if ! kill -0 "$workerPid" 2>/dev/null; then
|
||||
echo "::error::Celery worker exited during startup"
|
||||
cat "$workerlog" || true
|
||||
say "::endgroup::"
|
||||
return 1
|
||||
fi
|
||||
if grep -q "celery@.*ready" "$workerlog" 2>/dev/null; then
|
||||
say "Celery worker is ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
timeout=$((timeout - 1))
|
||||
done
|
||||
if [ $timeout -eq 0 ]; then
|
||||
echo "::error::Celery worker failed to become ready within 60 seconds"
|
||||
cat "$workerlog" || true
|
||||
kill "$workerPid" 2>/dev/null || true
|
||||
say "::endgroup::"
|
||||
return 1
|
||||
fi
|
||||
say "::endgroup::"
|
||||
|
||||
# The GAQ specs are excluded from every other project, so this is what makes
|
||||
# them loadable at all -- see the chromium-gaq project in playwright.config.ts.
|
||||
export INCLUDE_GAQ=true
|
||||
|
||||
local report="${GITHUB_WORKSPACE}/superset-frontend/playwright-gaq-report.json"
|
||||
rm -f "$report"
|
||||
export PLAYWRIGHT_JSON_OUTPUT_NAME="$report"
|
||||
# --workers=1: the fixtures all create charts as the same admin user, and
|
||||
# Superset's tag listener races on the shared `editor:<id>` tag (see the
|
||||
# chromium-gaq project in playwright.config.ts). That project's
|
||||
# `fullyParallel: false` only orders tests within one file -- Playwright
|
||||
# still runs separate files concurrently -- and this suite spans three, so
|
||||
# one worker is what actually serializes it.
|
||||
#
|
||||
# --project=chromium-gaq rather than a list of spec paths: that project's
|
||||
# testMatch (`**/global-async-query*.spec.ts`) is the same glob the default
|
||||
# and sqllab projects use to *exclude* these specs, so selecting by project
|
||||
# keeps both sides reading from one definition. A new spec matching the glob
|
||||
# is picked up here automatically; with a hand-maintained path list it would
|
||||
# be excluded from the required run and never added here, so it would run
|
||||
# nowhere and the zero-executed check below could not notice.
|
||||
export PLAYWRIGHT_EXTRA_ARGS="--reporter=list,json --workers=1 --project=chromium-gaq"
|
||||
|
||||
# `set -e` is on: without the guard a failing run would exit before the
|
||||
# worker log is emitted and before the did-it-actually-run check below.
|
||||
local status=0
|
||||
playwright-run "$APP_ROOT" || status=$?
|
||||
|
||||
unset PLAYWRIGHT_EXTRA_ARGS PLAYWRIGHT_JSON_OUTPUT_NAME INCLUDE_GAQ
|
||||
|
||||
say "::group::Celery worker log"
|
||||
cat "$workerlog" || true
|
||||
say "::endgroup::"
|
||||
kill "$workerPid" 2>/dev/null || true
|
||||
|
||||
# A suite that skips itself still exits 0. That is the failure mode this step
|
||||
# exists to prevent, so assert that tests actually ran.
|
||||
if [ ! -f "$report" ]; then
|
||||
echo "::error::No Playwright JSON report produced; cannot confirm the GAQ specs ran."
|
||||
return 1
|
||||
fi
|
||||
# "Did the suite run?" must count every outcome that proves a test executed,
|
||||
# not just first-attempt passes. A run where every GAQ test genuinely fails
|
||||
# (say the worker loses Redis mid-run) reports expected=0 too, and blaming
|
||||
# that on an inactive GLOBAL_ASYNC_QUERIES would point whoever is debugging
|
||||
# at the wrong layer -- the job is already red from the real failure.
|
||||
local expected unexpected flaky skipped executed
|
||||
expected=$(jq '.stats.expected // 0' "$report")
|
||||
unexpected=$(jq '.stats.unexpected // 0' "$report")
|
||||
flaky=$(jq '.stats.flaky // 0' "$report")
|
||||
skipped=$(jq '.stats.skipped // 0' "$report")
|
||||
executed=$((expected + unexpected + flaky))
|
||||
say "GAQ suite: ${expected} passed, ${unexpected} failed, ${flaky} flaky, ${skipped} skipped"
|
||||
if [ "$executed" -eq 0 ]; then
|
||||
echo "::error::The GAQ specs reported zero executed tests -- they skipped themselves, which means GLOBAL_ASYNC_QUERIES was not active on the server under test."
|
||||
return 1
|
||||
fi
|
||||
if [ "$skipped" -ne 0 ]; then
|
||||
echo "::error::${skipped} GAQ test(s) skipped; this step must run the whole suite."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
eyes-storybook-dependencies() {
|
||||
say "::group::install eyes-storyook dependencies"
|
||||
sudo apt-get update -y && sudo apt-get -y install gconf-service ca-certificates libxshmfence-dev fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libglib2.0-0 libgdk-pixbuf2.0-0 libgtk-3-0 libnspr4 libnss3 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release xdg-utils libappindicator1
|
||||
|
||||
6
.github/workflows/caches.js
vendored
6
.github/workflows/caches.js
vendored
@@ -49,10 +49,4 @@ module.exports = {
|
||||
// one is built with `npm run build`,
|
||||
// another is built with `npm run build-instrumented`
|
||||
'instrumented-assets': assetsConfig,
|
||||
cypress: {
|
||||
path: [`${homeDirectory}/.cache/Cypress`],
|
||||
hashFiles: [
|
||||
`${workspaceDirectory}/superset-frontend/cypress-base/package-lock.json`,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
134
.github/workflows/chromatic.yml
vendored
Normal file
134
.github/workflows/chromatic.yml
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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.
|
||||
|
||||
# Publishes superset-frontend's Storybook to Chromatic for visual
|
||||
# regression testing. See https://www.chromatic.com/docs/github-actions
|
||||
#
|
||||
# Runs on pushes to master (keeps the Chromatic baseline in sync with
|
||||
# mainline) and on pull requests that touch superset-frontend. Fork PRs
|
||||
# don't receive CHROMATIC_PROJECT_TOKEN -- GitHub withholds repository
|
||||
# secrets from pull_request runs triggered by a fork -- so the publish
|
||||
# steps below no-op for them (via the CHROMATIC_PROJECT_TOKEN != '' guard)
|
||||
# rather than failing.
|
||||
#
|
||||
# Non-blocking for now (exitZeroOnChanges: true): visual changes are
|
||||
# surfaced as a PR check/comment for review, not enforced as a merge gate.
|
||||
# A prior Chromatic setup here (#21095) was removed in #27232 for being
|
||||
# unmaintained and overlapping with Applitools (since also discontinued).
|
||||
# Keep this one simple and watch it before considering a required check.
|
||||
#
|
||||
# The master (push) run auto-accepts its own changes so the baseline stays
|
||||
# current; otherwise every subsequent PR would keep reporting the same
|
||||
# already-merged diff against a stale baseline. PR runs are left alone so a
|
||||
# human still reviews what changed.
|
||||
name: Chromatic
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "superset-frontend/**"
|
||||
- ".github/workflows/chromatic.yml"
|
||||
pull_request:
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
paths:
|
||||
- "superset-frontend/**"
|
||||
workflow_dispatch: {}
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TAG: apache/superset:chromatic-${{ github.run_id }}
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
chromatic:
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 30
|
||||
# pull-requests: write lets chromaui/action post its check and PR
|
||||
# comment. Withheld automatically by GitHub for fork-triggered
|
||||
# pull_request runs, same as CHROMATIC_PROJECT_TOKEN above.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# TurboSnap (onlyChanged below) needs history to diff against the
|
||||
# baseline commit, but NOT fetch-depth: 0. This Chromatic project
|
||||
# was dormant for ~2 years (Chromatic here shipped in #21095,
|
||||
# removed in #27232) before this workflow, so its last known
|
||||
# baseline predates thousands of commits on a very active repo.
|
||||
# With full history, Chromatic's CLI tries to `git log` every
|
||||
# commit back to that ancient baseline as individual CLI args and
|
||||
# hits the OS ARG_MAX limit (E2BIG) -- see
|
||||
# https://github.com/chromaui/chromatic-cli/issues/432, where the
|
||||
# Chromatic team's own recommended workaround is exactly this: a
|
||||
# bounded depth, since a multi-year-old baseline isn't useful
|
||||
# anyway. 500 is far more than any realistic PR needs once a
|
||||
# recent baseline exists (i.e. after this workflow's own first
|
||||
# successful run on master).
|
||||
fetch-depth: 500
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Build Docker Image
|
||||
if: ${{ env.CHROMATIC_PROJECT_TOKEN != '' }}
|
||||
run: |
|
||||
docker buildx build \
|
||||
-t $TAG \
|
||||
--cache-from=type=registry,ref=apache/superset-cache:3.11-slim-trixie \
|
||||
--target superset-node-ci \
|
||||
.
|
||||
|
||||
# --webpack-stats-json writes preview-stats.json into storybook-static.
|
||||
# TurboSnap (onlyChanged below) needs it to trace which stories a
|
||||
# changed file affects; without it, chromaui/action fails with "Could
|
||||
# not retrieve dependent story files" since storybookBuildDir points
|
||||
# at an already-built Storybook it can't inject its own stats
|
||||
# collection into.
|
||||
- name: Build Storybook
|
||||
if: ${{ env.CHROMATIC_PROJECT_TOKEN != '' }}
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/superset-frontend/storybook-static
|
||||
docker run \
|
||||
-v ${{ github.workspace }}/superset-frontend/storybook-static:/app/superset-frontend/storybook-static \
|
||||
--rm $TAG \
|
||||
bash -c "npm i && npm run build-storybook -- --webpack-stats-json"
|
||||
|
||||
- name: Publish to Chromatic
|
||||
if: ${{ env.CHROMATIC_PROJECT_TOKEN != '' }}
|
||||
uses: chromaui/action@db3ceb5efb0cd7447d63c2d8cce7da732ce89322 # v18.10.1
|
||||
with:
|
||||
projectToken: ${{ env.CHROMATIC_PROJECT_TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
workingDir: superset-frontend
|
||||
storybookBuildDir: storybook-static
|
||||
# TurboSnap: only re-snapshot stories affected by files changed
|
||||
# since the baseline build.
|
||||
onlyChanged: true
|
||||
exitZeroOnChanges: true
|
||||
autoAcceptChanges: ${{ github.event_name == 'push' }}
|
||||
zip: true
|
||||
15
.github/workflows/codeql-analysis.yml
vendored
15
.github/workflows/codeql-analysis.yml
vendored
@@ -19,14 +19,13 @@ permissions:
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
outputs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
languages: ${{ steps.check.outputs.languages }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -44,9 +43,9 @@ jobs:
|
||||
# Skip on PRs that touch neither code group (e.g. docs-only) so the
|
||||
# analysis runners don't spin up. push/schedule runs always proceed:
|
||||
# the change-detector returns "all changed" for non-PR events.
|
||||
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
|
||||
if: ${{ contains(fromJSON(needs.changes.outputs.languages), 'python') || contains(fromJSON(needs.changes.outputs.languages), 'javascript') }}
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["python", "javascript"]
|
||||
language: ${{ fromJson(needs.changes.outputs.languages) }}
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
@@ -67,7 +66,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -78,6 +77,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -482,7 +482,7 @@ jobs:
|
||||
contains(fromJson('["superset", "lean"]'), matrix.build_preset) &&
|
||||
steps.grype-scan.outputs.sarif != ''
|
||||
continue-on-error: true
|
||||
uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
sarif_file: ${{ steps.grype-scan.outputs.sarif }}
|
||||
# Both `superset` and `lean` are scanned now; without a distinct
|
||||
|
||||
4
.github/workflows/superset-docs-deploy.yml
vendored
4
.github/workflows/superset-docs-deploy.yml
vendored
@@ -141,7 +141,7 @@ jobs:
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics (if triggered by integration tests)
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477 # v27
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
path: docs/src/data/
|
||||
- name: Try to download latest diagnostics (for push/dispatch triggers)
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477 # v27
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
|
||||
2
.github/workflows/superset-docs-verify.yml
vendored
2
.github/workflows/superset-docs-verify.yml
vendored
@@ -140,7 +140,7 @@ jobs:
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics from integration tests
|
||||
uses: dawidd6/action-download-artifact@d63b86af1b34672e53c440b1b83979861906bad7 # v24
|
||||
uses: dawidd6/action-download-artifact@eab87c9830c39eff17e5a6eadb20bfb4bc880477 # v27
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
372
.github/workflows/superset-e2e.yml
vendored
372
.github/workflows/superset-e2e.yml
vendored
@@ -9,10 +9,6 @@ on:
|
||||
types: [synchronize, opened, reopened, ready_for_review]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
use_dashboard:
|
||||
description: "Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work."
|
||||
required: false
|
||||
default: "false"
|
||||
ref:
|
||||
description: "The branch or tag to checkout"
|
||||
required: false
|
||||
@@ -50,156 +46,6 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
cypress-matrix:
|
||||
needs: changes
|
||||
if: (needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true') && github.event.pull_request.draft == false
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
strategy:
|
||||
# when one test fails, DO NOT cancel the other
|
||||
# app_root variant, because this will kill Cypress processes
|
||||
# leaving the Dashboard hanging ...
|
||||
# https://github.com/cypress-io/github-action/issues/48
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: ["chrome"]
|
||||
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
|
||||
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
REDIS_PORT: 16379
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
# Only use dashboard when explicitly requested via workflow_dispatch
|
||||
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true' || 'false' }}
|
||||
services:
|
||||
postgres:
|
||||
image: ghcr.io/apache/superset/ci/postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: superset
|
||||
POSTGRES_PASSWORD: superset
|
||||
ports:
|
||||
- 15432:5432
|
||||
redis:
|
||||
image: ghcr.io/apache/superset/ci/redis:7-alpine
|
||||
ports:
|
||||
- 16379:6379
|
||||
steps:
|
||||
# -------------------------------------------------------
|
||||
# Conditional checkout based on context
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
submodules: recursive
|
||||
# -------------------------------------------------------
|
||||
- name: Setup Python
|
||||
uses: $/.github/actions/setup-backend/
|
||||
- name: Setup postgres
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Import test data
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
- name: Install npm dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Build javascript packages
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Install cypress
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: cypress-install
|
||||
- name: Run Cypress
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
env:
|
||||
CYPRESS_BROWSER: ${{ matrix.browser }}
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
with:
|
||||
run: cypress-run-all ${{ env.USE_DASHBOARD }} ${{ matrix.app_root }}
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
env:
|
||||
APP_ROOT: ${{ matrix.app_root }}
|
||||
run: |
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: ${{ github.workspace }}/superset-frontend/cypress-base/cypress/screenshots
|
||||
name: cypress-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
||||
|
||||
playwright-tests:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
|
||||
@@ -215,8 +61,7 @@ jobs:
|
||||
matrix:
|
||||
browser: ["chromium"]
|
||||
# Subdirectory deployment (APPLICATION_ROOT) is a required-to-pass
|
||||
# dimension, not an optional one, so it runs on every event —
|
||||
# unlike cypress-matrix above, which only widens on push.
|
||||
# dimension, so it runs on every event.
|
||||
app_root: ["", "/app/prefix"]
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
@@ -239,7 +84,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
# -------------------------------------------------------
|
||||
# Conditional checkout based on context (same as Cypress workflow)
|
||||
# Conditional checkout based on context
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -414,44 +259,191 @@ jobs:
|
||||
${{ github.workspace }}/superset-frontend/test-results/
|
||||
name: playwright-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
||||
|
||||
# Stable required-status-check anchors. cypress-matrix and playwright-tests
|
||||
# are matrix jobs gated on change detection (python || frontend). On a PR
|
||||
# that touches neither — e.g. a docs-only PR — they are skipped at the job
|
||||
# level, which happens before matrix expansion, so the per-combination
|
||||
# contexts (`cypress-matrix (0, chrome)`, `playwright-tests (chromium)`) are
|
||||
# never produced and branch protection waits on them forever. These
|
||||
# always-running jobs report a single stable context that passes when the
|
||||
# underlying matrix job succeeded or was skipped, and fails only on a real
|
||||
# failure. Require these in .asf.yaml instead of the matrix-expanded names.
|
||||
# GAQ runs in its own job rather than as a step in playwright-tests above.
|
||||
# A step with no explicit `if:` implicitly inherits `if: success()`, so
|
||||
# when GAQ was a step after the other suites in that job, a failure in
|
||||
# either of those unrelated suites skipped GAQ entirely rather than failing it
|
||||
# -- silently leaving that commit with zero GAQ coverage instead of a visible
|
||||
# red check. A separate job can't share that fate: it either runs and reports
|
||||
# for itself, or it doesn't start (e.g. the environment itself never came up),
|
||||
# which is the only case where "no GAQ result" is actually the right outcome.
|
||||
playwright-tests-gaq:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 30
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: ["chromium"]
|
||||
app_root: ["", "/app/prefix"]
|
||||
env:
|
||||
SUPERSET_ENV: development
|
||||
SUPERSET_CONFIG: tests.integration_tests.superset_test_config_gaq
|
||||
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
REDIS_PORT: 16379
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
services:
|
||||
postgres:
|
||||
image: ghcr.io/apache/superset/ci/postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: superset
|
||||
POSTGRES_PASSWORD: superset
|
||||
ports:
|
||||
- 15432:5432
|
||||
redis:
|
||||
image: ghcr.io/apache/superset/ci/redis:7-alpine
|
||||
ports:
|
||||
- 16379:6379
|
||||
steps:
|
||||
# -------------------------------------------------------
|
||||
# Conditional checkout based on context
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
submodules: recursive
|
||||
# -------------------------------------------------------
|
||||
- name: Setup Python
|
||||
uses: $/.github/actions/setup-backend/
|
||||
- name: Setup postgres
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: setup-postgres
|
||||
- name: Import test data
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version-file: "./superset-frontend/.nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "superset-frontend/package-lock.json"
|
||||
- name: Install npm dependencies
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: npm-install
|
||||
- name: Build javascript packages
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: build-instrumented-assets
|
||||
- name: Install Playwright
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
with:
|
||||
run: playwright-install
|
||||
- name: Run Playwright (Global Async Queries Tests)
|
||||
# cached-dependencies is a submodule (not a plain directory), and
|
||||
# the $/ self-repository syntax resolves action files directly from
|
||||
# the repository without performing a real (submodule-aware)
|
||||
# checkout, so it can't see into a submodule's link. Keep this one
|
||||
# on the workspace-relative ./ form, consistent with every other
|
||||
# workflow in the repo that references this action.
|
||||
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
# Scoped to this step for the same reason as the Soft-delete, Embedded
|
||||
# and Mobile steps in playwright-tests above: GLOBAL_ASYNC_QUERIES
|
||||
# changes chart-data behaviour for every spec sharing a server.
|
||||
#
|
||||
# This suite has its own job, and is not required, because unlike the
|
||||
# other flag-gated suites it needs a Celery worker: submissions are
|
||||
# handed to Celery, so without one the API returns 202 and no job ever
|
||||
# executes. Putting a background worker and its startup window on the
|
||||
# required path would let a GAQ flake block PRs that have nothing to
|
||||
# do with async queries.
|
||||
#
|
||||
# `playwright-run-gaq` starts the worker and sets INCLUDE_GAQ, which
|
||||
# is what makes the chromium-gaq project exist at all.
|
||||
SUPERSET_FEATURE_GLOBAL_ASYNC_QUERIES: "true"
|
||||
with:
|
||||
# No spec list: playwright-run-gaq selects --project=chromium-gaq, whose
|
||||
# testMatch is the single definition of what the GAQ suite contains.
|
||||
run: playwright-run-gaq "${{ matrix.app_root }}"
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
env:
|
||||
APP_ROOT: ${{ matrix.app_root }}
|
||||
run: |
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Playwright Artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: |
|
||||
${{ github.workspace }}/superset-frontend/playwright-results/
|
||||
${{ github.workspace }}/superset-frontend/test-results/
|
||||
name: playwright-gaq-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
||||
|
||||
# Stable required-status-check anchor. playwright-tests is a matrix job
|
||||
# gated on change detection (python || frontend). On a PR that touches
|
||||
# neither — e.g. a docs-only PR — it is skipped at the job level, which
|
||||
# happens before matrix expansion, so the per-combination context
|
||||
# (`playwright-tests (chromium)`) is never produced and branch protection
|
||||
# waits on it forever. This always-running job reports a single stable
|
||||
# context that passes when the underlying matrix job succeeded or was
|
||||
# skipped, and fails only on a real failure. Require it in .asf.yaml instead
|
||||
# of the matrix-expanded name.
|
||||
#
|
||||
# A matrix job reads as "skipped" in two distinct cases, and only the first
|
||||
# is a legitimate pass: (a) change detection succeeded and gated the job off
|
||||
# (docs-only PR); (b) the `changes` job itself failed or was cancelled, in
|
||||
# which case GHA skips its dependents too. Accepting (b) would let a broken
|
||||
# change-detector report a false green, so each anchor first requires
|
||||
# change-detector report a false green, so the anchor first requires
|
||||
# `changes` to have succeeded before honouring a skip.
|
||||
cypress-matrix-required:
|
||||
needs: [changes, cypress-matrix]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 5
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Check cypress-matrix result
|
||||
env:
|
||||
CHANGES: ${{ needs.changes.result }}
|
||||
RESULT: ${{ needs.cypress-matrix.result }}
|
||||
run: |
|
||||
if [ "$CHANGES" != "success" ]; then
|
||||
echo "change detection did not succeed (result: $CHANGES); refusing to pass on a skipped matrix"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
|
||||
echo "cypress-matrix did not pass (result: $RESULT)"
|
||||
exit 1
|
||||
fi
|
||||
echo "cypress-matrix result: $RESULT (changes: $CHANGES)"
|
||||
|
||||
playwright-tests-required:
|
||||
needs: [changes, playwright-tests]
|
||||
if: always()
|
||||
@@ -475,7 +467,7 @@ jobs:
|
||||
echo "playwright-tests result: $RESULT (changes: $CHANGES)"
|
||||
|
||||
actions-timeline:
|
||||
needs: [cypress-matrix, playwright-tests, cypress-matrix-required, playwright-tests-required]
|
||||
needs: [playwright-tests, playwright-tests-required]
|
||||
if: always()
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
|
||||
19
.github/workflows/superset-helm-lint-test.yml
vendored
19
.github/workflows/superset-helm-lint-test.yml
vendored
@@ -43,38 +43,29 @@ jobs:
|
||||
with:
|
||||
install-superset: "false"
|
||||
|
||||
# Still vendored (not de-vendored like chart-releaser-action below): the
|
||||
# allowlisted helm/chart-testing-action@v2.8.0 depends internally on
|
||||
# astral-sh/setup-uv@v7.0.0, which isn't itself on the ASF Actions
|
||||
# allowlist (only v8.1.0+ are, at apache/infrastructure-actions'
|
||||
# actions.yml). Needs an INFRA request before this can de-vendor too.
|
||||
# chart-testing-action is a submodule (not a plain directory), and the
|
||||
# $/ self-repository syntax resolves action files directly from the
|
||||
# repository without performing a real (submodule-aware) checkout, so
|
||||
# it can't see into a submodule's link. Keep this one on the
|
||||
# workspace-relative ./ form, consistent with every other workflow in
|
||||
# the repo that references a submodule action.
|
||||
- name: Set up chart-testing
|
||||
uses: ./.github/actions/chart-testing-action # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
|
||||
uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0
|
||||
|
||||
- name: Run chart-testing (list-changed)
|
||||
id: list-changed
|
||||
run: |
|
||||
changed=$(ct list-changed --print-config)
|
||||
changed=$(ct list-changed --target-branch ${GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH} --print-config)
|
||||
if [[ -n "$changed" ]]; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
CT_CHART_DIRS: helm
|
||||
CT_SINCE: HEAD
|
||||
GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Run chart-testing (lint)
|
||||
run: ct lint --print-config
|
||||
run: ct lint --target-branch ${GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH} --print-config
|
||||
env:
|
||||
CT_CHART_DIRS: helm
|
||||
CT_LINT_CONF: lintconf.yaml
|
||||
CT_SINCE: HEAD
|
||||
CT_CHART_REPOS: bitnami=https://charts.bitnami.com/bitnami
|
||||
GITHUB_EVENT_REPOSITORY_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Set up helm-unittest
|
||||
run: helm plugin install https://github.com/helm-unittest/helm-unittest --version v1.1.1
|
||||
|
||||
2
.github/workflows/superset-translations.yml
vendored
2
.github/workflows/superset-translations.yml
vendored
@@ -60,6 +60,8 @@ jobs:
|
||||
run: |
|
||||
npm run build-translation
|
||||
|
||||
# This job id is a required status check in .asf.yaml; renaming it breaks
|
||||
# branch protection for every PR until .asf.yaml is updated to match.
|
||||
babel-extract:
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
|
||||
1
.github/workflows/testcontainers.yml
vendored
1
.github/workflows/testcontainers.yml
vendored
@@ -71,6 +71,7 @@ jobs:
|
||||
{"dialect": "trino", "timeout": 10},
|
||||
{"dialect": "mssql", "timeout": 10},
|
||||
{"dialect": "elasticsearch", "timeout": 10},
|
||||
{"dialect": "opensearch", "timeout": 10},
|
||||
{"dialect": "oracle", "timeout": 15},
|
||||
{"dialect": "db2", "timeout": 25},
|
||||
{"dialect": "mariadb", "timeout": 10},
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -21,9 +21,6 @@
|
||||
[submodule ".github/actions/cached-dependencies"]
|
||||
path = .github/actions/cached-dependencies
|
||||
url = https://github.com/apache-superset/cached-dependencies
|
||||
[submodule ".github/actions/chart-testing-action"]
|
||||
path = .github/actions/chart-testing-action
|
||||
url = https://github.com/helm/chart-testing-action
|
||||
[submodule ".github/actions/github-action-push-to-another-repository"]
|
||||
path = .github/actions/github-action-push-to-another-repository
|
||||
url = https://github.com/cpina/github-action-push-to-another-repository
|
||||
|
||||
@@ -105,10 +105,9 @@ repos:
|
||||
files: ^docs/.*\.(js|jsx|ts|tsx)$
|
||||
- id: type-checking-frontend
|
||||
name: Type-Checking (Frontend)
|
||||
entry: ./scripts/check-type.js package=superset-frontend excludeDeclarationDir=cypress-base
|
||||
entry: ./scripts/check-type.js package=superset-frontend
|
||||
language: system
|
||||
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
|
||||
exclude: ^superset-frontend/cypress-base\/
|
||||
require_serial: true
|
||||
# blacklist unsafe functions like make_url (see #19526)
|
||||
- repo: https://github.com/skorokithakis/blacklist-pre-commit-hook
|
||||
|
||||
10
AGENTS.md
10
AGENTS.md
@@ -44,8 +44,7 @@ Common pre-commit failures:
|
||||
### Testing Strategy Migration
|
||||
- **Prefer unit tests** over integration tests
|
||||
- **Prefer integration tests** over end-to-end tests
|
||||
- **Use Playwright for E2E tests** - Migrating from Cypress
|
||||
- **Cypress is deprecated** - Will be removed once migration is completed
|
||||
- **Use Playwright for E2E tests** - Cypress has been removed
|
||||
- **Use Jest + React Testing Library** for component testing
|
||||
- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
|
||||
|
||||
@@ -202,18 +201,13 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
|
||||
npm run test # All tests
|
||||
npm run test -- filename.test.tsx # Single file
|
||||
|
||||
# E2E Tests (Playwright - NEW)
|
||||
# E2E Tests (Playwright)
|
||||
npm run playwright:test # All Playwright tests
|
||||
npm run playwright:ui # Interactive UI mode
|
||||
npm run playwright:headed # See browser during tests
|
||||
npx playwright test tests/auth/login.spec.ts # Single file
|
||||
npm run playwright:debug tests/auth/login.spec.ts # Debug specific file
|
||||
|
||||
# E2E Tests (Cypress - DEPRECATED)
|
||||
cd superset-frontend/cypress-base
|
||||
npm run cypress-run-chrome # All Cypress tests (headless)
|
||||
npm run cypress-debug # Interactive Cypress UI
|
||||
|
||||
# Backend
|
||||
pytest # All tests
|
||||
pytest tests/unit_tests/specific_test.py # Single file
|
||||
|
||||
@@ -195,17 +195,15 @@ RUN mkdir -p \
|
||||
requirements \
|
||||
&& touch superset/static/version_info.json
|
||||
|
||||
# Install Playwright and optionally setup headless browsers
|
||||
# Optionally install Playwright and the headless Chromium used for screenshots
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers
|
||||
|
||||
ARG INCLUDE_CHROMIUM="false"
|
||||
ARG INCLUDE_FIREFOX="false"
|
||||
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
|
||||
if [ "${INCLUDE_CHROMIUM}" = "true" ] || [ "${INCLUDE_FIREFOX}" = "true" ]; then \
|
||||
if [ "${INCLUDE_CHROMIUM}" = "true" ]; then \
|
||||
uv pip install playwright && \
|
||||
playwright install-deps && \
|
||||
if [ "${INCLUDE_CHROMIUM}" = "true" ]; then playwright install chromium; fi && \
|
||||
if [ "${INCLUDE_FIREFOX}" = "true" ]; then playwright install firefox; fi; \
|
||||
playwright install chromium; \
|
||||
else \
|
||||
echo "Skipping browser installation"; \
|
||||
fi
|
||||
|
||||
8
Makefile
8
Makefile
@@ -103,14 +103,6 @@ flask-app:
|
||||
node-app:
|
||||
cd superset-frontend; npm run dev-server
|
||||
|
||||
build-cypress:
|
||||
cd superset-frontend; npm run build-instrumented
|
||||
cd superset-frontend/cypress-base; npm ci
|
||||
|
||||
open-cypress:
|
||||
if ! [ $(port) ]; then cd superset-frontend/cypress-base; CYPRESS_BASE_URL=http://localhost:9000 npm run cypress open; fi
|
||||
cd superset-frontend/cypress-base; CYPRESS_BASE_URL=http://localhost:$(port) npm run cypress open
|
||||
|
||||
report-celery-worker:
|
||||
celery --app=superset.tasks.celery_app:app worker
|
||||
|
||||
|
||||
@@ -40,10 +40,6 @@ under the License.
|
||||
| can read on Database |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
|
||||
| can write on Database |:heavy_check_mark:|O|O|O|O|
|
||||
| can read on Query |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|:heavy_check_mark:|
|
||||
| can this form get on ResetPasswordView |:heavy_check_mark:|O|O|O|O|
|
||||
| can this form post on ResetPasswordView |:heavy_check_mark:|O|O|O|O|
|
||||
| can this form get on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| can this form post on ResetMyPasswordView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| can this form get on UserInfoEditView |:heavy_check_mark:|O|O|O|O|
|
||||
| can this form post on UserInfoEditView |:heavy_check_mark:|O|O|O|O|
|
||||
| can show on UserDBModelView |:heavy_check_mark:|O|O|O|O|
|
||||
@@ -52,8 +48,6 @@ under the License.
|
||||
| can add on UserDBModelView |:heavy_check_mark:|O|O|O|O|
|
||||
| can list on UserDBModelView |:heavy_check_mark:|O|O|O|O|
|
||||
| can userinfo on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| resetmypassword on UserDBModelView |:heavy_check_mark:|:heavy_check_mark:|:heavy_check_mark:|O|O|
|
||||
| resetpasswords on UserDBModelView |:heavy_check_mark:|O|O|O|O|
|
||||
| userinfoedit on UserDBModelView |:heavy_check_mark:|O|O|O|O|
|
||||
| can show on RoleModelView |:heavy_check_mark:|O|O|O|O|
|
||||
| can edit on RoleModelView |:heavy_check_mark:|O|O|O|O|
|
||||
|
||||
187
UPDATING.md
187
UPDATING.md
@@ -24,6 +24,60 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
- Example export (`/export_as_example/`) rejects dashboards whose charts or
|
||||
native-filter targets use semantic views; use the ordinary chart/dashboard
|
||||
bundle export instead.
|
||||
|
||||
- Semantic-view chart and dashboard bundles use typed UUID references and require
|
||||
a destination with support for this format and an already provisioned, accessible
|
||||
view with the same UUID. They do not provision semantic layers/views or export
|
||||
their configuration or credentials. Older readers cannot import these semantic
|
||||
bundles. Older dashboard bundles with semantic filter targets but no typed
|
||||
`datasourceRef` are also rejected; re-export them with this version rather than
|
||||
binding their local IDs to unrelated destination tables. Imports fail explicitly
|
||||
if a semantic dependency is missing, inaccessible, feature-disabled or
|
||||
provider-unregistered. Chart and dashboard exports omit references for missing
|
||||
views so unrelated assets can still be archived; orphaned charts and dashboard
|
||||
targets cannot be imported without repairing their datasources. Existing-view
|
||||
access, feature and provider checks remain enforced, including in full asset
|
||||
exports. Ordinary table bundles retain their existing format. The examples
|
||||
loader rejects semantic bundles;
|
||||
use the chart, dashboard or assets importer instead.
|
||||
|
||||
### Semantic-view Table charts without a temporal axis
|
||||
|
||||
Semantic-view Table charts omit recognized dormant time grains from
|
||||
frontend-generated aggregate queries when no temporal axis is present. The saved
|
||||
grain and Time Grain control visibility are unchanged. Direct API payloads and
|
||||
saved chart-data GET requests that bypass frontend rebuilding retain strict
|
||||
validation; some old stored query contexts can therefore still fail. Deploy
|
||||
updated frontend assets with this change.
|
||||
|
||||
### SQLite time filters on `DATE` columns
|
||||
|
||||
On SQLite, Shillelagh and the Superset meta database, a time filter on a `DATE`
|
||||
column writes its bounds as a date, such as `'2026-09-20'`, and no longer uses
|
||||
the column's **Datetime format** (`python_date_format`) or the database's
|
||||
`python_date_format_by_column_name`. This fixes ranges that started and ended
|
||||
one day late on `DATE` columns holding `YYYY-MM-DD` text. A `DATE` column that
|
||||
holds values in another format, such as `20260920`, `09/20/2026` or epoch
|
||||
seconds, now matches no rows, even with a Datetime format set. Columns declared
|
||||
as `INTEGER` are not affected. On Shillelagh and the meta database, a bound
|
||||
with a time of day is cut to its date.
|
||||
|
||||
### Apache Doris connection form and `DBS_AVAILABLE_DENYLIST`
|
||||
|
||||
`DBS_AVAILABLE_DENYLIST` is matched against an engine spec's `default_driver`.
|
||||
The Doris engine spec's `default_driver` is `mysqldb` (the driver pydoris
|
||||
registers), so a deployment that hides Doris with `{"pydoris": {"pydoris"}}`
|
||||
sees it listed again; change the entry to `{"pydoris": {"mysqldb"}}`.
|
||||
|
||||
The Doris connection form's SSL switch sets `ssl_mode=VERIFY_CA`. With MariaDB
|
||||
Connector/C this also verifies the server hostname, so the switch needs
|
||||
`ssl_ca=<path>` in Additional parameters and a Doris FE certificate matching the
|
||||
host. A Doris FE on its default self-signed certificate fails to connect once
|
||||
the switch is on.
|
||||
|
||||
### Empty MCP chart previews
|
||||
|
||||
Saved Bubble and Histogram Vega-Lite previews with zero rows return an empty
|
||||
@@ -67,6 +121,32 @@ format. Use a consistent timezone and precision for stored strings and bounds.
|
||||
able to provision a user regardless of `AUTH_USER_REGISTRATION`; it now
|
||||
requires the same gate as `/register/`.
|
||||
|
||||
### SQL execution request limits are safety caps
|
||||
|
||||
The MCP `execute_sql` request's `limit` no longer overrides a smaller outer SQL
|
||||
LIMIT. It caps the last statement at the smaller of the SQL LIMIT and the request
|
||||
limit, subject to `SQL_MAX_ROW`. For example, SQL `LIMIT 5` with request `limit: 10`
|
||||
keeps `LIMIT 5`. To return more rows, increase or remove the SQL LIMIT explicitly.
|
||||
Omitting the request limit still leaves SQL limits unchanged. Inner query and CTE
|
||||
limits are not changed.
|
||||
|
||||
This contract change also applies to the public `Database.execute()` and
|
||||
`Database.execute_async()` APIs through `QueryOptions.limit`, including dry runs.
|
||||
|
||||
SQL Lab also recognizes literal `FETCH FIRST` counts and parenthesized literal
|
||||
limits. SQL Server `TOP ... PERCENT` and `TOP ... WITH TIES` are no longer
|
||||
interpreted as fixed row counts: SQL Lab uses the row-limit dropdown (subject to
|
||||
server limits) and rewrites these clauses to a fixed `TOP` cap. For example,
|
||||
`TOP 5 PERCENT` with a 1000-row dropdown can return up to 1000 rows rather than
|
||||
the previous five.
|
||||
|
||||
`BigQueryEngineSpec.fetch_data` no longer falls back to a plain `fetchall()` once
|
||||
it has read its initial sample, since a forward-only cursor cannot replay those
|
||||
rows. A BigQuery driver error after the sample now fails the query instead of
|
||||
returning the remaining rows as a success. This applies to every caller,
|
||||
including legacy SQL Lab execution and dataset column discovery. An error on the
|
||||
initial read still falls back as before.
|
||||
|
||||
### Doris SSL requests require TLS
|
||||
|
||||
The Doris SSL toggle uses `ssl_mode=VERIFY_CA`. Saved `ssl_mode=REQUIRED` and
|
||||
@@ -126,6 +206,26 @@ get the same handling. With MariaDB Connector/Python
|
||||
ssl_verify_cert=True. Other MySQL-compatible engines such as OceanBase and
|
||||
StarRocks keep their existing SSL handling.
|
||||
|
||||
### SQL Lab decimal results use exact strings
|
||||
|
||||
SQL Lab represents database `DECIMAL`/`NUMERIC` values as JSON strings instead
|
||||
of JSON numbers, preserving precision and trailing zeros in the results grid
|
||||
and exports. The strings use fixed-point notation, and non-finite `NaN` and
|
||||
`Infinity` decimals are returned as `null`. Numeric sorting still compares
|
||||
their exact values. API consumers performing arithmetic should parse these
|
||||
strings with a decimal library, not JavaScript `Number`. This includes SQL Lab
|
||||
extensions: the `data` rows passed to `sqlLab.onDidQuerySuccess` listeners
|
||||
carry strings for `numeric` columns (for example PostgreSQL `SUM(bigint)`,
|
||||
`AVG` or `ROUND` results), so adding them with `+` concatenates instead of
|
||||
summing. The `sqleditor.extension.resultTable` override, which replaces
|
||||
`FilterableTable` in both SQL Lab results and table previews, also receives
|
||||
string rows but not the built-in exact-decimal comparator; extensions must
|
||||
implement their own decimal sorting. Chart and dataset queries also use fixed-point strings for Decimal
|
||||
columns that fall back to string conversion (for example, Decimals mixed with
|
||||
floats), and non-finite Decimals in those columns become null. Other chart
|
||||
result serialization and chart number formatting are unchanged. Re-run queries
|
||||
whose cached JSON results were produced before upgrading all workers.
|
||||
|
||||
### Version history retention setting
|
||||
|
||||
Use `VERSION_HISTORY_RETENTION_DAYS` for both the application setting and
|
||||
@@ -308,6 +408,15 @@ upgrading. See the two migrations' docstrings (`superset/migrations/versions/
|
||||
`..._00-01_3ce9a4572f8a_rename_deprecated_permissions_33272.py`) for the full
|
||||
per-permission mapping and reasoning.
|
||||
|
||||
- Snowflake stage file-management statements (`PUT`, `GET`, `REMOVE` and its
|
||||
`RM` alias) are rejected in user-submitted SQL (SQL Lab, the cost-estimate
|
||||
path and alert queries), regardless of the database's `allow_dml` setting.
|
||||
`PUT`/`GET` perform file I/O on the host running the
|
||||
query, and `REMOVE`/`RM` delete files within a stage; none of them read or
|
||||
write table data, so `allow_dml` does not govern them. Deployments that ran
|
||||
these through SQL Lab should manage stage files with Snowflake's own clients
|
||||
instead. `LIST`/`LS`, which only enumerate staged files, are unaffected.
|
||||
|
||||
### MySQL metadata database now actually defaults to READ COMMITTED
|
||||
|
||||
Superset has always *intended* to default the metadata-database isolation
|
||||
@@ -338,6 +447,36 @@ This fixed embedding cap is independent of the operator's
|
||||
`MCP_RESPONSE_SIZE_CONFIG['max_bytes']` (50,000 by default); it does not guarantee
|
||||
that every payload fits a configured response limit.
|
||||
|
||||
### Legacy FAB password reset pages are removed
|
||||
|
||||
The Flask-AppBuilder server-rendered password reset pages at
|
||||
`/resetpassword/form` (admin reset of another account) and
|
||||
`/resetmypassword/form` (self-service reset) are no longer registered; both
|
||||
routes answer 404, and the "Reset Password" and "Reset my password" buttons on
|
||||
the legacy FAB user pages that led to them are gone. `superset init` no longer
|
||||
assigns their permissions (`can this form get/post on ResetPasswordView` and
|
||||
`ResetMyPasswordView`, plus the `resetpasswords` and `resetmypassword` actions on
|
||||
`UserDBModelView`) to any role. Every flow they served lives in the SPA:
|
||||
administrators reset a user's password from the "New password" fields in the
|
||||
Users list edit modal (`PUT /api/v1/security/users/<id>`), users change their
|
||||
own from the "Reset my password" modal on their profile page (`PUT
|
||||
/api/v1/me/`, which requires `current_password` when the account already has one), and a pending forced password
|
||||
change (`ENABLE_FORCE_PASSWORD_CHANGE`) now redirects to that profile page
|
||||
instead of the removed form. Deployments that link to either legacy route should
|
||||
point at `/user_info/` or the Users list instead.
|
||||
|
||||
### Unused `INCLUDE_FIREFOX` build arg and dead config removed
|
||||
|
||||
Screenshots use only Playwright with Chromium, so the `INCLUDE_FIREFOX` Docker build
|
||||
arg, which installed a Firefox browser that nothing used, has been removed from the
|
||||
`Dockerfile` and the `docker-compose` files. Builds that still pass `INCLUDE_FIREFOX`
|
||||
keep working, though Docker may warn that the build argument is unused. Use
|
||||
`INCLUDE_CHROMIUM=true` to add the browser that screenshots need.
|
||||
|
||||
The `EMAIL_PAGE_RENDER_WAIT` config key and the `ENABLE_PLAYWRIGHT` variable in
|
||||
`docker/.env` are also removed. Nothing in Superset read either of them, so setting
|
||||
them had no effect and they can be deleted from custom configs.
|
||||
|
||||
### Default Docker image is now batteries-included; the minimal image moves to `-lean`
|
||||
|
||||
The default `apache/superset` Docker image (the plain tags: `latest`, `master`,
|
||||
@@ -804,6 +943,54 @@ substitution is deliberately not blanked so the broken link is visible rather th
|
||||
silently truncated. Use `{datasource_id}` (still supported) to identify the dataset
|
||||
to your access-request system, and resolve the name there.
|
||||
|
||||
### Semantic member selection versions
|
||||
|
||||
Semantic providers can declare `selection_identity_version` when changing their
|
||||
member identity encoding. Cube's stable-member-ID adapter requires
|
||||
`cube-member-id-v1`. Deploy the compatible core library, host, frontend and Cube
|
||||
adapter together; do not roll back to title-keyed code after saving ID-based charts.
|
||||
|
||||
Saved Cube selections without this version are rejected, even when a saved title
|
||||
happens to equal a member ID. No historical title mapping is recovered. Open each
|
||||
chart in Explore, choose **Start field selection**, reselect its metrics,
|
||||
dimensions, filters and sorting, then save. Reset also clears field-dependent
|
||||
formatting; it does not overwrite the saved chart until Save is chosen. Native
|
||||
filters and chart customizations have a separate reset in their configuration
|
||||
form that clears fields, pre-filters, sorting, defaults and dependencies.
|
||||
|
||||
Dashboard filters retain their own saved generation. Old filter state and
|
||||
unversioned external member overrides cannot borrow a chart's version. Recreate
|
||||
old dashboard permalink/filter state after reselection. Cross-view overlays are
|
||||
rejected unless their source identity matches the receiving semantic view;
|
||||
matching display titles are not proof of matching members.
|
||||
|
||||
Dynamic group-by display controls are unsupported on versioned semantic views,
|
||||
including Cube. Remove those charts from the control's scope; resetting or
|
||||
reselecting the group-by fields cannot repair this control's missing selection
|
||||
identity evidence. Static chart group-by selections remain available after the
|
||||
chart's explicit reselection. Supporting dynamic group-by requires preserving the
|
||||
version in target rebuilds and recording the source identity in the emitted mask;
|
||||
that follow-up is not included here.
|
||||
|
||||
Matrixify All mode with A to Z or Z to A sorting cannot populate or refresh values
|
||||
for versioned semantic views. Saved values are preserved; for a new selection,
|
||||
switch to Members and enter values manually. The control explains this limitation
|
||||
when suggestions are unavailable.
|
||||
|
||||
The marker is a compatibility contract, not an authorization credential. API
|
||||
clients must rebuild their selections from current member IDs before supplying
|
||||
`extras.semantic_selection_version` on each query. Providers without a required
|
||||
version retain their existing behavior. No metadata-database migration or
|
||||
automatic rewrite of saved charts is performed.
|
||||
|
||||
Programmatic clients must explicitly select current member IDs. Chart-data API
|
||||
queries send `extras.semantic_selection_version`; name-based datasource queries
|
||||
and MCP `get_table` requests send top-level `semantic_selection_version`.
|
||||
Discover the value from datasource metadata (or each external metric returned by
|
||||
MCP `list_metrics`). The server never adds a missing marker from metadata.
|
||||
Explore query controls remain unavailable until explicit field initialization,
|
||||
so legacy filter subjects cannot trigger value suggestions before reselection.
|
||||
|
||||
### MCP tool results preserve stored string values
|
||||
|
||||
Structured MCP tool results no longer add `<UNTRUSTED-CONTENT>` wrappers or
|
||||
|
||||
@@ -75,7 +75,6 @@ x-common-build: &common-build
|
||||
args:
|
||||
DEV_MODE: "true"
|
||||
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}
|
||||
INCLUDE_FIREFOX: ${INCLUDE_FIREFOX:-false}
|
||||
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
|
||||
|
||||
services:
|
||||
|
||||
@@ -43,7 +43,6 @@ x-common-build: &common-build
|
||||
args:
|
||||
DEV_MODE: "true"
|
||||
INCLUDE_CHROMIUM: ${INCLUDE_CHROMIUM:-false}
|
||||
INCLUDE_FIREFOX: ${INCLUDE_FIREFOX:-false}
|
||||
BUILD_TRANSLATIONS: ${BUILD_TRANSLATIONS:-false}
|
||||
|
||||
services:
|
||||
@@ -125,8 +124,6 @@ services:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${SUPERSET_PORT:-8088}:8088
|
||||
# When in cypress-mode ->
|
||||
- ${CYPRESS_PORT:-8081}:8081
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
user: *superset-user
|
||||
|
||||
@@ -26,7 +26,6 @@ DEV_MODE=true
|
||||
# SUPERSET_PORT=8088
|
||||
# NODE_PORT=9000
|
||||
# WEBSOCKET_PORT=8080
|
||||
# CYPRESS_PORT=8081
|
||||
# DATABASE_PORT=5432
|
||||
# REDIS_PORT=6379
|
||||
|
||||
@@ -79,12 +78,10 @@ SUPERSET_LOAD_EXAMPLES=yes
|
||||
# examples anyway, e.g. after changing the example datasets or after a partial
|
||||
# load.
|
||||
#SUPERSET_FORCE_LOAD_EXAMPLES=no
|
||||
CYPRESS_CONFIG=false
|
||||
SUPERSET_PORT=8088
|
||||
MAPBOX_API_KEY=''
|
||||
|
||||
# Make sure you set this to a unique secure random value on production
|
||||
SUPERSET_SECRET_KEY=TEST_NON_DEV_SECRET
|
||||
ENABLE_PLAYWRIGHT=false
|
||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||
BUILD_SUPERSET_FRONTEND_IN_DOCKER=true
|
||||
|
||||
@@ -31,7 +31,6 @@ NGINX_PORT=81
|
||||
SUPERSET_PORT=8089
|
||||
NODE_PORT=9001
|
||||
WEBSOCKET_PORT=8082
|
||||
CYPRESS_PORT=8083
|
||||
DATABASE_PORT=5433
|
||||
REDIS_PORT=6380
|
||||
|
||||
|
||||
@@ -34,13 +34,6 @@ if [ "$DEV_MODE" == "true" ]; then
|
||||
fi
|
||||
REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt"
|
||||
PORT=${PORT:-8088}
|
||||
# If Cypress run – overwrite the password for admin and export env variables
|
||||
if [ "$CYPRESS_CONFIG" == "true" ]; then
|
||||
export SUPERSET_TESTENV=true
|
||||
export POSTGRES_DB=superset_cypress
|
||||
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
|
||||
PORT=8081
|
||||
fi
|
||||
# Skip postgres requirements installation for workers to avoid conflicts
|
||||
if [[ "$DATABASE_DIALECT" == postgres* ]] && [ "$(whoami)" = "root" ] && [ "$1" != "worker" ] && [ "$1" != "beat" ]; then
|
||||
# older images may not have the postgres dev requirements installed
|
||||
|
||||
@@ -36,13 +36,6 @@ Init Step ${1}/${STEP_CNT} [${2}] -- ${3}
|
||||
EOF
|
||||
}
|
||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}"
|
||||
# If Cypress run – overwrite the password for admin and export env variables
|
||||
if [ "$CYPRESS_CONFIG" == "true" ]; then
|
||||
ADMIN_PASSWORD="general"
|
||||
export SUPERSET_TESTENV=true
|
||||
export POSTGRES_DB=superset_cypress
|
||||
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
|
||||
fi
|
||||
# Initialize the database
|
||||
echo_step "1" "Starting" "Applying DB migrations"
|
||||
superset db upgrade
|
||||
@@ -50,16 +43,12 @@ echo_step "1" "Complete" "Applying DB migrations"
|
||||
|
||||
# Create an admin user
|
||||
echo_step "2" "Starting" "Setting up admin user ( admin / $ADMIN_PASSWORD )"
|
||||
if [ "$CYPRESS_CONFIG" == "true" ]; then
|
||||
superset load_test_users
|
||||
else
|
||||
superset fab create-admin \
|
||||
--username admin \
|
||||
--email admin@superset.com \
|
||||
--password "$ADMIN_PASSWORD" \
|
||||
--firstname Superset \
|
||||
--lastname Admin
|
||||
fi
|
||||
superset fab create-admin \
|
||||
--username admin \
|
||||
--email admin@superset.com \
|
||||
--password "$ADMIN_PASSWORD" \
|
||||
--firstname Superset \
|
||||
--lastname Admin
|
||||
echo_step "2" "Complete" "Setting up admin user"
|
||||
# Create default roles and permissions
|
||||
echo_step "3" "Starting" "Setting up roles and perms"
|
||||
@@ -97,12 +86,8 @@ PY
|
||||
if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then
|
||||
echo_step "4" "Starting" "Loading examples"
|
||||
|
||||
# Cypress runs always load, since they need a distinct set of test data
|
||||
# (`--load-test-data`) in a separate database. Set
|
||||
# SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
|
||||
if [ "$CYPRESS_CONFIG" == "true" ]; then
|
||||
superset load_examples --load-test-data
|
||||
elif [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
|
||||
# Set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
|
||||
if [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
|
||||
echo "Examples already loaded, skipping (set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload them)"
|
||||
else
|
||||
superset load_examples
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from celery.schedules import crontab
|
||||
from flask_caching.backends.filesystemcache import FileSystemCache
|
||||
@@ -139,18 +138,6 @@ SQLLAB_CTAS_NO_LIMIT = True
|
||||
log_level_text = os.getenv("SUPERSET_LOG_LEVEL", "INFO")
|
||||
LOG_LEVEL = getattr(logging, log_level_text.upper(), logging.INFO)
|
||||
|
||||
if os.getenv("CYPRESS_CONFIG") == "true":
|
||||
# When running the service as a cypress backend, we need to import the config
|
||||
# located @ tests/integration_tests/superset_test_config.py
|
||||
base_dir = os.path.dirname(__file__)
|
||||
module_folder = os.path.abspath(
|
||||
os.path.join(base_dir, "../../tests/integration_tests/")
|
||||
)
|
||||
sys.path.insert(0, module_folder)
|
||||
from superset_test_config import * # noqa
|
||||
|
||||
sys.path.pop(0)
|
||||
|
||||
#
|
||||
# Optionally import superset_config_docker.py (which will have been included on
|
||||
# the PYTHONPATH) in order to allow for local settings to be overridden
|
||||
|
||||
@@ -50,13 +50,11 @@ Screenshots will be taken but no messages actually sent as long as `ALERT_REPORT
|
||||
|
||||
#### In your `Dockerfile`
|
||||
|
||||
You'll need to extend the Superset image to include a headless browser. Your options include:
|
||||
As of 7.0.0, Superset takes screenshots only with [Playwright](https://playwright.dev/python/) driving a headless Chromium browser. Selenium, Firefox, geckodriver and the `WEBDRIVER_TYPE` setting are not supported, and no feature flag is needed to use Playwright. Prior to 7.0.0, users were responsible for installing their own headless browser, and could use Selenium if desired.
|
||||
|
||||
- Use Playwright with Chromium: this is the recommended approach as of version 4.1.x or greater. Playwright always uses Chromium — the `WEBDRIVER_TYPE` config setting has no effect when Playwright is active. A working example of a Dockerfile that installs these tools is provided under "Building your own production Docker image" on the [Docker Builds](/admin-docs/installation/docker-builds#building-your-own-production-docker-image) page. Enable the `PLAYWRIGHT_REPORTS_AND_THUMBNAILS` feature flag in your config to activate it.
|
||||
- Use Firefox (Selenium): you'll need to install geckodriver and Firefox. Set `WEBDRIVER_TYPE` to `"firefox"` in your `superset_config.py`.
|
||||
- Use Chrome (Selenium): you'll need to install Chrome. Set `WEBDRIVER_TYPE` to `"chrome"` in your `superset_config.py`.
|
||||
|
||||
In Superset versions <=4.0x, users installed Firefox or Chrome and that was documented here.
|
||||
- The default Superset image (for example the `latest` and `<version>` tags) includes Playwright and Chromium.
|
||||
- The `-lean` and `-dev` images do not. To add them to an image based on `-lean`, see the example Dockerfile under "Building your own production Docker image" on the [Docker Builds](/admin-docs/installation/docker-builds#building-your-own-production-docker-image) page.
|
||||
- To build the `dev` target from source with Chromium, pass `--build-arg INCLUDE_CHROMIUM=true`. With Docker Compose, run `INCLUDE_CHROMIUM=true docker compose build`.
|
||||
|
||||
Only the worker container needs the browser.
|
||||
|
||||
@@ -388,20 +386,13 @@ SMTP_PASSWORD = "your_password" # use the empty string "" if using an unauthenti
|
||||
SMTP_MAIL_FROM = "noreply@youremail.com"
|
||||
EMAIL_REPORTS_SUBJECT_PREFIX = "[Superset] " # optional - overwrites default value in config.py of "[Report] "
|
||||
|
||||
# WebDriver configuration
|
||||
# If you use Firefox or Playwright with Chrome, you can stick with default values
|
||||
# If you use Chrome and are *not* using Playwright, then add the following WEBDRIVER_TYPE and WEBDRIVER_OPTION_ARGS
|
||||
WEBDRIVER_TYPE = "chrome"
|
||||
WEBDRIVER_OPTION_ARGS = [
|
||||
"--force-device-scale-factor=2.0",
|
||||
"--high-dpi-support=2.0",
|
||||
"--headless",
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-extensions",
|
||||
]
|
||||
# Screenshot browser configuration
|
||||
# Playwright always launches Chromium, so there is no browser type to set, and the
|
||||
# defaults work for most deployments. The values below are the defaults. To render
|
||||
# sharper screenshots, raise pixel_density in WEBDRIVER_WINDOW. Extra Chromium
|
||||
# launch arguments can be passed with WEBDRIVER_OPTION_ARGS.
|
||||
# WEBDRIVER_WINDOW = {"dashboard": (1600, 2000), "slice": (3000, 1200), "pixel_density": 1}
|
||||
# WEBDRIVER_OPTION_ARGS = []
|
||||
|
||||
# This is for internal use, you can keep http
|
||||
WEBDRIVER_BASEURL = "http://superset:8088" # This is also the default for Docker Compose, where the app service is named "superset"
|
||||
@@ -429,10 +420,11 @@ Please refer to `ExecutorType` in the codebase for other executor types.
|
||||
|
||||
**Important notes**
|
||||
|
||||
- Be mindful of the concurrency setting for celery (using `-c 4`). Selenium/webdriver instances can
|
||||
consume a lot of CPU / memory on your servers.
|
||||
- In some cases, if you notice a lot of leaked geckodriver processes, try running your celery
|
||||
processes with `celery worker --pool=prefork --max-tasks-per-child=128 ...`
|
||||
- Be mindful of the concurrency setting for celery (using `-c 4`). Each worker process keeps its own
|
||||
headless Chromium instance, and these can consume a lot of CPU / memory on your servers.
|
||||
- If worker memory grows over time, try running your celery processes with
|
||||
`celery worker --pool=prefork --max-tasks-per-child=128 ...` so that worker processes, and the
|
||||
Chromium instances they own, are recycled periodically.
|
||||
- It is recommended to run separate workers for the `sql_lab` and `email_reports` tasks. This can be
|
||||
done using the `queue` field in `task_annotations`.
|
||||
- Adjust `WEBDRIVER_BASEURL` in your configuration file if celery workers can’t access Superset via
|
||||
@@ -610,11 +602,22 @@ omitted. Cookies, authentication headers, URLs, SQL, and query payloads are not
|
||||
included in these transport diagnostics. HTTP 400 therefore remains a failure to
|
||||
investigate, not a reason to repeat the same request.
|
||||
|
||||
### Check web browser and webdriver installation
|
||||
### Check the Playwright and Chromium installation
|
||||
|
||||
To take a screenshot, the worker visits the dashboard or chart using a headless browser, then takes a screenshot. If you are able to send a chart as CSV, XLSX, or text but can't send as PNG, your problem may lie with the browser.
|
||||
To take a screenshot, the worker visits the dashboard or chart using a headless Chromium browser controlled by Playwright, then takes a screenshot. If you are able to send a chart as CSV, XLSX, or text but can't send as PNG, your problem may lie with the browser. If the Playwright package isn't installed, screenshot attempts fail with an error that includes `Playwright is required for screenshots`. If Playwright is installed but Chromium is missing or fails to launch, the error includes `Failed to launch the headless browser for screenshots`, followed by the underlying launch error.
|
||||
|
||||
If you are handling the installation of the headless browser on your own, do your own verification to ensure that the headless browser opens successfully in the worker environment.
|
||||
If you are handling the installation of the headless browser on your own, confirm that Chromium launches in the worker environment. Start Python in your worker environment and run:
|
||||
|
||||
```python
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
print(browser.version)
|
||||
browser.close()
|
||||
```
|
||||
|
||||
This should print the Chromium version.
|
||||
|
||||
### Send a test email
|
||||
|
||||
|
||||
@@ -241,7 +241,9 @@ FEATURE_FLAGS = {
|
||||
}
|
||||
```
|
||||
|
||||
By default thumbnails are rendered per user, and will fall back to the Selenium user for anonymous users.
|
||||
By default thumbnails are rendered as the user who requests them. Anonymous requests have no user
|
||||
to render as, so they get no thumbnail unless you add a fallback executor, for example
|
||||
`THUMBNAIL_EXECUTORS = [ExecutorType.CURRENT_USER, FixedExecutor("admin")]`.
|
||||
To always render thumbnails as a fixed user (`admin` in this example), use the following configuration:
|
||||
|
||||
```python
|
||||
@@ -286,7 +288,7 @@ THUMBNAIL_CACHE_CONFIG = init_thumbnail_cache
|
||||
```
|
||||
|
||||
Using the above example cache keys for dashboards will be `superset_thumb__dashboard__{ID}`. You can
|
||||
override the base URL for Selenium using:
|
||||
override the base URL that the headless browser uses to reach Superset using:
|
||||
|
||||
```
|
||||
WEBDRIVER_BASEURL = "https://superset.company.com"
|
||||
@@ -311,21 +313,40 @@ CACHE_WARMUP_EXECUTORS = [FixedExecutor("admin")]
|
||||
Use a dedicated read-only service account here rather than a personal admin account, so that
|
||||
thumbnail rendering and cache warmup tasks don't fail if a specific user's credentials change.
|
||||
|
||||
Additional Selenium WebDriver configuration can be set using `WEBDRIVER_CONFIGURATION`. You can
|
||||
implement a custom function to authenticate Selenium. The default function uses the `flask-login`
|
||||
session cookie. Here's an example of a custom function signature:
|
||||
Thumbnails are rendered with Playwright and a headless Chromium browser. Extra Chromium launch
|
||||
arguments can be set using `WEBDRIVER_OPTION_ARGS`. You can implement a custom function to
|
||||
authenticate the Playwright browser context. The default function uses the `flask-login` session
|
||||
cookie. A custom function replaces that default, so it must authenticate as `user` itself.
|
||||
Here's an example of a custom function:
|
||||
|
||||
```python
|
||||
def auth_driver(driver: WebDriver, user: "User") -> WebDriver:
|
||||
pass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from flask_appbuilder.security.sqla.models import User
|
||||
from playwright.sync_api import BrowserContext
|
||||
|
||||
|
||||
def auth_browser_context(
|
||||
browser_context: "BrowserContext", user: "User"
|
||||
) -> "BrowserContext":
|
||||
# This replaces Superset's default session-cookie login, so it must
|
||||
# authenticate as `user` itself. For example, send a bearer token:
|
||||
# `get_token_for_user` is a placeholder for your own token lookup.
|
||||
token = get_token_for_user(user)
|
||||
browser_context.set_extra_http_headers({"Authorization": f"Bearer {token}"})
|
||||
return browser_context
|
||||
```
|
||||
|
||||
Then on configuration:
|
||||
|
||||
```
|
||||
WEBDRIVER_AUTH_FUNC = auth_driver
|
||||
WEBDRIVER_AUTH_FUNC = auth_browser_context
|
||||
```
|
||||
|
||||
To replace the authentication logic entirely, subclass `superset.utils.machine_auth.MachineAuthProvider`,
|
||||
override `authenticate_browser_context()`, and set `MACHINE_AUTH_PROVIDER_CLASS` to the fully-qualified dotted path of your class (for example, `"myapp.auth.MyMachineAuthProvider"`).
|
||||
|
||||
## ETag Support for Thumbnails
|
||||
|
||||
Thumbnail and screenshot endpoints return `ETag` response headers based on the cached content digest. Clients can use conditional requests to avoid downloading unchanged images:
|
||||
|
||||
@@ -352,7 +352,7 @@ Routes on the health blueprint, including `/health`, `/healthcheck`, `/ping`, an
|
||||
`/version`, are exempt from forced password changes. Even with
|
||||
`ENABLE_FORCE_PASSWORD_CHANGE = True`, a probe carrying a session cookie for a user
|
||||
with a pending password change receives the route's normal response rather than a
|
||||
redirect to the password-reset form. This exemption does not bypass other security
|
||||
redirect to the profile page's password change. This exemption does not bypass other security
|
||||
checks or change which version details the user may see.
|
||||
|
||||
If the load balancer is inserting `X-Forwarded-For/X-Forwarded-Proto` headers, you should set
|
||||
|
||||
@@ -174,9 +174,9 @@ scheduled table in the warehouse so each chart query is a cheap lookup.
|
||||
- See [Feature Flags](./feature-flags.mdx) for the full list of supported
|
||||
flags and their lifecycle stages.
|
||||
- Server-side screenshot jobs (alerts, scheduled reports, thumbnails)
|
||||
render the dashboard in a headless, webdriver-controlled browser, which
|
||||
render the dashboard in a headless Chromium browser driven by Playwright, which
|
||||
intentionally bypasses row virtualization so the rendered artifact
|
||||
includes every chart, not just the ones above the fold. User-triggered
|
||||
includes every chart on the active tab, not just the ones above the fold. User-triggered
|
||||
"download as image/PDF" is different: it captures whatever's currently
|
||||
rendered in the user's own browser, so it's still subject to
|
||||
virtualization like any other page view. Metadata/YAML dashboard export
|
||||
|
||||
49
docs/admin_docs/configuration/google-sheets.mdx
Normal file
49
docs/admin_docs/configuration/google-sheets.mdx
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Google Sheets credentials
|
||||
---
|
||||
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
|
||||
## Service-account connections
|
||||
|
||||
When configuring Google Sheets with service-account JSON in the database dialog,
|
||||
share private spreadsheets with the service account's `client_email`.
|
||||
Both catalog validation and queries use the service account, not the Google
|
||||
identity of the administrator adding the spreadsheet. This applies when creating
|
||||
a connection and when editing its catalog.
|
||||
|
||||
The dialog's stored `impersonate_user` flag does not enable domain-wide delegation
|
||||
for this service-account credential path. A spreadsheet shared only with the
|
||||
administrator cannot be validated on that basis, even if the service account has
|
||||
domain-wide delegation enabled. Creating an empty connection and editing it later
|
||||
does not change the identity used to access the spreadsheet.
|
||||
|
||||
## Upload values
|
||||
|
||||
Date and datetime upload values are sent as ISO strings rather than numeric
|
||||
nanosecond counts. Timezone-aware datetimes are converted to UTC, then uploaded
|
||||
without a timezone suffix so Google Sheets can parse them as datetime cells
|
||||
with `USER_ENTERED` input. For example, `2024-03-01 00:30:00+02:00` is uploaded as
|
||||
`2024-02-29 22:30:00`. The original timezone is not retained; interpret these cell
|
||||
values as UTC. Naive datetimes keep their original clock time.
|
||||
|
||||
Nullable integer columns retain integer values in the upload payload, and missing
|
||||
values become empty cells. Time-only values use `HH:MM:SS[.ffffff]` strings.
|
||||
Durations are sent as signed `H:MM:SS[.ffffff]` strings, with hours including days.
|
||||
@@ -112,6 +112,38 @@ When importing a dashboard ZIP with the **overwrite** option enabled, any existi
|
||||
|
||||
If you import without the overwrite flag, existing charts with conflicting UUIDs are left unchanged and the import skips those objects. Use overwrite when you want to push a fully updated dashboard (including chart definitions) from a development or staging environment to production.
|
||||
|
||||
## Charts and dashboards using semantic views
|
||||
|
||||
Semantic charts and dashboard filter/display-control targets can be restored against
|
||||
an **existing semantic view with the same UUID**. The destination must enable
|
||||
`SEMANTIC_LAYERS`, register the view's provider, and grant the importing user access
|
||||
to the view or its parent semantic layer, in addition to the usual asset import
|
||||
permissions. Missing, inaccessible or disabled dependencies fail the import rather
|
||||
than producing a partially imported bundle.
|
||||
|
||||
Chart bundles identify these sources with `datasource_ref: {type: semantic_view,
|
||||
uuid: <view-uuid>}`. Dashboard targets use `datasourceRef` with the same shape.
|
||||
Import resolves the UUID and rewrites local datasource IDs; it never substitutes a
|
||||
table with the same numeric ID. Ordinary table bundles retain `dataset_uuid`.
|
||||
Older Superset readers that do not support semantic references cannot import these
|
||||
semantic bundles.
|
||||
|
||||
Older dashboard bundles whose semantic filter targets lack `datasourceRef` are
|
||||
rejected rather than binding their local IDs to unrelated tables. Re-export those
|
||||
dashboards with a version that supports typed references. If a chart or dashboard
|
||||
filter target references a deleted semantic view, export omits that reference so
|
||||
other assets in the archive can still be exported. Repair the orphaned datasource
|
||||
before exporting it for import elsewhere. Checks for inaccessible, disabled, and
|
||||
unregistered existing semantic dependencies remain enforced, including in full
|
||||
asset exports. Disabling `SEMANTIC_LAYERS` or removing a provider registration
|
||||
therefore still blocks exports containing references to existing semantic views.
|
||||
|
||||
This is **reference restoration, not semantic-layer provisioning**. Bundles do not
|
||||
include semantic-layer/view configuration or credentials, and importing does not
|
||||
create provider connections or views, or contact providers. Provision the matching
|
||||
view and access grants separately before importing. Full cross-workspace semantic
|
||||
provisioning is outside this workflow.
|
||||
|
||||
## UUIDs in API Responses
|
||||
|
||||
The REST API POST endpoints for **datasets**, **charts**, and **dashboards** include the auto-generated `uuid` field in the response body:
|
||||
|
||||
@@ -41,10 +41,139 @@ flowchart LR
|
||||
C --> D[("Database<br/>(Postgres)")]
|
||||
```
|
||||
|
||||
## Tool deadlines and concurrency
|
||||
|
||||
Protected synchronous and asynchronous MCP tools run in isolated thread workers,
|
||||
each with its own Flask app
|
||||
context and metadata database session. Warehouse I/O does not run on the
|
||||
transport event loop. This includes SQL execution, chart data and previews,
|
||||
chart compilation, dataset queries, and tools that discover warehouse metadata.
|
||||
|
||||
`MCP_TOOL_WORKERS` sets the maximum admitted warehouse-capable calls per
|
||||
application/process. A full pool immediately returns an **MCP server busy**
|
||||
error; it does not queue requests behind slow queries.
|
||||
|
||||
Tools that only read Superset's metadata database (the `list_*` and
|
||||
`get_*_info` tools, `get_schema`, `get_instance_info`, `health_check`, and
|
||||
similar) are admitted under a separate bound, `MCP_METADATA_TOOL_WORKERS`, so
|
||||
they keep answering while slow warehouse queries fill `MCP_TOOL_WORKERS`. A
|
||||
metadata-only tool that does reach a warehouse must first take a
|
||||
`MCP_TOOL_WORKERS` slot, and reports **MCP server busy** if none is free.
|
||||
Setting `MCP_METADATA_TOOL_WORKERS = 0` admits every tool under
|
||||
`MCP_TOOL_WORKERS`.
|
||||
|
||||
Both bounds come out of one budget: the metadata pool's capacity,
|
||||
`pool_size + max_overflow`. A warehouse-capable call can hold a metadata
|
||||
connection for the whole of its warehouse query and cancelling it needs
|
||||
another; a metadata-only call holds at most one; and the server's own
|
||||
metadata I/O (tool-list filtering, audit writes, error hooks) runs on one
|
||||
bounded thread, and API-key lookups on another of their own, so requests
|
||||
with unknown keys cannot delay audit writes or RBAC filtering. Admission keeps
|
||||
`2 * MCP_TOOL_WORKERS + MCP_METADATA_TOOL_WORKERS + 2` within the capacity,
|
||||
so even with every slot in use no metadata connection checkout waits for
|
||||
another holder. By default (`None`) warehouse-capable calls get about a third
|
||||
of the connections left after those two threads, and metadata-only calls
|
||||
what remains, each up to 16. With SQLAlchemy's default pool (5 + 10
|
||||
connections) that is 4 warehouse-capable and 5 metadata-only calls. To admit
|
||||
more, raise the pool for the MCP process, for example
|
||||
`SQLALCHEMY_ENGINE_OPTIONS = {"pool_size": 20, "max_overflow": 20}` (12 and
|
||||
14). Explicit values above the budget are reduced with a startup warning, and
|
||||
a pool with fewer than 4 connections is rejected at startup. The server never
|
||||
waits for a metadata connection on its event loop.
|
||||
|
||||
Pool logs report initialization and connection-budget clamping without logging
|
||||
configuration values or engine-derived counts. Successful transport-layer API-key
|
||||
validation logs the user id rather than the username or API key.
|
||||
|
||||
Tool-list and search permission checks, success/error audit writes, response-size
|
||||
audits, and error hooks run in metadata threads with independent Flask contexts
|
||||
and scoped sessions. These sessions are cleaned up when the operation finishes,
|
||||
including after failure or client disconnection; inherited request contexts do
|
||||
not share their ORM user/session with the thread.
|
||||
|
||||
`execute_sql` uses its request's `timeout` (default: 30 seconds). Tools without
|
||||
a timeout parameter use `SQLLAB_TIMEOUT`. The deadline covers the complete tool
|
||||
call, including composed queries, and returns a clear timeout error with a call
|
||||
ID. Superset requests warehouse cancellation using its engine cancellation
|
||||
hooks, including for connection prequeries, on a separate bounded pool.
|
||||
Cancellation connections run independently of the expired tool deadline, including
|
||||
connection prequeries such as PostgreSQL schema selection. Trino's client only
|
||||
learns a query's `queryId` once it is running and then blocks until results
|
||||
arrive, so the cancellation thread reads that id from the live cursor and kills
|
||||
the query with `system.runtime.kill_query`. Engines whose specs expose no
|
||||
cancellation handle (for example BigQuery, Databricks native and Athena) are not
|
||||
cancelled. Drivers without cancellation support can
|
||||
continue running after the caller receives the timeout; the worker retains its
|
||||
slot and owns session cleanup until it finishes, while metadata-only tools keep
|
||||
their own bound. A stuck cancellation also
|
||||
retains its call's slot rather than exhausting cancellation capacity for new
|
||||
calls. One warning with the call ID records abandonment. A timeout does not guarantee that warehouse writes were
|
||||
rolled back, so do not blindly retry mutating SQL.
|
||||
|
||||
## Native filter defaults
|
||||
|
||||
`manage_native_filters` accepts `default_value` for select filters. An empty
|
||||
list clears the selection. With `enable_empty_filter: true`, that explicit
|
||||
empty default matches no rows in both the dashboard UI and server-side
|
||||
dashboard chart queries, including for UI-created non-exact select filters.
|
||||
Creating a required select filter without `default_value` also stores this
|
||||
match-nothing default, unless `default_to_first_item` is enabled.
|
||||
Inverse-selection filters retain the UI's no-predicate behavior for empty
|
||||
selections. Disabling `enable_empty_filter` removes the empty default's
|
||||
match-nothing predicate without requiring another `default_value` update.
|
||||
Explicit defaults, including an empty list, cannot be combined with
|
||||
`default_to_first_item: true`. Creating a first-item filter or enabling that
|
||||
option on an existing filter leaves the stored selection unset so the dashboard
|
||||
can choose the first available item after loading options. Toggling
|
||||
`enable_empty_filter` on a first-item filter preserves that behavior rather than
|
||||
storing an explicit empty selection or match-nothing predicate. Any update to
|
||||
a first-item filter also clears a defined legacy stored selection (including
|
||||
`null`) when first-item mode remains enabled, allowing automatic selection.
|
||||
Select filters with legacy string-column
|
||||
targets support both default updates and applied selections.
|
||||
|
||||
## Bounding dashboard dataset metadata
|
||||
|
||||
`get_dashboard_datasets` lists a dashboard's datasets and semantic views with
|
||||
column and metric details. Its optional `max_columns` and `max_metrics`
|
||||
parameters limit the number of columns (0–100, default 100) and metrics (0–50,
|
||||
default 50) returned **per dataset**. Columns and metrics are sorted by name
|
||||
before the caps apply, so repeated calls return the same subset. For a wide
|
||||
dashboard, start with smaller caps or omit the details entirely:
|
||||
|
||||
```json
|
||||
{
|
||||
"request": {
|
||||
"identifier": 123,
|
||||
"max_columns": 0,
|
||||
"max_metrics": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Dataset identities, chart counts, and total column and metric counts are
|
||||
retained. `columns_truncated` and `metrics_truncated` are true whenever fewer
|
||||
items are returned than the dataset contains, including when a cap of 0 omits a
|
||||
nonempty list. Use `get_dataset_info` to fetch details for an individual
|
||||
dataset. Omitting both parameters returns up to 100 columns and 50 metrics per
|
||||
dataset. The caps bound per-dataset details, not the number of datasets or the
|
||||
entire response's byte size; a dashboard with many datasets can still exceed the
|
||||
server's configured byte limit.
|
||||
|
||||
## Dashboard filter notifications
|
||||
|
||||
`apply_dashboard_filters` supports exact-match select filters without inverse
|
||||
selection, and time filters. Inverse-selection configurations are rejected.
|
||||
selection, time filters, numeric range filters, and time-grain filters. Range
|
||||
bounds must be finite numbers representable as floats, or null; booleans are
|
||||
rejected. Range permalink labels use the dashboard's smart number formatting
|
||||
with ASCII minus signs, falling back to plain numeric representations if
|
||||
formatting overflows; time-grain labels come from
|
||||
the target datasource's grain names when available. Time grains must be supported
|
||||
by the filter's target dataset's database engine, as listed in the datasource's
|
||||
`time_grain_sqla` options. If the filter has a nonempty configured allowlist, the
|
||||
selected grain must also appear in that list. An unresolved target dataset is
|
||||
rejected.
|
||||
Inverse-selection configurations are rejected.
|
||||
Select filters configured for contains, starts-with, or ends-with matching are
|
||||
rejected rather than silently applying exact matching. A single-select filter
|
||||
accepts at most one value; a longer list is rejected rather than stored as a
|
||||
@@ -96,11 +225,13 @@ The MCP process and websocket transport must use the same
|
||||
Framework feature flag.
|
||||
|
||||
The response's `live_update_pushed` field reports publication success, not
|
||||
browser receipt or application. Without a backend, an identifiable principal,
|
||||
or a successful publish, it is `false`; the created `dashboard_url` remains
|
||||
the fallback. An open dashboard resolves matching notifications through the
|
||||
existing dashboard permalink API and applies the returned filters without a
|
||||
reload. A “Filters applied from chat” toast offers **Undo**, restoring the
|
||||
browser receipt or application. When `WEBSOCKET_ENABLE` is disabled or unset,
|
||||
no notification is published and this field is `false`. Without a backend,
|
||||
`can_read` on `Realtime` for the calling principal, an identifiable principal,
|
||||
or a successful publish, it is also `false`; the created
|
||||
`dashboard_url` remains the fallback. An open dashboard resolves matching
|
||||
notifications through the existing dashboard permalink API and applies the
|
||||
returned filters without a reload. A “Filters applied from chat” toast offers **Undo**, restoring the
|
||||
affected filters' previous selections. The toast carries an action, so it has
|
||||
no auto-dismiss timer and stays up until Undo or its close button is used,
|
||||
which keeps the action reachable for keyboard and assistive-technology users.
|
||||
@@ -114,6 +245,77 @@ latest-per-user permalink lookup, and replaying a previously received permalink
|
||||
could overwrite manual changes or undo. If a notification is missed or its
|
||||
permalink cannot be resolved, open `dashboard_url` from chat.
|
||||
|
||||
## Dashboard filter scope
|
||||
|
||||
A client that answers questions about a dashboard the user is viewing (for
|
||||
example an assistant embedded next to it) can make every tool call honor that
|
||||
dashboard's active filters by sending them in the `X-Superset-Dashboard-Scope`
|
||||
request header on each MCP HTTP request of the conversation turn. The model
|
||||
issuing tool calls never sees the header, so it cannot drop or loosen the
|
||||
filters, and retried calls carry them again.
|
||||
|
||||
The header value is `base64url(zlib(JSON))` of:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"dashboard_id": 12,
|
||||
"chart_filters": {
|
||||
"101": {"filters": [{"col": "client", "op": "IN", "val": ["A"]}],
|
||||
"time_range": "Last quarter"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`chart_filters` maps each chart with active filters to the `extra_form_data`
|
||||
the dashboard applies to it. A malformed header refuses every tool call rather
|
||||
than being ignored. Without the header, behavior is unchanged.
|
||||
|
||||
While a scope with active filters is present:
|
||||
|
||||
- Chart data tools (`get_chart_data`, `get_chart_preview`, `get_dashboard_data`)
|
||||
apply each chart's dashboard filters and AND any `extra_form_data` the model
|
||||
supplies with them. Filters can be added but not removed, and time windows,
|
||||
time columns and relative anchors the dashboard sets (including time-series
|
||||
cross-filters) cannot be changed. Charts that are not on the scoped
|
||||
dashboard, and unsaved chart state (`form_data_key`), are refused.
|
||||
- `query_dataset`, `get_table` (datasets) and `execute_sql` apply the
|
||||
filters from dashboard charts backed by the queried dataset, respecting
|
||||
native-filter chart scoping. Datasets outside the dashboard use the combined
|
||||
dashboard-wide filters. A model-supplied time range is intersected with the
|
||||
applicable dashboard time range. `execute_sql` replaces each filtered table
|
||||
read with a sub-query, the same way row-level security is applied, and refuses
|
||||
SQL whose tables are not registered datasets that have every filtered column, as well
|
||||
as SQL that could read tables it cannot see: opaque statements, functions
|
||||
the SQL parser does not model, and table functions. For `execute_sql`, template
|
||||
syntax in SQL or dashboard filter values is refused rather than rendered after
|
||||
scoping.
|
||||
For all three tools, the time-filter column is resolved from the affected
|
||||
dashboard charts' temporal filters (or legacy chart time-column settings),
|
||||
after dashboard overrides. The charts must agree on one column. A chart with
|
||||
no temporal target, conflicting targets, or a dataset outside the dashboard
|
||||
with an applicable time window causes a refusal; the dataset's main datetime
|
||||
column and a model-supplied column are never used to guess the dashboard target.
|
||||
Malformed saved chart filter lists also cause an explicit scope refusal before
|
||||
dashboard overrides are merged; no query is run.
|
||||
Without an applicable dashboard time window, no chart temporal target is
|
||||
required, and model-supplied time filtering behaves as before.
|
||||
- Writes that change what charts or datasets query (adding a chart to a
|
||||
dashboard, saving a chart, defining a virtual dataset, editing a dataset or
|
||||
metric, restoring a chart or dataset) are refused while the scope is
|
||||
active.
|
||||
- Where the filters cannot be applied (for example, a column the dataset lacks,
|
||||
or a column filtered differently by different charts) the call fails with a
|
||||
tool error beginning `Dashboard filter scope refused this call:`. Tools that
|
||||
return no dataset rows are unaffected; any other tool, including extension
|
||||
tools, is refused.
|
||||
- MCP response caching is bypassed for scoped calls, and the scope is applied
|
||||
before any query is built, so Superset's query cache never serves an
|
||||
unscoped result.
|
||||
|
||||
The scope is a correctness control for answers, not an access control.
|
||||
Permissions and row-level security apply exactly as they do without it.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
@@ -776,12 +978,40 @@ MCP_TOOL_SEARCH_CONFIG = {
|
||||
| `always_visible` | See above | Tools that always appear in `list_tools`, regardless of search |
|
||||
| `include_schemas` | `True` | Include the full `inputSchema` in search results by default. When `False` ("summary mode"), search results omit `inputSchema` entirely and include a lightweight `parameters_hint` listing top-level parameter names. Full schemas are always used when a tool is actually invoked via `call_tool`. |
|
||||
| `compact_schemas` | `True` | Legacy setting selecting the default description limit (300 when `True`, 0 when `False`) if `max_description_length` is omitted. Input schemas preserve `$defs`, `$ref`, nullable unions, and constraints in either mode; titles are omitted. Clients should resolve references within each tool's `inputSchema`. |
|
||||
| `max_description_length` | `300` | Truncate tool descriptions in search results (0 = no truncation). Applies in both summary and full-schema modes. |
|
||||
| `max_description_length` | `300` | Budget for discovery prose and request-wrapper instructions (0 = no prose truncation). Complete paragraphs are retained, followed by complete sentences of the next paragraph unless it is a list, heading, or IMPORTANT block. Applies in both summary and full-schema modes. |
|
||||
|
||||
The no-query catalog uses the same bounded serializer as targeted search.
|
||||
Chart creation/update and virtual-dataset tools keep request wrapping, save/preview
|
||||
behavior, required URL display, and workflow guidance in request metadata rather
|
||||
than relying on truncated introductory prose. Paragraph truncation never leaves a
|
||||
partial IMPORTANT block or numbered workflow in the catalog.
|
||||
|
||||
Calling constraints are authored as a `Field(description=...)` on the tool's
|
||||
`request` parameter and served as the `request` property's schema description, so
|
||||
schema-first clients can identify the wrapper without remembering previous calls.
|
||||
Summary mode preserves these instructions in `parameters_hint`. Instructions are
|
||||
never truncated: their length is deducted from the prose budget first. A request
|
||||
model's own docstring is not a calling instruction and is never deducted. If a
|
||||
configured limit is smaller than the instructions, prose is omitted rather than
|
||||
cutting the instructions. Built-in calling instructions are capped at 300
|
||||
characters by regression tests, and the complete tool entries retain per-tool
|
||||
size budgets. Field-level descriptions in the default full input schema also
|
||||
preserve tool-specific search semantics, sortable columns, chart pagination, and
|
||||
layout guidance. These details are not included in schema-free summary mode.
|
||||
This setting does not change result-size limits or schema validation.
|
||||
|
||||
:::tip
|
||||
Set `enabled: False` to revert to the traditional "show all tools at once" behavior, which some clients or workflows may prefer.
|
||||
:::
|
||||
|
||||
With either the `bm25` or `regex` strategy, a query matching a tool name is
|
||||
promoted ahead of other matches before the `max_results` limit is applied. Matching
|
||||
ignores case and treats underscores and whitespace as equivalent separators
|
||||
(including leading, trailing, or repeated whitespace). Only tools visible to
|
||||
the caller are eligible. Pinned `always_visible` tools remain in `list_tools`
|
||||
rather than search results; non-exact queries retain BM25 ranking or regex
|
||||
catalog ordering.
|
||||
|
||||
Tool search reduces the initial token cost from ~15–20K tokens (full catalog) down to ~4–5K tokens (pinned tools + search interface) — roughly 85% savings at the start of each conversation.
|
||||
|
||||
### Session & CSRF
|
||||
@@ -819,6 +1049,7 @@ Each tool declares one or more required FAB permissions. The table below maps to
|
||||
| `list_dashboards`, `get_dashboard_info`, `generate_dashboard`, `add_chart_to_existing_dashboard` | `can_read` on `Dashboard` (read), `can_write` on `Dashboard` (mutate) |
|
||||
| `get_dashboard_data` | `can_read` on `Dashboard` **and** `can_read` on `Chart` (it reads chart data across the dashboard) |
|
||||
| `list_datasets`, `get_dataset_info`, `create_virtual_dataset` | `can_read` on `Dataset` (read), `can_write` on `Dataset` (mutate) |
|
||||
| `list_metrics`, `get_table`, `get_compatible_dimensions`, `get_compatible_metrics` | `can_read` on `Dataset`, plus a data-model metadata permission (`can_get_drill_info`, `can_get_or_create_dataset`, or `can_write` on `Dataset`); external semantic views also enforce their own datasource access check |
|
||||
| `list_databases`, `get_database_info` | `can_read` on `Database` |
|
||||
| `get_catalog` | `can_read` on the requested asset class (`Database`, `Dataset`, `Chart`, or `Dashboard`); databases and datasets also require data-model metadata access, otherwise the page is marked `restricted` |
|
||||
| `execute_sql` | `can_execute_sql_query` on `SQLLab` |
|
||||
@@ -1047,6 +1278,23 @@ while page["next_cursor"]:
|
||||
)
|
||||
```
|
||||
|
||||
## Chart datasource metadata
|
||||
|
||||
For roles allowed to view data-model metadata, `get_chart_info` includes
|
||||
`datasource_id` in its default columns. Dashboard chart summaries include
|
||||
`datasource_type` alongside `datasource_id`, which is only unique within its type.
|
||||
Both fields are redacted for users without data-model metadata access.
|
||||
Chart and dashboard chart summaries resolve dataset names from the live table
|
||||
or semantic view, so renames are reflected without resaving the chart. Query and
|
||||
saved-query charts retain their stored datasource names; missing tables or
|
||||
semantic views do not fall back to stale names. Dataset-access validation remains
|
||||
unchanged.
|
||||
|
||||
`list_charts` filters on `datasource_name` use these same live names before
|
||||
pagination and counting. For tables with a schema, filter using
|
||||
`schema.table_name`. Data-model metadata permission checks also apply to these
|
||||
filters and returned identifiers.
|
||||
|
||||
## Trash Listing (Soft-Deleted Objects)
|
||||
|
||||
When the `SOFT_DELETE` feature flag is enabled, deleting a chart or dashboard moves it to trash rather than removing it outright. The `list_charts` and `list_dashboards` tools accept a `deleted_state` parameter to surface those trashed objects:
|
||||
@@ -1191,6 +1439,36 @@ filters update the `BASE_AXIS` column's `timeGrain`, regardless of its position
|
||||
in the query's `columns` list. Other columns are left untouched. Queries without
|
||||
a `BASE_AXIS` column continue to use `extras.time_grain_sqla`.
|
||||
|
||||
### Exploring large dashboard layouts
|
||||
|
||||
`get_dashboard_layout` returns the full parsed layout by default. For large
|
||||
dashboards, pass `tabs_only=true` to retrieve just the tab tree in `tab_tree`:
|
||||
`id`, `name` (the title), `parent_tab_id`, zero-based `depth`, and
|
||||
`chart_count` (distinct charts in the tab and its descendants). In this mode
|
||||
`tabs` and `charts` are empty by request, not because the dashboard is empty.
|
||||
Then pass `tab="<ID or exact title>"` to retrieve that tab and its descendants
|
||||
with chart positions. Matching is case-sensitive; IDs take precedence and
|
||||
ambiguous titles require an ID. Both options can be combined to summarize a
|
||||
subtree. Parent IDs, depths, and chart paths retain their full-dashboard context.
|
||||
|
||||
Charts placed outside every tab are counted in `untabbed_chart_count` in every
|
||||
response; pass `untabbed_only=true` (without `tab` or `tabs_only`) to list only
|
||||
those charts. The `scope` field echoes the applied `tabs_only`, resolved
|
||||
`tab_id`, and `untabbed_only` values, and is `null` for the full layout.
|
||||
Scoping only removes tabs and chart placements from the layout the caller can
|
||||
already retrieve. Conflicting scope options return an actionable
|
||||
`invalid_scope` error naming the options to remove; blank tab selectors return
|
||||
`tab_not_found` rather than widening the scope. On dashboards without tabs,
|
||||
`tabs_only=true` returns `tab_not_found` with guidance to use
|
||||
`untabbed_only=true` or request the full layout instead.
|
||||
|
||||
Permalink active-tab and filter state remain unchanged; only an explicit `tab`
|
||||
scopes the response. Oversized layout errors suggest the next narrower scope:
|
||||
the tab tree and untabbed charts for a full tabbed layout, a top-level tab for
|
||||
an oversized tab tree, a nested tab for a tab that has one, and `list_charts`
|
||||
paging when a dashboard without tabs or a tab without nested tabs is still too
|
||||
large.
|
||||
|
||||
### Reading applied dashboard filters
|
||||
|
||||
When answering about a filtered dashboard, call `get_dashboard_info` with the
|
||||
@@ -1240,6 +1518,57 @@ sub-500 exception remains a warning even if it carries a connection error reason
|
||||
the generic engine reason can also describe a caller's malformed column, and a
|
||||
connection access denial is still a permission failure.
|
||||
|
||||
The error hook runs off the transport loop in its own application and metadata
|
||||
session scope, without reloading the request user. Reporting an error therefore
|
||||
does not require metadata database access unless the hook itself uses it. Use the
|
||||
provided hook context for caller identity rather than `g.user`.
|
||||
|
||||
## SQL execution row limits
|
||||
|
||||
The `execute_sql` tool treats an explicit request `limit` as a safety cap on the
|
||||
last statement's outer SQL LIMIT. The smaller limit wins: SQL `LIMIT 5` with
|
||||
request `limit: 10` returns at most five rows, while SQL `LIMIT 10` with request
|
||||
`limit: 5` also returns at most five. Equal limits and SQL `LIMIT 0` are preserved.
|
||||
If SQL has no outer limit, the explicit request cap is applied. Inner query and
|
||||
CTE limits and SQL offsets are preserved. Literal `FETCH FIRST` counts and
|
||||
parenthesized literal limits follow the same rule. `PERCENT`, `WITH TIES`, and
|
||||
limit expressions that cannot be interpreted as a fixed row count are preserved
|
||||
inside a subquery with an outer row cap, rather than replaced. This conservative
|
||||
fallback cannot increase the original result's row count. For SQL Server, these
|
||||
restrictions are left unchanged instead: a derived table could reject duplicate
|
||||
or unnamed output columns. The executor caps the last statement's returned rows
|
||||
at the smaller of the explicit request limit and `SQL_MAX_ROW`, without changing
|
||||
percentage, tie, or expression semantics. Cursor reads are bounded by the same
|
||||
cap plus one probe row, including across multiple fetch batches, preserving
|
||||
engine-specific result processing. Only successfully returned rows count against the cursor
|
||||
budget, so a failed fetch leaves that budget available for an engine fallback.
|
||||
BigQuery only falls back if its initial fetch fails; errors after a successful
|
||||
sample, including during the EOF probe, fail the query rather than silently
|
||||
discarding buffered rows.
|
||||
This does not bound the work performed by the database.
|
||||
Because this cap applies to any row-returning last statement, a non-query
|
||||
statement such as `SHOW TABLES` or DML with `RETURNING` can also be capped. Each
|
||||
statement reports `truncated: true` when a fetch cap omits rows from the
|
||||
executed SQL result, including engine memory caps such as `BQ_FETCH_MAX_MB`.
|
||||
Exactly-at-cap and smaller results report `false` when no rows are omitted. This
|
||||
flag does not indicate rows excluded by a SQL LIMIT visible in `executed_sql`,
|
||||
and does not bound or count affected rows for a write with `RETURNING`. In
|
||||
`dry_run`, the SQL Server restriction remains unchanged because no results are
|
||||
fetched.
|
||||
|
||||
Omitting `limit` leaves the SQL limit unchanged. Explicit request caps remain
|
||||
subject to the server's `SQL_MAX_ROW` configuration. This behavior also applies
|
||||
to `dry_run`, which returns the transformed SQL without executing it.
|
||||
|
||||
The SQL-only `Database.apply_limit_to_sql()` helper used by dataset column
|
||||
discovery and alerts does not use the executor's bounded cursor. It retains the
|
||||
legacy SQL Server behavior of replacing `PERCENT`, `WITH TIES`, and non-fixed
|
||||
limit expressions with a fixed `TOP` cap, without introducing derived tables
|
||||
that could reject unnamed or duplicate columns. Smaller fixed limits, including
|
||||
zero, are preserved unless the caller explicitly requests `force=True`. This
|
||||
legacy helper's replacement semantics are separate from MCP execution's
|
||||
preservation of complex SQL restrictions.
|
||||
|
||||
## Result serialization
|
||||
|
||||
Data-bearing tool responses sanitize row values, column samples, and statistics
|
||||
@@ -1255,6 +1584,39 @@ For saved and unsaved charts, `get_chart_data` column metadata excludes missing
|
||||
values from `unique_count`. Values serialized as JSON `null`, including NaN,
|
||||
NaT, and non-finite numbers, contribute to `null_count` instead.
|
||||
|
||||
## Big Number headlines
|
||||
|
||||
For Big Number charts (`big_number` and `big_number_total`), `get_chart_data`
|
||||
and `get_dashboard_data` return a `headline` object holding the number the chart
|
||||
displays. Agents should report `headline.value` rather than reading or summing
|
||||
the data rows: for the trendline variant the rows are a time series, and the
|
||||
displayed number is derived from the whole series by the chart's aggregation.
|
||||
|
||||
| Field | Contents |
|
||||
| --- | --- |
|
||||
| `value` | The displayed number, or `null` when it cannot be computed exactly |
|
||||
| `aggregation` | `total` for `big_number_total`; otherwise the chart's aggregation (`LAST_VALUE`, `sum`, `mean`, `min`, `max`, `median`, or `raw`) |
|
||||
| `rows_used` | The number of non-null values aggregated |
|
||||
| `reason` | Why `value` is `null` |
|
||||
|
||||
A headline is only returned when it matches the rendered chart, so `value` is
|
||||
`null` with a `reason` when:
|
||||
|
||||
- The query returned no rows, the metric column is missing, or every metric
|
||||
value is null.
|
||||
- The chart applies a rolling window or resampling that the executed query did
|
||||
not include, or the fetch was truncated at the row limit.
|
||||
- The aggregation picks the latest value (`LAST_VALUE`) and some rows have no
|
||||
usable timestamp. The chart's ordering of such rows depends on row order, so
|
||||
the value it shows cannot be determined exactly. Order-independent
|
||||
aggregations still include those rows.
|
||||
- The aggregation is `raw` ("Overall value") and the chart has no saved query
|
||||
context. That value comes from a second query that only a saved query context
|
||||
carries, so unsaved and `form_data_key` requests return `null`.
|
||||
|
||||
When `form_data_key` is supplied, the headline follows the unsaved visualization
|
||||
type, including when switching to or from a Big Number chart.
|
||||
|
||||
## Dataset query dimensions and nested fields
|
||||
|
||||
The `query_dataset` tool accepts exact column names registered on the selected
|
||||
@@ -1410,3 +1772,53 @@ predicate, whose behavior varied by database. It is not an indexed
|
||||
full-text search: large catalogs and long descriptions can increase scan time.
|
||||
Benchmark with your metadata database and catalog size when tuning discovery
|
||||
latency.
|
||||
|
||||
### Managing and applying dashboard native filters
|
||||
|
||||
`manage_native_filters` requires a numeric dataset column when adding a
|
||||
`filter_range` filter or changing its dataset/column target. Select filters
|
||||
continue to accept nonnumeric columns.
|
||||
|
||||
For `apply_dashboard_filters`, pass `time_grain` as a list containing at most
|
||||
one datasource-supported duration (for example, `["P1D"]`). Custom engine
|
||||
durations are accepted when exposed in the datasource's time-grain options,
|
||||
rather than limited to a fixed enum.
|
||||
A nonempty `time_grains` list in the saved filter configuration restricts the
|
||||
allowed selection to grains that appear in both that list and the target dataset's
|
||||
supported time grains; missing or empty lists use the dataset's supported grains.
|
||||
Pass `[]` to clear an optional time-grain filter; required filters cannot be
|
||||
cleared.
|
||||
|
||||
## Missing chart columns
|
||||
|
||||
`generate_chart`, `update_chart`, `update_chart_preview` and
|
||||
`generate_explore_link` all validate column references through
|
||||
`validate_and_compile` and return the same structured errors.
|
||||
|
||||
A reference to a missing column returns `column_not_found` with up to three
|
||||
similar physical column names ("Did you mean: ..."), or explicit no-match
|
||||
guidance when nothing is close enough. Saved metrics — both names and
|
||||
expressions — are excluded from these candidates for dimension and `WHERE`
|
||||
references, because only a physical column is legal there and suggesting a
|
||||
metric leads to invalid `SUM(metric)` SQL. A `HAVING` filter subject may
|
||||
legitimately name a saved metric, so metrics are included for that slot. When a
|
||||
reference in a metric slot (`y`, `metric`, or an aggregated table column, for
|
||||
example) near-misses a saved metric name, the error adds a hint to reference it
|
||||
as `{"name": "<metric>", "saved_metric": true}` instead. Dimension references
|
||||
never receive this hint, even when they carry an `aggregate`.
|
||||
|
||||
When more than one distinct column is missing, the error type is
|
||||
`multiple_invalid_columns`. The invalid names appear in escaped, length-limited
|
||||
details, and the three-candidate cap is **shared across all missing columns**,
|
||||
filled round-robin so each missing column gets a turn.
|
||||
|
||||
Column errors carry a bounded `dataset_context`: up to ten column names and ten
|
||||
saved-metric names from the authorized dataset, names only — never SQL
|
||||
expressions. Suggested candidates are listed first so they are never cut by the
|
||||
bound (a hinted saved metric leads `available_metrics` the same way), and the
|
||||
error says `Showing 10 of N columns` or `Showing 10 of N saved metrics` when a
|
||||
list is partial.
|
||||
Context names are returned verbatim per the Tool Result Value Contract.
|
||||
"Did you mean" column candidates also preserve dataset spelling, including
|
||||
quotes and ampersands, with a 200-character bound per candidate. Use
|
||||
`get_dataset_info` for the complete schema.
|
||||
|
||||
@@ -201,7 +201,6 @@ came out of, credit to the community for working it out.
|
||||
`docker-compose` build to take much less time and resources
|
||||
- `INCLUDE_CHROMIUM`: whether to include chromium in the backend build so that it can be
|
||||
used as a headless browser for workloads related to "Alerts & Reports" and thumbnail generation
|
||||
- `INCLUDE_FIREFOX`: same as above, but for firefox
|
||||
- `PY_VER`: specifying the base image for the python backend, we don't recommend altering
|
||||
this setting if you're not working on forwards or backwards compatibility
|
||||
|
||||
|
||||
@@ -200,6 +200,11 @@ when authorization is denied or callback state is missing or invalid. Provider
|
||||
error text is not echoed in the response; authorization denials use the message
|
||||
"The OAuth2 provider denied the request". Restart the OAuth2 flow to retry.
|
||||
|
||||
When a database rejects an OAuth2 access token, connection and metadata
|
||||
inspection attempt one forced refresh and retry. Reopening a connection inside
|
||||
that query retry does not trigger another exchange. If the replacement token is
|
||||
also rejected, Superset asks the user to sign in again.
|
||||
|
||||
#### Data Exploration
|
||||
|
||||
<details>
|
||||
|
||||
@@ -95,8 +95,7 @@ documentation.
|
||||
Affecting the Docker build process:
|
||||
|
||||
- **SUPERSET_BUILD_TARGET (default=dev):** which --target to build, either `lean` or `dev` are commonly used
|
||||
- **INCLUDE_FIREFOX (default=false):** whether to include the Firefox headless browser in the build
|
||||
- **INCLUDE_CHROMIUM (default=false):** whether to include the Chromium headless browser in the build
|
||||
- **INCLUDE_CHROMIUM (default=false):** whether to include Playwright and the Chromium headless browser in the build, which Alerts & Reports and thumbnails need to take screenshots
|
||||
- **BUILD_TRANSLATIONS(default=false):** whether to compile the translations from the .po files available
|
||||
- **SUPERSET_LOAD_EXAMPLES (default=yes):** whether to load the examples into the database upon startup,
|
||||
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`. Once the example
|
||||
@@ -579,7 +578,7 @@ There are three types of assets you can build:
|
||||
|
||||
1. `npm run build`: the production assets, CSS/JSS minified and optimized
|
||||
2. `npm run dev-server`: local development assets, with sourcemaps and hot refresh support
|
||||
3. `npm run build-instrumented`: instrumented application code for collecting code coverage from Cypress tests
|
||||
3. `npm run build-instrumented`: instrumented application code for collecting code coverage from end-to-end tests
|
||||
|
||||
If while using the above commands you encounter an error related to the limit of file watchers:
|
||||
|
||||
@@ -722,7 +721,7 @@ can find all of the workflows and other assets under the `.github/` folder. This
|
||||
|
||||
- running the backend unit test suites (`tests/`)
|
||||
- running the frontend test suites (`superset-frontend/src/**.*.test.*`)
|
||||
- running our Playwright end-to-end tests (`superset-frontend/playwright/`) and legacy Cypress tests (`superset-frontend/cypress-base/`)
|
||||
- running our Playwright end-to-end tests (`superset-frontend/playwright/`)
|
||||
- linting the codebase, including all Python, Typescript and Javascript, yaml and beyond
|
||||
- checking for all sorts of other rules conventions
|
||||
|
||||
@@ -797,7 +796,7 @@ pytest tests/unit_tests/*
|
||||
|
||||
#### Integration Tests
|
||||
|
||||
For more complex pytest-defined integration tests (not to be confused with our end-to-end Cypress tests), many tests will require having a working test environment. Some tests require a database, Celery, and potentially other services or libraries installed.
|
||||
For more complex pytest-defined integration tests (not to be confused with our end-to-end Playwright tests), many tests will require having a working test environment. Some tests require a database, Celery, and potentially other services or libraries installed.
|
||||
|
||||
### Running Tests with `act`
|
||||
|
||||
@@ -908,8 +907,6 @@ superset:
|
||||
user: "root"
|
||||
depends_on: *superset-depends-on
|
||||
volumes: *superset-volumes
|
||||
environment:
|
||||
CYPRESS_CONFIG: "${CYPRESS_CONFIG}"
|
||||
```
|
||||
|
||||
Start Superset as usual
|
||||
|
||||
@@ -107,6 +107,17 @@ npm link superset-plugin-chart-hello-world
|
||||
7. **Import and register in Superset**:
|
||||
Edit `superset-frontend/src/visualizations/presets/MainPreset.ts` to include your plugin.
|
||||
|
||||
### Sharing colors in composed charts
|
||||
|
||||
`ChartProps.colorScale` accepts an optional `CategoricalColorScale`. A parent
|
||||
visualization can pass the same scale to its child chart transforms so categories
|
||||
use consistent colors across the composition. Create a fresh scale for each parent
|
||||
transformation using the selected color scheme. Child plugins must explicitly use
|
||||
the supplied scale, falling back to their own scale when it is absent.
|
||||
|
||||
Cartodiagram uses this contract with Pie charts to coordinate category colors
|
||||
across map locations in Explore.
|
||||
|
||||
## Testing
|
||||
|
||||
### Python Testing
|
||||
@@ -166,11 +177,9 @@ npm run test -- MyComponent.test.tsx
|
||||
|
||||
### E2E Integration Testing
|
||||
|
||||
We support both Playwright (recommended) and Cypress for end-to-end testing.
|
||||
We use Playwright for end-to-end testing.
|
||||
|
||||
#### Playwright (Recommended - NEW)
|
||||
|
||||
Playwright is our new E2E testing framework, gradually replacing Cypress.
|
||||
#### Playwright
|
||||
|
||||
```bash
|
||||
# Navigate to frontend directory
|
||||
@@ -199,30 +208,6 @@ npm run playwright:debug tests/auth/login.spec.ts
|
||||
npm run playwright:report
|
||||
```
|
||||
|
||||
#### Cypress (DEPRECATED - will be removed)
|
||||
|
||||
Cypress is being phased out in favor of Playwright but is still available:
|
||||
|
||||
```bash
|
||||
# Set base URL for Cypress
|
||||
export CYPRESS_BASE_URL='http://localhost:8088'
|
||||
export CYPRESS_DATABASE=test
|
||||
export CYPRESS_USERNAME=admin
|
||||
export CYPRESS_PASSWORD=admin
|
||||
|
||||
# Navigate to Cypress directory
|
||||
cd superset-frontend/cypress-base
|
||||
|
||||
# Run interactively
|
||||
npm run cypress-debug
|
||||
|
||||
# Run headless (like CI)
|
||||
npm run cypress-run-chrome
|
||||
|
||||
# Run specific file
|
||||
npm run cypress-run-chrome -- --spec "cypress/e2e/explore/chart.test.js"
|
||||
```
|
||||
|
||||
### Debugging Server App
|
||||
|
||||
For debugging the Flask backend:
|
||||
|
||||
@@ -36,7 +36,7 @@ This is a list of statements that describe how we do frontend development in Sup
|
||||
- We use [@emotion](https://emotion.sh/docs/introduction) to provide styling for our components, co-locating styling within component files.
|
||||
- See: [SIP-37](https://github.com/apache/superset/issues/9145)
|
||||
- See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines.md)
|
||||
- We use Jest for unit tests, React Testing Library for component tests, and Cypress for end-to-end tests.
|
||||
- We use Jest for unit tests, React Testing Library for component tests, and Playwright for end-to-end tests.
|
||||
- See: [SIP-56](https://github.com/apache/superset/issues/11830)
|
||||
- See: [Testing Guidelines and Best Practices](../testing/testing-guidelines.md)
|
||||
- We add tests for every new component or file added to the frontend.
|
||||
|
||||
@@ -71,6 +71,7 @@ The Alerts & Reports feature relies on Celery for task scheduling and execution.
|
||||
|
||||
- Redis running on `localhost:6379`
|
||||
- [MailHog](https://github.com/mailhog/MailHog) installed (a local SMTP server with a web UI for viewing caught emails)
|
||||
- Playwright and Chromium installed, for report screenshots
|
||||
|
||||
### superset_config.py
|
||||
|
||||
@@ -124,8 +125,6 @@ ALERT_REPORTS_EXECUTORS = [ExecutorType.EDITOR]
|
||||
|
||||
FEATURE_FLAGS = {
|
||||
"ALERT_REPORTS": True,
|
||||
# Recommended for better screenshot support (WebGL/DeckGL charts)
|
||||
"PLAYWRIGHT_REPORTS_AND_THUMBNAILS": True,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -24,12 +24,10 @@ under the License.
|
||||
|
||||
# End-to-End Testing
|
||||
|
||||
Apache Superset uses Playwright for end-to-end testing, migrating from the legacy Cypress tests.
|
||||
Apache Superset uses Playwright for end-to-end testing.
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Playwright (Recommended)
|
||||
|
||||
```bash
|
||||
cd superset-frontend
|
||||
|
||||
@@ -53,16 +51,6 @@ npm run playwright:debug tests/auth/login.spec.ts
|
||||
# or: npx playwright test --debug tests/auth/login.spec.ts
|
||||
```
|
||||
|
||||
### Cypress (Deprecated)
|
||||
|
||||
Cypress tests are being migrated to Playwright. For legacy tests:
|
||||
|
||||
```bash
|
||||
cd superset-frontend/cypress-base
|
||||
npm run cypress-run-chrome # Headless
|
||||
npm run cypress-debug # Interactive UI
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
|
||||
```
|
||||
@@ -179,10 +167,10 @@ All debugging artifacts are available in the HTML report for easy analysis.
|
||||
|
||||
## Configuration
|
||||
|
||||
- **Config**: `playwright.config.ts` - matches Cypress settings
|
||||
- **Config**: `playwright.config.ts`
|
||||
- **Base URL**: `http://localhost:8088` (assumes Superset running)
|
||||
- **Browsers**: Chrome only for Phase 1 (YAGNI)
|
||||
- **Retries**: 2 in CI, 0 locally (matches Cypress)
|
||||
- **Retries**: 2 in CI, 0 locally
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
@@ -207,16 +195,6 @@ All debugging artifacts are available in the HTML report for easy analysis.
|
||||
3. **Actions and queries only** - no assertions
|
||||
4. **Use existing components** for DOM interactions
|
||||
|
||||
## Migration from Cypress
|
||||
|
||||
When porting Cypress tests:
|
||||
|
||||
1. **Port the logic**, not the implementation
|
||||
2. **Use page objects** instead of inline selectors
|
||||
3. **Replace `cy.intercept/cy.wait`** with `page.waitForRequest()`
|
||||
4. **Use shared constants** from `utils/urls.ts`
|
||||
5. **Follow the established patterns** shown in `tests/auth/login.spec.ts`
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Centralize selectors** in page objects
|
||||
|
||||
@@ -169,7 +169,7 @@ npm run test:coverage
|
||||
|
||||
- **Jest** - Unit and integration testing framework
|
||||
- **React Testing Library** - Component testing utilities
|
||||
- **Playwright** - End-to-end testing (replacing Cypress)
|
||||
- **Playwright** - End-to-end testing
|
||||
- **Storybook** - Component development and testing
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ under the License.
|
||||
|
||||
We feel that tests are an important part of a feature and not an additional or optional effort. That's why we colocate test files with functionality and sometimes write tests upfront to help validate requirements and shape the API of our components. Every new component or file added should have an associated test file with the `.test` extension.
|
||||
|
||||
We use Jest, React Testing Library (RTL), and Cypress to write our unit, integration, and end-to-end tests. For each type, we have a set of best practices/tips described below:
|
||||
We use Jest, React Testing Library (RTL), and Playwright to write our unit, integration, and end-to-end tests. For each type, we have a set of best practices/tips described below:
|
||||
|
||||
## Jest
|
||||
|
||||
@@ -80,9 +80,9 @@ Prefer the [user-event](https://github.com/testing-library/user-event) library,
|
||||
|
||||
You can find an example of a test [here](https://github.com/apache/superset/blob/master/superset-frontend/src/dashboard/components/PublishedStatus/PublishedStatus.test.tsx).
|
||||
|
||||
## Cypress
|
||||
## Playwright
|
||||
|
||||
### Prefer to use Cypress for e2e testing and RTL for integration testing
|
||||
### Prefer to use Playwright for e2e testing and RTL for integration testing
|
||||
|
||||
Here it's important to make the distinction between e2e and integration testing. This [article](https://www.onpathtesting.com/blog/end-to-end-vs-integration-testing) gives an excellent definition:
|
||||
|
||||
@@ -91,30 +91,11 @@ Here it's important to make the distinction between e2e and integration testing.
|
||||
> A typical software project consists of multiple software units, usually coded by different developers. Integration testing combines those software units logically and tests them as a group.
|
||||
> Essentially, integration testing verifies whether or not the individual modules or services that make up your application work well together. The purpose of this level of testing is to expose defects in the interaction between these software modules when they are integrated.
|
||||
|
||||
Do not use Cypress when RTL can do it better and faster. Many of the Cypress tests that we have right now, fall into the integration testing category and can be ported to RTL which is much faster and gives more immediate feedback. Cypress should be used mainly for end-to-end testing, replicating the user experience, with positive and negative flows.
|
||||
Do not use Playwright when RTL can do it better and faster. Tests that fall into the integration testing category belong in RTL, which is much faster and gives more immediate feedback. Playwright should be used mainly for end-to-end testing, replicating the user experience, with positive and negative flows. See [End-to-End Testing](./e2e-testing.md) for the Playwright setup and conventions.
|
||||
|
||||
### Isolated and standalone tests
|
||||
|
||||
Tests should never rely on other tests to pass. This might be hard when a single user is used for testing as data will be stored in the database. At every new test, we should reset the database.
|
||||
|
||||
### Cleaning state
|
||||
|
||||
Cleaning the state of the application, such as resetting the DB, or in general, any state that might affect consequent tests should always be done in the `beforeEach` hook and never in the `afterEach` one as the `beforeEach` is guaranteed to run, while the test might never reach the point to run the `afterEach` hook. One example would be if you refresh Cypress in the middle of the test. At this point, you will have built up a partial state in the database, and your clean-up function will never get called. You can read more about it [here](https://docs.cypress.io/guides/references/best-practices#Using-after-or-afterEach-hooks).
|
||||
|
||||
### Unnecessary use of `cy.wait`
|
||||
|
||||
- Unnecessary when using `cy.request()` as it will resolve when a response is received from the server
|
||||
- Unnecessary when using `cy.visit()` as it resolves only when the page fires the load event
|
||||
- Unnecessary when using `cy.get()`. When the selector should wait for a request to happen, aliases would come in handy:
|
||||
|
||||
```js
|
||||
cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }]).as(
|
||||
'getUsers',
|
||||
);
|
||||
cy.get('#fetch').click();
|
||||
cy.wait('@getUsers'); // <--- wait explicitly for this route to finish
|
||||
cy.get('table tr').should('have.length', 2);
|
||||
```
|
||||
Tests should never rely on other tests to pass. Create the data a test needs through the API helpers and the `testAssets` fixture, which cleans up what it tracked, instead of depending on state left behind by another test.
|
||||
|
||||
### Accessibility and Resilience
|
||||
|
||||
@@ -127,5 +108,5 @@ The same accessibility principles in the RTL section apply here. Use accessible
|
||||
- [Fix the "not wrapped in act(...)" warning](https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning)
|
||||
- [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
|
||||
- [ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
|
||||
- [Cypress - Best Practices](https://docs.cypress.io/guides/references/best-practices)
|
||||
- [Playwright - Best Practices](https://playwright.dev/docs/best-practices)
|
||||
- [End-to-end vs integration tests: what's the difference?](https://www.onpathtesting.com/blog/end-to-end-vs-integration-testing)
|
||||
|
||||
@@ -54,6 +54,9 @@ username: admin
|
||||
password: admin
|
||||
```
|
||||
|
||||
The personalized Home page requires you to sign in. Opening Home from the login
|
||||
page or a public dashboard redirects you to the login page first.
|
||||
|
||||
#### 🎉 Congratulations! Superset is now up and running on your machine! 🎉
|
||||
|
||||
### Wrapping Up
|
||||
|
||||
@@ -350,6 +350,24 @@ Only enable HTML rendering for columns sourced from data you control. Rendering
|
||||
|
||||
Column headers display a tooltip with the column's **Description** from the dataset editor when the user hovers over them. Keep dataset column descriptions up to date to improve chart discoverability.
|
||||
|
||||
#### Multi-level Header
|
||||
|
||||
The **Table** and **AG Grid Interactive Table** chart types can group column headers under shared parent labels. Open the **Customize** tab and find the **Multi-level header** section. Use **Column groups** to nest related columns — for example, several sales metrics under a **Sales** header.
|
||||
|
||||
To create a group:
|
||||
|
||||
1. Click **Add group**.
|
||||
2. Enter a **Name**. A group cannot be saved without one.
|
||||
3. Select one or more **Columns**. A column can belong to only one group. Percent metrics (`%metric`) stay independently groupable and are not reserved by the matching base metric.
|
||||
4. Optionally set **Label position** (left, center, or right) and **Table side** (left or right) to control where the group header sits.
|
||||
5. Click **Apply**.
|
||||
|
||||
After a group is saved, click its numbered label (**Group 1**, **Group 2**, …) to edit it. Drag the handle on a group to reorder groups. While editing, **Add subgroup** nests another header under the current group (up to three levels). Subgroups have the same name and column requirements as top-level groups.
|
||||
|
||||
When **Advanced Analytics** → **Time Comparison** is enabled on an **Aggregate** query with **Calculation type** set to **Values**, Superset automatically creates one group per metric (`Main`, `#`, `△`, `%`). Those groups use the metric's dataset label, lock their name and columns, and disappear when time comparison is turned off. You can still add separate manual groups for other columns, including percent metrics.
|
||||
|
||||
On the standard Table chart, **Allow columns to be rearranged** is hidden while header groups or time comparison are active, so body column order stays aligned with the grouped header.
|
||||
|
||||
#### Display Controls
|
||||
|
||||
In dashboard view mode (without entering Edit mode), charts with configurable display options expose a **Display Controls** panel accessible from the chart's context menu. This surfaces controls such as Time Grain, Time Column, and layer visibility for applicable chart types — making it easy to adjust a chart's view without going to Explore.
|
||||
@@ -379,6 +397,8 @@ AG Grid supports server-side column filters that query the full dataset — not
|
||||
|
||||
AG Grid Interactive Table supports **Time Shift** (time comparison), matching the behavior of the standard Table chart. In the **Advanced Analytics** → **Time Comparison** section of the chart configuration, enter a shift expression (e.g., `1 year ago`, `minus 7 days`) to add comparison columns showing values from the offset period. Dashboard-level time range overrides apply to both the base and comparison periods.
|
||||
|
||||
When time comparison is active in **Aggregate** / **Values** mode, the **Customize** → **Multi-level header** section automatically groups each metric's comparison columns. See [Multi-level Header](#multi-level-header) for creating additional groups by hand.
|
||||
|
||||
#### Show Summary
|
||||
|
||||
The **Show summary** checkbox lives at the top of the **Visual formatting** section in the **Customize** tab, for both **Aggregate** and **Raw Records** query modes. Enabling it pins a summary row to the bottom of the grid whenever there is something to summarize: at least one metric in **Aggregate** mode, or at least one eligible numeric column in **Raw Records** mode. Otherwise no summary row is added.
|
||||
@@ -386,8 +406,16 @@ The **Show summary** checkbox lives at the top of the **Visual formatting** sect
|
||||
- In **Aggregate** mode, the summary row applies each metric's own aggregation (or the **Summary aggregation** override, where available) across the full filtered dataset.
|
||||
- In **Raw Records** mode, the summary row defaults to a server-side `SUM` for each numeric column that's backed by a physical or calculated dataset column; the **Summary aggregation** control can override this to `AVG` as well. Non-numeric cells and columns built from free-form SQL expressions stay blank.
|
||||
|
||||
Changing **Summary aggregation** refreshes the summary row automatically; you do not need to click **Update chart**.
|
||||
|
||||
In both modes, the summary is computed across the full result set, independent of the chart's row limit and pagination, and it reflects dashboard and chart-level filters. It does not reflect AG Grid's own server-side column filters (the per-column filter UI in the grid header), which are excluded from the summary query.
|
||||
|
||||
#### JSON values
|
||||
|
||||
A text cell that holds a JSON object or array keeps an icon on the right. A column format that changes the displayed text keeps that text. In a column that wraps text, the JSON wraps onto extra lines. In a column that truncates long cells, it is shown as one line and expands while the pointer is over the cell. A click on the text selects the row and can apply a cross-filter. A click on the icon, or Enter while the cell is focused, opens the value in a dialog, with a control to copy the original text.
|
||||
|
||||
**Customize** → **Options** → **JSON in cell** adds an arrow on the left and keeps the value on one line. A click on the arrow expands and collapses the JSON in the row. The icon on the right still opens the dialog.
|
||||
|
||||
### Dynamic Currency Formatting
|
||||
|
||||
Chart metric values can display currencies dynamically rather than using a fixed currency code. To enable:
|
||||
|
||||
@@ -163,7 +163,7 @@ One caveat when the host page lives on a different site than Superset: the defau
|
||||
## Security Notes
|
||||
|
||||
- **Guest tokens expire** — their lifetime is controlled by the `GUEST_TOKEN_JWT_EXP_SECONDS` config (default: 5 minutes). Refresh tokens before they expire using a token refresh mechanism in your host app.
|
||||
- **Row-level security** — pass `rls` rules in the guest token request to restrict which rows are visible to the embedded user.
|
||||
- **Row-level security** — pass `rls` rules in the guest token request to restrict which rows are visible to the embedded user. Semantic views cannot enforce guest-token SQL clauses, so queries and cached-result reads are rejected when a global rule or a rule scoped to the semantic view applies. For hosts, a global guest rule deliberately refuses every semantic view, and a dataset-scoped rule can also refuse a semantic view even when the rule was intended for a SQL dataset. Read permissions do not override these restrictions. Semantic views remain accessible when no guest rule applies; SQL datasets continue to enforce their applicable rules.
|
||||
- **Allowed domains** — restrict which host origins can embed a dashboard by setting **Allowed Domains** per-dashboard in the _Embed_ settings modal. Superset checks the request's `Referer` header against this list before serving the embedded view; an empty list allows any origin, so configure this explicitly for production.
|
||||
- **Redacted errors** — API responses to a guest token report a generic `An error occurred while fetching the data.` instead of the underlying error, since engine errors quote catalog, schema, table and column names. Errors Superset raises itself — access denials, timeouts, payload validation — keep their message, and the full error is always available in the server logs.
|
||||
|
||||
|
||||
@@ -24,6 +24,25 @@ The following information about each flight is given:
|
||||
- Information about the origin and destination.
|
||||
- The distance between the origin and destination, in kilometers (km).
|
||||
|
||||
### Semantic Table time grains
|
||||
|
||||
For a Table chart backed by a semantic view, Time Grain applies when grouping by
|
||||
a temporal dimension. Removing the last temporal dimension leaves the saved
|
||||
choice intact, but the frontend omits a recognized dormant grain from aggregate
|
||||
requests whose selected columns are known to be non-temporal (including no
|
||||
grouping columns). Adding the temporal dimension again reuses the saved choice.
|
||||
Unknown columns or grain values are not silently normalized. Ordinary dataset
|
||||
charts and raw-record queries are unchanged.
|
||||
|
||||
This applies to requests rebuilt by the updated frontend in Explore and
|
||||
dashboards; it does not rewrite saved charts. Direct API requests and saved
|
||||
chart-data GET paths that bypass rebuilding remain subject to strict validation,
|
||||
so old stored query contexts can still fail. Updated frontend assets are required.
|
||||
Dashboards reuse each chart's saved `temporal_columns_lookup` rather than
|
||||
recomputing it. A grouped chart saved before this change may lack the
|
||||
non-temporal column metadata needed to omit a dormant grain, so its dashboard
|
||||
requests can keep sending that grain until the chart is re-saved in Explore.
|
||||
|
||||
### Enabling Data Upload Functionality
|
||||
|
||||
You may need to enable the functionality to upload a CSV or Excel file to your database. The following section
|
||||
|
||||
@@ -90,3 +90,19 @@ Use these when a metric's underlying values are stored in meters or centimeters
|
||||
## Currency
|
||||
|
||||
Some chart types also expose currency-specific formatting, including a dynamic mode that reads the currency from a column value. See [Dynamic Currency Formatting](./creating-your-first-dashboard#dynamic-currency-formatting) for details.
|
||||
|
||||
## Exact decimals in SQL Lab
|
||||
|
||||
SQL Lab displays database `DECIMAL` and `NUMERIC` results as exact fixed-point
|
||||
text, including trailing zeros and never in scientific notation. Its JSON
|
||||
responses use strings so a browser does not round values such as
|
||||
`12345678901234567890.123456789012345678`. Sorting remains numeric and
|
||||
compares all digits; CSV exports, including streaming exports, retain those
|
||||
digits and use fixed-point notation for finite decimals too. Extensions overriding
|
||||
`sqleditor.extension.resultTable` replace the built-in table in SQL Lab results
|
||||
and table previews. They receive decimal strings but not the built-in comparator,
|
||||
so they must implement their own exact-decimal sorting. Chart and dataset
|
||||
queries share the result-set loader: Decimal columns that fall back to strings
|
||||
(for example, Decimals mixed with floats) also use fixed-point notation, with
|
||||
non-finite Decimals converted to null. Other chart result serialization and
|
||||
chart number formatting are unchanged.
|
||||
|
||||
@@ -115,6 +115,16 @@ The `deletion_retention.purge_soft_deleted` background task purges eligible
|
||||
archived objects older than the retention window. The default Celery beat
|
||||
configuration schedules it daily at 00:00 in the configured Celery timezone.
|
||||
The task skips purging when `SOFT_DELETE` is off or the window is zero.
|
||||
Before scanning a model's rows, it checks for a purge policy. Unsupported
|
||||
soft-deletable models are retained without a scan or a per-row purge attempt.
|
||||
Both live and dry runs report `unsupported_models`, a map from table name to
|
||||
`1` for each skipped model (not a row count), and log one warning per model.
|
||||
Each skip increments `deletion_retention.unsupported_models.<table>` once per
|
||||
run. Unsupported models are excluded from `would_purge` and do not count as
|
||||
`cascade_failures`; supported models continue to use their existing policies.
|
||||
An empty `unsupported_models` map means no registered models were skipped for
|
||||
this reason. This reporting does not add purge support for host-defined models.
|
||||
|
||||
The window resolves in this order:
|
||||
|
||||
1. A host policy, when one is installed as `SOFT_DELETE_RETENTION_DAYS_FUNC`.
|
||||
|
||||
62
docs/docs/using-superset/semantic-member-selections.mdx
Normal file
62
docs/docs/using-superset/semantic-member-selections.mdx
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Semantic member selections
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Semantic member selection versions
|
||||
|
||||
Semantic providers can declare `selection_identity_version` when changing their
|
||||
member identity encoding. Cube's stable-member-ID adapter requires
|
||||
`cube-member-id-v1`. Deploy the compatible core library, host, frontend and Cube
|
||||
adapter together; do not roll back to title-keyed code after saving ID-based charts.
|
||||
|
||||
Saved Cube selections without this version are rejected, even when a saved title
|
||||
happens to equal a member ID. No historical title mapping is recovered. Open each
|
||||
chart in Explore, choose **Start field selection**, reselect its metrics,
|
||||
dimensions, filters and sorting, then save. Reset also clears field-dependent
|
||||
formatting; it does not overwrite the saved chart until Save is chosen. Native
|
||||
filters and chart customizations have a separate reset in their configuration
|
||||
form that clears fields, pre-filters, sorting, defaults and dependencies.
|
||||
|
||||
Dashboard filters retain their own saved generation. Old filter state and
|
||||
unversioned external member overrides cannot borrow a chart's version. Recreate
|
||||
old dashboard permalink/filter state after reselection. Cross-view overlays are
|
||||
rejected unless their source identity matches the receiving semantic view;
|
||||
matching display titles are not proof of matching members.
|
||||
|
||||
The marker is a compatibility contract, not an authorization credential. API
|
||||
clients must rebuild their selections from current member IDs before supplying
|
||||
`extras.semantic_selection_version` on each query. Providers without a required
|
||||
version retain their existing behavior. No metadata-database migration or
|
||||
automatic rewrite of saved charts is performed.
|
||||
|
||||
Programmatic clients must explicitly select current member IDs. Chart-data API
|
||||
queries send `extras.semantic_selection_version`; name-based datasource queries
|
||||
and MCP `get_table` requests send top-level `semantic_selection_version`.
|
||||
Discover the value from datasource metadata (or each external metric returned by
|
||||
MCP `list_metrics`). The server never adds a missing marker from metadata.
|
||||
Explore query controls remain unavailable until explicit field initialization,
|
||||
so legacy filter subjects cannot trigger value suggestions before reselection.
|
||||
|
||||
For versioned semantic views, Matrixify **All** mode with **A to Z** or **Z to A**
|
||||
sorting cannot populate or refresh dimension values from suggestions. Existing
|
||||
saved values are preserved. For a new selection, switch to **Members** and enter
|
||||
values manually; the control displays this guidance when suggestions are unavailable.
|
||||
@@ -126,6 +126,32 @@ Build ad-hoc SQL datasets that can be used as the basis for charts:
|
||||
> "Create a dataset from: SELECT region, SUM(revenue) as total_revenue FROM orders GROUP BY region"
|
||||
> "Make a virtual dataset called 'monthly_signups' from the users table filtered to last 12 months"
|
||||
|
||||
### Query the Semantic Layer
|
||||
|
||||
Discover and query metrics across both built-in datasets and external semantic
|
||||
views (when your deployment has semantic views registered), without writing SQL:
|
||||
|
||||
- **Discover metrics** -- search metrics by name or description across every dataset and semantic view you can access, optionally including each metric's compatible dimensions
|
||||
- **Query by metric and dimension** -- get tabular results by naming metrics and dimensions instead of writing SQL, with optional filters and time ranges
|
||||
- **Progressively refine a query** -- given metrics and/or dimensions already picked, see which dimensions and metrics are compatible with that selection
|
||||
|
||||
**Example prompts:**
|
||||
|
||||
> "What metrics are available related to revenue?"
|
||||
> "Show me revenue by region for the last 30 days"
|
||||
> "I've picked the revenue metric -- what dimensions can I break it down by?"
|
||||
|
||||
:::tip Recommended workflow
|
||||
1. **`list_metrics`** -- search for a metric; note its `dataset_id` (built-in) or `view_id` (external semantic view), plus its `semantic_selection_version` when non-null
|
||||
2. **`get_table`** -- pass that `dataset_id`/`view_id` (and, for a versioned external view, its `semantic_selection_version`) along with metric and dimension names to get results
|
||||
3. **`get_compatible_dimensions`** / **`get_compatible_metrics`** -- as the user adds selections, check what can validly be combined. `get_compatible_dimensions` returns the full compatible set (including dimensions already selected), so filter out your current picks client-side; `get_compatible_metrics` already excludes selected metrics for built-in datasets but not for external semantic views
|
||||
|
||||
Built-in datasets accept any combination of saved metrics and groupby-enabled
|
||||
columns since a SQL `GROUP BY` has no metric-level constraints. External
|
||||
semantic views can impose additional compatibility rules, which these tools
|
||||
surface instead of failing the query outright.
|
||||
:::
|
||||
|
||||
### Run SQL Queries
|
||||
|
||||
Execute SQL directly through your AI assistant:
|
||||
@@ -292,6 +318,15 @@ that those charts may need repair; deletion does not rewrite charts. References
|
||||
embedded in SQL expressions are not inferred. If a chart has malformed query-context
|
||||
JSON, impact reporting still checks its saved form data and other accessible charts.
|
||||
|
||||
### Semantic Layer
|
||||
|
||||
| Tool | Description |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `list_metrics` | Discover metrics by name/description across built-in datasets and external semantic views; pass `include_compatible_dimensions` to embed each metric's compatible dimensions (off by default) |
|
||||
| `get_table` | Query a dataset or semantic view by metric and dimension names, with optional filters, time range, and sorting |
|
||||
| `get_compatible_dimensions` | Given metrics/dimensions already selected, return the full set of dimensions compatible with that selection (may include ones already selected) |
|
||||
| `get_compatible_metrics` | Given metrics/dimensions already selected, return the compatible metrics; already-selected metrics are excluded for built-in datasets, while external semantic views return whatever their connector reports and may include ones already selected |
|
||||
|
||||
### Charts
|
||||
|
||||
| Tool | Description |
|
||||
@@ -320,6 +355,53 @@ JSON, impact reporting still checks its saved form data and other accessible cha
|
||||
| `delete_dashboard` | Delete a dashboard by ID, UUID, or slug; leaves its charts intact (soft-deletes to trash when `SOFT_DELETE` is enabled; fails if alerts/reports are still attached, checked before editorship; otherwise returns `permission_denied` if the caller isn't an editor of the dashboard — owners, Admins, and explicitly granted editors qualify) |
|
||||
| `restore_dashboard` | Restore a soft-deleted dashboard from trash by ID or UUID (slug lookup does not cover trashed dashboards); not gated on `SOFT_DELETE`, so rows trashed while it was enabled stay recoverable, but only soft-deleted dashboards can be restored — permanently deleted dashboards are unrecoverable; returns `permission_denied` if the caller can see the dashboard but isn't an editor of it, or `NotFound` if the dashboard is outside the caller's RBAC visibility entirely |
|
||||
|
||||
### Dashboard text components
|
||||
|
||||
Use `manage_dashboard_markdown` to add, update, or remove markdown tiles,
|
||||
section headers, and dividers without replacing the dashboard's raw layout.
|
||||
The caller needs Dashboard write permission and editorship of the dashboard.
|
||||
|
||||
```json
|
||||
{
|
||||
"request": {
|
||||
"dashboard_id": 42,
|
||||
"add": [
|
||||
{"component_type": "header", "text": "Sales overview"},
|
||||
{"component_type": "markdown", "code": "**Updated daily**", "width": 12},
|
||||
{"component_type": "divider"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Markdown tiles are appended in their own rows; headers and dividers are appended
|
||||
directly to the grid or tab. Each addition can specify `target_tab` by name or
|
||||
component ID (available from `get_dashboard_layout`), including nested tabs.
|
||||
Without it, additions use the first top-level tab, or the grid on dashboards
|
||||
without top-level tabs. Markdown width is 1–12 columns (default 4); height is in
|
||||
8-pixel grid units (default 50). Width updates
|
||||
must fit the space remaining beside other components in the same 12-column row;
|
||||
otherwise, the error reports the available width. Components removed in the same
|
||||
request do not count toward that row width. If any operation is rejected, the
|
||||
layout remains unchanged.
|
||||
|
||||
The response includes generated `added_component_ids` and a `components` summary.
|
||||
Reuse those IDs in `update`, for example
|
||||
`{"id": "MARKDOWN-…", "code": "Revised notes"}`, or in `remove`, for example
|
||||
`["MARKDOWN-…"]`. Updates merge only supplied non-null fields and cannot change a
|
||||
component's type or placement. Each update requires at least one non-null field
|
||||
besides `id`; ID-only or all-null updates are rejected. The reserved dashboard-title `HEADER_ID` is not a
|
||||
section header and cannot be changed through this tool.
|
||||
|
||||
Operations in one request are validated before saving. Existing charts, native
|
||||
filters, and unrelated dashboard properties are preserved. Removing a tile prunes
|
||||
empty row/column wrappers but retains tabs. Invalid component IDs, incompatible
|
||||
fields, invalid layouts, and conflicting update/removal operations return errors
|
||||
without saving partial changes. Markdown uses the dashboard's existing renderer
|
||||
and HTML sanitization configuration. Concurrent layout edits are last-write-wins;
|
||||
avoid simultaneous edits from the UI and MCP clients. This tool does not reorder
|
||||
or move existing components; use the dashboard editor for those operations.
|
||||
|
||||
### SQL
|
||||
|
||||
| Tool | Description |
|
||||
@@ -328,6 +410,14 @@ JSON, impact reporting still checks its saved form data and other accessible cha
|
||||
| `save_sql_query` | Persist a SQL query to SQL Lab's saved queries |
|
||||
| `open_sql_lab_with_context` | Open SQL Lab with a pre-populated query |
|
||||
|
||||
`execute_sql` returns the last data-bearing statement's `rows`, `columns`, and
|
||||
`row_count` at the top level. Each entry in `statements` retains execution
|
||||
metadata, but only earlier data-bearing statements include `data.rows` and
|
||||
`data.columns`. The last data-bearing statement's `data` is `null`, so its result
|
||||
is not repeated, even if DML statements follow it. Statements without result data
|
||||
also have `data: null`. For each statement, `executed_sql` is `null` when it equals
|
||||
`original_sql`; use `original_sql` in that case.
|
||||
|
||||
### Databases
|
||||
|
||||
| Tool | Description |
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@storybook/addon-docs": "^10.6.0",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.16.2",
|
||||
"@swc/core": "^1.16.12",
|
||||
"antd": "^6.6.5",
|
||||
"docusaurus-plugin-openapi-docs": "^5.2.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.2.0",
|
||||
@@ -83,8 +83,8 @@
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^19.1.8",
|
||||
"oxfmt": "^0.70.0",
|
||||
"oxlint": "^1.85.0",
|
||||
"oxlint-tsgolint": "^7.0.2002",
|
||||
"oxlint": "^1.86.0",
|
||||
"oxlint-tsgolint": "^7.0.2003",
|
||||
"typescript": "7.0.2",
|
||||
"webpack": "^5.111.1"
|
||||
},
|
||||
|
||||
@@ -686,6 +686,7 @@
|
||||
"pypi_packages": ["pydoris"],
|
||||
"connection_string": "doris://{username}:{password}@{host}:{port}/{catalog}.{database}",
|
||||
"default_port": 9030,
|
||||
"notes": "The SSL switch sets ssl_mode=VERIFY_CA. With MariaDB Connector/C, which mysqlclient uses in the Superset image, this also verifies the server hostname. Add ssl_ca=<path to the CA certificate> in Additional parameters and use a Doris FE certificate that matches the host. A Doris FE on its default self-signed certificate fails to connect once the switch is on.",
|
||||
"parameters": {
|
||||
"username": "User name",
|
||||
"password": "Password",
|
||||
@@ -6964,6 +6965,25 @@
|
||||
},
|
||||
"notes": "Uses the Data API for serverless access. Standard PostgreSQL connections also work with psycopg2.",
|
||||
"categories": ["Cloud - AWS", "Hosted Open Source"]
|
||||
},
|
||||
{
|
||||
"name": "ClickHouse Managed Postgres",
|
||||
"description": "Managed PostgreSQL from ClickHouse.",
|
||||
"logo": "clickhouse.png",
|
||||
"homepage_url": "https://clickhouse.com/cloud/postgres",
|
||||
"pypi_packages": ["psycopg2"],
|
||||
"connection_string": "postgresql+psycopg2://{username}:{password}@{host}:{port}/{database}?sslmode=verify-full&sslrootcert={ca_certificate_path}",
|
||||
"parameters": {
|
||||
"username": "Database user",
|
||||
"password": "URL-encoded database password",
|
||||
"host": "Direct PostgreSQL hostname from the Connect dialog",
|
||||
"port": "Port from the Connect dialog (default: 5432)",
|
||||
"database": "Database name",
|
||||
"ca_certificate_path": "URL-encoded absolute path to the instance CA certificate on the Superset server"
|
||||
},
|
||||
"notes": "In ClickHouse Cloud, open the Connect menu and use the Directly option. Download the instance-specific CA certificate from Settings and make it available to each Superset process that connects to the database. Use verify-full to verify the certificate and hostname.",
|
||||
"docs_url": "https://clickhouse.com/docs/products/managed-postgres/connection",
|
||||
"categories": ["HOSTED_OPEN_SOURCE"]
|
||||
}
|
||||
],
|
||||
"category": "Traditional RDBMS",
|
||||
|
||||
27
docs/static/resources/openapi.json
vendored
27
docs/static/resources/openapi.json
vendored
@@ -1245,6 +1245,11 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"semantic_selection_version": {
|
||||
"description": "Identity encoding used for semantic member selections.",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"time_grain_sqla": {
|
||||
"description": "To what level of granularity should the temporal column be aggregated. Supports [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.",
|
||||
"example": "P1D",
|
||||
@@ -3686,7 +3691,7 @@
|
||||
"CurrentUserPutSchema": {
|
||||
"properties": {
|
||||
"current_password": {
|
||||
"description": "The current user's existing password",
|
||||
"description": "The current user's existing password; required when the account has one",
|
||||
"type": "string",
|
||||
"writeOnly": true
|
||||
},
|
||||
@@ -3703,7 +3708,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"description": "The current user's password for authentication",
|
||||
"description": "The current user's new password; requires current_password when the account already has one",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -7087,6 +7092,12 @@
|
||||
"arrow"
|
||||
]
|
||||
},
|
||||
"semantic_selection_version": {
|
||||
"default": null,
|
||||
"description": "Identity version from datasource metadata, after selecting its current metric and dimension IDs. Never infer legacy IDs.",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"time_column": {
|
||||
"default": null,
|
||||
"description": "Temporal column the time range applies to. Inferred from the datasource when omitted.",
|
||||
@@ -19732,7 +19743,7 @@
|
||||
},
|
||||
"/api/v1/dashboard/{pk}/export_as_example/": {
|
||||
"get": {
|
||||
"description": "Exports a dashboard with its charts and datasets in the example format used by the Superset example loading system. The export includes Parquet data files and YAML configuration files.",
|
||||
"description": "Exports a dashboard with its charts and datasets in the example format used by the Superset example loading system. The export includes Parquet data files and YAML configuration files. Charts and native-filter targets that use semantic views are not supported.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The dashboard id",
|
||||
@@ -19782,6 +19793,9 @@
|
||||
"404": {
|
||||
"$ref": "#/components/responses/404"
|
||||
},
|
||||
"422": {
|
||||
"description": "The dashboard contains unsupported semantic-view assets"
|
||||
},
|
||||
"500": {
|
||||
"$ref": "#/components/responses/500"
|
||||
}
|
||||
@@ -25326,6 +25340,13 @@
|
||||
]
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"suggestions_status": {
|
||||
"description": "Suggestions are disabled for versioned semantic views; enter values manually.",
|
||||
"enum": [
|
||||
"unavailable_versioned_view"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
431
docs/yarn.lock
431
docs/yarn.lock
@@ -3173,130 +3173,130 @@
|
||||
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.70.0.tgz#6d8d33d38063498ad9ead3b3f9f1dd2aa6ce03be"
|
||||
integrity sha512-HlIZEn+WzLQL0DszNzldiRl/DPRCX5R0Vkt6qeUPR1YHwy52hZZo4x6HoTOVmKRP2wUiwPGtKsihNY/f8KRaBg==
|
||||
|
||||
"@oxlint-tsgolint/darwin-arm64@7.0.2002":
|
||||
version "7.0.2002"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2002.tgz#75cc95455005802d26065af49fd47dfb32830a36"
|
||||
integrity sha512-E3yYb/tI+5M3SqGgKwrwUD0nmjA2FMGD1xFbma46qi8Y7+nPTlGY6bZk/hqxijc9Vda/tdtUxQ2d5zGIEX9qlg==
|
||||
"@oxlint-tsgolint/darwin-arm64@7.0.2003":
|
||||
version "7.0.2003"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2003.tgz#a9048bb88e36fb7d3523d4c444509d96e06d5d46"
|
||||
integrity sha512-TgV33rXr6ueXBwvc+0nssUkTBSXHJxv77I8p4RCjDjCnvexHtmoPiudVRDfj3S3puMDdeQdHW6jdUgUPy3nr/w==
|
||||
|
||||
"@oxlint-tsgolint/darwin-x64@7.0.2002":
|
||||
version "7.0.2002"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2002.tgz#fea67ca1fe83d25d22d2378b347a9a751d7ca4a0"
|
||||
integrity sha512-qEaMySdvzs8faaJwMO4dFtPFCa+degFCz+HbkFQ0REcIJqJ16KmC0smt30FAI9TeEHCoAtz6sU/6acJvi4uaxg==
|
||||
"@oxlint-tsgolint/darwin-x64@7.0.2003":
|
||||
version "7.0.2003"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2003.tgz#34933fbc2f36c418591808a75a4a164eb83afdf0"
|
||||
integrity sha512-hY3FMAjIaPDdK3FNxyRXRfHPOFeoBn6dmnLyKZgQ2IbTTD1adXhl6PfCIqHhCPvurigv7nf7mYuWZZ19MmJzmg==
|
||||
|
||||
"@oxlint-tsgolint/linux-arm64@7.0.2002":
|
||||
version "7.0.2002"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2002.tgz#f2d49e0c76b7f257ebb10f902201c3dde7ff0e46"
|
||||
integrity sha512-bqPObCNIR6tDQSc0Xn9Nu7EfWRXFB+qFzADRl/q6YA8zT9YUg3pGUbmicV2hV91uLpiot74so9ocGjx35SaX6w==
|
||||
"@oxlint-tsgolint/linux-arm64@7.0.2003":
|
||||
version "7.0.2003"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2003.tgz#b222cb1f8366ea7de441551268301efa82efc379"
|
||||
integrity sha512-eAET4JpyfBbg8SO0K74o4R55tEjVC292pIyxGOw2XGf8x/HrPNyxwQ478jMYOM54FIVOHc8sJ6VRHxluy6lucw==
|
||||
|
||||
"@oxlint-tsgolint/linux-x64@7.0.2002":
|
||||
version "7.0.2002"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2002.tgz#397d85a072b9d4a38a4d61c96a85f45127024d89"
|
||||
integrity sha512-E5CcRALAYdiQMbdYjEqlLOiYuEdt21RtXiyxIoD8bfypdfkpWppacijKOR4i0DtLXFRVwCGQggZBWk5bOIpU1A==
|
||||
"@oxlint-tsgolint/linux-x64@7.0.2003":
|
||||
version "7.0.2003"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2003.tgz#1e1339078dd464757f543eca9a60dc9d86ad6e75"
|
||||
integrity sha512-GXdyO/XyqDJ3s/llR/oOktLsYNjZtWSQBy0JMc+/0gsNPvTcseKPhn9c6KcFyWmnr6H4vljYALbujonqSzzEGw==
|
||||
|
||||
"@oxlint-tsgolint/win32-arm64@7.0.2002":
|
||||
version "7.0.2002"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2002.tgz#2701a93c74484c3b560090ca1ce78b4c7d8ffaab"
|
||||
integrity sha512-L+ul0ZFDiz/ZmTwCQ1J/qS7S6s45ktiaFqb+ZIzx+1sSdtdRxm8/H0+p4VDQYrCKBFzKNb6VjUlm48bNEe3BUg==
|
||||
"@oxlint-tsgolint/win32-arm64@7.0.2003":
|
||||
version "7.0.2003"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2003.tgz#b4d0e6c6e7c0212be92c447e64b49f0d8d8173db"
|
||||
integrity sha512-TWauXnPfet0VgpmrstoAK58eJ4gbuwPUuUTQmYQAzE/RG1CpnxXtrKUJULf6lyerPE4KN3gM+DflIA1hOH+38Q==
|
||||
|
||||
"@oxlint-tsgolint/win32-x64@7.0.2002":
|
||||
version "7.0.2002"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2002.tgz#6d3a6c4d9229765ad45eb2ecdecd8d1ceffefee4"
|
||||
integrity sha512-zkZeJfo4UmYiCzj77u85eCDpdxOiQiSAsV6j9Pvj5e7Vu5+MvwLtK1xmVutE77tJmUJ3eBOSXHR6WgY4AwHHGQ==
|
||||
"@oxlint-tsgolint/win32-x64@7.0.2003":
|
||||
version "7.0.2003"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2003.tgz#08d5ae59e655e32d332ec1a74190b8009754d176"
|
||||
integrity sha512-DoRmfe7j8VqNlukp8liRVW45GQDhzRccNenjD/pdzelgtffW47pCMd1xbJLkPaPbKnTwID3onn3VZlL7JbebEA==
|
||||
|
||||
"@oxlint/binding-android-arm-eabi@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.85.0.tgz#b95305795de282c081492653abada2a46f68c714"
|
||||
integrity sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg==
|
||||
"@oxlint/binding-android-arm-eabi@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.86.0.tgz#53c7e6c799761229eddfd16ae0cf4df852d72afc"
|
||||
integrity sha512-63Ozq6yQn79B2UxjSZ9huJmI+3/ZPI7hxAe6TyFh9oQL447KnIMoffOdtog+h4KILbQr61ZRvFPW5gYbAUgpgQ==
|
||||
|
||||
"@oxlint/binding-android-arm64@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.85.0.tgz#afe58d09f29ba7a56415fefcf5a55d6baa758d50"
|
||||
integrity sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA==
|
||||
"@oxlint/binding-android-arm64@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.86.0.tgz#7e4b00a16f9849a76757d4aee825a4961e4cb308"
|
||||
integrity sha512-JdLGp44phbf/0Du236Dt06yfK6Qw8xhTFeDvHJdjk167r59OfP5+BL31mgMYVnswS8e+j6tFuccSY0QDC/lgCw==
|
||||
|
||||
"@oxlint/binding-darwin-arm64@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.85.0.tgz#d63f7e3fefa7073edd5a42b0a6ad07a458fbffef"
|
||||
integrity sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA==
|
||||
"@oxlint/binding-darwin-arm64@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.86.0.tgz#12b0d390e913da1ebb14190f9cbcb6e366c7525d"
|
||||
integrity sha512-h+vkOr4ik6KLFCXdtNVEj9xfnXfNUpPoF95QjORdhiSpLqQePzcN81kGHjAgsw1/G7WBWK5KsFKbI2UK8EyzMA==
|
||||
|
||||
"@oxlint/binding-darwin-x64@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.85.0.tgz#41c75b0181f360b7cc2654e478c95037be780708"
|
||||
integrity sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw==
|
||||
"@oxlint/binding-darwin-x64@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.86.0.tgz#864c33d7aa3c0e4547604054fe4101e00f61685b"
|
||||
integrity sha512-wbglRuH5eKsp68keTOOeKDHU1astWSRYFsh2bwKQTAPb44rD6kVgzYI1ZrAJRTc6/7PH55ZXFCQx0rQXrj0UdA==
|
||||
|
||||
"@oxlint/binding-freebsd-x64@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.85.0.tgz#252b099dd4c4c3d5ee7fc53a774d58a37ac6a948"
|
||||
integrity sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg==
|
||||
"@oxlint/binding-freebsd-x64@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.86.0.tgz#62b09121f28b3ed7664dc718ad3cd15acf6153bb"
|
||||
integrity sha512-yTQ7QSOlNfQsrfbeVweTiegQJLKPjUKgXp/JpGiFzza6Hp29S9LAoH77TM3L0RjK39iQRZB5lk5iq2t241Mfag==
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.85.0.tgz#ae16c5b5f55480dba86ce7de377ede00964ea756"
|
||||
integrity sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw==
|
||||
"@oxlint/binding-linux-arm-gnueabihf@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.86.0.tgz#81f07f56e0e69d26387be6a1a185512873642737"
|
||||
integrity sha512-QYJGy0E2Tv8GAayyBIeHFTwQb8u+zrvrOYrifuDGZDr9ad5XsDhiMScpZymhS9fwWjqrgG2ZUwT4vpY0ISqp4Q==
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.85.0.tgz#a903f6043725cc311c81e2c5a1e3383621b0d2d9"
|
||||
integrity sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g==
|
||||
"@oxlint/binding-linux-arm-musleabihf@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.86.0.tgz#7a3a20e9f5bb04a4d0d4111bd300b7cf8dcb85a4"
|
||||
integrity sha512-oQo83CafLT8p3qh0AGkN0tSUIh5Hm63mfDsoqUmBQUBnSZLNBAbyRaAftfGY5a0cKZ34dG6jAtMYgcY5kZqt5g==
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.85.0.tgz#8afee8ea5be0c4edb74d2a25eee1e510c77dbf8e"
|
||||
integrity sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg==
|
||||
"@oxlint/binding-linux-arm64-gnu@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.86.0.tgz#b876d20c845fb14f89a3e2bcdff4be8d127c9169"
|
||||
integrity sha512-EM6wy5c2UM12qPb7iDJDBAZvHyfEJGH6iosFgaFiaRZpirz99//EHcUyD2KAskz07x1TULuiAtsojf4nvWWksw==
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.85.0.tgz#62cc783fb610f1b0be9328364eec8a07add3e3e7"
|
||||
integrity sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw==
|
||||
"@oxlint/binding-linux-arm64-musl@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.86.0.tgz#c9ee28093e7524364b918e1cd025286ff4c2fa82"
|
||||
integrity sha512-vCiUQb9ZNzZalxYRU4mlZwUqT5f+c7Sk9DwUOplWHtik3Dl4r3DVnUB5tBeRuTAUe6MUVCTylNW7TkkN8J1oTw==
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.85.0.tgz#530ec6f185f8bc7f86819b44483e987599ccbcf1"
|
||||
integrity sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw==
|
||||
"@oxlint/binding-linux-ppc64-gnu@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.86.0.tgz#7964bf1e7593403e6b5013eece02cd3018a7ed1b"
|
||||
integrity sha512-T0AzE89yjTYmOhB/hb6i+rWTpe2mYt+xiguBhs9DEPIJTZ1H8OYqsE4VxJHHw2Wlq8Mztwvp2P8NV87ZOuhPRw==
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.85.0.tgz#35c430e60ef8d8dddafd16f79adc1bf936ddeddb"
|
||||
integrity sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg==
|
||||
"@oxlint/binding-linux-riscv64-gnu@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.86.0.tgz#dd843ff3e41715311a30ce693ab4df91150dcb38"
|
||||
integrity sha512-s49r0K2f5Oc7fJoMUO1WkdhYK5Ob79uYRwWQ/XNoGysaOq3RUGmGUbHSuXiyydTTA+P9xEdDEnnlZVXiaSJhlw==
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.85.0.tgz#426eb8fb43852707dd872bbc0e8415832f9307e8"
|
||||
integrity sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA==
|
||||
"@oxlint/binding-linux-riscv64-musl@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.86.0.tgz#7bb8bdb9c03b651f57f868ed2279f940ef19b8fc"
|
||||
integrity sha512-JIa2tYR8ayb3nvcMSZzn4O/xTy13S7MHllu7cBD+B5qDQXXSRy/Vwk1aNpiTjZmBJGwKCHOy/NvnY5Xcwg27uQ==
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.85.0.tgz#f53c7132389e71dc68cad7a319fc0a92abbb1fcf"
|
||||
integrity sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ==
|
||||
"@oxlint/binding-linux-s390x-gnu@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.86.0.tgz#14aba52915f8df19b2802a5ec5d2308ff8f2ddb2"
|
||||
integrity sha512-XwXMyBsAuWZkhZU74UNcfQKr1CJJAKnS80dZzOnjl6A4fKcf91IjFgidegQugMHA5SN8ulwDXEDV1yPntjXgQQ==
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.85.0.tgz#74769aa504ce7c2571574684ed36387b40c9bfb7"
|
||||
integrity sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg==
|
||||
"@oxlint/binding-linux-x64-gnu@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.86.0.tgz#6bb162b2359860ec6c0cb1d0ad7001045ac21d7e"
|
||||
integrity sha512-C1WjukSyMnr66b+w1/tV8RFVv6d9v0MzDf4p9IxVXknqgmTHBgZh1pccN1eHzFr0b9Tbb3OXoPsAAdAuHAQfeA==
|
||||
|
||||
"@oxlint/binding-linux-x64-musl@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.85.0.tgz#0d99815e87e85c51c4ab0e351ddf9b643578e3b6"
|
||||
integrity sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA==
|
||||
"@oxlint/binding-linux-x64-musl@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.86.0.tgz#9235a290fc704fb75ec62302c4f935f5ee966d33"
|
||||
integrity sha512-ap6KLmvC38c6MdYzsIh25cXQupqYvjd37tMNftzrX1DCtkX1Gcf2B+S2B17dD4lKa5c5gJog7DQJyDo82PBKyw==
|
||||
|
||||
"@oxlint/binding-openharmony-arm64@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.85.0.tgz#051fc72cd1a103b73d758f3f2eb9d3748ba4aafc"
|
||||
integrity sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA==
|
||||
"@oxlint/binding-openharmony-arm64@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.86.0.tgz#d7ebd7d549912d1dd567a9f0778e9a270021244e"
|
||||
integrity sha512-996Q9w/2GLwNyy5Djs39Jt09aFlr4b7TPhoj7dvo5FHmcyiF68nPz73HNc5UAYKIYeK+M9JzPQ4Wg1r1dXvP6A==
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.85.0.tgz#6bd2fd33ae029685f0f162c8ba5976c3e2059a6b"
|
||||
integrity sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA==
|
||||
"@oxlint/binding-win32-arm64-msvc@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.86.0.tgz#902c2cd7292dfaf0d889a094806d337848ba7486"
|
||||
integrity sha512-9uTCHYTknNkug5+46ViSoUMuOuGWK61D87QnblU6CXaWKJC630Z7dbls61vdHxya/cR2ycQZ6Mm6ckgF6Cag5g==
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.85.0.tgz#d8ab96fd47c575baf78713425680e0533aa2436f"
|
||||
integrity sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A==
|
||||
"@oxlint/binding-win32-ia32-msvc@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.86.0.tgz#f06500f4b1c3abac324710eded785537aa6799e3"
|
||||
integrity sha512-a47ga7EKVfMU8rO0i1m3mGwfNRj3FhQyt1d/ukO/E9u1HHF3KpR3tyb3VhuJBGEVPF8uR4exSyHUyx7xbE+uxw==
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc@1.85.0":
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.85.0.tgz#d8ab9af17878dfee4f6b57581eb116cb10a9abd6"
|
||||
integrity sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA==
|
||||
"@oxlint/binding-win32-x64-msvc@1.86.0":
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.86.0.tgz#bef9b82d6408a5a228b0e31902c8bdd482ad169c"
|
||||
integrity sha512-az4dzifCRzES1crwu6pDFXzJTIanrdxXFL9TbiJWrx9LKcuacbR83aPvxDA1LKLNswi/RoqitC7Afcflhu3zBQ==
|
||||
|
||||
"@parcel/watcher-android-arm64@2.5.6":
|
||||
version "2.5.6"
|
||||
@@ -4830,86 +4830,86 @@
|
||||
dependencies:
|
||||
apg-lite "^1.0.4"
|
||||
|
||||
"@swc/core-darwin-arm64@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.2.tgz#b180d8647923c75e5aabbf4ea6e543d113aa3c79"
|
||||
integrity sha512-i/j0HNbnn79qnTVPicvay92Nark8fW8NQqn1e2mGERjUXNpBV0+SwQxlRpk2zBhn6laJ8PDI6Kn1nHZhnz3LCA==
|
||||
"@swc/core-darwin-arm64@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.12.tgz#925a647db6fb235f6d7dff0e3c886ede8ad7fa55"
|
||||
integrity sha512-eu616yP7s+M+crBEEO8Pg8DooMNliTQrXD4NyiBCXIFGSNSEbn4kQRACWuR2O+OyoMBwk6FXL3zFVvR+iJs/XA==
|
||||
|
||||
"@swc/core-darwin-x64@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.16.2.tgz#ae8286ddd5a6f1b0f1c540e3338682fed7ceb2ab"
|
||||
integrity sha512-HrwqHyEyHVXO3qTk8EkNK7/b6sOZSEoNh+pot6RdE5x0LbNqfo8LtJUvi3UTXr+5ja/o5HbJdW80eCXo+NjbiA==
|
||||
"@swc/core-darwin-x64@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.16.12.tgz#642d80a8f99f4e296b0e23918169ea6c06ab4fed"
|
||||
integrity sha512-vOT0kyUHp0lLBrcN0/ycxtOaVEttt7TLO7ztPrDhAPn26qq1g7vp+1xA5Dp5v9Woj+6+bS3YPCIdhhkgqXcMcg==
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.2.tgz#9879d6eb91d3663dfcae557cde866642b21b7590"
|
||||
integrity sha512-MdXi83Z/gGp1LIrg+h7HKxiul/z/Bty/ZJSvYAFqDl9zteC1XLSAZdScquKtXPp50rdyXqritTDCqQBhwVfZKA==
|
||||
"@swc/core-linux-arm-gnueabihf@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.12.tgz#9897f5c66151183ec0c664bc237294d0a1a6822c"
|
||||
integrity sha512-FVA9a7sxeisBzSh+z+4em/3GUhzzU7E1wjGl34RLklzeznoKZxjRCgnJGr4VjeCLeL+hh46qQtbj5LcMIzPi0w==
|
||||
|
||||
"@swc/core-linux-arm64-gnu@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.2.tgz#9684df59917e4b0de8f528f04cf7f6e7526ca7b1"
|
||||
integrity sha512-/jcTmK6Ktz3owM3YtiKvjofV6p3VpHnYzTIrOGwDIOsDigRAAVuZ8east33wYO/7UTdKYFlyHNnJNT0WJqOA3Q==
|
||||
"@swc/core-linux-arm64-gnu@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.12.tgz#d610b9168fcc568dec24eab77d41873edd09437a"
|
||||
integrity sha512-K0RcZG1Xy0A+DprzW0A2z7vM/p5cPV7qYvv/LjnwRfS6Fph9HrI8PnplVycKrwLWfNyAcMrurFsYDWVPALJnwQ==
|
||||
|
||||
"@swc/core-linux-arm64-musl@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.2.tgz#36b59ed1a46795755e6d083e6693753c5ea1e7d0"
|
||||
integrity sha512-4gFarKaFnlJTSlJYKmMhV4u+3YE4uYfiydpBoYjmgQhCf9lAieOq+WilZaK9vVSHeqLuQpTEiGULZqAdsRX5Dw==
|
||||
"@swc/core-linux-arm64-musl@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.12.tgz#77b57cb2fb87891a9c1b72349480f95545a51a50"
|
||||
integrity sha512-nqXzwLk9aLb3ZH4r5WVtn8B3RKLFqYjy/ApK753QYYoQvnYJipKnF09vUI9m65roOPdf/wt+LKvZ72EgUvPBiw==
|
||||
|
||||
"@swc/core-linux-ppc64-gnu@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.2.tgz#b0f648e760031e4674e0d07eb4991dfb468d21ee"
|
||||
integrity sha512-syqSLGd6KlZ1PciNzs6bIUlhOuFztZufebOHaERjc4N4SqNZxyqYd4I+jj/EfOYnpe0kNjccn9HJLN1p5dz3+w==
|
||||
"@swc/core-linux-ppc64-gnu@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.12.tgz#2d51317abae9c297a8dace3f7cbab8b2d444580a"
|
||||
integrity sha512-kJab4XOOe590gTj2HOEqIrKJjwNaJBZQsd1fn1yYYg3C0YMG8kgXsEsIpZNgdxw8LsUeeS7iwUSSwEHY3PVSEQ==
|
||||
|
||||
"@swc/core-linux-s390x-gnu@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.2.tgz#7757e4999f4e236a8bb05bd04367574ce926d4cc"
|
||||
integrity sha512-ZBBLK+ewGyXLzWeMS7wbKtWBdnif6etn7xvPY/iOfbdsjX/+bgkp1pQt2lWF2wlu2hXYZuhJ/tHZE/QR8/apzg==
|
||||
"@swc/core-linux-s390x-gnu@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.12.tgz#2659cee9526ad644945874a8ffdcc30335462663"
|
||||
integrity sha512-FWttNqogiRwZwmqRdPC7vEBa9HV9RbZBoUqLP0CbWhUPmf6TxYTRTjh9JA5EAsAlRJ7YiwXSoCT0ersJfTUiXw==
|
||||
|
||||
"@swc/core-linux-x64-gnu@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.2.tgz#a25271e8199c46e54f1042c9d98fbf38fdb4736c"
|
||||
integrity sha512-LyHJgxCA4Tje0ysBMbEb0tt/ie8kgUKoFE3JAKFhpevmTmhYEoC0H9s47WuDsqiFckF1ITUguZIXJG6K5e0dvg==
|
||||
"@swc/core-linux-x64-gnu@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.12.tgz#f127eeaa6602a61fa13a2f072b79b9987186b4ad"
|
||||
integrity sha512-8JJ0qouw0h8IXNXj+GLL8kPwcjhPI11/mW4ocmeweMJdC/WjuDQGaDbYErKRqwbkTUjArLx/938CQa2ulUtCnw==
|
||||
|
||||
"@swc/core-linux-x64-musl@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.2.tgz#afc4e5f5e519e4e2ca75a560496fbc6b5164718d"
|
||||
integrity sha512-PghXJlVM1cgtLfNUR1vxFo1z+PDRAe8cWAJlZZ7spmeiN7BospGXg/MHUg7oNSgwSX7Zo//YKv9P5yD9apsFJQ==
|
||||
"@swc/core-linux-x64-musl@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.12.tgz#978eed0a392b3341cad7152cb5d88b00e4fec2da"
|
||||
integrity sha512-0ev8zRnVSaqJEi63reVHyv81Xce1x6BhoASBPX4nd3recoExu+2Hlp70kQThe1MDohfyYzksErwjPb1Nj0rCFQ==
|
||||
|
||||
"@swc/core-win32-arm64-msvc@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.2.tgz#98b936a9cf2faad87c92c979161f17f22c55b742"
|
||||
integrity sha512-StTOSefYBxemvNYYUI3UmO1a8y+hSPjjfHogC2TEHL+Z1PlEBim/XtLas5rS04jAzT9RrNmbtX911SZ42H9jSQ==
|
||||
"@swc/core-win32-arm64-msvc@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.12.tgz#24c6bab47eda721780db54843929b89d0db2e9d5"
|
||||
integrity sha512-0Cje91Dhwv18EwK4EVJ6UVFsaz19ndeHvmR+55UkENe3CLxq5Qk6+KkFSbxFgfHwYwXmiMpVVJt5MjfEsXy8yA==
|
||||
|
||||
"@swc/core-win32-ia32-msvc@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.2.tgz#2e30f6a8f78b0c79268e6d39f0ae3b378919449c"
|
||||
integrity sha512-fycER209DYIzsibpTMC+chND05OfOjgztWL9U8OE6/uUlsOUZH3eh98isBLEnOymYUhlJLEt5++W1+KL/FOh5Q==
|
||||
"@swc/core-win32-ia32-msvc@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.12.tgz#94400b876320acd0827bedaf31f5726a0f82622a"
|
||||
integrity sha512-1kVJta17y80uk0eBOQ5UYpr3JvYgg4TxPO4nAErz34f7ATE7ZSnVnMRHC4+QqFNZLdbtww/QqwhQW1El7pcUkQ==
|
||||
|
||||
"@swc/core-win32-x64-msvc@1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.2.tgz#a25e080ed09770e68c1dc5a2a7e973a01dc40b2f"
|
||||
integrity sha512-cSd1z6ivSrJPVr+moVwOHWjeKy6TpO4/Shwcv5KCrKYXCccxwh4pRy1C3fDioNx2PF1jPZWHKZjtXt+Be9VbaQ==
|
||||
"@swc/core-win32-x64-msvc@1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.12.tgz#e092d6fe1de40e4975693fba7c57a7cb693c896d"
|
||||
integrity sha512-urCL+y3TohYUEwlXDjhfqxSUU/17N8iCZdhK6HxGXZgMQB1fK6nSu4zQ33oh2mBvFnIXbG6VM9eK3QYrepPc3A==
|
||||
|
||||
"@swc/core@^1.15.40", "@swc/core@^1.16.2":
|
||||
version "1.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.16.2.tgz#6cc0c7f27938bea7208467a18ab518fa23b78367"
|
||||
integrity sha512-95I4kiSMeveI/Mhi+tE4fiWcWLUMfzfKrk0jtr8LRMqHgOgq+xHS+zExkDqoO4b5OeeuXHMWVdD5MeP3X6sULw==
|
||||
"@swc/core@^1.15.40", "@swc/core@^1.16.12":
|
||||
version "1.16.12"
|
||||
resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.16.12.tgz#92693396dca6e9d88a763994be447ad907d8e8f6"
|
||||
integrity sha512-ksgw/DvuDrxAFi7tfdndxKSyoAls7VNkIlqlASySpe0tbPmvMH2JcAoHMbRZyE6f10EAf289blkvbe/KCOblxw==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
"@swc/types" "^0.1.28"
|
||||
optionalDependencies:
|
||||
"@swc/core-darwin-arm64" "1.16.2"
|
||||
"@swc/core-darwin-x64" "1.16.2"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.16.2"
|
||||
"@swc/core-linux-arm64-gnu" "1.16.2"
|
||||
"@swc/core-linux-arm64-musl" "1.16.2"
|
||||
"@swc/core-linux-ppc64-gnu" "1.16.2"
|
||||
"@swc/core-linux-s390x-gnu" "1.16.2"
|
||||
"@swc/core-linux-x64-gnu" "1.16.2"
|
||||
"@swc/core-linux-x64-musl" "1.16.2"
|
||||
"@swc/core-win32-arm64-msvc" "1.16.2"
|
||||
"@swc/core-win32-ia32-msvc" "1.16.2"
|
||||
"@swc/core-win32-x64-msvc" "1.16.2"
|
||||
"@swc/core-darwin-arm64" "1.16.12"
|
||||
"@swc/core-darwin-x64" "1.16.12"
|
||||
"@swc/core-linux-arm-gnueabihf" "1.16.12"
|
||||
"@swc/core-linux-arm64-gnu" "1.16.12"
|
||||
"@swc/core-linux-arm64-musl" "1.16.12"
|
||||
"@swc/core-linux-ppc64-gnu" "1.16.12"
|
||||
"@swc/core-linux-s390x-gnu" "1.16.12"
|
||||
"@swc/core-linux-x64-gnu" "1.16.12"
|
||||
"@swc/core-linux-x64-musl" "1.16.12"
|
||||
"@swc/core-win32-arm64-msvc" "1.16.12"
|
||||
"@swc/core-win32-ia32-msvc" "1.16.12"
|
||||
"@swc/core-win32-x64-msvc" "1.16.12"
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
@@ -6957,13 +6957,14 @@ compressible@~2.0.18:
|
||||
mime-db ">= 1.43.0 < 2"
|
||||
|
||||
compression@^1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79"
|
||||
integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.2.tgz#781397127ae2da00c0c5f7c76bda5b2a9b32a49f"
|
||||
integrity sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
compressible "~2.0.18"
|
||||
debug "2.6.9"
|
||||
destroy "1.2.0"
|
||||
negotiator "~0.6.4"
|
||||
on-headers "~1.1.0"
|
||||
safe-buffer "5.2.1"
|
||||
@@ -9100,9 +9101,9 @@ htmlparser2@^8.0.1:
|
||||
entities "^4.4.0"
|
||||
|
||||
http-cache-semantics@^4.1.1:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5"
|
||||
integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz#09eead3b16c6d85552857cc3fbd888d8353a2aaf"
|
||||
integrity sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==
|
||||
|
||||
http-deceiver@^1.2.7:
|
||||
version "1.2.7"
|
||||
@@ -9600,9 +9601,9 @@ jiti@^1.20.0:
|
||||
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
|
||||
|
||||
joi@^17.9.2:
|
||||
version "17.13.7"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.7.tgz#92e212c50dbbbcb1a1592424f84083eb265cc778"
|
||||
integrity sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==
|
||||
version "17.13.8"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.8.tgz#92069d521eea09fd709d8abbb86b0ef02862224f"
|
||||
integrity sha512-iPKOGmiRw1jxf/JOPwxmCcUQAOdF359mdzYiP2DJ+TMX0YK2zjK3D+zYOaGjpumWxOFF/l2xVWjRVK5bGSLdEw==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.3.0"
|
||||
"@hapi/topo" "^5.1.0"
|
||||
@@ -11517,42 +11518,42 @@ oxfmt@^0.70.0:
|
||||
"@oxfmt/binding-win32-ia32-msvc" "0.70.0"
|
||||
"@oxfmt/binding-win32-x64-msvc" "0.70.0"
|
||||
|
||||
oxlint-tsgolint@^7.0.2002:
|
||||
version "7.0.2002"
|
||||
resolved "https://registry.yarnpkg.com/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2002.tgz#ac3ba9afb73193dcaabf2d4121e83bbf87571396"
|
||||
integrity sha512-rQAj2RAQM57nCwTd3PhtnAkREGW2StqI47tzudNlJF8WwGommM7rVpeP4O1O8/ZiuDZ9f2GnTESzLHGTluwkvQ==
|
||||
oxlint-tsgolint@^7.0.2003:
|
||||
version "7.0.2003"
|
||||
resolved "https://registry.yarnpkg.com/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2003.tgz#ad35592d0fde9ffd279c9befe3275a81a787573f"
|
||||
integrity sha512-VnK4zlqgmgq/7ZcjzCk/WpN8kKFsYGcB85Io9qT3wL4K8Un3RKmJkp793crNETieMusPMKOA4a+Mw2wbteI6TQ==
|
||||
optionalDependencies:
|
||||
"@oxlint-tsgolint/darwin-arm64" "7.0.2002"
|
||||
"@oxlint-tsgolint/darwin-x64" "7.0.2002"
|
||||
"@oxlint-tsgolint/linux-arm64" "7.0.2002"
|
||||
"@oxlint-tsgolint/linux-x64" "7.0.2002"
|
||||
"@oxlint-tsgolint/win32-arm64" "7.0.2002"
|
||||
"@oxlint-tsgolint/win32-x64" "7.0.2002"
|
||||
"@oxlint-tsgolint/darwin-arm64" "7.0.2003"
|
||||
"@oxlint-tsgolint/darwin-x64" "7.0.2003"
|
||||
"@oxlint-tsgolint/linux-arm64" "7.0.2003"
|
||||
"@oxlint-tsgolint/linux-x64" "7.0.2003"
|
||||
"@oxlint-tsgolint/win32-arm64" "7.0.2003"
|
||||
"@oxlint-tsgolint/win32-x64" "7.0.2003"
|
||||
|
||||
oxlint@^1.85.0:
|
||||
version "1.85.0"
|
||||
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.85.0.tgz#a783acb92bd70986b2586a7457984126daf4e52e"
|
||||
integrity sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg==
|
||||
oxlint@^1.86.0:
|
||||
version "1.86.0"
|
||||
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.86.0.tgz#6b5d3f095eb32a8003839bffaf39e4203df644e3"
|
||||
integrity sha512-og0lhgvZfgGF//gOOmZXvtr+GmBbAGEnbEhv/QUg7UW2Wi4wHMnJbnMD+zuHgPdJxdklfgpPupdlAaAySyxrZg==
|
||||
optionalDependencies:
|
||||
"@oxlint/binding-android-arm-eabi" "1.85.0"
|
||||
"@oxlint/binding-android-arm64" "1.85.0"
|
||||
"@oxlint/binding-darwin-arm64" "1.85.0"
|
||||
"@oxlint/binding-darwin-x64" "1.85.0"
|
||||
"@oxlint/binding-freebsd-x64" "1.85.0"
|
||||
"@oxlint/binding-linux-arm-gnueabihf" "1.85.0"
|
||||
"@oxlint/binding-linux-arm-musleabihf" "1.85.0"
|
||||
"@oxlint/binding-linux-arm64-gnu" "1.85.0"
|
||||
"@oxlint/binding-linux-arm64-musl" "1.85.0"
|
||||
"@oxlint/binding-linux-ppc64-gnu" "1.85.0"
|
||||
"@oxlint/binding-linux-riscv64-gnu" "1.85.0"
|
||||
"@oxlint/binding-linux-riscv64-musl" "1.85.0"
|
||||
"@oxlint/binding-linux-s390x-gnu" "1.85.0"
|
||||
"@oxlint/binding-linux-x64-gnu" "1.85.0"
|
||||
"@oxlint/binding-linux-x64-musl" "1.85.0"
|
||||
"@oxlint/binding-openharmony-arm64" "1.85.0"
|
||||
"@oxlint/binding-win32-arm64-msvc" "1.85.0"
|
||||
"@oxlint/binding-win32-ia32-msvc" "1.85.0"
|
||||
"@oxlint/binding-win32-x64-msvc" "1.85.0"
|
||||
"@oxlint/binding-android-arm-eabi" "1.86.0"
|
||||
"@oxlint/binding-android-arm64" "1.86.0"
|
||||
"@oxlint/binding-darwin-arm64" "1.86.0"
|
||||
"@oxlint/binding-darwin-x64" "1.86.0"
|
||||
"@oxlint/binding-freebsd-x64" "1.86.0"
|
||||
"@oxlint/binding-linux-arm-gnueabihf" "1.86.0"
|
||||
"@oxlint/binding-linux-arm-musleabihf" "1.86.0"
|
||||
"@oxlint/binding-linux-arm64-gnu" "1.86.0"
|
||||
"@oxlint/binding-linux-arm64-musl" "1.86.0"
|
||||
"@oxlint/binding-linux-ppc64-gnu" "1.86.0"
|
||||
"@oxlint/binding-linux-riscv64-gnu" "1.86.0"
|
||||
"@oxlint/binding-linux-riscv64-musl" "1.86.0"
|
||||
"@oxlint/binding-linux-s390x-gnu" "1.86.0"
|
||||
"@oxlint/binding-linux-x64-gnu" "1.86.0"
|
||||
"@oxlint/binding-linux-x64-musl" "1.86.0"
|
||||
"@oxlint/binding-openharmony-arm64" "1.86.0"
|
||||
"@oxlint/binding-win32-arm64-msvc" "1.86.0"
|
||||
"@oxlint/binding-win32-ia32-msvc" "1.86.0"
|
||||
"@oxlint/binding-win32-x64-msvc" "1.86.0"
|
||||
|
||||
p-cancelable@^3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -12513,9 +12514,9 @@ proto-list@~1.2.1:
|
||||
integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==
|
||||
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.8.tgz#624e760fd8ab06b1b8320c2f8506d4422cf8e932"
|
||||
integrity sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==
|
||||
dependencies:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
@@ -13665,9 +13666,9 @@ shebang-regex@^3.0.0:
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
shell-quote@^1.8.4:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.9.0.tgz#e108b1a136586d5964edb3300016d4bedba0fe57"
|
||||
integrity sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.12.0.tgz#e98fdfd2e921d8a58ae2820f4fc0f75c39f09675"
|
||||
integrity sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==
|
||||
|
||||
shelljs@^0.8.5:
|
||||
version "0.8.5"
|
||||
@@ -13841,9 +13842,9 @@ sort-css-media-queries@2.2.0:
|
||||
integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==
|
||||
|
||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.2.tgz#79fb88d97535a21cb27162cb05d211b67acea4ab"
|
||||
integrity sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==
|
||||
|
||||
source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
|
||||
@@ -42,7 +42,7 @@ dependencies = [
|
||||
# ``google-auth`` 2.53+ dropped it, so Superset must declare it
|
||||
# explicitly to keep fresh ``pip install apache-superset`` working
|
||||
# without the ``base.txt`` lock file (#40962).
|
||||
"cachetools>=7.1.8, <8",
|
||||
"cachetools>=7.2.0, <8",
|
||||
"celery>=5.6.3, <6.0.0",
|
||||
"click>=8.5.0",
|
||||
"click-option-group",
|
||||
@@ -71,12 +71,12 @@ dependencies = [
|
||||
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
|
||||
"hashids>=1.3.1, <2",
|
||||
# holidays>=0.45 required for security fix
|
||||
"holidays>=0.104, <1",
|
||||
"holidays>=0.105, <1",
|
||||
"humanize",
|
||||
"isodate",
|
||||
"jsonpath-ng>=1.8.0, <2",
|
||||
"Mako>=1.4.1",
|
||||
"markdown>=3.10.3",
|
||||
"markdown>=3.11",
|
||||
# marshmallow 4 compatibility: see superset/marshmallow_compatibility.py for a
|
||||
# Flask-AppBuilder workaround. Tracking issue:
|
||||
# https://github.com/apache/superset/issues/33162
|
||||
@@ -94,7 +94,6 @@ dependencies = [
|
||||
"parsedatetime",
|
||||
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
|
||||
"Pillow>=12.3.0, <13", # raise floor to match resolved pin; closes SCA false-positive on 11.x-range CVEs already fixed in 12.3.0
|
||||
"polyline>=2.0.4, <3.0",
|
||||
"pydantic>=2.13.5",
|
||||
"pyparsing>=3.3.2, <4",
|
||||
"python-dateutil",
|
||||
@@ -116,7 +115,7 @@ dependencies = [
|
||||
# Dialect-specific gaps/bugs against this pin are worked around in
|
||||
# superset/sql/dialects/ (e.g. starrocks.py); check there for anything
|
||||
# that can be cleaned up when bumping
|
||||
"sqlglot>=30.18.0, <31", # 30.16.0 adds Trino inline UDF IF/CASE routine statement parsing
|
||||
"sqlglot>=30.19.0, <31", # 30.16.0 adds Trino inline UDF IF/CASE routine statement parsing
|
||||
# newer pandas needs 0.9+
|
||||
"tabulate>=0.10.0, <1.0",
|
||||
"typing-extensions>=4.16.0, <5",
|
||||
@@ -143,7 +142,7 @@ bigquery = [
|
||||
"sqlalchemy-bigquery>=1.17.2",
|
||||
"google-cloud-bigquery>=3.45.2",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.8.0, <2.0"]
|
||||
clickhouse = ["clickhouse-connect>=1.9.0, <2.0"]
|
||||
# The `cockroachdb` PyPI package (last released 2021) is abandoned and its
|
||||
# SQLAlchemy dialect cannot even import under SQLAlchemy 2.0 (it references
|
||||
# sqlalchemy.dialects.postgresql.psycopg2.PGCompiler_psycopg2, removed in
|
||||
@@ -162,7 +161,7 @@ crate = ["sqlalchemy-cratedb>=0.43.1, <1"]
|
||||
d1 = ["sqlalchemy-d1>=0.2.0, <1"]
|
||||
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
|
||||
databricks = [
|
||||
"databricks-sql-connector>=4.5.0, <4.6.0",
|
||||
"databricks-sql-connector>=4.6.0, <4.7.0",
|
||||
"databricks-sqlalchemy>=2.0.1, <3",
|
||||
]
|
||||
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"]
|
||||
@@ -199,7 +198,7 @@ excel-export = ["boto3"]
|
||||
# superset.utils.gcs, so this extra is an alternative to excel-export, not an
|
||||
# addition to it -- pick whichever matches your bucket's provider.
|
||||
# >=1.37 for ``Blob.open``, which superset.utils.gcs streams downloads through.
|
||||
excel-export-gcs = ["google-cloud-storage>=1.37"]
|
||||
excel-export-gcs = ["google-cloud-storage>=3.15.0"]
|
||||
fastmcp = [
|
||||
"fastmcp>=3.4.7,<4.0",
|
||||
# fastmcp 3.x itself requires mcp<2.0; the upper bound here was looser
|
||||
@@ -319,7 +318,17 @@ development = [
|
||||
"pre-commit",
|
||||
"progress>=1.6.1,<2",
|
||||
"psutil",
|
||||
"pyfakefs",
|
||||
# 5.3.5 (the previously resolved version) predates pyfakefs's own Python
|
||||
# 3.13 support: under 3.13 it leaves os.chdir patched to the fake
|
||||
# filesystem after the `fs` fixture is undone, so the chdir back to the
|
||||
# rootdir in pytest-cov's own teardown aborts the whole session with an
|
||||
# INTERNALERROR. 5.6.0 added preliminary 3.13 support, which is what stops
|
||||
# that crash, and 5.7.0 made 3.13 support official. 5.7.4 is the floor
|
||||
# rather than 5.7.0 because it also fixes module/session-scoped fixtures
|
||||
# under 3.13 (pyfakefs #1101), and the `fs` tests in
|
||||
# tests/integration_tests/cli_tests.py -- the only consumers of that
|
||||
# fixture -- run alongside session- and module-scoped fixtures.
|
||||
"pyfakefs>=5.7.4",
|
||||
"pyinstrument>=5.1.3,<6",
|
||||
"pylint",
|
||||
"pytest<10.0.0", # cap below the next major until validated; the earlier <8 pin (current_app proxy timing) no longer reproduces
|
||||
@@ -344,7 +353,7 @@ combine_as_imports = true
|
||||
include_trailing_comma = true
|
||||
line_length = 88
|
||||
known_first_party = "superset, apache-superset-core, apache-superset-extensions-cli"
|
||||
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, marshmallow-union, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, polyline, rison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, syntaqlite, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
|
||||
known_third_party = "alembic, apispec, backoff, celery, click, colorama, cron_descriptor, croniter, cryptography, dateutil, deprecation, flask, flask_appbuilder, flask_babel, flask_caching, flask_compress, flask_jwt_extended, flask_login, flask_migrate, flask_sqlalchemy, flask_talisman, flask_testing, flask_wtf, freezegun, geohash, geopy, holidays, humanize, isodate, jinja2, jwt, markdown, markupsafe, marshmallow, marshmallow-union, msgpack, nh3, numpy, pandas, parameterized, parsedatetime, rison, progress, pyarrow, sqlalchemy_bigquery, pyhive, pyparsing, pytest, pytest_mock, pytz, redis, requests, setuptools, shillelagh, simplejson, slack, sqlalchemy, sqlalchemy_utils, syntaqlite, typing_extensions, urllib3, werkzeug, wtforms, wtforms_json, yaml"
|
||||
multi_line_output = 3
|
||||
order_by_type = false
|
||||
|
||||
@@ -395,14 +404,16 @@ module = [
|
||||
]
|
||||
warn_unused_ignores = false
|
||||
|
||||
# The mirror case: superset-core declares SqlaTable's id/uuid/perm as plain
|
||||
# value types, so column expressions like SqlaTable.uuid.in_() only need a
|
||||
# type: ignore when superset-core's sources are part of the same mypy run
|
||||
# (full-repo lint) and not when only changed files are checked.
|
||||
# The mirror case: superset-core declares SqlaTable's id/uuid/perm, and
|
||||
# Chart's uuid, as plain value types, so column expressions like
|
||||
# SqlaTable.uuid.in_() and Slice.uuid.in_() only need a type: ignore when
|
||||
# superset-core's sources are part of the same mypy run (full-repo lint)
|
||||
# and not when only changed files are checked.
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"superset.mcp_service.dataset_scope",
|
||||
"tests.unit_tests.mcp_service.dataset.tool.test_dataset_tools",
|
||||
"superset.commands.chart.importers.v1",
|
||||
]
|
||||
warn_unused_ignores = false
|
||||
|
||||
@@ -580,10 +591,6 @@ authorized_licenses = [
|
||||
]
|
||||
[tool.liccheck.authorized_packages]
|
||||
# --------------------------------------------------------------
|
||||
# These are ok, checked manually
|
||||
# Seems ok, might need legal review
|
||||
# https://github.com/urschrei/pypolyline/blob/master/LICENSE.md
|
||||
polyline = "2"
|
||||
# NumPy 2.x reports a combined SPDX license expression covering vendored
|
||||
# code (BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0), all of which
|
||||
# are permissive OSI-approved licenses; the package itself remains
|
||||
|
||||
@@ -44,7 +44,7 @@ cachelib==0.17.0
|
||||
# via
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==7.1.8
|
||||
cachetools==7.2.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
cattrs==25.1.1
|
||||
# via requests-cache
|
||||
@@ -172,7 +172,7 @@ gunicorn==26.2.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
hashids==1.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
holidays==0.104
|
||||
holidays==0.105
|
||||
# via apache-superset (pyproject.toml)
|
||||
humanize==4.12.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -212,7 +212,7 @@ mako==1.4.1
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
markdown==3.10.3
|
||||
markdown==3.11
|
||||
# via apache-superset (pyproject.toml)
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
@@ -279,8 +279,6 @@ pillow==12.3.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
polyline==2.0.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
prompt-toolkit==3.0.51
|
||||
@@ -397,7 +395,7 @@ sqlalchemy-utils==0.42.1
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
# flask-appbuilder
|
||||
sqlglot==30.18.0
|
||||
sqlglot==30.21.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
|
||||
@@ -112,7 +112,7 @@ cachelib==0.17.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==7.1.8
|
||||
cachetools==7.2.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -176,7 +176,7 @@ click-repl==0.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# celery
|
||||
clickhouse-connect==1.8.0
|
||||
clickhouse-connect==1.9.0
|
||||
# via apache-superset
|
||||
cmdstanpy==1.1.0
|
||||
# via prophet
|
||||
@@ -451,7 +451,7 @@ hashids==1.3.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
holidays==0.104
|
||||
holidays==0.105
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -568,7 +568,7 @@ mako==1.4.1
|
||||
# -c requirements/base-constraint.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
markdown==3.10.3
|
||||
markdown==3.11
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -742,10 +742,6 @@ pluggy==1.5.0
|
||||
# via pytest
|
||||
polib==1.2.0
|
||||
# via apache-superset
|
||||
polyline==2.0.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
pre-commit==4.1.0
|
||||
# via apache-superset
|
||||
prison==0.2.1
|
||||
@@ -824,7 +820,7 @@ pydata-google-auth==1.9.0
|
||||
# via pandas-gbq
|
||||
pydruid==0.6.9
|
||||
# via apache-superset
|
||||
pyfakefs==5.3.5
|
||||
pyfakefs==6.2.0
|
||||
# via apache-superset
|
||||
pygeohash==3.2.2
|
||||
# via
|
||||
@@ -1088,7 +1084,7 @@ sqlalchemy-utils==0.42.1
|
||||
# apache-superset
|
||||
# apache-superset-core
|
||||
# flask-appbuilder
|
||||
sqlglot==30.18.0
|
||||
sqlglot==30.21.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
@@ -52,9 +52,16 @@ PATTERNS = {
|
||||
r"^requirements/.+\.txt",
|
||||
r"^pyproject\.toml",
|
||||
r"^.pylintrc",
|
||||
# See the note in "frontend": setup-backend/change-detector are this
|
||||
# group's own setup.
|
||||
r"^\.github/actions/",
|
||||
],
|
||||
"frontend": [
|
||||
r"^\.github/workflows/.*(bashlib|frontend|e2e)",
|
||||
# Composite actions are shared setup, matched by nothing else. Listed
|
||||
# under "python" too -- setup-backend is a backend action, so gating it
|
||||
# on "frontend" alone would skip the Python jobs whose setup changed.
|
||||
r"^\.github/actions/",
|
||||
r"^superset-frontend/",
|
||||
],
|
||||
"docker": [
|
||||
@@ -74,6 +81,54 @@ PATTERNS = {
|
||||
}
|
||||
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
|
||||
|
||||
# Maps each PATTERNS group to the CodeQL language(s) its files are written
|
||||
# in, by file extension rather than directory. PATTERNS above groups files by
|
||||
# project area (e.g. "frontend" means "under superset-frontend/"), which is
|
||||
# right for gating CI jobs by area but wrong for picking which CodeQL
|
||||
# languages to scan: a .js file outside superset-frontend/ (e.g. under
|
||||
# scripts/ or superset/mcp_service/) is grouped under "python" by PATTERNS,
|
||||
# so a consumer that reused the "frontend" group as a stand-in for
|
||||
# "javascript changed" would silently skip scanning it. This reuses the same
|
||||
# group names as PATTERNS rather than introducing new ones, so a group's
|
||||
# language(s) can be looked up directly instead of via a separate mapping.
|
||||
# See https://github.com/apache/superset/issues/44822.
|
||||
GROUP_LANGUAGE_EXTENSIONS: dict[str, dict[str, str]] = {
|
||||
"python": {".py": "python"},
|
||||
"frontend": {
|
||||
".js": "javascript",
|
||||
".jsx": "javascript",
|
||||
".ts": "javascript",
|
||||
".tsx": "javascript",
|
||||
".mjs": "javascript",
|
||||
".cjs": "javascript",
|
||||
},
|
||||
}
|
||||
# Flattened for a direct extension -> language lookup, independent of which
|
||||
# PATTERNS group (i.e. directory) the file lives in.
|
||||
LANGUAGE_EXTENSIONS: dict[str, str] = {
|
||||
ext: lang
|
||||
for extensions in GROUP_LANGUAGE_EXTENSIONS.values()
|
||||
for ext, lang in extensions.items()
|
||||
}
|
||||
|
||||
|
||||
def detect_languages(files: Optional[List[str]]) -> List[str]:
|
||||
"""Returns the CodeQL languages actually touched, by file extension.
|
||||
|
||||
Unlike the PATTERNS groups above, this ignores which directory a file
|
||||
lives in: a .js file is "javascript" whether it's under
|
||||
superset-frontend/ or not. `files is None` (workflow_dispatch/schedule)
|
||||
means "assume everything changed", so every known language is returned.
|
||||
"""
|
||||
if files is None:
|
||||
return sorted(set(LANGUAGE_EXTENSIONS.values()))
|
||||
languages = set()
|
||||
for file in files:
|
||||
_, ext = os.path.splitext(file)
|
||||
if lang := LANGUAGE_EXTENSIONS.get(ext):
|
||||
languages.add(lang)
|
||||
return sorted(languages)
|
||||
|
||||
|
||||
def _is_rate_limited(err: HTTPError) -> bool:
|
||||
"""Whether a 403 is GitHub throttling rather than a missing token scope."""
|
||||
@@ -217,6 +272,16 @@ def main(event_type: str, sha: str, repo: str) -> None:
|
||||
files, patterns_compiled
|
||||
)
|
||||
|
||||
# The 100-file API cap below treats a push/PR touching that many files as
|
||||
# "everything changed" for the PATTERNS groups; language detection
|
||||
# honors the same assumption so a consumer combining both outputs never
|
||||
# sees a language silently excluded by the cap.
|
||||
languages = (
|
||||
sorted(set(LANGUAGE_EXTENSIONS.values()))
|
||||
if files is not None and len(files) >= 99
|
||||
else detect_languages(files)
|
||||
)
|
||||
|
||||
# Output results
|
||||
output_path = os.getenv("GITHUB_OUTPUT") or "/tmp/GITHUB_OUTPUT.txt" # noqa: S108
|
||||
with open(output_path, "a") as f:
|
||||
@@ -227,6 +292,8 @@ def main(event_type: str, sha: str, repo: str) -> None:
|
||||
if changed or (files is not None and len(files) >= 99):
|
||||
print(f"{check}=true", file=f)
|
||||
print(f"Triggering group: {check}")
|
||||
print(f"languages={json.dumps(languages)}", file=f)
|
||||
print(f"Languages detected: {languages}")
|
||||
|
||||
|
||||
def get_git_sha() -> str:
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
# 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 argparse
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1280x1024x24' "
|
||||
REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
|
||||
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME") or "push"
|
||||
CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
|
||||
|
||||
|
||||
def generate_build_id() -> str:
|
||||
"""Generates a build ID based on the current timestamp."""
|
||||
now = datetime.now()
|
||||
rounded_minute = now.minute - (now.minute % 20)
|
||||
rounded_time = now.replace(minute=rounded_minute, second=0, microsecond=0)
|
||||
return (os.getenv("GITHUB_SHA") or "DUMMY")[:8] + rounded_time.strftime(
|
||||
"%Y%m%d%H%M"
|
||||
)
|
||||
|
||||
|
||||
def run_cypress_for_test_file(
|
||||
test_file: str, retries: int, use_dashboard: bool, group: str, dry_run: bool, i: int
|
||||
) -> int:
|
||||
"""Runs Cypress for a single test file and retries upon failure."""
|
||||
cypress_cmd = "./node_modules/.bin/cypress run"
|
||||
os.environ["TERM"] = "xterm"
|
||||
os.environ["ELECTRON_DISABLE_GPU"] = "true"
|
||||
build_id = generate_build_id()
|
||||
browser = os.getenv("CYPRESS_BROWSER", "chrome")
|
||||
chrome_flags = "--disable-dev-shm-usage"
|
||||
|
||||
for attempt in range(retries):
|
||||
# Create Cypress command for a single test file
|
||||
cmd: str = ""
|
||||
if use_dashboard:
|
||||
# If/when we want to use cypress' dashboard feature to record the run
|
||||
group_id = f"matrix{group}-file{i}-{attempt}"
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f'{cypress_cmd} --spec "{test_file}" '
|
||||
f"--config numTestsKeptInMemory=0 "
|
||||
f"--browser {browser} "
|
||||
f"--record --group {group_id} --tag {REPO},{GITHUB_EVENT_NAME} "
|
||||
f"--ci-build-id {build_id} "
|
||||
f"-- {chrome_flags}"
|
||||
)
|
||||
else:
|
||||
os.environ.pop("CYPRESS_RECORD_KEY", None)
|
||||
cmd = (
|
||||
f"{XVFB_PRE_CMD} "
|
||||
f"{cypress_cmd} "
|
||||
f"--browser {browser} "
|
||||
f"--config numTestsKeptInMemory=0 "
|
||||
f'--spec "{test_file}" '
|
||||
f"-- {chrome_flags}"
|
||||
)
|
||||
print(f"RUN: {cmd} (Attempt {attempt + 1}/{retries})")
|
||||
if dry_run:
|
||||
# Print the command instead of executing it
|
||||
print(f"DRY RUN: {cmd}")
|
||||
return 0
|
||||
|
||||
process = subprocess.Popen( # noqa: S602
|
||||
cmd,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True,
|
||||
)
|
||||
|
||||
# Stream stdout in real-time
|
||||
if process.stdout:
|
||||
for stdout_line in iter(process.stdout.readline, ""):
|
||||
print(stdout_line, end="")
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
print(f"Test {test_file} succeeded on attempt {attempt + 1}")
|
||||
return 0
|
||||
else:
|
||||
print(f"Test {test_file} failed on attempt {attempt + 1}")
|
||||
|
||||
print(f"Test {test_file} failed after {retries} retries.")
|
||||
return process.returncode
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run Cypress tests with retries per test file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use-dashboard",
|
||||
action="store_true",
|
||||
help="Use Cypress Dashboard for parallelization",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--filter", type=str, required=False, default=None, help="Filter to test"
|
||||
)
|
||||
parser.add_argument("--group", type=str, default="Default", help="Group name")
|
||||
parser.add_argument(
|
||||
"--retries", type=int, default=3, help="Number of retries per test file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print the command instead of executing it",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
cypress_base_path = "superset-frontend/cypress-base/"
|
||||
cypress_base_full_path = os.path.join(script_dir, "../", cypress_base_path)
|
||||
cypress_tests_path = os.path.join(cypress_base_full_path, "cypress/e2e")
|
||||
|
||||
test_files = []
|
||||
file_count = 0
|
||||
skipped_count = 0
|
||||
for root, _, files in os.walk(cypress_tests_path):
|
||||
for file in files:
|
||||
if file.endswith("test.ts") or file.endswith("test.js"):
|
||||
# Skip files prefixed with _skip. (excluded by excludeSpecPattern)
|
||||
if file.startswith("_skip."):
|
||||
skipped_count += 1
|
||||
continue
|
||||
file_count += 1
|
||||
test_files.append(
|
||||
os.path.join(root, file).replace(cypress_base_full_path, "")
|
||||
)
|
||||
print(f"Found {file_count} test files ({skipped_count} skipped).")
|
||||
|
||||
# Sort test files for deterministic, readable run order.
|
||||
spec_list = sorted(test_files)
|
||||
|
||||
# Run each test file independently with retry logic or dry-run
|
||||
processed_file_count: int = 0
|
||||
for i, test_file in enumerate(spec_list):
|
||||
result = run_cypress_for_test_file(
|
||||
test_file, args.retries, args.use_dashboard, args.group, args.dry_run, i
|
||||
)
|
||||
if result != 0:
|
||||
print(f"Exiting due to failure in {test_file}")
|
||||
exit(result)
|
||||
processed_file_count += 1
|
||||
print(f"Ran {processed_file_count} test files successfully.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -98,7 +98,6 @@ BASE_NGINX=80
|
||||
BASE_SUPERSET=8088
|
||||
BASE_NODE=9000
|
||||
BASE_WEBSOCKET=8080
|
||||
BASE_CYPRESS=8081
|
||||
BASE_DATABASE=5432
|
||||
BASE_REDIS=6379
|
||||
|
||||
@@ -108,7 +107,6 @@ find_and_claim_port $BASE_NGINX NGINX_PORT
|
||||
find_and_claim_port $BASE_SUPERSET SUPERSET_PORT
|
||||
find_and_claim_port $BASE_NODE NODE_PORT
|
||||
find_and_claim_port $BASE_WEBSOCKET WEBSOCKET_PORT
|
||||
find_and_claim_port $BASE_CYPRESS CYPRESS_PORT
|
||||
find_and_claim_port $BASE_DATABASE DATABASE_PORT
|
||||
find_and_claim_port $BASE_REDIS REDIS_PORT
|
||||
|
||||
@@ -144,7 +142,6 @@ export NGINX_PORT
|
||||
export SUPERSET_PORT
|
||||
export NODE_PORT
|
||||
export WEBSOCKET_PORT
|
||||
export CYPRESS_PORT
|
||||
export DATABASE_PORT
|
||||
export REDIS_PORT
|
||||
|
||||
@@ -188,7 +185,6 @@ case "${1:-}" in
|
||||
echo "export SUPERSET_PORT=$SUPERSET_PORT"
|
||||
echo "export NODE_PORT=$NODE_PORT"
|
||||
echo "export WEBSOCKET_PORT=$WEBSOCKET_PORT"
|
||||
echo "export CYPRESS_PORT=$CYPRESS_PORT"
|
||||
echo "export DATABASE_PORT=$DATABASE_PORT"
|
||||
echo "export REDIS_PORT=$REDIS_PORT"
|
||||
exit 0
|
||||
|
||||
@@ -49,8 +49,14 @@ pybabel extract \
|
||||
--project=Superset \
|
||||
-k _ -k __ -k t -k tn:1,2 -k tct .
|
||||
|
||||
# Normalize .pot file
|
||||
msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.pot -o superset/translations/messages.pot
|
||||
# Normalize the .pot: sort by msgid, keep each message on one line, drop source
|
||||
# locations. msgcat's sort flag is `--sort-output`; the `--sort-by-msgid`
|
||||
# spelling used here before is not a msgcat option, and gettext rejects the call
|
||||
# with `unrecognized option` (verified on 0.23.2 and 1.0). With no `set -e` in
|
||||
# this script that failure was non-fatal, so the step never ran and the template
|
||||
# was published unnormalized. `|| exit 1` makes a failed normalization stop the
|
||||
# script instead.
|
||||
msgcat --sort-output --no-wrap --no-location superset/translations/messages.pot -o superset/translations/messages.pot || exit 1
|
||||
|
||||
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
|
||||
&& mv messages.pot.tmp superset/translations/messages.pot
|
||||
@@ -80,6 +86,9 @@ pybabel update \
|
||||
--no-fuzzy-matching
|
||||
|
||||
# Chop off last blankline from po/pot files, see https://github.com/python-babel/babel/issues/799
|
||||
# Only a blank last line is removed: the .pot comes out of msgcat above, which
|
||||
# writes no trailing blank line, so an unconditional delete would drop the
|
||||
# last entry's msgstr and leave a template gettext rejects.
|
||||
for file in $( find superset/translations/** );
|
||||
do
|
||||
extension=${file##*.}
|
||||
@@ -87,7 +96,7 @@ do
|
||||
if [ $extension == "po" ] || [ $extension == "pot" ]
|
||||
then
|
||||
mv $file $file.tmp
|
||||
sed "$ d" $file.tmp > $file
|
||||
sed '${/^$/d;}' $file.tmp > $file
|
||||
rm $file.tmp
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -71,6 +71,8 @@ class QueryOptions:
|
||||
# Basic options
|
||||
catalog: str | None = None
|
||||
schema: str | None = None
|
||||
# Cap the last statement's outer SQL limit without increasing a smaller one.
|
||||
# None leaves SQL limits unchanged; explicit caps also respect SQL_MAX_ROW.
|
||||
limit: int | None = None
|
||||
timeout_seconds: int | None = None
|
||||
|
||||
@@ -100,6 +102,7 @@ class StatementResult:
|
||||
data: pd.DataFrame | None = None
|
||||
row_count: int = 0
|
||||
execution_time_ms: float | None = None
|
||||
truncated: bool = False # Rows omitted by the fetch cap, beyond executed_sql
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -236,3 +236,4 @@ class SemanticQuery:
|
||||
limit: int | None = None
|
||||
offset: int | None = None
|
||||
group_limit: GroupLimit | None = None
|
||||
selection_identity_version: str | None = None
|
||||
|
||||
@@ -50,6 +50,20 @@ class SemanticView(ABC):
|
||||
# consumers can rely on the attribute existing and degrade to the
|
||||
# conservative (Saved-only) picker for views that declare nothing.
|
||||
features: frozenset[SemanticViewFeature] = frozenset()
|
||||
selection_identity_version: str | None = None
|
||||
|
||||
def validate_selection_version(self, version: object) -> None:
|
||||
"""Reject selections made under a different member identity contract."""
|
||||
if (
|
||||
self.selection_identity_version is not None
|
||||
and version != self.selection_identity_version
|
||||
):
|
||||
raise ValueError(
|
||||
"Saved semantic selections use an older identity format. "
|
||||
"Reset the chart selections, explicitly reselect its metrics "
|
||||
"and dimensions, and save the chart. Display titles cannot be "
|
||||
"automatically mapped to member IDs."
|
||||
)
|
||||
|
||||
# Implementations must expose a display name for the view.
|
||||
# Declared here as a type annotation (not abstract) so that existing
|
||||
|
||||
228
superset-embedded-sdk/package-lock.json
generated
228
superset-embedded-sdk/package-lock.json
generated
@@ -17,12 +17,12 @@
|
||||
"@babel/core": "^8.0.6",
|
||||
"@babel/preset-env": "^8.0.6",
|
||||
"@babel/preset-typescript": "^8.0.1",
|
||||
"@types/node": "^26.6.2",
|
||||
"@types/node": "^26.6.3",
|
||||
"babel-loader": "^10.1.1",
|
||||
"jsdom": "^30.1.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^5.0.1",
|
||||
"vitest": "^5.0.2",
|
||||
"webpack": "^5.111.1",
|
||||
"webpack-cli": "^7.2.3"
|
||||
}
|
||||
@@ -1846,9 +1846,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.149.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz",
|
||||
"integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==",
|
||||
"version": "0.151.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz",
|
||||
"integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -1857,9 +1857,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm-eabi": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz",
|
||||
"integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz",
|
||||
"integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1875,9 +1875,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz",
|
||||
"integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz",
|
||||
"integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1893,9 +1893,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz",
|
||||
"integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz",
|
||||
"integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1911,9 +1911,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz",
|
||||
"integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz",
|
||||
"integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1929,9 +1929,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz",
|
||||
"integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz",
|
||||
"integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1947,9 +1947,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz",
|
||||
"integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz",
|
||||
"integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1965,9 +1965,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz",
|
||||
"integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1986,9 +1986,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz",
|
||||
"integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz",
|
||||
"integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2007,9 +2007,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz",
|
||||
"integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2028,9 +2028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz",
|
||||
"integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2049,9 +2049,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz",
|
||||
"integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz",
|
||||
"integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2070,9 +2070,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz",
|
||||
"integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz",
|
||||
"integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2091,9 +2091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz",
|
||||
"integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz",
|
||||
"integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2109,9 +2109,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz",
|
||||
"integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz",
|
||||
"integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2127,9 +2127,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz",
|
||||
"integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz",
|
||||
"integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2205,9 +2205,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz",
|
||||
"integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==",
|
||||
"version": "26.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
|
||||
"integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2555,14 +2555,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz",
|
||||
"integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz",
|
||||
"integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@vitest/spy": "5.0.1",
|
||||
"@vitest/spy": "5.0.2",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^1.2.3"
|
||||
},
|
||||
@@ -2583,9 +2583,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz",
|
||||
"integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz",
|
||||
"integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -3977,9 +3977,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz",
|
||||
"integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4510,14 +4510,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz",
|
||||
"integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==",
|
||||
"version": "1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz",
|
||||
"integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.149.0",
|
||||
"@oxc-project/types": "=0.151.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4527,21 +4527,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.8",
|
||||
"@rolldown/binding-android-arm64": "1.2.8",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.8",
|
||||
"@rolldown/binding-darwin-x64": "1.2.8",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.8",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.8",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.8",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.8",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.8",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.8",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.8",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.8",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.8",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.8",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.8"
|
||||
"@rolldown/binding-android-arm-eabi": "1.2.11",
|
||||
"@rolldown/binding-android-arm64": "1.2.11",
|
||||
"@rolldown/binding-darwin-arm64": "1.2.11",
|
||||
"@rolldown/binding-darwin-x64": "1.2.11",
|
||||
"@rolldown/binding-freebsd-x64": "1.2.11",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.2.11",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.2.11",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.2.11",
|
||||
"@rolldown/binding-linux-x64-musl": "1.2.11",
|
||||
"@rolldown/binding-openharmony-arm64": "1.2.11",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.2.11",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
@@ -4626,13 +4626,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||
@@ -4657,9 +4650,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -4677,13 +4670,6 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
|
||||
@@ -5013,9 +4999,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz",
|
||||
"integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==",
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz",
|
||||
"integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -5023,7 +5009,7 @@
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.7",
|
||||
"postcss": "^8.5.28",
|
||||
"rolldown": "~1.2.6",
|
||||
"rolldown": "~1.2.9",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
@@ -5092,14 +5078,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz",
|
||||
"integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz",
|
||||
"integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/mocker": "5.0.1",
|
||||
"@vitest/mocker": "5.0.2",
|
||||
"chai": "^6.2.2",
|
||||
"es-module-lexer": "^2.3.2",
|
||||
"expect-type": "^1.4.0",
|
||||
@@ -5107,10 +5093,10 @@
|
||||
"obug": "^2.1.4",
|
||||
"picomatch": "^4.0.7",
|
||||
"std-env": "^4.2.0",
|
||||
"tinybench": "6.1.4",
|
||||
"tinyexec": "1.3.0",
|
||||
"tinybench": "^6.1.4",
|
||||
"tinyexec": "^1.3.0",
|
||||
"tinyglobby": "^0.2.17",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
"why-is-node-running": "^3.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
@@ -5125,12 +5111,12 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "5.0.1",
|
||||
"@vitest/browser-preview": "5.0.1",
|
||||
"@vitest/browser-playwright": "5.0.2",
|
||||
"@vitest/browser-preview": "5.0.2",
|
||||
"@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
|
||||
"@vitest/coverage-istanbul": "5.0.1",
|
||||
"@vitest/coverage-v8": "5.0.1",
|
||||
"@vitest/ui": "5.0.1",
|
||||
"@vitest/coverage-istanbul": "5.0.2",
|
||||
"@vitest/coverage-v8": "5.0.2",
|
||||
"@vitest/ui": "5.0.2",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
|
||||
@@ -5370,20 +5356,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz",
|
||||
"integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
"node": ">=20.11"
|
||||
}
|
||||
},
|
||||
"node_modules/wildcard": {
|
||||
|
||||
@@ -41,12 +41,12 @@
|
||||
"@babel/core": "^8.0.6",
|
||||
"@babel/preset-env": "^8.0.6",
|
||||
"@babel/preset-typescript": "^8.0.1",
|
||||
"@types/node": "^26.6.2",
|
||||
"@types/node": "^26.6.3",
|
||||
"babel-loader": "^10.1.1",
|
||||
"jsdom": "^30.1.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^5.0.1",
|
||||
"vitest": "^5.0.2",
|
||||
"webpack": "^5.111.1",
|
||||
"webpack-cli": "^7.2.3"
|
||||
},
|
||||
|
||||
89
superset-embedded-sdk/testrig/README.md
Normal file
89
superset-embedded-sdk/testrig/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
|
||||
# Embedded SDK test rig
|
||||
|
||||
A host app and a stand-in for Superset's embedded page, on two origins, so the
|
||||
parts of the SDK that only exist between two documents can be exercised for
|
||||
real: the MessageChannel handshake, what survives a navigation the dashboard
|
||||
makes on its own, and how many times the host's guest-token endpoint is asked
|
||||
for a token.
|
||||
|
||||
The unit tests mock `MessageChannel` and `Switchboard`. This rig mocks
|
||||
neither — it loads the built UMD bundle in Chromium and talks to a page that
|
||||
speaks the same handshake as `superset-frontend/src/embedded/index.tsx`, over
|
||||
the real `@superset-ui/switchboard`.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
cd superset-embedded-sdk
|
||||
node testrig/drive.mjs # headless, builds the bundle if it is stale
|
||||
node testrig/drive.mjs --headed # watch it happen
|
||||
node testrig/drive.mjs --verbose # + the pages' console output
|
||||
```
|
||||
|
||||
Needs a `chromium`, `chromium-browser` or `google-chrome` on `PATH`, or
|
||||
`CHROMIUM_PATH` pointing at one. No npm dependencies: the driver speaks the
|
||||
DevTools protocol over Node's built-in WebSocket.
|
||||
|
||||
Exit code is 0 only if every check passed.
|
||||
|
||||
## Poke at it by hand
|
||||
|
||||
```bash
|
||||
node testrig/server.mjs # then open http://localhost:8100
|
||||
```
|
||||
|
||||
The host app has buttons for embedding, pushing a theme, calling
|
||||
`getActiveTabs()` and unmounting; the dashboard page has links that navigate it
|
||||
the way a tab or a link in a Markdown chart would. The right-hand pane logs
|
||||
what the host saw, and the counter under the frame is how many guest tokens the
|
||||
host's endpoint has minted.
|
||||
|
||||
`?ttl=15` on the host app shortens the guest token's life to 15 seconds, so the
|
||||
refresh timer fires while you are watching. `?hang=1` makes the dashboard page
|
||||
answer no `get` at all, which is how the rig produces a call that is still in
|
||||
flight when the user clicks a link. `?slowfirst=1` holds the very first
|
||||
`fetchGuestToken()` open until you settle it yourself with
|
||||
`rigReleaseFirstToken()` or `rigFailFirstToken()` from the console, and
|
||||
`?failnth=N` makes call N reject — together they are how the rig reaches the
|
||||
race between the initial fetch and a navigation that happens during it.
|
||||
|
||||
## What the pieces are
|
||||
|
||||
| file | what it stands for |
|
||||
| --- | --- |
|
||||
| `server.mjs` | the host app's backend (`/guest-token`, which counts what it mints) on `:8100`, and the Superset instance on `:8200` |
|
||||
| `host.html` | the host app: calls `embedDashboard`, drives the public API, records everything into `window.rig` |
|
||||
| `embedded.html` | Superset's embedded page: the same port-transfer handshake, the same method names, plus links that navigate itself |
|
||||
| `drive.mjs` | headless Chromium driver and the checks |
|
||||
|
||||
`embedded.html` reports what it experienced to the host through an ordinary
|
||||
`postMessage` to `parent`. That side channel is the rig's own and has nothing
|
||||
to do with the SDK's MessageChannel — it is how the driver can see what
|
||||
happened inside a cross-origin frame.
|
||||
|
||||
## What it checks
|
||||
|
||||
- a first embed renders, on exactly one minted token
|
||||
- a navigation internal to the dashboard re-authenticates the new document
|
||||
- the host's theme is re-applied to that document, after its token
|
||||
- a `get` still in flight when the user navigates is rejected as a
|
||||
`PortClosedError` rather than left hanging, and so is one caught by `unmount()`
|
||||
- a navigation that does not land on the embedded page mints no token at all,
|
||||
and coming back to the dashboard afterwards works
|
||||
- the refresh timer follows the current document, fires once, and leaves the
|
||||
document that is gone alone
|
||||
- an initial token fetch that fails *after* a navigation already recovered the
|
||||
embed leaves that working dashboard alone, and `embedDashboard` resolves
|
||||
- a token from a superseded cycle is handed straight to the document in front of
|
||||
the user when that document has none, rather than waiting out a retry
|
||||
|
||||
Run it against `git show HEAD~1:...` of `src/index.ts` to watch the checks fail.
|
||||
655
superset-embedded-sdk/testrig/drive.mjs
Normal file
655
superset-embedded-sdk/testrig/drive.mjs
Normal file
@@ -0,0 +1,655 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Drives the rig in headless Chromium over the DevTools protocol, with no
|
||||
// dependencies beyond a chromium binary. `node drive.mjs [--headed] [--verbose]`
|
||||
|
||||
import { spawn, execFileSync } from "node:child_process";
|
||||
import { mkdtempSync, existsSync, statSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { start, HOST_PORT } from "./server.mjs";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const sdkRoot = join(here, "..");
|
||||
const headed = process.argv.includes("--headed");
|
||||
const verbose = process.argv.includes("--verbose");
|
||||
|
||||
const CHROMIUM =
|
||||
process.env.CHROMIUM_PATH ||
|
||||
["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"].find(
|
||||
(bin) => {
|
||||
try {
|
||||
execFileSync("which", [bin], { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------------------ results
|
||||
const results = [];
|
||||
function check(name, ok, detail = "") {
|
||||
results.push({ name, ok, detail });
|
||||
const mark = ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
||||
console.log(` ${mark} ${name}${detail && !ok ? `\n ${detail}` : ""}`);
|
||||
}
|
||||
|
||||
// Waits for something to become true, and records the wait itself as a check,
|
||||
// so a run against code that never gets there reports a failure per scenario
|
||||
// instead of stopping at the first one.
|
||||
async function expect(name, predicate, timeoutMs = 15_000) {
|
||||
try {
|
||||
await waitFor(predicate, name, timeoutMs);
|
||||
check(name, true);
|
||||
return true;
|
||||
} catch (err) {
|
||||
check(name, false, err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- cdp client
|
||||
class CDP {
|
||||
constructor(ws) {
|
||||
this.ws = ws;
|
||||
this.nextId = 0;
|
||||
this.pending = new Map();
|
||||
this.listeners = [];
|
||||
ws.addEventListener("message", (event) => {
|
||||
// Nothing upstream catches a throw from this listener, so a frame that
|
||||
// does not parse is dropped rather than taking the whole run down.
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(event.data);
|
||||
} catch (err) {
|
||||
if (verbose) console.error(` [cdp] dropped an unparsable frame: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
if (msg.id && this.pending.has(msg.id)) {
|
||||
const { resolve, reject } = this.pending.get(msg.id);
|
||||
this.pending.delete(msg.id);
|
||||
if (msg.error) reject(new Error(JSON.stringify(msg.error)));
|
||||
else resolve(msg.result);
|
||||
} else {
|
||||
this.listeners.forEach((fn) => fn(msg));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
send(method, params = {}, sessionId) {
|
||||
const id = (this.nextId += 1);
|
||||
const payload = { id, method, params };
|
||||
if (sessionId) payload.sessionId = sessionId;
|
||||
this.ws.send(JSON.stringify(payload));
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject });
|
||||
setTimeout(() => {
|
||||
if (this.pending.delete(id)) reject(new Error(`${method} timed out`));
|
||||
}, 30_000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The `stop` of every browser still running, so an interrupted run can stop
|
||||
// them too.
|
||||
const runningBrowsers = new Set();
|
||||
|
||||
async function launchBrowser() {
|
||||
const userDataDir = mkdtempSync(join(tmpdir(), "embedded-sdk-rig-"));
|
||||
const args = [
|
||||
...(headed ? [] : ["--headless=new"]),
|
||||
"--remote-debugging-port=0",
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-gpu",
|
||||
"--disable-dev-shm-usage",
|
||||
"about:blank",
|
||||
];
|
||||
// In a process group of its own, so the helper processes chromium forks can
|
||||
// be killed along with it: left alive, they write back into the profile
|
||||
// after it has been removed.
|
||||
const child = spawn(CHROMIUM, args, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
});
|
||||
const exited = new Promise((resolve) => {
|
||||
child.once("exit", resolve);
|
||||
child.once("error", resolve);
|
||||
});
|
||||
let stopping;
|
||||
const stop = () =>
|
||||
(stopping ??= (async () => {
|
||||
runningBrowsers.delete(stop);
|
||||
try {
|
||||
process.kill(-child.pid, "SIGKILL");
|
||||
} catch {
|
||||
// already gone, or never started
|
||||
}
|
||||
await exited;
|
||||
rmSync(userDataDir, { recursive: true, force: true, maxRetries: 3 });
|
||||
})());
|
||||
runningBrowsers.add(stop);
|
||||
// Until `stop` is handed back, nobody else can clean up after this browser.
|
||||
try {
|
||||
return { cdp: await connect(child), stop };
|
||||
} catch (err) {
|
||||
await stop();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function connect(child) {
|
||||
const wsUrl = await new Promise((resolve, reject) => {
|
||||
let buffered = "";
|
||||
const onChunk = (chunk) => {
|
||||
buffered += chunk;
|
||||
const match = buffered.match(/ws:\/\/\S+/);
|
||||
if (match) resolve(match[0]);
|
||||
};
|
||||
child.stdout.on("data", onChunk);
|
||||
child.stderr.on("data", onChunk);
|
||||
child.on("exit", (code) =>
|
||||
reject(new Error(`chromium exited (${code}) before listening:\n${buffered}`)),
|
||||
);
|
||||
child.on("error", (err) =>
|
||||
reject(new Error(`could not start ${CHROMIUM}: ${err.message}`)),
|
||||
);
|
||||
setTimeout(() => reject(new Error("chromium never reported a devtools url")), 20_000);
|
||||
});
|
||||
const ws = new WebSocket(wsUrl);
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.addEventListener("open", resolve, { once: true });
|
||||
ws.addEventListener(
|
||||
"error",
|
||||
() => reject(new Error(`could not connect to chromium's devtools at ${wsUrl}`)),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
return new CDP(ws);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------- a page
|
||||
async function openPage(cdp, url) {
|
||||
const { targetId } = await cdp.send("Target.createTarget", { url: "about:blank" });
|
||||
const { sessionId } = await cdp.send("Target.attachToTarget", {
|
||||
targetId,
|
||||
flatten: true,
|
||||
});
|
||||
await cdp.send("Runtime.enable", {}, sessionId);
|
||||
await cdp.send("Page.enable", {}, sessionId);
|
||||
if (verbose) {
|
||||
cdp.listeners.push((msg) => {
|
||||
if (msg.method === "Runtime.consoleAPICalled" && msg.sessionId === sessionId) {
|
||||
const text = msg.params.args
|
||||
.map((a) => a.value ?? a.description ?? a.type)
|
||||
.join(" ");
|
||||
console.log(` [page] ${text}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const evaluate = async (expression) => {
|
||||
const r = await cdp.send(
|
||||
"Runtime.evaluate",
|
||||
{ expression, awaitPromise: true, returnByValue: true },
|
||||
sessionId,
|
||||
);
|
||||
if (r.exceptionDetails) {
|
||||
throw new Error(
|
||||
r.exceptionDetails.exception?.description ||
|
||||
r.exceptionDetails.text ||
|
||||
JSON.stringify(r.exceptionDetails),
|
||||
);
|
||||
}
|
||||
return r.result.value;
|
||||
};
|
||||
|
||||
await cdp.send("Page.navigate", { url }, sessionId);
|
||||
// Wait for the host app's own globals rather than a load event.
|
||||
await waitFor(
|
||||
() => evaluate("typeof window.rig === 'object' && !!window.supersetEmbeddedSdk"),
|
||||
"the host app to load",
|
||||
);
|
||||
return { evaluate, sessionId };
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
// A rejected predicate counts as "not yet", not as a failure: a navigation that
|
||||
// destroys the execution context mid-poll makes `evaluate` reject, and that
|
||||
// navigation is exactly the race this rig exists to reproduce.
|
||||
async function waitFor(predicate, what, timeoutMs = 15_000, intervalMs = 100) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastError;
|
||||
for (;;) {
|
||||
try {
|
||||
const value = await predicate();
|
||||
if (value) return value;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(
|
||||
`timed out waiting for ${what}${
|
||||
lastError ? ` (last error: ${lastError.message})` : ""
|
||||
}`,
|
||||
);
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ helpers
|
||||
function makeHelpers(evaluate) {
|
||||
return {
|
||||
evaluate,
|
||||
click: (id) => evaluate(`document.getElementById(${JSON.stringify(id)}).click()`),
|
||||
events: () => evaluate("window.rig.events"),
|
||||
errors: () => evaluate("window.rig.errors"),
|
||||
tokensMinted: () => evaluate("window.rigRefreshStats().then(s => s.tokensMinted)"),
|
||||
navigateGuest: (to) => evaluate(`window.rigNavigateGuest(${JSON.stringify(to)})`),
|
||||
embedState: () => evaluate("window.rig.embedState"),
|
||||
releaseFirstToken: () => evaluate("window.rigReleaseFirstToken()"),
|
||||
failFirstToken: () => evaluate("window.rigFailFirstToken()"),
|
||||
heldFirstFetch: () => evaluate("!!window.rig.heldFirstFetch"),
|
||||
hasIframe: () => evaluate("!!document.querySelector('#mount iframe')"),
|
||||
getActiveTabs: () => evaluate("window.rigGetActiveTabs()"),
|
||||
// Events the embedded page reported from a given page of the dashboard.
|
||||
eventsOn: async (page, name) =>
|
||||
(await evaluate("window.rig.events")).filter(
|
||||
(e) => e.page === String(page) && (!name || e.event === name),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------- the runs
|
||||
async function mainRun(cdp, hostOrigin) {
|
||||
console.log("\n\x1b[1mmain run\x1b[0m (300s tokens)");
|
||||
const { evaluate } = await openPage(cdp, `${hostOrigin}/`);
|
||||
const h = makeHelpers(evaluate);
|
||||
|
||||
// -- 1. the ordinary first embed -----------------------------------------
|
||||
await h.click("embed");
|
||||
await expect(
|
||||
"first load: the embedded page renders with a guest token",
|
||||
async () => (await h.eventsOn(1, "guestToken")).length === 1,
|
||||
);
|
||||
check(
|
||||
"first load: exactly one token minted",
|
||||
(await h.tokensMinted()) === 1,
|
||||
`minted ${await h.tokensMinted()}`,
|
||||
);
|
||||
|
||||
// -- 2. the bug this PR is about ------------------------------------------
|
||||
await evaluate("window.rig.dashboard.setThemeConfig({token:{colorPrimary:'#ff0066'}})");
|
||||
await evaluate("window.rig.dashboard.setThemeMode('dark')");
|
||||
await expect(
|
||||
"first load: the host's theme reaches the document",
|
||||
async () => (await h.eventsOn(1, "setThemeMode")).length === 1,
|
||||
);
|
||||
|
||||
await h.navigateGuest("/embedded/rig-dashboard?page=2");
|
||||
await expect(
|
||||
"internal navigation: the new document is re-authenticated",
|
||||
async () => (await h.eventsOn(2, "guestToken")).length === 1,
|
||||
);
|
||||
check(
|
||||
"internal navigation: one further token minted, not more",
|
||||
(await h.tokensMinted()) === 2,
|
||||
`minted ${await h.tokensMinted()}`,
|
||||
);
|
||||
|
||||
// The finding this round of fixes came from: methods were replayed, state
|
||||
// was not, so the dashboard came back in the default theme.
|
||||
await expect(
|
||||
"internal navigation: the host's theme is re-applied",
|
||||
async () => (await h.eventsOn(2, "setThemeMode")).length === 1,
|
||||
);
|
||||
const page2 = (await h.eventsOn(2)).map((e) => e.event);
|
||||
check(
|
||||
"internal navigation: the token arrives before the theme",
|
||||
page2.indexOf("guestToken") < page2.indexOf("setThemeConfig"),
|
||||
page2.join(" → "),
|
||||
);
|
||||
check(
|
||||
"internal navigation: the dark theme is actually on the new document",
|
||||
await evaluate(
|
||||
"document.querySelector('#mount iframe') && window.rig.events.some(e => e.page==='2' && e.event==='setThemeMode' && e.detail.mode==='dark')",
|
||||
),
|
||||
);
|
||||
|
||||
// -- 3. a call in flight when the document leaves --------------------------
|
||||
await h.navigateGuest("/embedded/rig-dashboard?page=3&hang=1");
|
||||
await expect(
|
||||
"a page that answers nothing still gets its token",
|
||||
async () => (await h.eventsOn(3, "guestToken")).length === 1,
|
||||
);
|
||||
await h.click("tabs"); // page 3 never answers this
|
||||
await sleep(300);
|
||||
await h.navigateGuest("/embedded/rig-dashboard?page=4");
|
||||
const rejected = await expect(
|
||||
"a call in flight when the user navigates is rejected, not left hanging",
|
||||
async () => (await h.errors()).length === 1,
|
||||
);
|
||||
if (rejected) {
|
||||
const [err] = await h.errors();
|
||||
check(
|
||||
"…and it is rejected as a PortClosedError",
|
||||
err.name === "PortClosedError",
|
||||
JSON.stringify(err),
|
||||
);
|
||||
}
|
||||
|
||||
// -- 4. a navigation that does not land on the embedded page --------------
|
||||
const beforeStray = await h.tokensMinted();
|
||||
await h.navigateGuest("/plain");
|
||||
// The handshake probe gives the new document 5s to answer.
|
||||
await sleep(7000);
|
||||
check(
|
||||
"a navigation away from the embedded page mints no guest token",
|
||||
(await h.tokensMinted()) === beforeStray,
|
||||
`minted ${await h.tokensMinted()}, was ${beforeStray}`,
|
||||
);
|
||||
|
||||
// -- 5. and back again ----------------------------------------------------
|
||||
// The stray page cannot navigate itself back for us, so the host reloads the
|
||||
// frame the way a host app would.
|
||||
await evaluate(
|
||||
"document.querySelector('#mount iframe').src = '__SUPERSET__/embedded/rig-dashboard?page=5&hang=1'".replace(
|
||||
"__SUPERSET__",
|
||||
await evaluate("SUPERSET_ORIGIN"),
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
"coming back to the embedded page re-authenticates again",
|
||||
async () => (await h.eventsOn(5, "guestToken")).length === 1,
|
||||
);
|
||||
check(
|
||||
"coming back mints exactly one more token",
|
||||
(await h.tokensMinted()) === beforeStray + 1,
|
||||
`minted ${await h.tokensMinted()}, was ${beforeStray}`,
|
||||
);
|
||||
|
||||
// -- 6. unmount -----------------------------------------------------------
|
||||
// Page 5 answers nothing either, so this call is still in flight.
|
||||
await h.click("tabs");
|
||||
await sleep(200);
|
||||
await evaluate("window.rig.dashboard.unmount()");
|
||||
if (
|
||||
await expect(
|
||||
"unmount rejects what is still in flight",
|
||||
async () => (await h.errors()).length === 2,
|
||||
)
|
||||
) {
|
||||
const errs = await h.errors();
|
||||
check(
|
||||
"…and it too is a PortClosedError",
|
||||
errs[1].name === "PortClosedError",
|
||||
JSON.stringify(errs[1]),
|
||||
);
|
||||
}
|
||||
const afterUnmount = await h.tokensMinted();
|
||||
await sleep(1000);
|
||||
check(
|
||||
"nothing is minted after unmount",
|
||||
(await h.tokensMinted()) === afterUnmount,
|
||||
);
|
||||
}
|
||||
|
||||
async function refreshRun(cdp, hostOrigin) {
|
||||
console.log("\n\x1b[1mrefresh run\x1b[0m (15s tokens, so the timer fires during the run)");
|
||||
await fetch(`${hostOrigin}/reset`);
|
||||
const { evaluate } = await openPage(cdp, `${hostOrigin}/?ttl=15`);
|
||||
const h = makeHelpers(evaluate);
|
||||
|
||||
await h.click("embed");
|
||||
await expect(
|
||||
"refresh run: the first token arrives",
|
||||
async () => (await h.eventsOn(1, "guestToken")).length === 1,
|
||||
);
|
||||
await h.navigateGuest("/embedded/rig-dashboard?page=2");
|
||||
await expect(
|
||||
"refresh run: the navigated-to page is authenticated",
|
||||
async () => (await h.eventsOn(2, "guestToken")).length === 1,
|
||||
);
|
||||
const mintedAfterNav = await h.tokensMinted();
|
||||
|
||||
// A 15s token refreshes 5s before it expires, so ~10s from when it was
|
||||
// issued. The point is which port that refresh lands on.
|
||||
await expect(
|
||||
"the refresh timer follows the current document",
|
||||
async () => (await h.eventsOn(2, "guestToken")).length === 2,
|
||||
20_000,
|
||||
);
|
||||
check(
|
||||
"the old document gets nothing",
|
||||
(await h.eventsOn(1, "guestToken")).length === 1,
|
||||
);
|
||||
check(
|
||||
"exactly one refresh, so there is only one timer chain",
|
||||
(await h.tokensMinted()) === mintedAfterNav + 1,
|
||||
`minted ${await h.tokensMinted()}, was ${mintedAfterNav}`,
|
||||
);
|
||||
await evaluate("window.rig.dashboard?.unmount()");
|
||||
}
|
||||
|
||||
|
||||
// The two directions of the race between the very first token fetch and a
|
||||
// navigation the dashboard makes while that fetch is still in flight. Neither
|
||||
// is reachable with a host endpoint that answers promptly, so the rig holds
|
||||
// the first fetch open and settles it by hand.
|
||||
async function staleInitialFetchRun(cdp, hostOrigin) {
|
||||
console.log(
|
||||
"\n\x1b[1mstale initial fetch run\x1b[0m (the first fetch fails after a navigation already recovered the embed)",
|
||||
);
|
||||
await fetch(`${hostOrigin}/reset`);
|
||||
const { evaluate } = await openPage(cdp, `${hostOrigin}/?slowfirst=1`);
|
||||
const h = makeHelpers(evaluate);
|
||||
|
||||
await h.click("embed");
|
||||
// The frame is up and page 1 is blank, waiting for a token that the rig is
|
||||
// sitting on.
|
||||
await expect("stale initial fetch: the first fetch is in flight", h.heldFirstFetch);
|
||||
await expect(
|
||||
"stale initial fetch: the first document is still waiting for a token",
|
||||
async () => (await h.eventsOn(1, "started")).length === 1,
|
||||
);
|
||||
check(
|
||||
"stale initial fetch: …and has not been given one",
|
||||
(await h.eventsOn(1, "guestToken")).length === 0,
|
||||
);
|
||||
|
||||
// The user clicks a link before the host's endpoint has answered.
|
||||
await h.navigateGuest("/embedded/rig-dashboard?page=2");
|
||||
await expect(
|
||||
"stale initial fetch: the navigated-to document authenticates on its own",
|
||||
async () => (await h.eventsOn(2, "guestToken")).length === 1,
|
||||
);
|
||||
const mintedWhileWorking = await h.tokensMinted();
|
||||
|
||||
// Only now does the original fetch give up. Nobody is waiting for it.
|
||||
await h.failFirstToken();
|
||||
await sleep(500);
|
||||
|
||||
check(
|
||||
"stale initial fetch: the working dashboard is not torn down",
|
||||
await h.hasIframe(),
|
||||
);
|
||||
check(
|
||||
"stale initial fetch: embedDashboard resolves rather than rejecting",
|
||||
(await h.embedState()) === "resolved",
|
||||
`embedState = ${await h.embedState()}`,
|
||||
);
|
||||
// The surest proof the embed is still live: the port still answers, with the
|
||||
// navigated-to document's own answer.
|
||||
const tabs = await h.getActiveTabs();
|
||||
check(
|
||||
"stale initial fetch: the port still answers, from the current document",
|
||||
tabs.ok && tabs.tabs?.[0] === "tab-on-page-2",
|
||||
JSON.stringify(tabs),
|
||||
);
|
||||
check(
|
||||
"stale initial fetch: the failure mints nothing further",
|
||||
(await h.tokensMinted()) === mintedWhileWorking,
|
||||
`minted ${await h.tokensMinted()}, was ${mintedWhileWorking}`,
|
||||
);
|
||||
await evaluate("window.rig.dashboard?.unmount()");
|
||||
}
|
||||
|
||||
async function supersededTokenRun(cdp, hostOrigin) {
|
||||
console.log(
|
||||
"\n\x1b[1msuperseded token run\x1b[0m (the navigation's own fetch fails, the first one succeeds late)",
|
||||
);
|
||||
await fetch(`${hostOrigin}/reset`);
|
||||
// Call #1 is held; call #2 — the one the navigation triggers — rejects.
|
||||
const { evaluate } = await openPage(cdp, `${hostOrigin}/?slowfirst=1&failnth=2`);
|
||||
const h = makeHelpers(evaluate);
|
||||
|
||||
await h.click("embed");
|
||||
await expect("superseded token: the first fetch is in flight", h.heldFirstFetch);
|
||||
// The first document has to be listening before it can be told to navigate:
|
||||
// the held fetch starts before the iframe has loaded.
|
||||
await expect(
|
||||
"superseded token: the first document is up and waiting for a token",
|
||||
async () => (await h.eventsOn(1, "started")).length === 1,
|
||||
);
|
||||
|
||||
await h.navigateGuest("/embedded/rig-dashboard?page=2");
|
||||
// The navigation's own fetch fails, so page 2 is blank and a retry is armed
|
||||
// ten seconds out.
|
||||
await expect(
|
||||
"superseded token: the navigated-to document reaches the handshake",
|
||||
async () => (await h.eventsOn(2, "started")).length === 1,
|
||||
);
|
||||
await expect(
|
||||
"superseded token: its own fetch failed, so it has no token yet",
|
||||
async () => (await evaluate("window.rig.tokenFetches")) === 2,
|
||||
);
|
||||
check(
|
||||
"superseded token: …and the document is still blank",
|
||||
(await h.eventsOn(2, "guestToken")).length === 0,
|
||||
);
|
||||
|
||||
// The first fetch finally answers. Its token is superseded but perfectly
|
||||
// valid, and the document in front of the user has none.
|
||||
await h.releaseFirstToken();
|
||||
// Well inside the 10s retry interval: holding the token back until the failed
|
||||
// cycle retries is exactly the blank page this is about, so a token that only
|
||||
// turns up on the retry is not a pass.
|
||||
await expect(
|
||||
"superseded token: the superseded token is handed straight to the current document",
|
||||
async () => (await h.eventsOn(2, "guestToken")).length === 1,
|
||||
3_000,
|
||||
);
|
||||
check(
|
||||
"superseded token: nothing is sent to the document that is gone",
|
||||
(await h.eventsOn(1, "guestToken")).length === 0,
|
||||
);
|
||||
check(
|
||||
"superseded token: one token minted, not a second for the retry",
|
||||
(await h.tokensMinted()) === 1,
|
||||
`minted ${await h.tokensMinted()}`,
|
||||
);
|
||||
check(
|
||||
"superseded token: embedDashboard resolves",
|
||||
(await h.embedState()) === "resolved",
|
||||
`embedState = ${await h.embedState()}`,
|
||||
);
|
||||
await evaluate("window.rig.dashboard?.unmount()");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------- main
|
||||
// Everything the bundle is built from, not just the sources: a change to the
|
||||
// build config or the dependencies is just as able to make it stale.
|
||||
const BUILD_INPUTS = [
|
||||
"webpack.config.js",
|
||||
"babel.config.js",
|
||||
"tsconfig.json",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
];
|
||||
|
||||
function buildIfStale() {
|
||||
const bundle = join(sdkRoot, "bundle", "index.js");
|
||||
const inputs = [
|
||||
...readdirSync(join(sdkRoot, "src"), { recursive: true }).map((f) =>
|
||||
join("src", f),
|
||||
),
|
||||
...BUILD_INPUTS,
|
||||
]
|
||||
.map((f) => join(sdkRoot, f))
|
||||
.filter((f) => existsSync(f) && statSync(f).isFile());
|
||||
const newestInput = Math.max(...inputs.map((f) => statSync(f).mtimeMs));
|
||||
if (existsSync(bundle) && statSync(bundle).mtimeMs > newestInput) return;
|
||||
console.log("building the sdk bundle…");
|
||||
execFileSync("npx", ["webpack", "--mode", "development"], {
|
||||
cwd: sdkRoot,
|
||||
stdio: verbose ? "inherit" : "ignore",
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!CHROMIUM) {
|
||||
console.error(
|
||||
"no chromium found. Install one, or set CHROMIUM_PATH to a chrome binary.",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
buildIfStale();
|
||||
// Both are started inside the `try`, so whichever of them did come up is
|
||||
// stopped even when the other one fails to.
|
||||
let server;
|
||||
let browser;
|
||||
try {
|
||||
server = await start();
|
||||
browser = await launchBrowser();
|
||||
await mainRun(browser.cdp, server.hostOrigin);
|
||||
await refreshRun(browser.cdp, server.hostOrigin);
|
||||
await staleInitialFetchRun(browser.cdp, server.hostOrigin);
|
||||
await supersededTokenRun(browser.cdp, server.hostOrigin);
|
||||
} finally {
|
||||
await browser?.stop();
|
||||
server?.stop();
|
||||
}
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
console.log(
|
||||
`\n${results.length - failed.length}/${results.length} checks passed`,
|
||||
);
|
||||
process.exit(failed.length ? 1 : 0);
|
||||
}
|
||||
|
||||
// Chromium runs in its own process group, so a Ctrl-C no longer reaches it on
|
||||
// its own. The servers need nothing: they go with this process.
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.once(signal, async () => {
|
||||
console.error(`\n${signal}: stopping the rig`);
|
||||
await Promise.all([...runningBrowsers].map((stop) => stop()));
|
||||
process.exit(130);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("\nrig failed:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
127
superset-embedded-sdk/testrig/embedded.html
Normal file
127
superset-embedded-sdk/testrig/embedded.html
Normal file
@@ -0,0 +1,127 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>rig: embedded dashboard</title>
|
||||
<style>
|
||||
:root { color-scheme: light; --bg: #fff; --fg: #1b1b1b; --muted: #666; }
|
||||
body.dark { color-scheme: dark; --bg: #16181d; --fg: #e8e8e8; --muted: #9aa; }
|
||||
body { margin: 0; padding: 20px; font: 14px/1.5 system-ui, sans-serif;
|
||||
background: var(--bg); color: var(--fg); }
|
||||
h1 { font-size: 16px; margin: 0 0 4px; }
|
||||
.muted { color: var(--muted); }
|
||||
.chart { margin: 16px 0; padding: 16px; border: 1px solid #8884; border-radius: 8px; }
|
||||
.blank { color: #b00; font-weight: 600; }
|
||||
a { color: inherit; }
|
||||
code { font-family: ui-monospace, monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Embedded dashboard — <span id="page">page 1</span></h1>
|
||||
<p class="muted">This page stands in for Superset's embedded page. It speaks
|
||||
the same handshake as <code>superset-frontend/src/embedded/index.tsx</code>.</p>
|
||||
|
||||
<div class="chart">
|
||||
<div id="content" class="blank">blank: waiting for a guest token…</div>
|
||||
<div class="muted" id="detail"></div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<!-- A link internal to the dashboard: a tab, a drill-down, a link in a
|
||||
Markdown chart. It navigates this same iframe. -->
|
||||
<a id="internal-link" href="#">go to page 2 (link internal to the dashboard)</a><br />
|
||||
<a id="offsite-link" href="/plain">go to a page that is not the embedded one</a>
|
||||
</p>
|
||||
|
||||
<script type="module">
|
||||
import Switchboard from "/vendor/switchboard.js";
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const page = params.get("page") || "1";
|
||||
document.getElementById("page").textContent = `page ${page}`;
|
||||
document.getElementById("internal-link").href =
|
||||
`${location.pathname}?page=${Number.isNaN(+page) ? 2 : +page + 1}` +
|
||||
(params.get("hang") ? "&hang=1" : "");
|
||||
|
||||
// The rig's own side channel, so the driver can see what this document
|
||||
// experienced. It is nothing to do with the SDK's MessageChannel.
|
||||
const report = (event, detail) =>
|
||||
parent.postMessage({ __rig: true, page, event, detail }, "*");
|
||||
|
||||
let tokensReceived = 0;
|
||||
|
||||
function render(detail) {
|
||||
const el = document.getElementById("content");
|
||||
el.className = "";
|
||||
el.textContent = `rendered with a guest token (page ${page})`;
|
||||
document.getElementById("detail").textContent = detail;
|
||||
}
|
||||
|
||||
// Mirrors the real page: everything is defined inside the port-transfer
|
||||
// handler, and `start()` comes last.
|
||||
window.addEventListener("message", function embeddedPageInitializer(event) {
|
||||
const port = event.ports?.[0];
|
||||
if (event.data?.type !== "__embedded_comms__") return;
|
||||
if (event.data.handshake === "port transfer" && port) {
|
||||
report("handshake");
|
||||
|
||||
Switchboard.init({ port, name: "superset", debug: false });
|
||||
|
||||
Switchboard.defineMethod("guestToken", ({ guestToken }) => {
|
||||
tokensReceived += 1;
|
||||
const claims = JSON.parse(atob(guestToken.split(".")[1]));
|
||||
render(`token #${claims.serial}, exp in ${
|
||||
claims.exp - Math.floor(Date.now() / 1000)
|
||||
}s, ${tokensReceived} received on this port`);
|
||||
report("guestToken", { serial: claims.serial, tokensReceived });
|
||||
});
|
||||
|
||||
Switchboard.defineMethod("getActiveTabs", () => {
|
||||
// `?hang=1` makes this page answer nothing, standing in for a reply
|
||||
// that is still being computed when the user clicks a link.
|
||||
if (params.get("hang")) return new Promise(() => {});
|
||||
return [`tab-on-page-${page}`];
|
||||
});
|
||||
|
||||
Switchboard.defineMethod("getScrollSize", () => {
|
||||
if (params.get("hang")) return new Promise(() => {});
|
||||
return { width: 800, height: 600 };
|
||||
});
|
||||
|
||||
Switchboard.defineMethod("setThemeConfig", ({ themeConfig }) => {
|
||||
report("setThemeConfig", themeConfig);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
Switchboard.defineMethod("setThemeMode", ({ mode }) => {
|
||||
document.body.classList.toggle("dark", mode === "dark");
|
||||
report("setThemeMode", { mode });
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
Switchboard.start();
|
||||
report("started");
|
||||
}
|
||||
});
|
||||
|
||||
// The rig drives navigation the way the dashboard itself would: this
|
||||
// document sets its own location. The host has no other way to reach into a
|
||||
// cross-origin frame, and neither would a real one.
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.data?.__rigCommand === "navigate") {
|
||||
location.href = event.data.to;
|
||||
}
|
||||
});
|
||||
|
||||
report("loaded");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
239
superset-embedded-sdk/testrig/host.html
Normal file
239
superset-embedded-sdk/testrig/host.html
Normal file
@@ -0,0 +1,239 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<!--
|
||||
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
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>embedded-sdk test rig — host app</title>
|
||||
<style>
|
||||
body { margin: 0; font: 14px/1.5 system-ui, sans-serif; display: grid;
|
||||
grid-template-columns: 1fr 420px; height: 100vh; }
|
||||
main { padding: 16px; overflow: auto; }
|
||||
aside { border-left: 1px solid #ddd; padding: 16px; overflow: auto; background: #fafafa; }
|
||||
h1 { font-size: 16px; margin: 0 0 12px; }
|
||||
#mount { border: 1px solid #ccc; border-radius: 8px; height: 320px; }
|
||||
#mount iframe { width: 100%; height: 100%; border: 0; border-radius: 8px; }
|
||||
button { font: inherit; margin: 0 6px 6px 0; padding: 4px 10px; }
|
||||
#log { font: 12px/1.45 ui-monospace, monospace; white-space: pre-wrap; }
|
||||
.t { color: #888; }
|
||||
.err { color: #b00; }
|
||||
.ok { color: #070; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Host app</h1>
|
||||
<div>
|
||||
<button id="embed">embed</button>
|
||||
<button id="theme">setThemeMode(dark) + setThemeConfig</button>
|
||||
<button id="tabs">getActiveTabs()</button>
|
||||
<button id="unmount">unmount</button>
|
||||
<button id="clear">clear log</button>
|
||||
</div>
|
||||
<div id="mount"></div>
|
||||
<p class="t">Tokens minted by this host's endpoint: <b id="minted">0</b></p>
|
||||
</main>
|
||||
<aside><div id="log"></div></aside>
|
||||
|
||||
<script src="/sdk.js"></script>
|
||||
<script>
|
||||
const SUPERSET_ORIGIN = "__SUPERSET_ORIGIN__";
|
||||
|
||||
// Everything the driver reads.
|
||||
const rig = (window.rig = {
|
||||
events: [], // what the embedded page reported over its side channel
|
||||
log: [], // what the host app saw
|
||||
errors: [], // rejections the SDK handed back
|
||||
tokenFetches: 0, // calls the SDK made to fetchGuestToken
|
||||
dashboard: null,
|
||||
embedState: "idle", // pending | resolved | rejected
|
||||
embedError: null,
|
||||
heldFirstFetch: null,
|
||||
});
|
||||
|
||||
function log(kind, ...parts) {
|
||||
const line = `${new Date().toISOString().slice(11, 23)} ${kind} ${parts
|
||||
.map((p) => (typeof p === "string" ? p : JSON.stringify(p)))
|
||||
.join(" ")}`;
|
||||
rig.log.push(line);
|
||||
const el = document.createElement("div");
|
||||
el.className = kind === "error" ? "err" : kind === "ok" ? "ok" : "";
|
||||
el.textContent = line;
|
||||
document.getElementById("log").prepend(el);
|
||||
}
|
||||
|
||||
// The embedded page's side channel (not the SDK's MessageChannel).
|
||||
window.addEventListener("message", (event) => {
|
||||
if (!event.data?.__rig) return;
|
||||
rig.events.push({ ...event.data, at: Date.now() });
|
||||
log("guest", `page ${event.data.page}`, event.data.event, event.data.detail ?? "");
|
||||
});
|
||||
|
||||
async function refreshStats() {
|
||||
const stats = await fetch("/stats").then((r) => r.json());
|
||||
document.getElementById("minted").textContent = stats.tokensMinted;
|
||||
rig.tokensMinted = stats.tokensMinted;
|
||||
return stats;
|
||||
}
|
||||
window.rigRefreshStats = refreshStats;
|
||||
|
||||
// TTL is a query param so a run can exercise the refresh timer in seconds
|
||||
// rather than minutes.
|
||||
const search = new URLSearchParams(location.search);
|
||||
const ttl = search.get("ttl") || "300";
|
||||
// `?slowfirst=1` holds the very first fetchGuestToken() until the driver
|
||||
// settles it by hand, which is the only way to have a navigation happen
|
||||
// while the initial token is still in flight. `?failnth=N` makes call N
|
||||
// reject, standing in for a host endpoint that is down.
|
||||
const slowFirst = search.get("slowfirst");
|
||||
const failNth = Number(search.get("failnth") || 0);
|
||||
|
||||
async function mintToken() {
|
||||
const { token } = await fetch(`/guest-token?ttl=${ttl}`).then((r) => r.json());
|
||||
refreshStats();
|
||||
return token;
|
||||
}
|
||||
|
||||
async function fetchGuestToken() {
|
||||
rig.tokenFetches += 1;
|
||||
const call = rig.tokenFetches;
|
||||
log("host", `fetchGuestToken() #${call}`);
|
||||
if (call === failNth) {
|
||||
log("error", `fetchGuestToken() #${call} rejects (rig)`);
|
||||
throw new Error(`rig: host token endpoint is down (call #${call})`);
|
||||
}
|
||||
if (slowFirst && call === 1) {
|
||||
log("host", "fetchGuestToken() #1 held by the rig");
|
||||
return new Promise((resolve, reject) => {
|
||||
rig.heldFirstFetch = { resolve, reject };
|
||||
});
|
||||
}
|
||||
return mintToken();
|
||||
}
|
||||
|
||||
// The held first fetch, handed out once so it cannot be settled twice.
|
||||
function takeHeldFirstFetch(action) {
|
||||
const held = rig.heldFirstFetch;
|
||||
if (!held) {
|
||||
log("error", `${action}: no fetchGuestToken() is being held (needs ?slowfirst=1)`);
|
||||
return null;
|
||||
}
|
||||
rig.heldFirstFetch = null;
|
||||
return held;
|
||||
}
|
||||
|
||||
// Settle the held first fetch, long after the SDK asked for it.
|
||||
window.rigReleaseFirstToken = async () => {
|
||||
const held = takeHeldFirstFetch("rigReleaseFirstToken");
|
||||
if (!held) return;
|
||||
try {
|
||||
held.resolve(await mintToken());
|
||||
log("host", "released the held fetchGuestToken() #1");
|
||||
} catch (err) {
|
||||
// Never leave the SDK waiting on a fetch nobody will settle.
|
||||
held.reject(err);
|
||||
log("error", `could not mint the held token, failed fetchGuestToken() #1: ${err.message}`);
|
||||
}
|
||||
};
|
||||
window.rigFailFirstToken = () => {
|
||||
const held = takeHeldFirstFetch("rigFailFirstToken");
|
||||
if (!held) return;
|
||||
held.reject(new Error("rig: host token endpoint is down"));
|
||||
log("error", "failed the held fetchGuestToken() #1");
|
||||
};
|
||||
|
||||
document.getElementById("embed").onclick = () => {
|
||||
// Deliberately not awaited: a run needs to watch what happens while
|
||||
// `embedDashboard` is still pending, and to see how it ends up settling.
|
||||
rig.embedState = "pending";
|
||||
supersetEmbeddedSdk
|
||||
.embedDashboard({
|
||||
id: "rig-dashboard",
|
||||
supersetDomain: SUPERSET_ORIGIN,
|
||||
mountPoint: document.getElementById("mount"),
|
||||
fetchGuestToken,
|
||||
debug: true,
|
||||
dashboardUiConfig: search.get("hang")
|
||||
? { urlParams: { hang: "1" } }
|
||||
: undefined,
|
||||
})
|
||||
.then(
|
||||
(dashboard) => {
|
||||
rig.dashboard = dashboard;
|
||||
rig.embedState = "resolved";
|
||||
log("ok", "embedDashboard resolved");
|
||||
},
|
||||
(err) => {
|
||||
rig.embedState = "rejected";
|
||||
rig.embedError = { name: err.name, message: err.message };
|
||||
log("error", `embedDashboard rejected: ${err.message}`);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// The buttons are live before `embedDashboard` resolves, and stay live if it
|
||||
// rejects, so each one checks there is a dashboard to act on.
|
||||
function hasDashboard(action) {
|
||||
if (rig.dashboard) return true;
|
||||
log("error", `${action}: no dashboard yet (embedDashboard is ${rig.embedState})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
document.getElementById("theme").onclick = () => {
|
||||
if (!hasDashboard("theme")) return;
|
||||
rig.dashboard.setThemeConfig({ token: { colorPrimary: "#ff0066" } });
|
||||
rig.dashboard.setThemeMode("dark");
|
||||
log("host", "theme pushed");
|
||||
};
|
||||
|
||||
document.getElementById("tabs").onclick = () => {
|
||||
if (!hasDashboard("getActiveTabs()")) return;
|
||||
log("host", "getActiveTabs() called");
|
||||
rig.dashboard.getActiveTabs().then(
|
||||
(tabs) => log("ok", "getActiveTabs ->", tabs),
|
||||
(err) => {
|
||||
rig.errors.push({ name: err.name, message: err.message, at: Date.now() });
|
||||
log("error", `getActiveTabs rejected: ${err.name}: ${err.message}`);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
document.getElementById("unmount").onclick = () => {
|
||||
if (!hasDashboard("unmount()")) return;
|
||||
rig.dashboard.unmount();
|
||||
log("host", "unmount()");
|
||||
};
|
||||
|
||||
document.getElementById("clear").onclick = () => {
|
||||
document.getElementById("log").textContent = "";
|
||||
};
|
||||
|
||||
// Same call as the button, but handing the outcome back to the driver, so a
|
||||
// run can tell "the port answered" from "there was nothing to ask".
|
||||
window.rigGetActiveTabs = () => {
|
||||
if (!rig.dashboard) return Promise.resolve({ ok: false, name: "NoDashboard" });
|
||||
return rig.dashboard.getActiveTabs().then(
|
||||
(tabs) => ({ ok: true, tabs }),
|
||||
(err) => ({ ok: false, name: err.name, message: err.message }),
|
||||
);
|
||||
};
|
||||
|
||||
// Ask the embedded page to navigate itself, which is what a tab, a
|
||||
// drill-down or a link in a Markdown chart does.
|
||||
window.rigNavigateGuest = (to) => {
|
||||
document
|
||||
.querySelector("#mount iframe")
|
||||
.contentWindow.postMessage({ __rigCommand: "navigate", to }, SUPERSET_ORIGIN);
|
||||
log("host", "asked the dashboard to navigate to", to);
|
||||
};
|
||||
|
||||
refreshStats();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
199
superset-embedded-sdk/testrig/server.mjs
Normal file
199
superset-embedded-sdk/testrig/server.mjs
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Two origins, because that is what the SDK actually deals with: the host app
|
||||
// on one, the Superset instance on the other. No dependencies.
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const sdkRoot = join(here, "..");
|
||||
|
||||
export const HOST_PORT = Number(process.env.RIG_HOST_PORT || 8100);
|
||||
export const SUPERSET_PORT = Number(process.env.RIG_SUPERSET_PORT || 8200);
|
||||
|
||||
const hostOrigin = `http://localhost:${HOST_PORT}`;
|
||||
const supersetOrigin = `http://localhost:${SUPERSET_PORT}`;
|
||||
|
||||
/** Every guest token this rig has minted, so a test can count them. */
|
||||
const minted = [];
|
||||
|
||||
function base64url(value) {
|
||||
return Buffer.from(value).toString("base64url");
|
||||
}
|
||||
|
||||
/** A JWT the SDK can actually decode, since it reads `exp` off the token. */
|
||||
function mintGuestToken(ttlSeconds) {
|
||||
const claims = {
|
||||
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
|
||||
user: { username: "rig" },
|
||||
resources: [{ type: "dashboard", id: "rig-dashboard" }],
|
||||
serial: minted.length + 1,
|
||||
};
|
||||
const token = `${base64url('{"alg":"HS256","typ":"JWT"}')}.${base64url(
|
||||
JSON.stringify(claims),
|
||||
)}.rig-signature`;
|
||||
minted.push({ at: Date.now(), ttlSeconds, serial: claims.serial });
|
||||
return token;
|
||||
}
|
||||
|
||||
function send(res, status, body, type = "text/html; charset=utf-8") {
|
||||
res.writeHead(status, {
|
||||
"content-type": type,
|
||||
"cache-control": "no-store",
|
||||
// The host page needs to call the token endpoint from its own origin only,
|
||||
// but the Superset origin loads nothing from here, so this stays simple.
|
||||
"access-control-allow-origin": "*",
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
async function sendFile(res, path, type) {
|
||||
try {
|
||||
send(res, 200, await readFile(path), type);
|
||||
} catch (err) {
|
||||
send(res, 404, `not found: ${path}\n${err.message}`, "text/plain");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- host app
|
||||
const hostServer = createServer(async (req, res) => {
|
||||
const url = new URL(req.url, hostOrigin);
|
||||
|
||||
if (url.pathname === "/" || url.pathname === "/index.html") {
|
||||
const html = await readFile(join(here, "host.html"), "utf8");
|
||||
return send(
|
||||
res,
|
||||
200,
|
||||
html.replace("__SUPERSET_ORIGIN__", supersetOrigin),
|
||||
);
|
||||
}
|
||||
// The SDK exactly as a host app consumes it: the built UMD bundle.
|
||||
if (url.pathname === "/sdk.js") {
|
||||
return sendFile(res, join(sdkRoot, "bundle", "index.js"), "text/javascript");
|
||||
}
|
||||
if (url.pathname === "/sdk.js.map") {
|
||||
return sendFile(
|
||||
res,
|
||||
join(sdkRoot, "bundle", "index.js.map"),
|
||||
"application/json",
|
||||
);
|
||||
}
|
||||
// The host app's own guest-token endpoint. Counting the calls to it is how
|
||||
// the rig proves the SDK does not mint a token per stray navigation.
|
||||
if (url.pathname === "/guest-token") {
|
||||
const ttl = Number(url.searchParams.get("ttl") || 300);
|
||||
return send(
|
||||
res,
|
||||
200,
|
||||
JSON.stringify({ token: mintGuestToken(ttl) }),
|
||||
"application/json",
|
||||
);
|
||||
}
|
||||
if (url.pathname === "/stats") {
|
||||
return send(
|
||||
res,
|
||||
200,
|
||||
JSON.stringify({ tokensMinted: minted.length, minted }),
|
||||
"application/json",
|
||||
);
|
||||
}
|
||||
if (url.pathname === "/reset") {
|
||||
minted.length = 0;
|
||||
return send(res, 200, JSON.stringify({ ok: true }), "application/json");
|
||||
}
|
||||
return send(res, 404, "not found", "text/plain");
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------ fake superset
|
||||
const supersetServer = createServer(async (req, res) => {
|
||||
const url = new URL(req.url, supersetOrigin);
|
||||
|
||||
// What the SDK points the iframe at: /embedded/<uuid>
|
||||
if (url.pathname.startsWith("/embedded/")) {
|
||||
return sendFile(res, join(here, "embedded.html"), "text/html; charset=utf-8");
|
||||
}
|
||||
// A Superset page that is not the embedded one — what a link in a Markdown
|
||||
// chart can reach. It listens for nothing.
|
||||
if (url.pathname === "/plain") {
|
||||
return send(
|
||||
res,
|
||||
200,
|
||||
`<!doctype html><meta charset="utf-8"><title>a page that is not embedded</title>
|
||||
<body style="font:14px system-ui;padding:24px">
|
||||
<h1>Not the embedded page</h1>
|
||||
<p>No switchboard here. Nothing will answer the SDK's handshake.</p>
|
||||
<p><a id="back" href="/embedded/rig-dashboard?page=back">back to the dashboard</a></p>`,
|
||||
);
|
||||
}
|
||||
if (url.pathname === "/vendor/switchboard.js") {
|
||||
return sendFile(
|
||||
res,
|
||||
join(sdkRoot, "node_modules/@superset-ui/switchboard/esm/switchboard.js"),
|
||||
"text/javascript",
|
||||
);
|
||||
}
|
||||
return send(res, 404, "not found", "text/plain");
|
||||
});
|
||||
|
||||
// Rejects, rather than hanging, when either port cannot be bound (typically a
|
||||
// previous run still holding it), after closing whichever server did come up.
|
||||
export async function start() {
|
||||
const servers = [
|
||||
[hostServer, HOST_PORT],
|
||||
[supersetServer, SUPERSET_PORT],
|
||||
];
|
||||
const stop = () => {
|
||||
for (const [server] of servers) if (server.listening) server.close();
|
||||
};
|
||||
const outcomes = await Promise.allSettled(
|
||||
servers.map(
|
||||
([server, port]) =>
|
||||
new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(port, () => {
|
||||
server.off("error", reject);
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
const failure = outcomes.find((o) => o.status === "rejected");
|
||||
if (failure) {
|
||||
stop();
|
||||
throw new Error(
|
||||
`the rig could not start: ${failure.reason.message} ` +
|
||||
"(set RIG_HOST_PORT / RIG_SUPERSET_PORT to use other ports)",
|
||||
{ cause: failure.reason },
|
||||
);
|
||||
}
|
||||
return { hostOrigin, supersetOrigin, stop };
|
||||
}
|
||||
|
||||
// `node server.mjs` runs the rig for a human; the driver imports `start`.
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const { hostOrigin: h } = await start();
|
||||
console.log(`test rig up:
|
||||
host app ${h}
|
||||
fake superset ${supersetOrigin}
|
||||
open the host app and use the buttons. Ctrl-C to stop.`);
|
||||
}
|
||||
2
superset-frontend/.gitignore
vendored
2
superset-frontend/.gitignore
vendored
@@ -1,6 +1,4 @@
|
||||
coverage/*
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
playwright/.auth
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
@@ -90,7 +90,7 @@ export default {
|
||||
'@babel/plugin-transform-export-namespace-from',
|
||||
],
|
||||
},
|
||||
// build instrumented code for testing code coverage with Cypress
|
||||
// build instrumented code for testing code coverage with E2E tests
|
||||
instrumented: {
|
||||
plugins: [
|
||||
[
|
||||
|
||||
4
superset-frontend/cypress-base/.gitignore
vendored
4
superset-frontend/cypress-base/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
screenshots
|
||||
.nyc_output
|
||||
coverage
|
||||
coverage.json
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
const { verifyDownloadTasks } = require('cy-verify-downloads');
|
||||
|
||||
export default defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
defaultCommandTimeout: 8000,
|
||||
numTestsKeptInMemory: 3,
|
||||
// Disabled after realizing this MESSES UP rison encoding in intricate ways
|
||||
experimentalFetchPolyfill: false,
|
||||
experimentalMemoryManagement: true,
|
||||
requestTimeout: 10000,
|
||||
video: false,
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 1024,
|
||||
projectId: 'ud5x2f',
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
// ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
|
||||
// Check https://github.com/cypress-io/cypress/issues/27804 for context
|
||||
// TODO: This workaround should be removed when upgrading Cypress
|
||||
on('before:browser:launch', (browser, launchOptions) => {
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
launchOptions.args = launchOptions.args.map(arg => {
|
||||
if (arg === '--headless') {
|
||||
return '--headless=new';
|
||||
}
|
||||
|
||||
return arg;
|
||||
});
|
||||
|
||||
launchOptions.args.push(
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
'--no-sandbox',
|
||||
'--disable-software-rasterizer',
|
||||
'--memory-pressure-off',
|
||||
'--js-flags=--max-old-space-size=4096',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-renderer-backgrounding',
|
||||
);
|
||||
}
|
||||
return launchOptions;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line global-require
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
on('task', verifyDownloadTasks);
|
||||
// eslint-disable-next-line global-require,import/extensions
|
||||
return config;
|
||||
},
|
||||
baseUrl: 'http://localhost:8088',
|
||||
excludeSpecPattern: ['**/_skip.*'],
|
||||
experimentalRunAllSpecs: true,
|
||||
specPattern: ['cypress/e2e/**/*.{js,jsx,ts,tsx}'],
|
||||
},
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
[
|
||||
{
|
||||
"slice_name": "1 - Sample chart",
|
||||
"description": "chart description",
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 2,
|
||||
"datasource_type": "table",
|
||||
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
|
||||
},
|
||||
{
|
||||
"slice_name": "2 - Sample chart",
|
||||
"description": "chart description",
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 2,
|
||||
"datasource_type": "table",
|
||||
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
|
||||
},
|
||||
{
|
||||
"slice_name": "3 - Sample chart",
|
||||
"description": "chart description",
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 2,
|
||||
"datasource_type": "table",
|
||||
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
|
||||
},
|
||||
{
|
||||
"slice_name": "4 - Sample chart",
|
||||
"description": "chart description",
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 2,
|
||||
"datasource_type": "table",
|
||||
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
|
||||
}
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
[
|
||||
{
|
||||
"dashboard_title": "1 - Sample dashboard",
|
||||
"slug": "1-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "2 - Sample dashboard",
|
||||
"slug": "2-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "3 - Sample dashboard",
|
||||
"slug": "3-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "4 - Sample dashboard",
|
||||
"slug": "4-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "5 - Sample dashboard",
|
||||
"slug": "5-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "6 - Sample dashboard",
|
||||
"slug": "6-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "7 - Sample dashboard",
|
||||
"slug": "7-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "8 - Sample dashboard",
|
||||
"slug": "8-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "9 - Sample dashboard",
|
||||
"slug": "9-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "10 - Sample dashboard",
|
||||
"slug": "10-sample-dashboard"
|
||||
},
|
||||
{
|
||||
"dashboard_title": "11 - Sample dashboard",
|
||||
"slug": "11-sample-dashboard"
|
||||
}
|
||||
]
|
||||
@@ -1,455 +0,0 @@
|
||||
/**
|
||||
* 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 '@cypress/code-coverage/support';
|
||||
import { expect } from 'chai';
|
||||
import rison from 'rison';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
require('cy-verify-downloads').addCustomCommand();
|
||||
|
||||
// fail on console error, allow config to override individual tests
|
||||
// these exceptions are a little pile of tech debt
|
||||
//
|
||||
|
||||
// DISABLING FOR NOW
|
||||
/*
|
||||
const { getConfig, setConfig } = failOnConsoleError({
|
||||
consoleMessages: [
|
||||
/\[webpack-dev-server\]/,
|
||||
'The pseudo class ":first-child" is potentially unsafe when doing server-side rendering. Try changing it to ":first-of-type".',
|
||||
'The pseudo class ":nth-child" is potentially unsafe when doing server-side rendering. Try changing it to ":nth-of-type".',
|
||||
'Error: Unknown Error',
|
||||
/Unable to infer path to ace from script src/,
|
||||
],
|
||||
includeConsoleTypes: ['error'],
|
||||
});
|
||||
*/
|
||||
|
||||
// Set individual tests to allow certain console errors to NOT fail, e.g
|
||||
// cy.allowConsoleErrors(['foo', /^some bar-regex.*/]);
|
||||
// This will be reset between tests.
|
||||
Cypress.Commands.addAll({
|
||||
getConsoleMessages: () => cy.wrap(getConfig()?.consoleMessages),
|
||||
allowConsoleErrors: (consoleMessages: (string | RegExp)[]) =>
|
||||
setConfig({ ...getConfig(), consoleMessages }),
|
||||
});
|
||||
|
||||
const BASE_EXPLORE_URL = '/explore/?form_data=';
|
||||
let DASHBOARD_FIXTURES: Record<string, any>[] = [];
|
||||
let CHART_FIXTURES: Record<string, any>[] = [];
|
||||
|
||||
Cypress.Commands.add('loadChartFixtures', () =>
|
||||
cy.fixture('charts.json').then(charts => {
|
||||
CHART_FIXTURES = charts;
|
||||
}),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('loadDashboardFixtures', () =>
|
||||
cy.fixture('dashboards.json').then(dashboards => {
|
||||
DASHBOARD_FIXTURES = dashboards;
|
||||
}),
|
||||
);
|
||||
|
||||
const PATHS_TO_SKIP_LOGIN = ['login', 'register'];
|
||||
|
||||
const skipLogin = () => {
|
||||
for (const path of PATHS_TO_SKIP_LOGIN) {
|
||||
if (Cypress.currentTest.title.toLowerCase().includes(path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
before(() => {
|
||||
if (skipLogin()) {
|
||||
return;
|
||||
}
|
||||
cy.login();
|
||||
Cypress.Cookies.defaults({ preserve: 'session' });
|
||||
cy.loadChartFixtures();
|
||||
cy.loadDashboardFixtures();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
if (skipLogin()) {
|
||||
return;
|
||||
}
|
||||
cy.cleanDashboards();
|
||||
cy.cleanCharts();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('cleanDashboards', () => {
|
||||
cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
|
||||
const deletableDashboards = [];
|
||||
for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
|
||||
const fixture = DASHBOARD_FIXTURES[i];
|
||||
const isInDb = sampleDashboards?.find(
|
||||
d => d.dashboard_title === fixture.dashboard_title,
|
||||
);
|
||||
if (isInDb) {
|
||||
deletableDashboards.push(isInDb.id);
|
||||
}
|
||||
}
|
||||
if (deletableDashboards.length) {
|
||||
cy.request({
|
||||
failOnStatusCode: false,
|
||||
method: 'DELETE',
|
||||
url: `api/v1/dashboard/?q=!(${deletableDashboards.join(',')})`,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
}).then(resp => resp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('cleanCharts', () => {
|
||||
cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
|
||||
const deletableCharts = [];
|
||||
for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
|
||||
const fixture = CHART_FIXTURES[i];
|
||||
const isInDb = sampleCharts?.find(
|
||||
c => c.slice_name === fixture.slice_name,
|
||||
);
|
||||
if (isInDb) {
|
||||
deletableCharts.push(isInDb.id);
|
||||
}
|
||||
}
|
||||
if (deletableCharts.length) {
|
||||
cy.request({
|
||||
failOnStatusCode: false,
|
||||
method: 'DELETE',
|
||||
url: `api/v1/chart/?q=!(${deletableCharts.join(',')})`,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
}).then(resp => resp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getBySel', (selector, ...args) =>
|
||||
cy.get(`[data-test=${selector}]`, ...args),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('getBySelLike', (selector, ...args) =>
|
||||
cy.get(`[data-test*=${selector}]`, ...args),
|
||||
);
|
||||
|
||||
/* eslint-disable consistent-return */
|
||||
Cypress.on('uncaught:exception', err => {
|
||||
// ignore ResizeObserver client errors, as they are unrelated to operation
|
||||
// and causing flaky test failures in CI
|
||||
if (err.message && /ResizeObserver loop limit exceeded/.test(err.message)) {
|
||||
// returning false here prevents Cypress from failing the test
|
||||
return false;
|
||||
}
|
||||
|
||||
return false; // TODO:@geido remove
|
||||
});
|
||||
/* eslint-enable consistent-return */
|
||||
|
||||
Cypress.Commands.add('login', () => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: '/login/',
|
||||
body: { username: 'admin', password: 'general' },
|
||||
}).then(response => {
|
||||
if (response.status === 302) {
|
||||
// If there's a redirect, follow it manually
|
||||
const redirectUrl = response.headers.location;
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: redirectUrl,
|
||||
}).then(finalResponse => {
|
||||
expect(finalResponse.status).to.eq(200);
|
||||
});
|
||||
} else {
|
||||
expect(response.status).to.eq(200);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('visitChartByName', name => {
|
||||
const query = rison.encode({
|
||||
columns: ['id'],
|
||||
filters: [{ col: 'slice_name', opr: 'eq', value: name }],
|
||||
});
|
||||
cy.request(`/api/v1/chart?q=${query}`).then(response => {
|
||||
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${response.body.result[0].id}}`);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('visitChartById', chartId =>
|
||||
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`),
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
'visitChartByParams',
|
||||
(formData: {
|
||||
datasource?: string;
|
||||
datasource_id?: number;
|
||||
datasource_type?: string;
|
||||
[key: string]: unknown;
|
||||
}) => {
|
||||
let datasource_id;
|
||||
let datasource_type;
|
||||
if (formData.datasource_id && formData.datasource_type) {
|
||||
({ datasource_id, datasource_type } = formData);
|
||||
} else {
|
||||
[datasource_id, datasource_type] = formData.datasource?.split('__') || [];
|
||||
}
|
||||
const accessToken = window.localStorage.getItem('access_token');
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: 'api/v1/explore/form_data',
|
||||
body: {
|
||||
datasource_id,
|
||||
datasource_type,
|
||||
form_data: JSON.stringify(formData),
|
||||
},
|
||||
headers: {
|
||||
...(accessToken && {
|
||||
Cookie: `csrf_access_token=${accessToken}`,
|
||||
'X-CSRFToken': accessToken,
|
||||
}),
|
||||
'Content-Type': 'application/json',
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
}).then(response => {
|
||||
const formDataKey = response.body.key;
|
||||
const url = `/explore/?form_data_key=${formDataKey}`;
|
||||
cy.visit(url);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add('verifySliceContainer', chartSelector => {
|
||||
// After a wait response check for valid slice container
|
||||
cy.get('.slice_container')
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
if (chartSelector) {
|
||||
cy.get(chartSelector)
|
||||
.should('be.visible')
|
||||
.then(chart => {
|
||||
expect(chart[0].clientWidth).greaterThan(0);
|
||||
expect(chart[0].clientHeight).greaterThan(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
return cy;
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
'verifySliceSuccess',
|
||||
({
|
||||
waitAlias,
|
||||
querySubstring,
|
||||
chartSelector,
|
||||
}: {
|
||||
waitAlias: string;
|
||||
chartSelector: JQuery.Selector;
|
||||
querySubstring?: string | RegExp;
|
||||
}) => {
|
||||
cy.wait(waitAlias).then(({ response }) => {
|
||||
cy.verifySliceContainer(chartSelector);
|
||||
const responseBody = response?.body;
|
||||
if (querySubstring) {
|
||||
const query: string =
|
||||
responseBody.query || responseBody.result[0].query || '';
|
||||
if (querySubstring instanceof RegExp) {
|
||||
expect(query).to.match(querySubstring);
|
||||
} else {
|
||||
expect(query).to.contain(querySubstring);
|
||||
}
|
||||
}
|
||||
});
|
||||
return cy;
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add('createSampleDashboards', (indexes?: number[]) =>
|
||||
cy.cleanDashboards().then(() => {
|
||||
for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
|
||||
if (indexes?.includes(i) || !indexes) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/v1/dashboard/`,
|
||||
body: DASHBOARD_FIXTURES[i],
|
||||
failOnStatusCode: false,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('createSampleCharts', (indexes?: number[]) =>
|
||||
cy.cleanCharts().then(() => {
|
||||
for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
|
||||
if (indexes?.includes(i) || !indexes) {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `/api/v1/chart/`,
|
||||
body: CHART_FIXTURES[i],
|
||||
failOnStatusCode: false,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
'deleteDashboardByName',
|
||||
(dashboardName: string, failOnStatusCode = false) =>
|
||||
cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
|
||||
const dashboard = sampleDashboards?.find(
|
||||
d => d.dashboard_title === dashboardName,
|
||||
);
|
||||
if (dashboard) {
|
||||
cy.deleteDashboard(dashboard.id, failOnStatusCode);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
'deleteDashboard',
|
||||
(id: number, failOnStatusCode = false) =>
|
||||
cy
|
||||
.request({
|
||||
failOnStatusCode,
|
||||
method: 'DELETE',
|
||||
url: `api/v1/dashboard/${id}`,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
})
|
||||
.then(resp => resp),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('getDashboards', () => {
|
||||
cy.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/dashboard/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(resp => resp.body.result);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getDashboard', (dashboardId: string | number) =>
|
||||
cy
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/dashboard/${dashboardId}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(resp => resp.body.result),
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
'updateDashboard',
|
||||
(dashboardId: number, body: Record<string, any>) =>
|
||||
cy
|
||||
.request({
|
||||
method: 'PUT',
|
||||
url: `api/v1/dashboard/${dashboardId}`,
|
||||
body,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(resp => resp.body.result),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('deleteChart', (id: number, failOnStatusCode = false) =>
|
||||
cy
|
||||
.request({
|
||||
failOnStatusCode,
|
||||
method: 'DELETE',
|
||||
url: `api/v1/chart/${id}`,
|
||||
headers: {
|
||||
Cookie: `csrf_access_token=${window.localStorage.getItem(
|
||||
'access_token',
|
||||
)}`,
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
|
||||
Referer: `${Cypress.config().baseUrl}/`,
|
||||
},
|
||||
})
|
||||
.then(resp => resp),
|
||||
);
|
||||
|
||||
Cypress.Commands.add('getCharts', () =>
|
||||
cy
|
||||
.request({
|
||||
method: 'GET',
|
||||
url: `api/v1/chart/`,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(resp => resp.body.result),
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
'deleteChartByName',
|
||||
(sliceName: string, failOnStatusCode = false) =>
|
||||
cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
|
||||
const chart = sampleCharts?.find(c => c.slice_name === sliceName);
|
||||
if (chart) {
|
||||
cy.deleteChart(chart.id, failOnStatusCode);
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
// eslint-disable-next-line spaced-comment
|
||||
/// <reference types="cypress" />
|
||||
type JSONPrimitive = string | number | boolean | null;
|
||||
type JSONValue = JSONPrimitive | JSONObject | JSONArray;
|
||||
type JSONObject = { [member: string]: JSONValue };
|
||||
type JSONArray = JSONValue[];
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Login test user.
|
||||
*/
|
||||
login(): void;
|
||||
|
||||
/**
|
||||
*
|
||||
* Utils
|
||||
*/
|
||||
|
||||
getBySel(selector: string): cy;
|
||||
getBySelLike(selector: string): cy;
|
||||
cleanCharts(): cy;
|
||||
cleanDashboards(): cy;
|
||||
loadChartFixtures(): cy;
|
||||
loadDashboardFixtures(): cy;
|
||||
allowConsoleErrors(consoleMessages: (string | RegExp)[]): cy;
|
||||
|
||||
visitChartByParams(params: string | Record<string, unknown>): cy;
|
||||
visitChartByName(name: string): cy;
|
||||
visitChartById(id: number): cy;
|
||||
|
||||
/**
|
||||
* Verify slice container renders.
|
||||
*/
|
||||
verifySliceContainer(chartSelector: JQuery.Selector): cy;
|
||||
|
||||
/**
|
||||
* Verify slice successfully loaded.
|
||||
*/
|
||||
verifySliceSuccess(options: {
|
||||
waitAlias: string;
|
||||
querySubstring?: string | RegExp;
|
||||
chartSelector?: JQuery.Selector;
|
||||
}): cy;
|
||||
|
||||
/**
|
||||
* Get
|
||||
*/
|
||||
getDashboards(): cy;
|
||||
getDashboard(dashboardId: string | number): Record<string, any>;
|
||||
getCharts(): cy;
|
||||
|
||||
/**
|
||||
* Create
|
||||
*/
|
||||
createSampleDashboards(indexes?: number[]): void;
|
||||
createSampleCharts(indexes?: number[]): void;
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*/
|
||||
deleteDashboard(id: number, failOnStatusCode: boolean): cy;
|
||||
deleteDashboardByName(dashboardName: string, failOnStatusCode: boolean): cy;
|
||||
deleteChartByName(name: string, failOnStatusCode: boolean): cy;
|
||||
deleteChart(id: number, failOnStatusCode: boolean): cy;
|
||||
|
||||
/**
|
||||
* Update
|
||||
*/
|
||||
updateDashboard(dashboardId: number, body: Record<string, any>): cy;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@cypress/code-coverage/task';
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* 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 { getChartAlias, Slice } from 'cypress/utils/vizPlugins';
|
||||
|
||||
export * from './vizPlugins';
|
||||
export { default as parsePostForm } from './parsePostForm';
|
||||
export interface ChartSpec {
|
||||
name: string;
|
||||
viz: string;
|
||||
}
|
||||
|
||||
const toSlicelike = ($chart: JQuery<HTMLElement>): Slice => {
|
||||
const chartId = $chart.attr('data-test-chart-id');
|
||||
const vizType = $chart.attr('data-test-viz-type');
|
||||
|
||||
return {
|
||||
slice_id: chartId ? parseInt(chartId, 10) : null,
|
||||
form_data: {
|
||||
viz_type: vizType || null,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function getChartGridComponent({ name, viz }: ChartSpec) {
|
||||
return cy
|
||||
.get(`[data-test-chart-name="${name}"]`)
|
||||
.should('have.attr', 'data-test-viz-type', viz);
|
||||
}
|
||||
|
||||
export function getChartAliasBySpec(chart: ChartSpec) {
|
||||
return getChartGridComponent(chart).then($chart =>
|
||||
cy.wrap(getChartAlias(toSlicelike($chart))),
|
||||
);
|
||||
}
|
||||
|
||||
export function getChartAliasesBySpec(charts: readonly ChartSpec[]) {
|
||||
const aliases: string[] = [];
|
||||
charts.forEach(chart =>
|
||||
getChartAliasBySpec(chart).then(alias => {
|
||||
aliases.push(alias);
|
||||
}),
|
||||
);
|
||||
// Wrapping the aliases is key.
|
||||
// That way callers can chain off this function
|
||||
// and actually get the list of aliases.
|
||||
return cy.wrap(aliases);
|
||||
}
|
||||
|
||||
export function waitForChartLoad(chart: ChartSpec) {
|
||||
return getChartGridComponent(chart).then(gridComponent => {
|
||||
const chartId = gridComponent.attr('data-test-chart-id');
|
||||
// the chart should load in under half a minute
|
||||
return (
|
||||
cy
|
||||
// this id only becomes visible when the chart is loaded
|
||||
.get(`#chart-id-${chartId}`, {
|
||||
timeout: 30000,
|
||||
})
|
||||
.should('be.visible')
|
||||
// return the chart grid component
|
||||
.then(() => gridComponent)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function resize(selector: string) {
|
||||
return {
|
||||
to(cordX: number, cordY: number) {
|
||||
cy.get(selector).trigger('mousedown', { which: 1, force: true });
|
||||
cy.get(selector).trigger('mousemove', {
|
||||
which: 1,
|
||||
cordX,
|
||||
cordY,
|
||||
force: true,
|
||||
});
|
||||
cy.get(selector).trigger('mouseup', { which: 1, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const setSelectSearchInput = (
|
||||
$input: any,
|
||||
value: string,
|
||||
async = false,
|
||||
) => {
|
||||
// Ant Design 5 Select crashes Chromium with type/click events when showSearch is true.
|
||||
// This copies the value directly to the input element as a workaround.
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
'value',
|
||||
)?.set;
|
||||
nativeInputValueSetter?.call($input[0], value);
|
||||
|
||||
// Trigger the input and change events
|
||||
if (async) {
|
||||
$input[0].dispatchEvent(new Event('mousedown', { bubbles: true }));
|
||||
}
|
||||
|
||||
$input[0].dispatchEvent(new Event('input', { bubbles: true }));
|
||||
$input[0].dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
cy.get('.ant-select-item-option-content').should('exist').first().click({
|
||||
force: true,
|
||||
});
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Parse multipart form data sent via POST requests.
|
||||
*/
|
||||
export default function parsePostForm(requestBody: ArrayBuffer) {
|
||||
type ParsedFields = Record<string, string[] | string>;
|
||||
if (requestBody.constructor.name !== 'ArrayBuffer') {
|
||||
return requestBody;
|
||||
}
|
||||
const lines = new TextDecoder('utf-8').decode(requestBody).split('\n');
|
||||
const fields: ParsedFields = {};
|
||||
let key = '';
|
||||
let value: string[] = [];
|
||||
|
||||
function addField(key: string, value: string) {
|
||||
if (key in fields) {
|
||||
if (Array.isArray(fields[key])) {
|
||||
(fields[key] as string[]).push(value);
|
||||
} else {
|
||||
fields[key] = [fields[key] as string, value];
|
||||
}
|
||||
} else {
|
||||
fields[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
lines.forEach(line => {
|
||||
const nameMatch = line.match(/Content-Disposition: form-data; name="(.*)"/);
|
||||
if (nameMatch) {
|
||||
if (key) {
|
||||
addField(key, value.join('\n'));
|
||||
}
|
||||
key = nameMatch[1];
|
||||
value = [];
|
||||
} else if (!/----.*FormBoundary/.test(line)) {
|
||||
value.push(line);
|
||||
}
|
||||
});
|
||||
if (key && value) {
|
||||
addField(key, value.join('\n'));
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
|
||||
export type JsonArray = JsonValue[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type JsonObject = { [member: string]: any };
|
||||
|
||||
export interface Slice {
|
||||
slice_id: number;
|
||||
form_data: {
|
||||
viz_type: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Dashboard {
|
||||
slices: Slice[];
|
||||
}
|
||||
|
||||
const V1_PLUGINS = [
|
||||
'box_plot',
|
||||
'echarts_timeseries',
|
||||
'word_cloud',
|
||||
'pie',
|
||||
'table',
|
||||
'big_number',
|
||||
'big_number_total',
|
||||
];
|
||||
export const DASHBOARD_CHART_ALIAS_PREFIX = 'getChartData_';
|
||||
|
||||
export function isLegacyChart(vizType: string): boolean {
|
||||
return !V1_PLUGINS.includes(vizType);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function isLegacyResponse(response: any): boolean {
|
||||
return !response.result;
|
||||
}
|
||||
|
||||
export function getSliceIdFromRequestUrl(url: string) {
|
||||
const address = new URL(url);
|
||||
const query = address.searchParams.get('form_data');
|
||||
return query?.match(/\d+/)?.[0];
|
||||
}
|
||||
|
||||
export function getChartDataRouteForSlice(slice: Slice) {
|
||||
const vizType = slice.form_data.viz_type;
|
||||
const isLegacy = isLegacyChart(vizType);
|
||||
const formData = encodeURIComponent(`{"slice_id":${slice.slice_id}}`);
|
||||
if (isLegacy) {
|
||||
return `**/superset/explore_json/?*${formData}*`;
|
||||
}
|
||||
return `**/api/v1/chart/data?*${formData}*`;
|
||||
}
|
||||
|
||||
export function getChartAlias(slice: Slice): string {
|
||||
const alias = `${DASHBOARD_CHART_ALIAS_PREFIX}${slice.slice_id}_${slice.form_data.viz_type}`;
|
||||
const route = getChartDataRouteForSlice(slice);
|
||||
cy.intercept('POST', route).as(alias);
|
||||
return `@${alias}`;
|
||||
}
|
||||
|
||||
export function getChartAliases(slices: Slice[]): string[] {
|
||||
return Array.from(slices).map(getChartAlias);
|
||||
}
|
||||
|
||||
export function interceptChart({
|
||||
sliceId,
|
||||
legacy = false,
|
||||
method = 'POST',
|
||||
}: {
|
||||
sliceId?: number;
|
||||
legacy?: boolean;
|
||||
method?: 'POST' | 'GET';
|
||||
}) {
|
||||
const urlBase = legacy ? '**/superset/explore_json/' : '**/api/v1/chart/data';
|
||||
let url;
|
||||
if (sliceId) {
|
||||
const encodedFormData = encodeURIComponent(
|
||||
JSON.stringify({ slice_id: sliceId }),
|
||||
);
|
||||
url = `${urlBase}?form_data=${encodedFormData}*`;
|
||||
} else {
|
||||
url = `${urlBase}**`;
|
||||
}
|
||||
return cy.intercept(method, url);
|
||||
}
|
||||
15006
superset-frontend/cypress-base/package-lock.json
generated
15006
superset-frontend/cypress-base/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "superset-cypress",
|
||||
"version": "1.0.0",
|
||||
"description": "run cypress against superset",
|
||||
"license": "Apache-2.0",
|
||||
"author": "Apache",
|
||||
"scripts": {
|
||||
"cypress": "cypress",
|
||||
"cypress-debug": "cypress open --config watchForFileChanges=true",
|
||||
"cypress-run-chrome": "cypress run --browser chrome --headless"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cypress/code-coverage": "^3.10.4",
|
||||
"@superset-ui/core": "^2.1.0",
|
||||
"brace": "^0.11.1",
|
||||
"chai": "^4.5.0",
|
||||
"cy-verify-downloads": "^0.2.5",
|
||||
"cypress-fail-on-console-error": "^4.0.3",
|
||||
"nanoid": "^6.0.0",
|
||||
"querystringify": "^2.2.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"rison": "^0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/querystringify": "^2.0.0",
|
||||
"cypress": "^11.2.0",
|
||||
"eslint-plugin-cypress": "^3.5.0",
|
||||
"tscw-config": "^1.1.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core": "^7.29.6",
|
||||
"@cypress/code-coverage": {
|
||||
"js-yaml": "^4.3.2"
|
||||
},
|
||||
"@cypress/request": "^3.0.0",
|
||||
"cypress": {
|
||||
"form-data": "^2.3.4"
|
||||
},
|
||||
"d3-interpolate": {
|
||||
"d3-color": "3.1.0"
|
||||
},
|
||||
"minimatch@<10": {
|
||||
"brace-expansion": "1.1.18"
|
||||
},
|
||||
"minimatch@>=10": {
|
||||
"brace-expansion": ">=5.0.9"
|
||||
},
|
||||
"qs": "^6.14.2",
|
||||
"uuid": "^11.1.1"
|
||||
},
|
||||
"nyc": {
|
||||
"reporter": [
|
||||
"html",
|
||||
"json"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"target": "es2019",
|
||||
"lib": ["es2019", "DOM"],
|
||||
"types": ["cypress", "node"],
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"files": ["cypress/support/index.d.ts"],
|
||||
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information 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.
|
||||
#
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
#run all the python steps in a background process
|
||||
time superset db upgrade
|
||||
time superset load_test_users
|
||||
time superset load_examples --load-test-data
|
||||
time superset init
|
||||
echo "[completed python build steps]"
|
||||
PORT='8081'
|
||||
flask run -p $PORT --with-threads --reload --debugger &
|
||||
|
||||
#block on the longer running javascript process
|
||||
time npm ci
|
||||
time npm run build-instrumented
|
||||
echo "[completed js build steps]"
|
||||
|
||||
#setup cypress
|
||||
cd cypress-base
|
||||
time npm ci
|
||||
export CYPRESS_BASE_URL="http://localhost:${PORT}"
|
||||
if [ -n "$1" ]; then
|
||||
CYPRESS_PATH='cypress/e2e/'${1}'/*'
|
||||
time npm run cypress-run-chrome -- --spec "$CYPRESS_PATH" --record false --config video=false || true
|
||||
else
|
||||
time npm run cypress-run-chrome -- --record false --config video=false || true
|
||||
fi
|
||||
kill %1
|
||||
@@ -120,7 +120,6 @@ export default [
|
||||
'**/spec/**',
|
||||
'**/__tests__/**',
|
||||
'**/__mocks__/**',
|
||||
'cypress-base/**',
|
||||
],
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 'off',
|
||||
|
||||
@@ -43,6 +43,12 @@ export default {
|
||||
// lodash-es is ESM (type: module) which jest.mock cannot intercept; alias to
|
||||
// the CJS lodash build (identical API) so module mocks work in tests.
|
||||
'^lodash-es$': '<rootDir>/node_modules/lodash',
|
||||
// @deck.gl/maplibre@9.4.0 ships ESM-only (no dist/*.cjs, no "require"
|
||||
// export condition), unlike every other @deck.gl/* package - force
|
||||
// resolution to its ESM entry point directly; transformIgnorePatterns
|
||||
// below still needs to let it (and its relative imports) through babel.
|
||||
'^@deck.gl/maplibre$':
|
||||
'<rootDir>/node_modules/@deck.gl/maplibre/dist/index.js',
|
||||
'^src/(.*)$': '<rootDir>/src/$1',
|
||||
'^spec/(.*)$': '<rootDir>/spec/$1',
|
||||
// mapping plugins of superset-ui to source code
|
||||
@@ -98,7 +104,11 @@ export default {
|
||||
// gettext-parser is ESM-only too, and scripts/po2json.js (which
|
||||
// spec/scripts/po2json.test.ts exercises directly against the real
|
||||
// package, not a mock, to catch a parsing regression) imports it.
|
||||
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format|selection)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|@x0k/.*|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|react-markdown|vfile|web-namespaces|html-void-elements|html-url-attributes|estree-util-is-identifier-name|trim-lines|is-plain-obj|trough|unist-.*|hast-.*|hastscript|refractor|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|character-reference-invalid|is-alphanumerical|is-alphabetical|is-decimal|is-hexadecimal|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact|@x0k/json-schema-merge|content-disposition|gettext-parser)',
|
||||
//
|
||||
// @deck.gl/maplibre@9.4.0 ships ESM-only (no dist/*.cjs), unlike every
|
||||
// other @deck.gl/* package - allowed through so babel-jest can convert
|
||||
// its import/export syntax (see the moduleNameMapper entry above).
|
||||
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format|selection)|internmap|@mapbox/tiny-sdf|@deck.gl/maplibre|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|@x0k/.*|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|react-markdown|vfile|web-namespaces|html-void-elements|html-url-attributes|estree-util-is-identifier-name|trim-lines|is-plain-obj|trough|unist-.*|hast-.*|hastscript|refractor|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|character-reference-invalid|is-alphanumerical|is-alphabetical|is-decimal|is-hexadecimal|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact|@x0k/json-schema-merge|content-disposition|gettext-parser)',
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
|
||||
|
||||
@@ -314,7 +314,6 @@
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"packages/generator-superset/**/*",
|
||||
"cypress-base/**",
|
||||
"**/node_modules/**",
|
||||
"build/**",
|
||||
"**/dist/**",
|
||||
|
||||
1724
superset-frontend/package-lock.json
generated
1724
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"_format": "oxfmt './{src,spec,cypress-base,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
|
||||
"_format": "oxfmt './{src,spec,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
|
||||
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --color --mode production",
|
||||
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
|
||||
"build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color",
|
||||
@@ -93,13 +93,13 @@
|
||||
"dependencies": {
|
||||
"@apache-superset/core": "file:packages/superset-core",
|
||||
"@braintree/sanitize-url": "^7.1.2",
|
||||
"@deck.gl/aggregation-layers": "~9.2.5",
|
||||
"@deck.gl/core": "~9.2.5",
|
||||
"@deck.gl/extensions": "~9.2.5",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@deck.gl/react": "~9.2.5",
|
||||
"@deck.gl/aggregation-layers": "~9.4.0",
|
||||
"@deck.gl/core": "~9.4.0",
|
||||
"@deck.gl/extensions": "~9.4.0",
|
||||
"@deck.gl/geo-layers": "~9.4.0",
|
||||
"@deck.gl/layers": "~9.4.0",
|
||||
"@deck.gl/mesh-layers": "~9.4.0",
|
||||
"@deck.gl/react": "~9.4.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@@ -110,21 +110,21 @@
|
||||
"@fontsource/fira-code": "^5.3.0",
|
||||
"@fontsource/ibm-plex-mono": "^5.3.0",
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@googleapis/sheets": "^18.0.0",
|
||||
"@googleapis/sheets": "^18.0.1",
|
||||
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
|
||||
"@jsonforms/core": "^3.7.0",
|
||||
"@jsonforms/react": "^3.7.0",
|
||||
"@jsonforms/vanilla-renderers": "^3.8.0",
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"@luma.gl/constants": "~9.4.0",
|
||||
"@luma.gl/core": "~9.4.0",
|
||||
"@luma.gl/engine": "~9.4.0",
|
||||
"@luma.gl/gltf": "~9.4.0",
|
||||
"@luma.gl/shadertools": "~9.4.0",
|
||||
"@luma.gl/webgl": "~9.4.0",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^6.10.1",
|
||||
"@rjsf/core": "^6.11.0",
|
||||
"@rjsf/utils": "^6.6.2",
|
||||
"@rjsf/validator-ajv8": "^6.10.1",
|
||||
"@rjsf/validator-ajv8": "^6.11.0",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
@@ -163,7 +163,7 @@
|
||||
"content-disposition": "^3.0.0",
|
||||
"d3-scale": "^4.0.2",
|
||||
"dayjs": "^1.11.23",
|
||||
"dom-to-image-more": "^3.10.2",
|
||||
"dom-to-image-more": "^3.11.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^6.1.0",
|
||||
"fuse.js": "^7.5.0",
|
||||
@@ -174,6 +174,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^11.1.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"immer": "^11.1.18",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
@@ -260,9 +261,9 @@
|
||||
"@superset-ui/eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
|
||||
"@superset-ui/eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.16.2",
|
||||
"@swc/plugin-emotion": "^16.0.0",
|
||||
"@swc/plugin-transform-imports": "^14.0.0",
|
||||
"@swc/core": "^1.16.12",
|
||||
"@swc/plugin-emotion": "^17.0.0",
|
||||
"@swc/plugin-transform-imports": "^15.0.0",
|
||||
"@testing-library/dom": "^10.4.2",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
@@ -275,7 +276,7 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^26.6.2",
|
||||
"@types/node": "^26.6.3",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
@@ -285,13 +286,13 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.70.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.71.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"babel-jest": "^30.5.2",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"baseline-browser-mapping": "^2.11.25",
|
||||
"baseline-browser-mapping": "^2.11.26",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.5",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
@@ -323,11 +324,11 @@
|
||||
"lerna": "^10.0.1",
|
||||
"lightningcss": "^1.33.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.11.0",
|
||||
"minimizer-webpack-plugin": "^5.12.0",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxfmt": "^0.70.0",
|
||||
"oxlint": "^1.85.0",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"oxlint": "^1.86.0",
|
||||
"postcss-styled-syntax": "^0.7.3",
|
||||
"process": "^0.11.10",
|
||||
"react-refresh": "^0.19.0",
|
||||
"react-resizable": "^4.0.2",
|
||||
@@ -343,13 +344,13 @@
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"wait-on": "^9.1.0",
|
||||
"wait-on": "^9.4.0",
|
||||
"webpack": "^5.111.1",
|
||||
"webpack-bundle-analyzer": "^5.4.0",
|
||||
"webpack-cli": "^7.2.3",
|
||||
"webpack-dev-server": "^6.0.0",
|
||||
"webpack-manifest-plugin": "^6.0.1",
|
||||
"webpack-sources": "^3.5.1",
|
||||
"webpack-sources": "^3.6.0",
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -363,14 +364,14 @@
|
||||
"@babel/eslint-parser": {
|
||||
"eslint": "$eslint"
|
||||
},
|
||||
"@deck.gl/aggregation-layers": "~9.2.5",
|
||||
"@deck.gl/core": "~9.2.5",
|
||||
"@deck.gl/extensions": "~9.2.5",
|
||||
"@deck.gl/geo-layers": "~9.2.5",
|
||||
"@deck.gl/layers": "~9.2.5",
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@deck.gl/react": "~9.2.5",
|
||||
"@deck.gl/widgets": "~9.2.5",
|
||||
"@deck.gl/aggregation-layers": "~9.4.0",
|
||||
"@deck.gl/core": "~9.4.0",
|
||||
"@deck.gl/extensions": "~9.4.0",
|
||||
"@deck.gl/geo-layers": "~9.4.0",
|
||||
"@deck.gl/layers": "~9.4.0",
|
||||
"@deck.gl/mesh-layers": "~9.4.0",
|
||||
"@deck.gl/react": "~9.4.0",
|
||||
"@deck.gl/widgets": "~9.4.0",
|
||||
"@great-expectations/jsonforms-antd-renderers": {
|
||||
"antd": "$antd"
|
||||
},
|
||||
@@ -382,17 +383,17 @@
|
||||
"@loaders.gl/compression": {
|
||||
"fflate": "^0.7.5"
|
||||
},
|
||||
"@luma.gl/constants": "~9.2.5",
|
||||
"@luma.gl/core": "~9.2.5",
|
||||
"@luma.gl/engine": "~9.2.5",
|
||||
"@luma.gl/gltf": "~9.2.5",
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"@luma.gl/constants": "~9.4.0",
|
||||
"@luma.gl/core": "~9.4.0",
|
||||
"@luma.gl/engine": "~9.4.0",
|
||||
"@luma.gl/gltf": "~9.4.0",
|
||||
"@luma.gl/shadertools": "~9.4.0",
|
||||
"@luma.gl/webgl": "~9.4.0",
|
||||
"core-js": "^3.38.1",
|
||||
"cosmiconfig": {
|
||||
"js-yaml": "^4.3.2"
|
||||
},
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.16",
|
||||
"esbuild": "^0.28.1",
|
||||
"eslint-plugin-import": {
|
||||
"eslint": "$eslint"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^6.0.0",
|
||||
"chalk": "^6.0.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"yeoman-generator": "^9.2.0",
|
||||
"yosay": "^3.0.0"
|
||||
|
||||
@@ -45,6 +45,13 @@ export const datasourceAndVizType: ControlPanelSectionConfig = {
|
||||
['datasource'],
|
||||
['viz_type'],
|
||||
[
|
||||
{
|
||||
name: 'semantic_selection_version',
|
||||
config: {
|
||||
type: 'HiddenControl',
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'slice_id',
|
||||
config: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user