Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa52a38be9 | ||
|
|
0ef7482713 | ||
|
|
21ae918656 | ||
|
|
ca94026e97 | ||
|
|
a3bc2d908c | ||
|
|
738d12677a | ||
|
|
6828e17516 | ||
|
|
d00bde7b8e | ||
|
|
fb5029d06d | ||
|
|
041d735ddb | ||
|
|
49a4086715 | ||
|
|
2eedc609a8 | ||
|
|
f4587218dd | ||
|
|
8967e6c2d3 | ||
|
|
81e431cd50 | ||
|
|
edfb009e1c | ||
|
|
e808fcbcad | ||
|
|
0a7ebe1dd1 | ||
|
|
dd1afb029f | ||
|
|
c068a8c09c | ||
|
|
b62ec512d2 | ||
|
|
1e65d93a83 | ||
|
|
856599027a | ||
|
|
c395b9a238 | ||
|
|
92728169de | ||
|
|
84c371d56e | ||
|
|
a4c47359e6 | ||
|
|
7c0c5283c3 | ||
|
|
9c5bde9491 | ||
|
|
3b99e092d0 | ||
|
|
acf39e3ef0 | ||
|
|
8523ea4d0a | ||
|
|
9bc3173e3a | ||
|
|
a6db0d1cde | ||
|
|
aec567f7d6 | ||
|
|
bacaf08a22 | ||
|
|
31218987ec | ||
|
|
de4def38cb | ||
|
|
4f77538bc2 | ||
|
|
5bbd4c6d23 | ||
|
|
3b98869e0a | ||
|
|
e7fa8529ce | ||
|
|
c487c006e9 | ||
|
|
fd27a0c2fd | ||
|
|
42c6746339 | ||
|
|
fbd2e6032e | ||
|
|
e66b2f8be5 | ||
|
|
04bd07ad6e | ||
|
|
1c98c0711e | ||
|
|
13dd061907 | ||
|
|
bf47750682 | ||
|
|
990785cfa4 | ||
|
|
7174af24ca | ||
|
|
bbd021925a | ||
|
|
464674f193 | ||
|
|
c7a83794d2 | ||
|
|
dbc561f80f | ||
|
|
86003221bc | ||
|
|
9615e7c024 | ||
|
|
80c8d9f388 | ||
|
|
59f1d91b07 | ||
|
|
a4fc9ae493 | ||
|
|
5bbe3ac750 | ||
|
|
1aeff038d7 | ||
|
|
b73f6a8bd5 | ||
|
|
1859b375a7 | ||
|
|
b85468ee17 | ||
|
|
2261c61791 | ||
|
|
9fe31e3ec5 | ||
|
|
c7f0167a28 | ||
|
|
52b5110a53 | ||
|
|
a33ab097e1 | ||
|
|
05ce4d41bf | ||
|
|
8cd636bcd0 | ||
|
|
18231d9bd4 | ||
|
|
ca17830175 | ||
|
|
2be95c6283 | ||
|
|
6edeafff6b | ||
|
|
b2d2aa4452 | ||
|
|
09a034c573 | ||
|
|
f2fd536823 | ||
|
|
9c6b1f0eb6 | ||
|
|
f2055d8446 | ||
|
|
fbd088af48 | ||
|
|
d7c34215a2 | ||
|
|
a82128cde0 | ||
|
|
9ff15cb53e | ||
|
|
ac7f53326f | ||
|
|
feb224686b | ||
|
|
f93cc1a67b | ||
|
|
d2911672bf | ||
|
|
f9dfea8791 | ||
|
|
a5aa619668 | ||
|
|
dfc745c922 | ||
|
|
936ec951d5 | ||
|
|
acc93dfe33 | ||
|
|
a973290890 | ||
|
|
377e8e6345 | ||
|
|
71443460d1 | ||
|
|
dcda71e869 | ||
|
|
d59120a632 | ||
|
|
ba795ccfa0 | ||
|
|
6a5049daed | ||
|
|
83e7098ee8 | ||
|
|
b71ea77080 | ||
|
|
b3435a04cf | ||
|
|
1771f05d33 | ||
|
|
3dfe193ca0 | ||
|
|
c54abcfda8 | ||
|
|
b2849d826a | ||
|
|
2cd9873540 | ||
|
|
8948b5af94 | ||
|
|
3bcb003eeb | ||
|
|
480fbe0e06 | ||
|
|
3abbe1c84a | ||
|
|
311b0087f3 | ||
|
|
0fea3c88a5 | ||
|
|
e61610bd98 | ||
|
|
9729306c06 | ||
|
|
93a02449db | ||
|
|
cbbcf1bed2 | ||
|
|
218c64296f | ||
|
|
39eced2c2e | ||
|
|
bef4680ffd | ||
|
|
5870259219 | ||
|
|
a4bcd2cc87 | ||
|
|
f8dd65cc0b | ||
|
|
b1bc8fa8f2 | ||
|
|
5d86215a9d | ||
|
|
f866f25546 | ||
|
|
3392576923 | ||
|
|
e95b37450e | ||
|
|
ff73f2c070 | ||
|
|
612fd0ac5d | ||
|
|
83dbdfa355 | ||
|
|
fc3872fe9e | ||
|
|
0946df50a1 | ||
|
|
d9c3a2cf4c | ||
|
|
f84d7c664e | ||
|
|
45fd4117a9 | ||
|
|
79acc3a712 | ||
|
|
62d0cfb6d3 | ||
|
|
b885657ad0 | ||
|
|
a191f72698 | ||
|
|
498400e974 | ||
|
|
b9a8321802 | ||
|
|
902ebcdf72 | ||
|
|
f76471f30f | ||
|
|
b671356748 | ||
|
|
ff273478ff | ||
|
|
f1cc9367de | ||
|
|
02e4b4be7d | ||
|
|
ecb4aa93f6 | ||
|
|
4e1913d436 | ||
|
|
b6ae8fb25d | ||
|
|
10f2461b5f | ||
|
|
4e715f66dc | ||
|
|
6dfae09cc5 | ||
|
|
a4f74b09bb | ||
|
|
9f39fe3e10 | ||
|
|
c6f8706aa1 | ||
|
|
07a6406d97 | ||
|
|
a585a0901e | ||
|
|
fff5351a33 | ||
|
|
b4f8d87a49 | ||
|
|
3256ee4fbb | ||
|
|
e5b3ba9efc | ||
|
|
ae93dc2041 | ||
|
|
792d0fd2b6 | ||
|
|
8dcb83fd3c | ||
|
|
8f07ab4840 | ||
|
|
89d64b89b8 | ||
|
|
4ce589dcda | ||
|
|
a31210df08 | ||
|
|
00e95dcb87 | ||
|
|
c9960002f1 | ||
|
|
dbac244857 | ||
|
|
4483cb09f0 | ||
|
|
28d977bc0c | ||
|
|
b248c4d7c0 | ||
|
|
3e9fdc4cdf | ||
|
|
387b742303 | ||
|
|
533f90e4c6 | ||
|
|
b887c30573 | ||
|
|
2cd369d02a | ||
|
|
4b3bbe19f6 | ||
|
|
28eae5956e | ||
|
|
239e3a4323 | ||
|
|
573054346f | ||
|
|
a42c7e3be0 | ||
|
|
935b4acdbd | ||
|
|
bba2d5f812 | ||
|
|
7d45b8ae7e | ||
|
|
5942d570c3 | ||
|
|
45adb8429d | ||
|
|
3a03f0a820 | ||
|
|
105f6d0bc7 | ||
|
|
c8d6faf7e8 | ||
|
|
ae15dd883d | ||
|
|
2afa775db5 | ||
|
|
e17d49bec0 | ||
|
|
68324050cd | ||
|
|
2a55ad77f4 | ||
|
|
cd535f89e2 | ||
|
|
8b2c07f5f3 | ||
|
|
a9045296d9 | ||
|
|
32cbc4a7a6 | ||
|
|
4f47eda5de | ||
|
|
ba2b0d6ea3 | ||
|
|
df176cfa57 | ||
|
|
66e911eeea | ||
|
|
759457264d | ||
|
|
5722b96280 | ||
|
|
deb6cacea7 | ||
|
|
0f8e2a274d | ||
|
|
7e0de356d4 | ||
|
|
4e93b58a61 | ||
|
|
45b232cba0 | ||
|
|
bcc58d27f8 | ||
|
|
b5df673d21 |
@@ -19,7 +19,7 @@
|
||||
|
||||
# Notify PMC members of changes to extension-related files
|
||||
|
||||
/docs/developer_portal/extensions/ @michael-s-molina @villebro @rusackas
|
||||
/docs/developer_docs/extensions/ @michael-s-molina @villebro @rusackas
|
||||
/superset-extensions-cli/ @michael-s-molina @villebro @rusackas @sadpandajoe
|
||||
/superset/extensions/ @michael-s-molina @villebro @rusackas @sadpandajoe
|
||||
/superset-frontend/src/extensions/ @michael-s-molina @villebro @rusackas @sadpandajoe
|
||||
|
||||
@@ -51,6 +51,53 @@ jobs:
|
||||
echo "matrix_config=${MATRIX_CONFIG}" >> $GITHUB_OUTPUT
|
||||
echo $GITHUB_OUTPUT
|
||||
|
||||
# Runs unconditionally (no dependency on `changes`, and no build-preset
|
||||
# matrix restriction) so a regression in the PY_VER override logic is
|
||||
# always caught on PRs. Without this, the real docker-build job only runs
|
||||
# when the change detector flags docker/python/frontend changes (a
|
||||
# workflow-only edit like this one does not), and even then the PR build
|
||||
# matrix never includes the "py311"/"py312" presets that logic protects -
|
||||
# so a break here would otherwise first surface on a push to master.
|
||||
pyver-override-check:
|
||||
name: verify docker build PY_VER override
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup supersetbot
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
- name: Assert PY_VER override applies to every preset except py311/py312
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Asserts against the actual buildx command line `supersetbot docker
|
||||
# --dry-run` would run, not just this repo's own extra-flags helper,
|
||||
# so a regression in supersetbot itself (dropping the py311/py312
|
||||
# PY_VER pin, or reordering args so our override no longer lands
|
||||
# last) is caught here too, instead of only surfacing on master.
|
||||
assert_effective_py_ver() {
|
||||
local preset="$1" expected="$2" extra_flags command actual
|
||||
extra_flags="$(scripts/docker-build-extra-flags.sh "$preset" dummy-tag)"
|
||||
command="$(supersetbot docker --preset "$preset" --platform linux/amd64 --extra-flags "$extra_flags" --dry-run)"
|
||||
# docker buildx keeps the LAST value of a repeated --build-arg key.
|
||||
actual="$(grep -oE -- '--build-arg PY_VER=[^[:space:]]+' <<<"$command" | tail -1)"
|
||||
if [ "$actual" != "--build-arg PY_VER=$expected" ]; then
|
||||
echo "::error::preset '$preset' expected effective --build-arg PY_VER=$expected, got: ${actual:-<none>} (full command: $command)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
for preset in dev lean websocket dockerize; do
|
||||
assert_effective_py_ver "$preset" "3.11.14-slim-trixie"
|
||||
done
|
||||
assert_effective_py_ver py311 "3.11-slim-bookworm"
|
||||
assert_effective_py_ver py312 "3.12-slim-bookworm"
|
||||
echo "PY_VER override logic verified against the assembled buildx command for all build presets"
|
||||
|
||||
docker-build:
|
||||
name: docker-build
|
||||
needs: [setup_matrix, changes]
|
||||
@@ -124,19 +171,21 @@ jobs:
|
||||
# the whole job. buildx reuses the buildkit layer cache from the
|
||||
# failed attempt, so a retry mostly re-does just the failed push.
|
||||
#
|
||||
# supersetbot's "dev"/"lean" presets pin their own --build-arg
|
||||
# PY_VER, which lands ahead of --extra-flags on the assembled
|
||||
# buildx command line; docker/buildx keeps the last value for a
|
||||
# repeated --build-arg key, so appending PY_VER here overrides
|
||||
# supersetbot's pin and keeps the build on the Dockerfile's own
|
||||
# supported Python version.
|
||||
# See scripts/docker-build-extra-flags.sh for why "py311"/"py312"
|
||||
# are excluded from the PY_VER override applied to every other
|
||||
# preset; that logic is also exercised on every PR by the
|
||||
# always-on pyver-override-check job below, since this job itself
|
||||
# only runs when the change detector flags docker/python/frontend
|
||||
# changes and the PR build matrix never includes py311/py312.
|
||||
EXTRA_FLAGS="$(scripts/docker-build-extra-flags.sh "$BUILD_PRESET" "$IMAGE_TAG")"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if supersetbot docker \
|
||||
$PUSH_OR_LOAD \
|
||||
--preset "$BUILD_PRESET" \
|
||||
--context "$EVENT" \
|
||||
--context-ref "$RELEASE" $FORCE_LATEST \
|
||||
--extra-flags "--build-arg PY_VER=3.11.14-slim-trixie --build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG" \
|
||||
--extra-flags "$EXTRA_FLAGS" \
|
||||
$PLATFORM_ARG; then
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
name: Frontend bundle size (nightly baseline + analyzer)
|
||||
|
||||
# Refreshes the bundle-size baseline that superset-frontend.yml's `bundle-size`
|
||||
# job compares PRs against, and publishes a browsable bundle-analyzer treemap
|
||||
# report of the same build. Deliberately NOT triggered on every push to
|
||||
# master: a day-old baseline/report is fine for catching relative
|
||||
# regressions on PRs and for browsing what's actually in the bundle, and
|
||||
# building the production bundle on every one of the many pushes master
|
||||
# gets per day would burn CI time for no benefit a nightly refresh doesn't
|
||||
# already cover.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TAG: apache/superset:bundle-size-nightly-${{ github.run_id }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
refresh-baseline:
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_BUNDLE_ANALYZER_SITE_ID }}
|
||||
steps:
|
||||
- name: "Checkout master"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: master
|
||||
|
||||
- name: Build Docker Image
|
||||
run: |
|
||||
docker buildx build \
|
||||
-t $TAG \
|
||||
--cache-from=type=registry,ref=apache/superset-cache:3.11-slim-trixie \
|
||||
--target superset-node-ci \
|
||||
.
|
||||
|
||||
# Same cache the PR-time bundle-size job restores/writes -- webpack's
|
||||
# persistent filesystem cache turns a warm production build into ~20s
|
||||
# instead of several minutes. See superset-frontend.yml for the
|
||||
# matching restore step and why it's keyed this way.
|
||||
- name: Restore webpack build cache
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: superset-frontend/.temp_cache
|
||||
key: >-
|
||||
webpack-prod-cache-${{ hashFiles('superset-frontend/package-lock.json',
|
||||
'superset-frontend/babel.config.js', 'superset-frontend/tsconfig.json',
|
||||
'superset-frontend/webpack.config.js') }}
|
||||
|
||||
# Only ever pull the last recorded data point off the cache, keyed by
|
||||
# run ID -- `restore-keys` prefix-matches the most recently created
|
||||
# entry. Absent on the very first run ever; benchmark-action starts a
|
||||
# fresh history in that case.
|
||||
- name: Restore bundle size history
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: bundle-size-history.json
|
||||
key: bundle-size-history-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
bundle-size-history-
|
||||
|
||||
# BUNDLE_ANALYZER rides along in the same build as BUNDLE_SIZE_STATS --
|
||||
# they're independent env-gated additions in webpack.config.js (one
|
||||
# sets `config.stats`, the other pushes plugins), so one production
|
||||
# build produces both the numeric stats.json and the analyzer's
|
||||
# report.html. Only report.html is mounted out, not
|
||||
# BUNDLE_ANALYZER's sibling `statistics.html` sunburst -- that file is
|
||||
# documented in webpack.config.js as routinely exceeding 100MB for
|
||||
# this app (it's .gitignore'd for exactly that reason), too large to
|
||||
# publish as a static site page.
|
||||
- name: Build production bundle with stats and analyzer report
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/superset-frontend/bundle-stats
|
||||
mkdir -p ${{ github.workspace }}/superset-frontend/.temp_cache
|
||||
mkdir -p ${{ github.workspace }}/superset/static/assets
|
||||
docker run \
|
||||
-v ${{ github.workspace }}/superset-frontend/bundle-stats:/app/superset-frontend/bundle-stats \
|
||||
-v ${{ github.workspace }}/superset-frontend/.temp_cache:/app/superset-frontend/.temp_cache \
|
||||
-v ${{ github.workspace }}/superset/static/assets:/app/superset/static/assets \
|
||||
--rm $TAG \
|
||||
bash -c \
|
||||
"npm i && BUNDLE_SIZE_STATS=true BUNDLE_ANALYZER=true npm run build -- --json=bundle-stats/stats.json"
|
||||
|
||||
- name: Summarize bundle size
|
||||
run: |
|
||||
node superset-frontend/scripts/bundle-size-summary.js \
|
||||
superset-frontend/bundle-stats/stats.json > bundle-size-summary.json
|
||||
rm -rf superset-frontend/bundle-stats
|
||||
|
||||
# No PR to comment on here, so comment-on-alert is off -- the job
|
||||
# summary (summary-always) is the only surface for this run.
|
||||
- name: Update bundle size baseline
|
||||
uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
|
||||
with:
|
||||
tool: customSmallerIsBetter
|
||||
output-file-path: bundle-size-summary.json
|
||||
external-data-json-path: bundle-size-history.json
|
||||
fail-on-alert: false
|
||||
summary-always: true
|
||||
|
||||
- name: Save bundle size history
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: bundle-size-history.json
|
||||
key: bundle-size-history-${{ github.run_id }}
|
||||
|
||||
# Publishes the treemap to Netlify (the same host already used for
|
||||
# superset-storybook.netlify.app and docs previews, reusing the
|
||||
# existing NETLIFY_AUTH_TOKEN). Skipped until
|
||||
# NETLIFY_BUNDLE_ANALYZER_SITE_ID exists -- create a new (free)
|
||||
# Netlify site named superset-bundle-analyzer and add its site ID as
|
||||
# that secret to turn this on; nothing else in this workflow depends
|
||||
# on it.
|
||||
- name: Publish bundle analyzer report to Netlify
|
||||
if: ${{ env.NETLIFY_SITE_ID != '' }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
run: |
|
||||
mkdir -p netlify-publish
|
||||
cp superset/static/assets/report.html netlify-publish/index.html
|
||||
# zizmor: ignore[adhoc-packages] - netlify-cli is a one-shot CI deploy
|
||||
# tool, not an application dependency; a global/npx install has no
|
||||
# lockfile context. Version pinned above the floor set by other
|
||||
# ad-hoc installs in this repo (bump deliberately when upgrading).
|
||||
npx --yes netlify-cli@27.0.1 deploy --prod --dir=netlify-publish
|
||||
@@ -45,5 +45,8 @@ jobs:
|
||||
- name: Run Script
|
||||
run: bash .github/workflows/github-action-validator.sh
|
||||
|
||||
- name: Test docs-deploy freshness gate
|
||||
run: bash .github/workflows/scripts/check-docs-deploy-freshness.test.sh
|
||||
|
||||
- name: Check for security issues on GHA workflows
|
||||
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/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.
|
||||
#
|
||||
# Shared freshness gate used by the Docs Deployment workflow
|
||||
# (superset-docs-deploy.yml) both up front (check-freshness) and again right
|
||||
# before the deploy step (recheck-freshness). Writes an output declaring
|
||||
# whether BUILD_SHA is still master's current tip, so a superseded run can
|
||||
# skip cleanly instead of racing (and clobbering, or being force-cancelled
|
||||
# by) a fresher run.
|
||||
#
|
||||
# Required env vars:
|
||||
# BUILD_SHA - the commit SHA this run is building
|
||||
# REPO - "owner/repo" to query, e.g. github.repository
|
||||
# OUTPUT_NAME - the GITHUB_OUTPUT key to write, e.g. "is-current"
|
||||
# GITHUB_OUTPUT - path to append outputs to (set by the Actions runner)
|
||||
# Optional env vars:
|
||||
# EVENT_NAME - if "workflow_dispatch", bypasses the check and always
|
||||
# reports current, since a manual dispatch is a deliberate,
|
||||
# one-off action rather than something racing other triggers
|
||||
# GH_TOKEN - passed through to `gh`, needed to call the GitHub API
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${EVENT_NAME:-}" = "workflow_dispatch" ]; then
|
||||
echo "${OUTPUT_NAME}=true" >>"$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
latest_sha="$(gh api "repos/${REPO}/commits/master" --jq .sha)"
|
||||
if [ "${latest_sha}" = "${BUILD_SHA}" ]; then
|
||||
echo "${OUTPUT_NAME}=true" >>"$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "${OUTPUT_NAME}=false" >>"$GITHUB_OUTPUT"
|
||||
echo "::notice::master has moved on to ${latest_sha} since ${BUILD_SHA} was triggered — skipping this stale run."
|
||||
fi
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/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.
|
||||
#
|
||||
# Exercises check-docs-deploy-freshness.sh against a stubbed `gh`, covering
|
||||
# the dispatch-bypass, current-tip and stale-tip branches so the output
|
||||
# contract (is-current / still-current) can't silently regress. Run
|
||||
# directly, no extra tooling required:
|
||||
# bash .github/workflows/scripts/check-docs-deploy-freshness.test.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
script_under_test="${script_dir}/check-docs-deploy-freshness.sh"
|
||||
|
||||
failures=0
|
||||
|
||||
# Runs the script under test with a stubbed `gh` reporting $1 as master's
|
||||
# latest sha, asserting that GITHUB_OUTPUT ends up containing exactly $4.
|
||||
run_case() {
|
||||
local case_name="$1"
|
||||
local latest_sha="$2"
|
||||
local build_sha="$3"
|
||||
local event_name="$4"
|
||||
local expected_line="$5"
|
||||
|
||||
local workdir
|
||||
workdir="$(mktemp -d)"
|
||||
trap 'rm -rf "${workdir}"' RETURN
|
||||
|
||||
# Fake `gh` that just echoes back the requested "latest" sha regardless of
|
||||
# arguments, so the script under test never touches the network.
|
||||
cat >"${workdir}/gh" <<EOF
|
||||
#!/bin/bash
|
||||
echo '${latest_sha}'
|
||||
EOF
|
||||
chmod +x "${workdir}/gh"
|
||||
|
||||
local output_file="${workdir}/github_output"
|
||||
: >"${output_file}"
|
||||
|
||||
if PATH="${workdir}:${PATH}" \
|
||||
GITHUB_OUTPUT="${output_file}" \
|
||||
OUTPUT_NAME="is-current" \
|
||||
REPO="apache/superset" \
|
||||
BUILD_SHA="${build_sha}" \
|
||||
EVENT_NAME="${event_name}" \
|
||||
GH_TOKEN="fake-token" \
|
||||
bash "${script_under_test}"; then
|
||||
:
|
||||
else
|
||||
echo "FAIL (${case_name}): script exited non-zero"
|
||||
failures=$((failures + 1))
|
||||
return
|
||||
fi
|
||||
|
||||
local actual
|
||||
actual="$(cat "${output_file}")"
|
||||
if [ "${actual}" = "${expected_line}" ]; then
|
||||
echo "PASS (${case_name})"
|
||||
else
|
||||
echo "FAIL (${case_name}): expected '${expected_line}', got '${actual}'"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# `gh` prints "should-not-be-called" for the dispatch case above the trick:
|
||||
# it's never actually invoked since the bypass short-circuits before the
|
||||
# `gh api` call, but the fake still needs a body.
|
||||
run_case "workflow_dispatch bypasses the check" \
|
||||
"unused" "abc123" "workflow_dispatch" \
|
||||
"is-current=true"
|
||||
|
||||
run_case "build sha matches master's tip" \
|
||||
"abc123" "abc123" "push" \
|
||||
"is-current=true"
|
||||
|
||||
run_case "build sha is stale" \
|
||||
"def456" "abc123" "push" \
|
||||
"is-current=false"
|
||||
|
||||
if [ "${failures}" -gt 0 ]; then
|
||||
echo "${failures} case(s) failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All cases passed"
|
||||
@@ -18,16 +18,6 @@ on:
|
||||
|
||||
workflow_dispatch: {}
|
||||
|
||||
# Serialize deploys: the action pushes to apache/superset-site without
|
||||
# rebasing, so concurrent runs race on the final push and the loser fails
|
||||
# with `! [rejected] asf-site -> asf-site (fetch first)`. Cancel any
|
||||
# in-progress run as soon as a newer one starts — the destination repo
|
||||
# isn't touched until the final push step, so canceling mid-build is safe,
|
||||
# and the freshest content always wins.
|
||||
concurrency:
|
||||
group: docs-deploy-asf-site
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -48,17 +38,69 @@ jobs:
|
||||
|
||||
env:
|
||||
SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}
|
||||
|
||||
# Master gets frequent, sometimes bursty pushes, and each one can trigger a
|
||||
# deploy attempt. Rather than let every superseded attempt get force-killed
|
||||
# by the build-deploy concurrency group below (which shows up as a
|
||||
# `cancelled` — i.e. red/failing-looking — check on that commit), have each
|
||||
# run check up front whether it's still building master's current tip and,
|
||||
# if not, skip cleanly. Deliberately outside the docs-deploy-asf-site
|
||||
# concurrency group so it runs immediately for every trigger without
|
||||
# blocking or being blocked by anything.
|
||||
check-freshness:
|
||||
runs-on: ubuntu-26.04
|
||||
outputs:
|
||||
is-current: ${{ steps.check.outputs.is-current }}
|
||||
steps:
|
||||
# Sparse checkout: this job's only job is to be fast, so it fetches
|
||||
# nothing but the freshness-check script itself.
|
||||
- name: Checkout freshness-check script
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
.github/workflows/scripts
|
||||
sparse-checkout-cone-mode: false
|
||||
- name: "Check whether this is still master's current commit"
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
OUTPUT_NAME: is-current
|
||||
run: .github/workflows/scripts/check-docs-deploy-freshness.sh
|
||||
|
||||
build-deploy:
|
||||
needs: config
|
||||
needs: [config, check-freshness]
|
||||
# Only the run for master's current tip proceeds; anything superseded
|
||||
# already skipped at check-freshness above instead of landing here.
|
||||
# For workflow_run triggers, only deploy when the triggering run originated
|
||||
# from this repository (not a fork), ensuring the checked-out code and any
|
||||
# local actions executed with deploy credentials are trusted.
|
||||
if: >-
|
||||
needs.config.outputs.has-secrets &&
|
||||
needs.check-freshness.outputs.is-current == 'true' &&
|
||||
(github.event_name != 'workflow_run' ||
|
||||
github.event.workflow_run.head_repository.full_name == github.repository)
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-26.04
|
||||
# Serialize deploys: the action pushes to apache/superset-site without
|
||||
# rebasing, so concurrent runs race on the final push and the loser fails
|
||||
# with `! [rejected] asf-site -> asf-site (fetch first)`. Queue instead of
|
||||
# canceling: a run that already passed check-freshness can still be
|
||||
# sitting in the queue for a runner when a newer run starts and finishes
|
||||
# first. cancel-in-progress would let that stale, queued run kill the
|
||||
# newer run's in-progress deploy the moment it's finally scheduled, and
|
||||
# then skip itself at the re-check below — losing the deploy entirely.
|
||||
# Queuing means the stale run just waits its turn and then no-ops at the
|
||||
# re-check, so the fresher content that already deployed is never
|
||||
# clobbered or lost. The check-freshness gate above means it should be
|
||||
# rare for more than one run to reach this point, so the queue stays
|
||||
# short in practice.
|
||||
concurrency:
|
||||
group: docs-deploy-asf-site
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
|
||||
with:
|
||||
@@ -130,7 +172,24 @@ jobs:
|
||||
working-directory: docs
|
||||
run: |
|
||||
yarn build
|
||||
# The check-freshness job above narrows the window but doesn't close it: an
|
||||
# older run can observe is-current=true, then sit through this build while a
|
||||
# newer run's own freshness check also passes and it deploys and finishes
|
||||
# first. If this (stale) run then wins entry into the concurrency group, it
|
||||
# would overwrite the newer content that already deployed. Re-check right
|
||||
# before the one step that actually mutates superset-site, so a stale run
|
||||
# skips deploying instead of clobbering a fresher one that already ran.
|
||||
- name: "Re-check freshness immediately before deploying"
|
||||
id: recheck-freshness
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
REPO: ${{ github.repository }}
|
||||
OUTPUT_NAME: still-current
|
||||
run: .github/workflows/scripts/check-docs-deploy-freshness.sh
|
||||
- name: deploy docs
|
||||
if: github.event_name == 'workflow_dispatch' || steps.recheck-freshness.outputs.still-current == 'true'
|
||||
uses: ./.github/actions/github-action-push-to-another-repository
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.SUPERSET_SITE_BUILD }}
|
||||
|
||||
@@ -212,3 +212,100 @@ jobs:
|
||||
- uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
# Compares a PR's own bundle size against the last nightly-recorded
|
||||
# baseline (see frontend-bundle-size-nightly.yml, which owns actually
|
||||
# persisting new baselines). PR-only: a push to master doesn't need this
|
||||
# check re-run against itself, and re-persisting the baseline on every
|
||||
# push to master -- which happens many times a day -- would burn a full
|
||||
# production build for no benefit nightly refresh doesn't already cover.
|
||||
bundle-size:
|
||||
needs: frontend-build
|
||||
if: needs.frontend-build.outputs.should-run == 'true' && github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: docker-image
|
||||
|
||||
- name: Load Docker Image
|
||||
run: |
|
||||
zstd -d < docker-image.tar.zst | docker load
|
||||
|
||||
# webpack's persistent filesystem cache (superset-frontend/webpack.config.js)
|
||||
# turns a warm production build into ~20s instead of several minutes,
|
||||
# but GH-hosted runners are fresh VMs with nothing carried over between
|
||||
# jobs -- without restoring it explicitly, every single PR would pay
|
||||
# the full cold-build cost. Keyed on the same files webpack's own
|
||||
# `buildDependencies` invalidates on, so a stale cache is never used.
|
||||
- name: Restore webpack build cache
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: superset-frontend/.temp_cache
|
||||
key: >-
|
||||
webpack-prod-cache-${{ hashFiles('superset-frontend/package-lock.json',
|
||||
'superset-frontend/babel.config.js', 'superset-frontend/tsconfig.json',
|
||||
'superset-frontend/webpack.config.js') }}
|
||||
|
||||
# Only ever pull the last recorded data point off the cache, keyed by
|
||||
# run ID -- `restore-keys` prefix-matches the most recently created
|
||||
# entry, which is always the latest nightly run. Absent before the
|
||||
# first nightly run ever happens; benchmark-action starts a fresh
|
||||
# history in that case.
|
||||
- name: Restore bundle size history
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: bundle-size-history.json
|
||||
key: bundle-size-history-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
bundle-size-history-
|
||||
|
||||
- name: Build production bundle with stats
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/superset-frontend/bundle-stats
|
||||
mkdir -p ${{ github.workspace }}/superset-frontend/.temp_cache
|
||||
docker run \
|
||||
-v ${{ github.workspace }}/superset-frontend/bundle-stats:/app/superset-frontend/bundle-stats \
|
||||
-v ${{ github.workspace }}/superset-frontend/.temp_cache:/app/superset-frontend/.temp_cache \
|
||||
--rm $TAG \
|
||||
bash -c \
|
||||
"npm i && BUNDLE_SIZE_STATS=true npm run build -- --json=bundle-stats/stats.json"
|
||||
|
||||
- name: Summarize bundle size
|
||||
run: |
|
||||
node superset-frontend/scripts/bundle-size-summary.js \
|
||||
superset-frontend/bundle-stats/stats.json > bundle-size-summary.json
|
||||
rm -rf superset-frontend/bundle-stats
|
||||
|
||||
# Comparison + alert only -- this job never persists. See
|
||||
# frontend-bundle-size-nightly.yml for why.
|
||||
#
|
||||
# comment-on-alert is gated to same-repo PRs: on a fork PR,
|
||||
# GITHUB_TOKEN is forced read-only regardless of the `permissions`
|
||||
# block above, so once the alert threshold is crossed the action's
|
||||
# `pulls.createReview` call 403s. That error isn't gated by
|
||||
# fail-on-alert (which only governs the deliberate alert-threshold
|
||||
# failure) -- it propagates and fails the job outright. Fork PRs
|
||||
# still get the comparison via the job summary (summary-always).
|
||||
- name: Compare bundle size against nightly baseline
|
||||
uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
|
||||
with:
|
||||
tool: customSmallerIsBetter
|
||||
output-file-path: bundle-size-summary.json
|
||||
external-data-json-path: bundle-size-history.json
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-on-alert: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
alert-threshold: "110%"
|
||||
fail-on-alert: false
|
||||
summary-always: true
|
||||
|
||||
@@ -167,7 +167,7 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
|
||||
### Generator Location
|
||||
- Script: `docs/scripts/generate-superset-components.mjs`
|
||||
- Wrapper: `docs/src/components/StorybookWrapper.jsx`
|
||||
- Output: `docs/developer_portal/components/`
|
||||
- Output: `docs/developer_docs/components/`
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
|
||||
@@ -35,4 +35,4 @@ The Developer Portal includes comprehensive guides for:
|
||||
- [Code Review Process](https://superset.apache.org/developer_portal/contributing/code-review)
|
||||
- [Development How-tos](https://superset.apache.org/developer_portal/contributing/howtos)
|
||||
|
||||
Source for the Developer Portal documentation is [located here](https://github.com/apache/superset/tree/master/docs/developer_portal).
|
||||
Source for the Developer Portal documentation is [located here](https://github.com/apache/superset/tree/master/docs/developer_docs).
|
||||
|
||||
@@ -31,6 +31,8 @@ under the License.
|
||||
[](https://github.com/apache/superset/pulls)
|
||||
[](https://bit.ly/join-superset-slack)
|
||||
[](https://superset.apache.org)
|
||||
[](https://superset-storybook.netlify.app)
|
||||
[](https://superset-bundle-analyzer.netlify.app)
|
||||
|
||||
<picture width="500">
|
||||
<source
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
title: Dashboard Performance
|
||||
hide_title: true
|
||||
sidebar_position: 5
|
||||
version: 1
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Dashboard Performance
|
||||
|
||||
A dashboard's perceived speed is determined by three independent things: how
|
||||
many charts have to render, how many queries the backend can execute
|
||||
concurrently, and how quickly the underlying data warehouse can return
|
||||
results. Superset gives you levers for the first two; the third belongs to
|
||||
your warehouse. This page covers the dashboard-side levers and the practical
|
||||
guidance around them.
|
||||
|
||||
## Is there a maximum chart count per dashboard?
|
||||
|
||||
**No hard limit is enforced** — Superset has no configuration key that
|
||||
caps the number of charts on a dashboard. In practice, dashboards behave
|
||||
well up to a few dozen charts. Beyond that, you'll typically feel friction
|
||||
on the initial load and during cross-filter / time-range updates, even with
|
||||
the lazy-loading optimizations described below.
|
||||
|
||||
Rough thresholds to keep in mind:
|
||||
|
||||
- **Under ~25 charts**: usually no perceptible problem.
|
||||
- **25–50 charts**: still fine, but you start to want tabs to break the
|
||||
page into chunks the user actually looks at.
|
||||
- **Over ~50 charts**: split into multiple dashboards or use tabs
|
||||
aggressively. The bottleneck is rarely Superset itself — it's the
|
||||
warehouse executing dozens of queries in parallel and the browser
|
||||
rendering dozens of chart frames.
|
||||
|
||||
These are guidelines, not guarantees. A dashboard of 100 sparkline-style
|
||||
charts hitting a fast cache behaves very differently from a dashboard of
|
||||
20 heavy aggregations against a cold warehouse.
|
||||
|
||||
## Lazy rendering — `DASHBOARD_VIRTUALIZATION`
|
||||
|
||||
Superset's dashboard layout is virtualized at the row level. Charts that
|
||||
are far below the user's current scroll position render a placeholder
|
||||
instead of their visualization until the user scrolls them into view, and
|
||||
go back to a placeholder if scrolled well past. The chart component itself
|
||||
stays mounted throughout — only the visualization is swapped for a
|
||||
placeholder — so this alone does **not** reduce backend query load; see
|
||||
[Deferred data fetch](#deferred-data-fetch--dashboard_virtualization_defer_data)
|
||||
below for that. This is on by default.
|
||||
|
||||
**Feature flag**: `DASHBOARD_VIRTUALIZATION` (default: `True`)
|
||||
|
||||
The flag is `stable` and marked for path-to-deprecation — meaning the
|
||||
behavior will eventually be non-optional, but the flag still exists so
|
||||
operators can disable it if a specific layout misbehaves.
|
||||
|
||||
**Behavior** (from `superset-frontend/src/dashboard/components/gridComponents/Row/Row.tsx`):
|
||||
|
||||
- A chart's visualization is rendered when its row scrolls within **1
|
||||
viewport height** of the visible area.
|
||||
- A chart's visualization is swapped back for a placeholder when its row
|
||||
scrolls more than **4 viewport heights** away from the visible area.
|
||||
- Tabs that aren't currently selected don't render their content at all
|
||||
(see below).
|
||||
- The placeholder-swap-back is skipped in **embedded** mode (so an
|
||||
embedded dashboard keeps its charts rendered once they've been seen,
|
||||
which avoids re-rendering on scroll-up). Both halves are skipped for
|
||||
**headless / bot** rendering (so screenshot / report jobs load every
|
||||
chart).
|
||||
|
||||
## Deferred data fetch — `DASHBOARD_VIRTUALIZATION_DEFER_DATA`
|
||||
|
||||
By default, `DASHBOARD_VIRTUALIZATION` only controls whether a chart's
|
||||
*visualization* is rendered — the chart component still mounts and issues
|
||||
its data request immediately, regardless of scroll position.
|
||||
`DASHBOARD_VIRTUALIZATION_DEFER_DATA` is a supplementary flag that skips
|
||||
the data request itself for charts that aren't currently in view, useful
|
||||
for backends where opening a connection or compiling a query is expensive
|
||||
even if the result would be thrown away. It only has an effect when
|
||||
`DASHBOARD_VIRTUALIZATION` is also enabled — with virtualization off,
|
||||
every chart is treated as in view, so there's nothing left to defer.
|
||||
|
||||
**Feature flag**: `DASHBOARD_VIRTUALIZATION_DEFER_DATA` (default: `False`)
|
||||
|
||||
Enable this if you see warehouse load spike on dashboard *open* even
|
||||
though most charts are off-screen.
|
||||
|
||||
## Per-tab lazy loading
|
||||
|
||||
**This is on by default and has no flag.** A tab's content is not rendered
|
||||
until the user activates that tab, so charts inside an unselected tab do
|
||||
not fetch data on dashboard open. When the user clicks the tab, that
|
||||
tab's charts mount and fetch in the normal way.
|
||||
|
||||
Practically: tabs are the single most effective tool for a large
|
||||
dashboard. Splitting 60 charts across 4 tabs effectively turns dashboard
|
||||
open into "load ~15 charts," and the remaining ones lazy-load only if the
|
||||
user goes looking.
|
||||
|
||||
## Is there a switch to cap concurrent chart queries?
|
||||
|
||||
**No.** Superset does not implement a frontend-side concurrent-request
|
||||
limiter. Each chart issues its own data request when it mounts, and the
|
||||
browser handles parallelism — typically ~6 in-flight requests per origin
|
||||
under HTTP/1.1, though HTTP/2 or HTTP/3 (if your deployment terminates
|
||||
TLS that way) can multiplex considerably more over a single connection.
|
||||
Backend throughput is bounded by your
|
||||
Gunicorn worker count for synchronous query execution, or by your Celery
|
||||
worker pool when [async queries](./async-queries-celery.mdx) are enabled.
|
||||
|
||||
If you need to throttle warehouse load, the right place is:
|
||||
|
||||
1. The warehouse itself (connection pool / concurrency limits).
|
||||
2. Superset's Celery configuration (smaller worker pool when async
|
||||
queries are on).
|
||||
3. Splitting heavy charts across tabs or separate dashboards (each
|
||||
dashboard load only fetches what's visible).
|
||||
|
||||
## Splitting strategies
|
||||
|
||||
When a dashboard outgrows comfortable performance, the options in order
|
||||
of effort:
|
||||
|
||||
**1. Move sections into tabs.** Same dashboard, but only the active tab's
|
||||
charts fetch. This is the cheapest change and often the only one needed.
|
||||
|
||||
**2. Cache aggressively.** A Redis cache backend (see
|
||||
[Caching](./cache.mdx)) means repeat dashboard loads serve from cache
|
||||
rather than re-hitting the warehouse. This is especially impactful for
|
||||
dashboards opened by many users in close succession.
|
||||
|
||||
**3. Enable async queries.** [Async query execution](./async-queries-celery.mdx)
|
||||
via Celery decouples query duration from request lifetime, so a slow
|
||||
chart doesn't block the page. The user sees other charts come in as
|
||||
their queries complete.
|
||||
|
||||
**4. Split into multiple dashboards.** Group related charts into purpose-
|
||||
specific dashboards rather than one mega-dashboard. Link them from a
|
||||
landing dashboard or a navigation menu.
|
||||
|
||||
**5. Pre-aggregate at the warehouse level.** If the same expensive
|
||||
aggregation appears across many charts, materialize it as a view or
|
||||
scheduled table in the warehouse so each chart query is a cheap lookup.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- The feature flags above are set in `superset_config.py`, e.g.:
|
||||
|
||||
```python
|
||||
FEATURE_FLAGS = {
|
||||
"DASHBOARD_VIRTUALIZATION": True,
|
||||
"DASHBOARD_VIRTUALIZATION_DEFER_DATA": True,
|
||||
}
|
||||
```
|
||||
|
||||
- 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
|
||||
intentionally bypasses row virtualization so the rendered artifact
|
||||
includes every chart, 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
|
||||
doesn't render the frontend at all, so virtualization doesn't apply to
|
||||
it either.
|
||||
@@ -519,6 +519,30 @@ sh -c "$(curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install
|
||||
|
||||
For those interested, you may also try out [avn](https://github.com/nvm-sh/nvm#deeper-shell-integration) to automatically switch to the node version that is required to run Superset frontend.
|
||||
|
||||
##### zstd
|
||||
|
||||
`npm run dev-server` proxies requests to your local Superset server and rewrites the HTML it returns, so it has to decompress responses sent with `Content-Encoding: zstd`. It does that with [`simple-zstd`](https://www.npmjs.com/package/simple-zstd), which wraps the system `zstd` binary instead of bundling one. That binary has to be on your `PATH`:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install zstd
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install zstd
|
||||
|
||||
# Windows
|
||||
choco install zstd
|
||||
```
|
||||
|
||||
`simple-zstd` looks for the binary when it is first imported, not when a response is decompressed, so a missing `zstd` stops the dev server at startup with:
|
||||
|
||||
```
|
||||
Error: Can not access zstd! Is it installed?
|
||||
at Object.<anonymous> (.../node_modules/simple-zstd/dist/src/index.js:102:11)
|
||||
```
|
||||
|
||||
The message names the dependency, but it surfaces from inside `webpack.proxy-config.js` while the webpack config is loading, which reads like a build-tooling failure rather than a missing system package.
|
||||
|
||||
#### Install dependencies
|
||||
|
||||
Install third-party dependencies listed in `package.json` via:
|
||||
|
||||
@@ -198,7 +198,7 @@ Each component should come with its dedicated storybook file.
|
||||
|
||||
**One component per story:** Each storybook file should only contain one component unless substantially different variants are required
|
||||
|
||||
**Component variants:** If the component behavior is substantially different when certain props are used, it is best to separate the story into different types. See the `superset-frontend/src/components/Select/Select.stories.tsx` as an example.
|
||||
**Component variants:** If the component behavior is substantially different when certain props are used, it is best to separate the story into different types. See the `superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx` as an example.
|
||||
|
||||
**Isolated state:** The storybook should show how the component works in an isolated state and with as few dependencies as possible
|
||||
|
||||
|
||||
@@ -58,12 +58,12 @@
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^10.5.6",
|
||||
"@storybook/addon-docs": "^10.5.7",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.47",
|
||||
"antd": "^6.5.3",
|
||||
"antd": "^6.5.4",
|
||||
"baseline-browser-mapping": "^2.11.12",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"caniuse-lite": "^1.0.30001807",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.3",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.3",
|
||||
"js-yaml": "^5.2.3",
|
||||
@@ -77,7 +77,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.2.0",
|
||||
"storybook": "^10.5.6",
|
||||
"storybook": "^10.5.7",
|
||||
"swagger-ui-react": "^5.32.12",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
|
||||
@@ -4095,23 +4095,23 @@
|
||||
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
|
||||
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
|
||||
|
||||
"@storybook/addon-docs@^10.5.6":
|
||||
version "10.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.6.tgz#445d4e0992a0862a22bffcea321ee2cb034846b5"
|
||||
integrity sha512-zyUJBrrpC9NTrmsREaVFNr+9WW6pikJtmRvo7GgZGqthEyhjQKSarHrW0aNWkwae2ep3jp1CZi8vIUVG1Dnp0w==
|
||||
"@storybook/addon-docs@^10.5.7":
|
||||
version "10.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.7.tgz#6d599c94fc871c248ce06a5c081f57655c83f40a"
|
||||
integrity sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==
|
||||
dependencies:
|
||||
"@mdx-js/react" "^3.0.0"
|
||||
"@storybook/csf-plugin" "10.5.6"
|
||||
"@storybook/csf-plugin" "10.5.7"
|
||||
"@storybook/icons" "^2.0.2"
|
||||
"@storybook/react-dom-shim" "10.5.6"
|
||||
"@storybook/react-dom-shim" "10.5.7"
|
||||
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/csf-plugin@10.5.6":
|
||||
version "10.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.6.tgz#9fca28f5fd7d545a32638bb4f08902f6887072b2"
|
||||
integrity sha512-PJLyOmcKe1OZDBw7RaGX/gjuiJuVfS5pVgc4W2RnHYOFpU6F5Bv9+9MqQwp0i7tWZBWc4fsCJudgVqwgjuTROA==
|
||||
"@storybook/csf-plugin@10.5.7":
|
||||
version "10.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz#bc73f164d1b5f8e2931b2774f4b389a06453cf6e"
|
||||
integrity sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==
|
||||
dependencies:
|
||||
unplugin "^2.3.5"
|
||||
|
||||
@@ -4125,10 +4125,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
|
||||
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
|
||||
|
||||
"@storybook/react-dom-shim@10.5.6":
|
||||
version "10.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.6.tgz#3685605c9dd27298fada7fef264801b2e7d62cbb"
|
||||
integrity sha512-dV3oOHc5ImggxEqeIiUj4vvnQO5SKScFtqAkxgIWLju1wiSZSIqQ5Q4Mp12Rhs9hQrjF039DueH7f2xJJZfvSw==
|
||||
"@storybook/react-dom-shim@10.5.7":
|
||||
version "10.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz#9a5aa0e0f89c09e71c6cbfc6bb1abeb537e5aabf"
|
||||
integrity sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==
|
||||
|
||||
"@superset-ui/core@^0.20.4":
|
||||
version "0.20.4"
|
||||
@@ -6164,10 +6164,10 @@ ansis@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
|
||||
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
|
||||
|
||||
antd@^6.5.3:
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.3.tgz#3c7d2ec4a20be116f72b7fbbdb4497d65ffd6dae"
|
||||
integrity sha512-Q5r8sztf9Yk9B70bSUjnPYMCJ4A/eZM7uMoTj8UAhlSKR9aftjEuBEPcNSmRux7hB+87rxO8vN1X4HNjR97qyQ==
|
||||
antd@^6.5.4:
|
||||
version "6.5.4"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.4.tgz#b41665e86a5f46ca761abd3b0abef7460116ca0d"
|
||||
integrity sha512-jchA6i0rEwHjLpgC+l6HeLHP0gL4Q4yjs6Mxqt6PlhGD5ArxCj3ZH+fKFbNquCtd6Rlzzi+emfNFpP2dGLwZzg==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.1"
|
||||
"@ant-design/cssinjs" "^2.1.2"
|
||||
@@ -6745,10 +6745,10 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001806:
|
||||
version "1.0.30001806"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz#1bc8e502b723fa393455dfbedd5ccec0c29bb74e"
|
||||
integrity sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001807:
|
||||
version "1.0.30001807"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz#a113854941fb45b4c1f51793f4636920489079b4"
|
||||
integrity sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -14765,10 +14765,10 @@ stop-iteration-iterator@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
internal-slot "^1.1.0"
|
||||
|
||||
storybook@^10.5.6:
|
||||
version "10.5.6"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.6.tgz#c91f22f617f3718dd06c58c87b46ff66f4ce7bf5"
|
||||
integrity sha512-VhYwqxPySa24CVXKoWD6gCZXx9//DTmo43YpusGuAoHDYj5Osjt8wuBRQVeGoaLUWnHiPWv8S+GYHrJEaBM6Rg==
|
||||
storybook@^10.5.7:
|
||||
version "10.5.7"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.7.tgz#adfc465e51f337291c095278c23f1b8024ef2da7"
|
||||
integrity sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==
|
||||
dependencies:
|
||||
"@storybook/global" "^5.0.0"
|
||||
"@storybook/icons" "^2.0.2"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# 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.
|
||||
|
||||
# Computes the `--extra-flags` value passed to `supersetbot docker` for a
|
||||
# given build preset. Factored out of .github/workflows/docker.yml so the
|
||||
# PY_VER override logic below can be exercised by an always-on CI check
|
||||
# (docker.yml's docker-build job only runs when the change detector's
|
||||
# docker/python/frontend outputs are true, and the PR build matrix never
|
||||
# includes py311/py312 at all, so a regression here would otherwise go
|
||||
# unnoticed until the fix actually runs on master) without duplicating -
|
||||
# and risking drift from - the logic used by the real build step.
|
||||
#
|
||||
# supersetbot's "py311"/"py312" presets pin their own --build-arg PY_VER,
|
||||
# which lands ahead of --extra-flags on the assembled buildx command line;
|
||||
# docker/buildx keeps the last value for a repeated --build-arg key, so
|
||||
# appending PY_VER here would override supersetbot's pin and silently make
|
||||
# "py311"/"py312" build the exact same image as "lean". Every other preset
|
||||
# gets the override so its build lands on the Dockerfile's own supported
|
||||
# Python version.
|
||||
#
|
||||
# Usage: docker-build-extra-flags.sh <build_preset> <image_tag>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_PRESET="${1:?usage: docker-build-extra-flags.sh <build_preset> <image_tag>}"
|
||||
IMAGE_TAG="${2:?usage: docker-build-extra-flags.sh <build_preset> <image_tag>}"
|
||||
|
||||
EXTRA_FLAGS="--build-arg INCLUDE_CHROMIUM=false --tag $IMAGE_TAG"
|
||||
if [ "$BUILD_PRESET" != "py311" ] && [ "$BUILD_PRESET" != "py312" ]; then
|
||||
EXTRA_FLAGS="--build-arg PY_VER=3.11.14-slim-trixie $EXTRA_FLAGS"
|
||||
fi
|
||||
|
||||
echo "$EXTRA_FLAGS"
|
||||
@@ -81,7 +81,7 @@
|
||||
"@visx/xychart": "^4.0.0",
|
||||
"ag-grid-community": "36.1.0",
|
||||
"ag-grid-react": "36.1.0",
|
||||
"antd": "^6.5.3",
|
||||
"antd": "^6.5.4",
|
||||
"chrono-node": "^2.10.1",
|
||||
"classnames": "^2.2.5",
|
||||
"content-disposition": "^2.0.1",
|
||||
@@ -108,7 +108,7 @@
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.28.0",
|
||||
"mapbox-gl": "^3.28.1",
|
||||
"markdown-to-jsx": "^9.10.2",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
@@ -116,7 +116,7 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^6.0.1",
|
||||
"ol": "^10.10.0",
|
||||
"query-string": "9.4.1",
|
||||
"query-string": "9.5.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.3.0",
|
||||
"react-arborist": "^3.16.0",
|
||||
@@ -180,9 +180,9 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.6",
|
||||
"@storybook/addon-links": "10.5.6",
|
||||
"@storybook/react-webpack5": "10.5.6",
|
||||
"@storybook/addon-docs": "10.5.7",
|
||||
"@storybook/addon-links": "10.5.7",
|
||||
"@storybook/react-webpack5": "10.5.7",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.47",
|
||||
@@ -235,7 +235,7 @@
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.6",
|
||||
"eslint-plugin-storybook": "10.5.7",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -266,13 +266,13 @@
|
||||
"source-map": "^0.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.6",
|
||||
"storybook": "10.5.7",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"swc-loader": "^0.2.7",
|
||||
"ts-jest": "^29.4.12",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.23.9",
|
||||
"tsx": "^4.23.10",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"wait-on": "^9.1.0",
|
||||
@@ -10741,16 +10741,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.6.tgz",
|
||||
"integrity": "sha512-zyUJBrrpC9NTrmsREaVFNr+9WW6pikJtmRvo7GgZGqthEyhjQKSarHrW0aNWkwae2ep3jp1CZi8vIUVG1Dnp0w==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.7.tgz",
|
||||
"integrity": "sha512-KNARJfjICaizinsR3INMEiipZm1ObYo+xw+E26gteu50Bcy2dIZUtk5uHY5XdtardU3AXX6yRXoBZ2HCY3lbHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.5.6",
|
||||
"@storybook/csf-plugin": "10.5.7",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@storybook/react-dom-shim": "10.5.6",
|
||||
"@storybook/react-dom-shim": "10.5.7",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -10761,7 +10761,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10770,9 +10770,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.6.tgz",
|
||||
"integrity": "sha512-PJLyOmcKe1OZDBw7RaGX/gjuiJuVfS5pVgc4W2RnHYOFpU6F5Bv9+9MqQwp0i7tWZBWc4fsCJudgVqwgjuTROA==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.7.tgz",
|
||||
"integrity": "sha512-IaX8FlM0H36HNFhJ2+4L9bCldqfvHGqcLg841SJNyK/DhfMlM7JsvY/GDH2ZFuWrUf8FSOx96GRRnHq6XfRKag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10785,7 +10785,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "10.5.6",
|
||||
"storybook": "10.5.7",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -10805,9 +10805,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.6.tgz",
|
||||
"integrity": "sha512-dV3oOHc5ImggxEqeIiUj4vvnQO5SKScFtqAkxgIWLju1wiSZSIqQ5Q4Mp12Rhs9hQrjF039DueH7f2xJJZfvSw==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz",
|
||||
"integrity": "sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -10819,7 +10819,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10831,9 +10831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.6.tgz",
|
||||
"integrity": "sha512-pw+OS/wUZ4ijdVGOsE5QOt59+C2i4fwtFBs2ircB7KMlwEE7gslovZEjnrz8bbaznvmoLoYGWYZxXcLi+bYmzg==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.7.tgz",
|
||||
"integrity": "sha512-17PxEOocLhAEaPeQ4q+8yul/LF9YEIePS1arknCAS7U1pQXTe0uj+R0pB6uPLVflM5gECQMiP4WzIj4tEiL6+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10846,7 +10846,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -10940,15 +10940,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.6.tgz",
|
||||
"integrity": "sha512-UdsC+IrZHBAtEvvDkfCPhg5sy5jnJAHT4RS3I8wNHVJ+93gaUrZLElSIV+w5UB1u/yghoqmQm7/LfpPpZrjPZQ==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.5.7.tgz",
|
||||
"integrity": "sha512-vvl07oXp2qfmHJHZ77Aw1F3LFOo7XubOta+lC8UmlEw3rDDJhQxJN3erJJVHavNhdA2jBTK6VUXQKdqQh7X7nQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-webpack5": "10.5.6",
|
||||
"@storybook/preset-react-webpack": "10.5.6",
|
||||
"@storybook/react": "10.5.6"
|
||||
"@storybook/builder-webpack5": "10.5.7",
|
||||
"@storybook/preset-react-webpack": "10.5.7",
|
||||
"@storybook/react": "10.5.7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -10957,7 +10957,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.6",
|
||||
"storybook": "10.5.7",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -10967,13 +10967,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.6.tgz",
|
||||
"integrity": "sha512-uWo/MzNC6HXMEpy8QQfbeYh1j6aOC6Ly0sAR6RE0LPvpyGEWC0VaVOBERIJJBjeuPuonDBVKfEjh1iUvZhJopg==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.5.7.tgz",
|
||||
"integrity": "sha512-4n4c60LihFivZnjAcXGO5+XbgZthoUtKb/nPKVgypj3MpEetzjq6XR83A4UNnRsXYmjqfn6bsDWNgEJ/RvQg5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.5.6",
|
||||
"@storybook/core-webpack": "10.5.7",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"cjs-module-lexer": "^1.2.3",
|
||||
"css-loader": "^7.1.2",
|
||||
@@ -10995,7 +10995,7 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -11004,9 +11004,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5/node_modules/@storybook/core-webpack": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.6.tgz",
|
||||
"integrity": "sha512-o5PP3K+NcJAitZF7Ywweow0d8dJrEA1jxV5T1LMGMiWHUrnpoaPTiK1HcYw39pOQkdKL88mMSfDDUipGurZthw==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.7.tgz",
|
||||
"integrity": "sha512-0dtDw/FNPREoeCHX2RgZz0OecxaAGol1R7bCobFevArxyFIPJisTfjDMUFHKr+3B7BilTd3vnatl7Nlvgs0EiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11017,17 +11017,17 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.6.tgz",
|
||||
"integrity": "sha512-QPUl2t+0VIp1Wy7JfqvV8cI1NrULUt+XFMKdIaNp39TuyMn3El4txvmxQWKhcYvnSOEzQ2SGNDqSWcJISwmeAA==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.5.7.tgz",
|
||||
"integrity": "sha512-xwNRcoVlIDx1/YYCFBAxfh/91vFiOgrVI+0Ir4u9eO87SH2leehRnJh619QEOrlQEU5px487y2BmL2ZVtmTpYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.5.6",
|
||||
"@storybook/core-webpack": "10.5.7",
|
||||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"magic-string": "^0.30.5",
|
||||
@@ -11044,7 +11044,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -11053,9 +11053,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack/node_modules/@storybook/core-webpack": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.6.tgz",
|
||||
"integrity": "sha512-o5PP3K+NcJAitZF7Ywweow0d8dJrEA1jxV5T1LMGMiWHUrnpoaPTiK1HcYw39pOQkdKL88mMSfDDUipGurZthw==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.5.7.tgz",
|
||||
"integrity": "sha512-0dtDw/FNPREoeCHX2RgZz0OecxaAGol1R7bCobFevArxyFIPJisTfjDMUFHKr+3B7BilTd3vnatl7Nlvgs0EiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -11066,18 +11066,18 @@
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.6.tgz",
|
||||
"integrity": "sha512-dXSdNoc9yAvpa4hiegQhmZPXOKunAxkPX94DxvRw/kM6+wujVFAGlZjYygKrWw357KOjPRK7SO1LRTc70mgrhQ==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.7.tgz",
|
||||
"integrity": "sha512-uFvty2MMdFXzW5PcQe1JqDAZkz6cQq7q/9G/cbGVnBEvP6zsOVeL+bmrQ0/WBlFQN0Ko9+ZoCTvaQ9s65zBa5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.5.6",
|
||||
"@storybook/react-dom-shim": "10.5.7",
|
||||
"react-docgen": "^8.0.2",
|
||||
"react-docgen-typescript": "^2.2.2"
|
||||
},
|
||||
@@ -11090,7 +11090,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.6",
|
||||
"storybook": "10.5.7",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -11106,9 +11106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.6.tgz",
|
||||
"integrity": "sha512-dV3oOHc5ImggxEqeIiUj4vvnQO5SKScFtqAkxgIWLju1wiSZSIqQ5Q4Mp12Rhs9hQrjF039DueH7f2xJJZfvSw==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.7.tgz",
|
||||
"integrity": "sha512-lxOkyh+wu/MiBXvYQHjZfD+DRKOa4bHBzbuGuiHXnHXmdOcTRdcrQTsoeN2FPtfugmmOG66cZUEgDwNX+k5eRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -11120,7 +11120,7 @@
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -15214,9 +15214,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/antd": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-6.5.3.tgz",
|
||||
"integrity": "sha512-Q5r8sztf9Yk9B70bSUjnPYMCJ4A/eZM7uMoTj8UAhlSKR9aftjEuBEPcNSmRux7hB+87rxO8vN1X4HNjR97qyQ==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-6.5.4.tgz",
|
||||
"integrity": "sha512-jchA6i0rEwHjLpgC+l6HeLHP0gL4Q4yjs6Mxqt6PlhGD5ArxCj3ZH+fKFbNquCtd6Rlzzi+emfNFpP2dGLwZzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^8.0.1",
|
||||
@@ -18913,9 +18913,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
|
||||
"integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.5.0.tgz",
|
||||
"integrity": "sha512-1BiQVoK8C9gUbQU6NzAtO/tkz2qOFpEObMWpcFvhx4fYnj4Oc5yzaJN/LD36ihkVUdXyh5ZekzX+yM+ty/SrPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
@@ -20522,9 +20522,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-storybook": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.6.tgz",
|
||||
"integrity": "sha512-uOXhNkIH+iTdyViSmWnCrwtapasL57M3nq5yfST1H7y9djRLyuAIfNcf9cPBedc2G1oqI8jn3up/VHdN3y3Btw==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.7.tgz",
|
||||
"integrity": "sha512-mLpamG1Rsica2jYbUzIZOEuy7Fm1IMtVLMvvxGTpjTVKUMxTXJsANx3MBpH2VSbGQB8Yzlt5399WL/O07K97Ig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -20533,7 +20533,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8",
|
||||
"storybook": "10.5.6"
|
||||
"storybook": "10.5.7"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-testing-library": {
|
||||
@@ -28748,9 +28748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mapbox-gl": {
|
||||
"version": "3.28.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.28.0.tgz",
|
||||
"integrity": "sha512-WEbvl2ju0MUZ+R83HeCosmJBTyYdhmFcajeQ7kwLyJ0EHUw9YG/k2QLcMmAQ8sXZpkWq1BbmfjT5lh/oInOnCw==",
|
||||
"version": "3.28.1",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.28.1.tgz",
|
||||
"integrity": "sha512-f8bCHFzZ51bKig7rnD7e08aoFLOV3MNFZduspZ4lgOgiaNVp9sw4NSWcgo3IWTyekYKKXjiICD2BP2o4DiYfxw==",
|
||||
"license": "SEE LICENSE IN LICENSE.txt",
|
||||
"workspaces": [
|
||||
"src/style-spec",
|
||||
@@ -33668,12 +33668,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "9.4.1",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.4.1.tgz",
|
||||
"integrity": "sha512-lSyJeN3RuaG7DZGWThtYRhk96+kEyZ/+doZpERuWbjeFL+Ok3vEat/swU498rAI0NcVt5/RJp8UDuLz7FckxrA==",
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.5.0.tgz",
|
||||
"integrity": "sha512-YlJmwNyi0RGYjlxYcuDncMsxFU7YyutbuI7gTm8ySxIGBlwx5yiBCOD5ig9ZNoHkawk/1Dey0N5mEfcUybMVAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.4.1",
|
||||
"decode-uri-component": "^0.5.0",
|
||||
"filter-obj": "^5.1.0",
|
||||
"split-on-first": "^3.0.0"
|
||||
},
|
||||
@@ -37991,9 +37991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/storybook": {
|
||||
"version": "10.5.6",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.6.tgz",
|
||||
"integrity": "sha512-VhYwqxPySa24CVXKoWD6gCZXx9//DTmo43YpusGuAoHDYj5Osjt8wuBRQVeGoaLUWnHiPWv8S+GYHrJEaBM6Rg==",
|
||||
"version": "10.5.7",
|
||||
"resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.7.tgz",
|
||||
"integrity": "sha512-oiKvWIwIoOhFP1i6dASYyMXwPHKEtVZMshqSB7EvIVYjWRh0l9H7gHEt1z4Gh2rLGFMekWdsm4s94rvwpR7gkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -39966,9 +39966,9 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.23.9",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.9.tgz",
|
||||
"integrity": "sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw==",
|
||||
"version": "4.23.10",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.10.tgz",
|
||||
"integrity": "sha512-0Vb9eKU47njkxv/6B8CRZRDsxNDT/Pz+BIU+M5jw7xL3TdzAjSxlZUxu0xFL/kLpaG3sHZ0LH2wbK1T1yo7CUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -43353,6 +43353,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/superset-ui-core/node_modules/dompurify": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"packages/superset-ui-core/node_modules/react-ace": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-14.0.1.tgz",
|
||||
@@ -43936,7 +43945,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"mapbox-gl": "^3.28.0",
|
||||
"mapbox-gl": "^3.28.1",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"react-map-gl": "^8.1.2",
|
||||
"supercluster": "^8.0.1"
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"playwright:debug": "playwright test --debug",
|
||||
"playwright:report": "playwright show-report",
|
||||
"docs:screenshots": "playwright test --config=playwright/generators/playwright.config.ts docs/",
|
||||
"playwright:thumbnails": "CAPTURE_THUMBNAILS=1 playwright test tests/tools/capture-viz-thumbnails.spec.ts --project chromium",
|
||||
"prod": "npm run build",
|
||||
"prune": "rm -rf ./{packages,plugins}/*/{node_modules,lib,esm,tsconfig.tsbuildinfo,package-lock.json} ./.temp_cache",
|
||||
"storybook": "cross-env NODE_ENV=development BABEL_ENV=development storybook dev -p 6006",
|
||||
@@ -158,7 +159,7 @@
|
||||
"@visx/xychart": "^4.0.0",
|
||||
"ag-grid-community": "36.1.0",
|
||||
"ag-grid-react": "36.1.0",
|
||||
"antd": "^6.5.3",
|
||||
"antd": "^6.5.4",
|
||||
"chrono-node": "^2.10.1",
|
||||
"classnames": "^2.2.5",
|
||||
"content-disposition": "^2.0.1",
|
||||
@@ -185,7 +186,7 @@
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.28.0",
|
||||
"mapbox-gl": "^3.28.1",
|
||||
"markdown-to-jsx": "^9.10.2",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
@@ -193,7 +194,7 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^6.0.1",
|
||||
"ol": "^10.10.0",
|
||||
"query-string": "9.4.1",
|
||||
"query-string": "9.5.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.3.0",
|
||||
"react-arborist": "^3.16.0",
|
||||
@@ -257,9 +258,9 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.6",
|
||||
"@storybook/addon-links": "10.5.6",
|
||||
"@storybook/react-webpack5": "10.5.6",
|
||||
"@storybook/addon-docs": "10.5.7",
|
||||
"@storybook/addon-links": "10.5.7",
|
||||
"@storybook/react-webpack5": "10.5.7",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.47",
|
||||
@@ -312,7 +313,7 @@
|
||||
"eslint-plugin-no-only-tests": "^3.4.0",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.6",
|
||||
"eslint-plugin-storybook": "10.5.7",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -343,13 +344,13 @@
|
||||
"source-map": "^0.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.6",
|
||||
"storybook": "10.5.7",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"swc-loader": "^0.2.7",
|
||||
"ts-jest": "^29.4.12",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.23.9",
|
||||
"tsx": "^4.23.10",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"wait-on": "^9.1.0",
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Crawls every dashboard on the target instance and refreshes viz-picker
|
||||
* thumbnails and example galleries from live example charts.
|
||||
*
|
||||
* This is a maintenance tool, not a test: charts are DISCOVERED via the
|
||||
* API (dashboards -> charts), so it keeps working as example dashboards
|
||||
* evolve. Every distinct chart found for a viz type gets captured — the
|
||||
* preferred (or alphabetically-first) chart becomes the 512x512
|
||||
* `thumbnail.png`, and each additional distinct chart fills the next
|
||||
* `exampleGallery` slot declared for that viz type, so the gallery shows
|
||||
* real variety instead of the same chart resized. The two static pieces
|
||||
* are the viz type -> image path maps below (VIZ_TYPE_THUMBNAILS,
|
||||
* VIZ_TYPE_GALLERY), which change when a plugin's images or gallery
|
||||
* shape change — never when examples change. Viz types found on
|
||||
* dashboards but missing from both maps, and gallery slots left unfilled
|
||||
* for lack of enough distinct example charts, are reported at the end
|
||||
* without failing the run.
|
||||
*
|
||||
* It only runs when CAPTURE_THUMBNAILS=1 is set, so the regular
|
||||
* Playwright suites never execute it.
|
||||
*
|
||||
* Usage (requires a running Superset with examples loaded):
|
||||
* npm run playwright:thumbnails
|
||||
* VIZ_TYPES=bullet,rose npm run playwright:thumbnails # subset
|
||||
*
|
||||
* Notes:
|
||||
* - Dark variants (`thumbnail-dark.png`, `example-dark.jpg`) are captured
|
||||
* via prefers-color-scheme emulation whenever the sibling file exists;
|
||||
* if the app ignores the emulation (dark theming disabled) the dark
|
||||
* file is left untouched.
|
||||
* - A brand-new gallery image (rather than an existing file being
|
||||
* refreshed) still needs to be registered in the plugin's metadata
|
||||
* before it renders in the gallery; the capture logs a reminder.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const THUMBNAIL_SIZE = 512;
|
||||
const RENDERED_CHART_SELECTOR =
|
||||
'[data-test="chart-container"]:has(svg, canvas, table):not(:has([data-test="loading-indicator"]))';
|
||||
/**
|
||||
* Charts that render plain markup — no svg/canvas/table for the rendered
|
||||
* selector to key on — get a looser signal plus a longer settle.
|
||||
*/
|
||||
const TEXT_ONLY_VIZ_TYPES = new Set([
|
||||
'ag-grid-table',
|
||||
'big_number_total',
|
||||
'handlebars',
|
||||
'pop_kpi',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Hover this element just before the screenshot, so charts whose identity
|
||||
* benefits from an interaction (a visible tooltip) capture mid-hover.
|
||||
*/
|
||||
const HOVER_BEFORE_CAPTURE: Record<string, string> = {
|
||||
cal_heatmap: '[data-test="chart-container"] svg rect[class*=" r"]',
|
||||
};
|
||||
const TEXT_RENDERED_CHART_SELECTOR =
|
||||
'[data-test="chart-container"]:not(:has([data-test="loading-indicator"]))';
|
||||
|
||||
/** superset-frontend root, resolved from this spec's location. */
|
||||
const FRONTEND_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
const ECHARTS = 'plugins/plugin-chart-echarts/src';
|
||||
const DECKGL = 'plugins/preset-chart-deckgl/src/layers';
|
||||
|
||||
/**
|
||||
* Where each viz type's gallery thumbnail lives, relative to
|
||||
* superset-frontend. One entry per registered viz type with a thumbnail;
|
||||
* add a line when a new plugin ships.
|
||||
*/
|
||||
const VIZ_TYPE_THUMBNAILS: Record<string, string> = {
|
||||
'ag-grid-table':
|
||||
'plugins/plugin-chart-ag-grid-table/src/images/thumbnail.png',
|
||||
big_number: `${ECHARTS}/BigNumber/BigNumberWithTrendline/images/thumbnail.png`,
|
||||
big_number_total: `${ECHARTS}/BigNumber/BigNumberTotal/images/thumbnail.png`,
|
||||
box_plot: `${ECHARTS}/BoxPlot/images/thumbnail.png`,
|
||||
bubble_v2: `${ECHARTS}/Bubble/images/thumbnail.png`,
|
||||
bullet: `${ECHARTS}/Bullet/images/thumbnail.png`,
|
||||
cal_heatmap: 'plugins/plugin-chart-calendar/src/images/thumbnail.png',
|
||||
cartodiagram: 'plugins/plugin-chart-cartodiagram/src/images/thumbnail.png',
|
||||
chord: 'plugins/plugin-chart-chord/src/images/thumbnail.png',
|
||||
country_map: 'plugins/plugin-chart-country-map/src/images/thumbnail.png',
|
||||
deck_arc: `${DECKGL}/Arc/images/thumbnail.png`,
|
||||
deck_contour: `${DECKGL}/Contour/images/thumbnail.png`,
|
||||
deck_geojson: `${DECKGL}/Geojson/images/thumbnail.png`,
|
||||
deck_grid: `${DECKGL}/Grid/images/thumbnail.png`,
|
||||
deck_heatmap: `${DECKGL}/Heatmap/images/thumbnail.png`,
|
||||
deck_hex: `${DECKGL}/Hex/images/thumbnail.png`,
|
||||
deck_multi: 'plugins/preset-chart-deckgl/src/Multi/images/thumbnail.png',
|
||||
deck_path: `${DECKGL}/Path/images/thumbnail.png`,
|
||||
deck_polygon: `${DECKGL}/Polygon/images/thumbnail.png`,
|
||||
deck_scatter: `${DECKGL}/Scatter/images/thumbnail.png`,
|
||||
deck_screengrid: `${DECKGL}/Screengrid/images/thumbnail.png`,
|
||||
echarts_area: `${ECHARTS}/Timeseries/Area/images/thumbnail.png`,
|
||||
echarts_timeseries: `${ECHARTS}/Timeseries/images/thumbnail.png`,
|
||||
echarts_timeseries_bar: `${ECHARTS}/Timeseries/Regular/Bar/images/thumbnail.png`,
|
||||
echarts_timeseries_line: `${ECHARTS}/Timeseries/Regular/Line/images/thumbnail.png`,
|
||||
echarts_timeseries_scatter: `${ECHARTS}/Timeseries/Regular/Scatter/images/thumbnail.png`,
|
||||
echarts_timeseries_smooth: `${ECHARTS}/Timeseries/Regular/SmoothLine/images/thumbnail.png`,
|
||||
echarts_timeseries_step: `${ECHARTS}/Timeseries/Step/images/thumbnail.png`,
|
||||
funnel: `${ECHARTS}/Funnel/images/thumbnail.png`,
|
||||
gantt_chart: `${ECHARTS}/Gantt/images/thumbnail.png`,
|
||||
gauge_chart: `${ECHARTS}/Gauge/images/thumbnail.png`,
|
||||
graph_chart: `${ECHARTS}/Graph/images/thumbnail.png`,
|
||||
// handlebars intentionally unmapped: its generic logo represents the
|
||||
// template-anything nature of the chart better than any one example.
|
||||
heatmap_v2: `${ECHARTS}/Heatmap/images/thumbnail.png`,
|
||||
histogram_v2: `${ECHARTS}/Histogram/images/thumbnail.png`,
|
||||
horizon: 'plugins/plugin-chart-horizon/src/images/thumbnail.png',
|
||||
mixed_timeseries: `${ECHARTS}/MixedTimeseries/images/thumbnail.png`,
|
||||
paired_ttest: 'plugins/plugin-chart-paired-t-test/src/images/thumbnail.png',
|
||||
para: 'plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png',
|
||||
partition: 'plugins/plugin-chart-partition/src/images/thumbnail.png',
|
||||
pie: `${ECHARTS}/Pie/images/thumbnail.png`,
|
||||
pivot_table_v2: 'plugins/plugin-chart-pivot-table/src/images/thumbnail.png',
|
||||
point_cluster_map:
|
||||
'plugins/plugin-chart-point-cluster-map/src/images/thumbnail.png',
|
||||
pop_kpi: `${ECHARTS}/BigNumber/BigNumberPeriodOverPeriod/images/thumbnail.png`,
|
||||
radar: `${ECHARTS}/Radar/images/thumbnail.png`,
|
||||
rose: `${ECHARTS}/Rose/images/thumbnail.png`,
|
||||
sankey_v2: `${ECHARTS}/Sankey/images/thumbnail.png`,
|
||||
sunburst_v2: `${ECHARTS}/Sunburst/images/thumbnail.png`,
|
||||
table: 'plugins/plugin-chart-table/src/images/thumbnail.png',
|
||||
time_pivot: `${ECHARTS}/TimePivot/images/thumbnail.png`,
|
||||
time_table: 'src/visualizations/TimeTable/images/thumbnail.png',
|
||||
tree_chart: `${ECHARTS}/Tree/images/thumbnail.png`,
|
||||
treemap_v2: `${ECHARTS}/Treemap/images/thumbnail.png`,
|
||||
waterfall: `${ECHARTS}/Waterfall/images/thumbnail.png`,
|
||||
word_cloud: 'plugins/plugin-chart-word-cloud/src/images/thumbnail.png',
|
||||
world_map: 'plugins/plugin-chart-world-map/src/images/thumbnail.png',
|
||||
};
|
||||
|
||||
/**
|
||||
* When several example charts share a viz type, prefer these slices over
|
||||
* the default alphabetically-first pick. Missing slices fall back to the
|
||||
* default, so stale entries degrade gracefully.
|
||||
*/
|
||||
const PREFERRED_SLICES: Record<string, string> = {
|
||||
big_number: 'Sales Year over Year',
|
||||
bubble_v2: 'Life Expectancy VS Rural %',
|
||||
bullet: 'Total Sales Bullet',
|
||||
cal_heatmap: 'Sales Calendar Heatmap',
|
||||
chord: 'Product Line Chord',
|
||||
echarts_area: 'Sales Stacked Area',
|
||||
echarts_timeseries_bar: 'Sales Stacked Bars',
|
||||
echarts_timeseries_line: 'Monthly Sales Line',
|
||||
echarts_timeseries_smooth: 'Monthly Sales Smooth',
|
||||
echarts_timeseries_step: 'Quarterly Sales Steps',
|
||||
funnel: 'Population Funnel',
|
||||
gauge_chart: 'Rural Population Gauge',
|
||||
heatmap_v2: 'Sales Grid Heatmap',
|
||||
histogram_v2: 'Life Expectancy Histogram',
|
||||
horizon: 'Population Growth Horizon',
|
||||
mixed_timeseries: 'Sales Mixed Chart',
|
||||
paired_ttest: 'Population Paired t-Test',
|
||||
partition: 'Population Partition',
|
||||
pie: 'Product Line Donut',
|
||||
pivot_table_v2: 'Sales Pivot Highlights',
|
||||
radar: 'Game Sales Radar',
|
||||
rose: 'Population Nightingale Rose',
|
||||
sunburst_v2: 'Population Sunburst',
|
||||
table: 'Sales Summary Table',
|
||||
time_pivot: 'Sales Period Pivot',
|
||||
time_table: 'Product Line Time Table',
|
||||
tree_chart: 'Sales Territory Tree',
|
||||
treemap_v2: 'Population Treemap',
|
||||
waterfall: 'Quarterly Sales Waterfall',
|
||||
};
|
||||
|
||||
/** Gallery example images use a wide aspect, matching the existing art. */
|
||||
const EXAMPLE_WIDTH = 800;
|
||||
const EXAMPLE_HEIGHT = 460;
|
||||
|
||||
/**
|
||||
* Every plugin's `exampleGallery` image slots (never including
|
||||
* `thumbnail.png`/`thumbnail-dark.png`, which stays governed by
|
||||
* VIZ_TYPE_THUMBNAILS), in the order they appear in that plugin's
|
||||
* `index.ts`. One entry per viz type that declares a gallery — including
|
||||
* `handlebars`, whose gallery is filled even though its picker thumbnail
|
||||
* intentionally stays the generic logo (unmapped in VIZ_TYPE_THUMBNAILS).
|
||||
*
|
||||
* For a viz type with N slots here, the crawler assigns its (N+1)
|
||||
* distinct dashboard charts (the thumbnail's pick, then this many more)
|
||||
* to thumbnail, slot 1, slot 2, ... in that order, so gallery images show
|
||||
* different charts than the thumbnail and each other rather than the same
|
||||
* chart resized. Fewer distinct charts than slots just leaves the
|
||||
* trailing slots untouched (reported, not failed) — add more example
|
||||
* charts to fill them.
|
||||
*
|
||||
* Update this when a plugin's exampleGallery array changes shape.
|
||||
*/
|
||||
const VIZ_TYPE_GALLERY: Record<string, string[]> = {
|
||||
'ag-grid-table': [
|
||||
'plugins/plugin-chart-ag-grid-table/src/images/Table.jpg',
|
||||
'plugins/plugin-chart-ag-grid-table/src/images/Table2.jpg',
|
||||
'plugins/plugin-chart-ag-grid-table/src/images/Table3.jpg',
|
||||
],
|
||||
big_number: [
|
||||
`${ECHARTS}/BigNumber/BigNumberWithTrendline/images/Big_Number_Trendline.jpg`,
|
||||
],
|
||||
big_number_total: [
|
||||
`${ECHARTS}/BigNumber/BigNumberTotal/images/BigNumber.jpg`,
|
||||
`${ECHARTS}/BigNumber/BigNumberTotal/images/BigNumber2.jpg`,
|
||||
],
|
||||
box_plot: [`${ECHARTS}/BoxPlot/images/BoxPlot.jpg`],
|
||||
bubble_v2: [
|
||||
`${ECHARTS}/Bubble/images/example1.png`,
|
||||
`${ECHARTS}/Bubble/images/example2.png`,
|
||||
],
|
||||
bullet: [`${ECHARTS}/Bullet/images/example.jpg`],
|
||||
cal_heatmap: ['plugins/plugin-chart-calendar/src/images/example.jpg'],
|
||||
cartodiagram: [
|
||||
'plugins/plugin-chart-cartodiagram/src/images/example1.png',
|
||||
'plugins/plugin-chart-cartodiagram/src/images/example2.png',
|
||||
],
|
||||
chord: ['plugins/plugin-chart-chord/src/images/chord.jpg'],
|
||||
country_map: [
|
||||
'plugins/plugin-chart-country-map/src/images/exampleUsa.jpg',
|
||||
'plugins/plugin-chart-country-map/src/images/exampleGermany.jpg',
|
||||
],
|
||||
echarts_area: [`${ECHARTS}/Timeseries/Area/images/Area1.png`],
|
||||
echarts_timeseries: [`${ECHARTS}/Timeseries/images/Time-series_Chart.jpg`],
|
||||
echarts_timeseries_bar: [
|
||||
`${ECHARTS}/Timeseries/Regular/Bar/images/Bar1.png`,
|
||||
`${ECHARTS}/Timeseries/Regular/Bar/images/Bar2.png`,
|
||||
`${ECHARTS}/Timeseries/Regular/Bar/images/Bar3.png`,
|
||||
],
|
||||
echarts_timeseries_line: [
|
||||
`${ECHARTS}/Timeseries/Regular/Line/images/Line1.png`,
|
||||
`${ECHARTS}/Timeseries/Regular/Line/images/Line2.png`,
|
||||
`${ECHARTS}/Timeseries/Regular/Line/images/Line3.png`,
|
||||
],
|
||||
echarts_timeseries_scatter: [
|
||||
`${ECHARTS}/Timeseries/Regular/Scatter/images/Scatter1.png`,
|
||||
],
|
||||
echarts_timeseries_smooth: [
|
||||
`${ECHARTS}/Timeseries/Regular/SmoothLine/images/SmoothLine1.png`,
|
||||
],
|
||||
echarts_timeseries_step: [
|
||||
`${ECHARTS}/Timeseries/Step/images/Step1.png`,
|
||||
`${ECHARTS}/Timeseries/Step/images/Step2.png`,
|
||||
],
|
||||
funnel: [`${ECHARTS}/Funnel/images/example.jpg`],
|
||||
gantt_chart: [
|
||||
`${ECHARTS}/Gantt/images/example1.png`,
|
||||
`${ECHARTS}/Gantt/images/example2.png`,
|
||||
],
|
||||
gauge_chart: [
|
||||
`${ECHARTS}/Gauge/images/example1.jpg`,
|
||||
`${ECHARTS}/Gauge/images/example2.jpg`,
|
||||
],
|
||||
graph_chart: [`${ECHARTS}/Graph/images/example.jpg`],
|
||||
handlebars: [
|
||||
'plugins/plugin-chart-handlebars/src/images/example1.jpg',
|
||||
'plugins/plugin-chart-handlebars/src/images/example2.jpg',
|
||||
],
|
||||
heatmap_v2: [
|
||||
`${ECHARTS}/Heatmap/images/example1.png`,
|
||||
`${ECHARTS}/Heatmap/images/example2.png`,
|
||||
`${ECHARTS}/Heatmap/images/example3.png`,
|
||||
],
|
||||
histogram_v2: [
|
||||
`${ECHARTS}/Histogram/images/example1.png`,
|
||||
`${ECHARTS}/Histogram/images/example2.png`,
|
||||
],
|
||||
horizon: ['plugins/plugin-chart-horizon/src/images/Horizon_Chart.jpg'],
|
||||
mixed_timeseries: [`${ECHARTS}/MixedTimeseries/images/example.jpg`],
|
||||
paired_ttest: ['plugins/plugin-chart-paired-t-test/src/images/example.jpg'],
|
||||
para: [
|
||||
'plugins/plugin-chart-parallel-coordinates/src/images/example1.jpg',
|
||||
'plugins/plugin-chart-parallel-coordinates/src/images/example2.jpg',
|
||||
],
|
||||
partition: ['plugins/plugin-chart-partition/src/images/example.jpg'],
|
||||
pie: [
|
||||
`${ECHARTS}/Pie/images/Pie1.jpg`,
|
||||
`${ECHARTS}/Pie/images/Pie2.jpg`,
|
||||
`${ECHARTS}/Pie/images/Pie3.jpg`,
|
||||
`${ECHARTS}/Pie/images/Pie4.jpg`,
|
||||
],
|
||||
pivot_table_v2: ['plugins/plugin-chart-pivot-table/src/images/example.jpg'],
|
||||
point_cluster_map: [
|
||||
'plugins/plugin-chart-point-cluster-map/src/images/MapBox.jpg',
|
||||
'plugins/plugin-chart-point-cluster-map/src/images/MapBox2.jpg',
|
||||
],
|
||||
radar: [
|
||||
`${ECHARTS}/Radar/images/example1.jpg`,
|
||||
`${ECHARTS}/Radar/images/example2.jpg`,
|
||||
],
|
||||
rose: [
|
||||
`${ECHARTS}/Rose/images/example1.jpg`,
|
||||
`${ECHARTS}/Rose/images/example2.jpg`,
|
||||
],
|
||||
sankey_v2: [
|
||||
`${ECHARTS}/Sankey/images/example1.png`,
|
||||
`${ECHARTS}/Sankey/images/example2.png`,
|
||||
],
|
||||
sunburst_v2: [
|
||||
`${ECHARTS}/Sunburst/images/Sunburst1.png`,
|
||||
`${ECHARTS}/Sunburst/images/Sunburst2.png`,
|
||||
],
|
||||
table: [
|
||||
'plugins/plugin-chart-table/src/images/Table.jpg',
|
||||
'plugins/plugin-chart-table/src/images/Table2.jpg',
|
||||
'plugins/plugin-chart-table/src/images/Table3.jpg',
|
||||
],
|
||||
time_pivot: [`${ECHARTS}/TimePivot/images/example.jpg`],
|
||||
time_table: ['src/visualizations/TimeTable/images/example.jpg'],
|
||||
tree_chart: [`${ECHARTS}/Tree/images/tree.png`],
|
||||
treemap_v2: [
|
||||
`${ECHARTS}/Treemap/images/treemap_v2_1.png`,
|
||||
`${ECHARTS}/Treemap/images/treemap_v2_2.jpg`,
|
||||
],
|
||||
waterfall: [
|
||||
`${ECHARTS}/Waterfall/images/example1.png`,
|
||||
`${ECHARTS}/Waterfall/images/example2.png`,
|
||||
`${ECHARTS}/Waterfall/images/example3.png`,
|
||||
],
|
||||
word_cloud: [
|
||||
'plugins/plugin-chart-word-cloud/src/images/Word_Cloud.jpg',
|
||||
'plugins/plugin-chart-word-cloud/src/images/Word_Cloud_2.jpg',
|
||||
],
|
||||
world_map: [
|
||||
'plugins/plugin-chart-world-map/src/images/WorldMap1.jpg',
|
||||
'plugins/plugin-chart-world-map/src/images/WorldMap2.jpg',
|
||||
],
|
||||
deck_arc: [`${DECKGL}/Arc/images/example.png`],
|
||||
deck_contour: [`${DECKGL}/Contour/images/example.png`],
|
||||
deck_geojson: [`${DECKGL}/Geojson/images/example.png`],
|
||||
deck_grid: [`${DECKGL}/Grid/images/example.png`],
|
||||
deck_heatmap: [`${DECKGL}/Heatmap/images/example.png`],
|
||||
deck_hex: [`${DECKGL}/Hex/images/example.png`],
|
||||
deck_multi: ['plugins/preset-chart-deckgl/src/Multi/images/example.png'],
|
||||
deck_path: [`${DECKGL}/Path/images/example.png`],
|
||||
deck_polygon: [`${DECKGL}/Polygon/images/example.png`],
|
||||
deck_scatter: [`${DECKGL}/Scatter/images/example.png`],
|
||||
deck_screengrid: [`${DECKGL}/Screengrid/images/example.png`],
|
||||
};
|
||||
|
||||
interface ExampleChart {
|
||||
id: number;
|
||||
sliceName: string;
|
||||
vizType: string;
|
||||
}
|
||||
|
||||
interface DashboardRow {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface DashboardChartRow {
|
||||
id?: number;
|
||||
slice_name?: string;
|
||||
form_data?: { viz_type?: string };
|
||||
}
|
||||
|
||||
/** Pages through a list endpoint, returning every result row. */
|
||||
async function fetchAllPages<T>(page: Page, endpoint: string): Promise<T[]> {
|
||||
const rows: T[] = [];
|
||||
const pageSize = 100;
|
||||
for (let pageNum = 0; ; pageNum += 1) {
|
||||
const q = encodeURIComponent(
|
||||
`(page_size:${pageSize},page:${pageNum},order_direction:asc)`,
|
||||
);
|
||||
const response = await page.request.get(`${endpoint}?q=${q}`);
|
||||
expect(response.ok(), `GET ${endpoint} page ${pageNum}`).toBeTruthy();
|
||||
const { result }: { result: T[] } = await response.json();
|
||||
rows.push(...result);
|
||||
if (result.length < pageSize) return rows;
|
||||
}
|
||||
}
|
||||
|
||||
/** Discovers every chart placed on any dashboard. */
|
||||
async function discoverDashboardCharts(page: Page): Promise<ExampleChart[]> {
|
||||
const dashboards = await fetchAllPages<DashboardRow>(
|
||||
page,
|
||||
'/api/v1/dashboard/',
|
||||
);
|
||||
const chartsById = new Map<number, ExampleChart>();
|
||||
for (const dashboard of dashboards) {
|
||||
const response = await page.request.get(
|
||||
`/api/v1/dashboard/${dashboard.id}/charts`,
|
||||
);
|
||||
if (!response.ok()) continue;
|
||||
const { result }: { result: DashboardChartRow[] } = await response.json();
|
||||
for (const chart of result) {
|
||||
const vizType = chart.form_data?.viz_type;
|
||||
if (chart.id && chart.slice_name && vizType) {
|
||||
chartsById.set(chart.id, {
|
||||
id: chart.id,
|
||||
sliceName: chart.slice_name,
|
||||
vizType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...chartsById.values()];
|
||||
}
|
||||
|
||||
/** thumbnail.png -> thumbnail-dark.png, example.jpg -> example-dark.jpg */
|
||||
function darkSibling(output: string): string {
|
||||
return output.replace(/\.(png|jpg)$/, '-dark.$1');
|
||||
}
|
||||
|
||||
async function renderAndShoot(
|
||||
page: Page,
|
||||
chart: ExampleChart,
|
||||
colorScheme: 'light' | 'dark',
|
||||
): Promise<Buffer> {
|
||||
// An explicit navigation timeout keeps one hung load from stalling the
|
||||
// whole crawl until the test timeout.
|
||||
await page.goto(`/explore/?slice_id=${chart.id}&standalone=1`, {
|
||||
timeout: 60_000,
|
||||
});
|
||||
// Dashboards render charts on colorBgContainer cards, but the standalone
|
||||
// explore page paints the gray colorBgLayout (via the antd Layout
|
||||
// wrapper); match the dashboard context so thumbnails look like charts
|
||||
// do where users see them.
|
||||
await page
|
||||
.addStyleTag({
|
||||
content: `body, .ant-layout { background: ${
|
||||
colorScheme === 'dark' ? '#141414' : '#ffffff'
|
||||
} !important; }`,
|
||||
})
|
||||
.catch(() => {});
|
||||
const textOnly = TEXT_ONLY_VIZ_TYPES.has(chart.vizType);
|
||||
await page
|
||||
.locator(textOnly ? TEXT_RENDERED_CHART_SELECTOR : RENDERED_CHART_SELECTOR)
|
||||
.first()
|
||||
.waitFor({ state: 'visible', timeout: 60_000 });
|
||||
// Give animations/map tiles (or text-only chart data) time to settle
|
||||
await page.waitForTimeout(textOnly ? 4_000 : 2_000);
|
||||
// Some thumbnails read better mid-interaction (e.g. the calendar heatmap
|
||||
// showing its tooltip); hover the configured element before the still.
|
||||
const hoverSelector = HOVER_BEFORE_CAPTURE[chart.vizType];
|
||||
if (hoverSelector) {
|
||||
// Hover a mid-chart element rather than the first (often an empty
|
||||
// corner cell), falling back to the first when there are few.
|
||||
const cells = page.locator(hoverSelector);
|
||||
const count = await cells.count().catch(() => 0);
|
||||
await cells
|
||||
.nth(Math.floor(count / 2))
|
||||
.hover({ timeout: 5_000 })
|
||||
.catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
return page.screenshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a chart light and (when a dark variant is wanted) dark. Dark
|
||||
* rendering relies on the app following prefers-color-scheme (theme mode
|
||||
* SYSTEM); if the dark render is byte-identical to the light one the app
|
||||
* ignored the emulation, and the dark file is left untouched rather than
|
||||
* overwritten with light-theme art.
|
||||
*/
|
||||
async function captureChart(
|
||||
page: Page,
|
||||
chart: ExampleChart,
|
||||
output: string,
|
||||
size: { width: number; height: number },
|
||||
): Promise<void> {
|
||||
const outputPath = path.join(FRONTEND_ROOT, output);
|
||||
const darkPath = path.join(FRONTEND_ROOT, darkSibling(output));
|
||||
const isNewImage = !fs.existsSync(outputPath);
|
||||
const wantDark = fs.existsSync(darkPath) || isNewImage;
|
||||
|
||||
await page.setViewportSize(size);
|
||||
await page.emulateMedia({ colorScheme: 'light' });
|
||||
const lightShot = await renderAndShoot(page, chart, 'light');
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, lightShot);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`captured ${chart.sliceName} (${chart.vizType}) -> ${output}`);
|
||||
|
||||
if (wantDark) {
|
||||
await page.emulateMedia({ colorScheme: 'dark' });
|
||||
const darkShot = await renderAndShoot(page, chart, 'dark');
|
||||
if (darkShot.equals(lightShot)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`SKIPPED dark variant for ${chart.sliceName}: the app ignored the dark color-scheme emulation (is dark theming enabled?)`,
|
||||
);
|
||||
} else {
|
||||
fs.writeFileSync(darkPath, darkShot);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`captured ${chart.sliceName} (dark) -> ${darkSibling(output)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewImage) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`NOTE: ${output} is a new gallery image — register it in the plugin metadata (exampleGallery) to surface it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('capture viz thumbnails', () => {
|
||||
test.skip(
|
||||
!process.env.CAPTURE_THUMBNAILS,
|
||||
'Thumbnail capture only runs with CAPTURE_THUMBNAILS=1',
|
||||
);
|
||||
|
||||
test('crawls example dashboards and refreshes gallery thumbnails', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(90 * 60_000);
|
||||
|
||||
const vizTypeFilter = process.env.VIZ_TYPES
|
||||
? new Set(process.env.VIZ_TYPES.split(',').map(v => v.trim()))
|
||||
: null;
|
||||
|
||||
const charts = await discoverDashboardCharts(page);
|
||||
expect(
|
||||
charts.length,
|
||||
'no dashboard charts found — are examples loaded?',
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// Every distinct chart per viz type gets captured: the preferred (or
|
||||
// alphabetically-first) chart becomes the picker thumbnail, and each
|
||||
// subsequent distinct chart fills the next declared gallery slot, so
|
||||
// gallery images show real variety instead of the same chart resized.
|
||||
const byVizType = new Map<string, ExampleChart[]>();
|
||||
for (const chart of charts) {
|
||||
const group = byVizType.get(chart.vizType) ?? [];
|
||||
group.push(chart);
|
||||
byVizType.set(chart.vizType, group);
|
||||
}
|
||||
|
||||
const thumbnailSize = { width: THUMBNAIL_SIZE, height: THUMBNAIL_SIZE };
|
||||
const exampleSize = { width: EXAMPLE_WIDTH, height: EXAMPLE_HEIGHT };
|
||||
const unmapped: string[] = [];
|
||||
const underfilledGalleries: string[] = [];
|
||||
const failures: string[] = [];
|
||||
for (const [vizType, group] of [...byVizType.entries()].sort()) {
|
||||
if (vizTypeFilter && !vizTypeFilter.has(vizType)) continue;
|
||||
|
||||
const thumbOutput = VIZ_TYPE_THUMBNAILS[vizType];
|
||||
const galleryOutputs = VIZ_TYPE_GALLERY[vizType] ?? [];
|
||||
if (!thumbOutput && !galleryOutputs.length) {
|
||||
unmapped.push(vizType);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preferred slice (if present) leads; the rest follow alphabetically.
|
||||
group.sort((a, b) => a.sliceName.localeCompare(b.sliceName));
|
||||
const preferredIndex = group.findIndex(
|
||||
c => c.sliceName === PREFERRED_SLICES[vizType],
|
||||
);
|
||||
const ordered =
|
||||
preferredIndex > 0
|
||||
? [
|
||||
group[preferredIndex],
|
||||
...group.slice(0, preferredIndex),
|
||||
...group.slice(preferredIndex + 1),
|
||||
]
|
||||
: group;
|
||||
|
||||
// Only advance past the thumbnail's chart when a thumbnail is
|
||||
// actually captured (e.g. handlebars has no VIZ_TYPE_THUMBNAILS
|
||||
// entry by design, so its gallery gets the full ordered list).
|
||||
let nextIndex = 0;
|
||||
if (thumbOutput) {
|
||||
const chart = ordered[0];
|
||||
nextIndex = 1;
|
||||
try {
|
||||
await captureChart(page, chart, thumbOutput, thumbnailSize);
|
||||
} catch (error) {
|
||||
failures.push(`${vizType} (${chart.sliceName}): ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const missingSlots: string[] = [];
|
||||
for (const output of galleryOutputs) {
|
||||
const chart = ordered[nextIndex];
|
||||
nextIndex += 1;
|
||||
if (!chart) {
|
||||
missingSlots.push(output);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await captureChart(page, chart, output, exampleSize);
|
||||
} catch (error) {
|
||||
failures.push(`${vizType} gallery (${chart.sliceName}): ${error}`);
|
||||
}
|
||||
}
|
||||
if (missingSlots.length) {
|
||||
underfilledGalleries.push(
|
||||
`${vizType}: not enough distinct dashboard charts for ${missingSlots.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (unmapped.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`viz types on dashboards with no thumbnail or gallery mapping (add to VIZ_TYPE_THUMBNAILS/VIZ_TYPE_GALLERY if wanted): ${unmapped.join(', ')}`,
|
||||
);
|
||||
}
|
||||
if (underfilledGalleries.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`gallery slots left untouched for lack of distinct example charts:\n${underfilledGalleries.join('\n')}`,
|
||||
);
|
||||
}
|
||||
expect(failures, failures.join('\n')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 32 KiB |
@@ -19,14 +19,14 @@
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import example1 from './images/Table.jpg';
|
||||
import example1Dark from './images/Table-dark.jpg';
|
||||
import example2 from './images/Table2.jpg';
|
||||
import example2Dark from './images/Table2-dark.jpg';
|
||||
import example3 from './images/Table3.jpg';
|
||||
import example3Dark from './images/Table3-dark.jpg';
|
||||
import thumbnail from './images/custom_thumb_thumbnail.png';
|
||||
import thumbnailDark from './images/custom_thumb_thumbnail-dark.png';
|
||||
import example1 from './images/custom_thumb_Table.jpg';
|
||||
import example1Dark from './images/custom_thumb_Table-dark.jpg';
|
||||
import example2 from './images/custom_thumb_Table2.jpg';
|
||||
import example2Dark from './images/custom_thumb_Table2-dark.jpg';
|
||||
import example3 from './images/custom_thumb_Table3.jpg';
|
||||
import example3Dark from './images/custom_thumb_Table3-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
import buildQuery from './buildQuery';
|
||||
import { TableChartFormData, TableChartProps } from './types';
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
@@ -19,8 +19,8 @@
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import transformProps from './transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import example from './images/custom_thumb_example.jpg';
|
||||
import exampleDark from './images/custom_thumb_example-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
|
||||
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 268 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
@@ -21,12 +21,12 @@ import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from '../images/thumbnail.png';
|
||||
import thumbnailDark from '../images/thumbnail-dark.png';
|
||||
import example1 from '../images/example1.png';
|
||||
import example1Dark from '../images/example1-dark.png';
|
||||
import example2 from '../images/example2.png';
|
||||
import example2Dark from '../images/example2-dark.png';
|
||||
import thumbnail from '../images/custom_thumb_thumbnail.png';
|
||||
import thumbnailDark from '../images/custom_thumb_thumbnail-dark.png';
|
||||
import example1 from '../images/custom_thumb_example1.png';
|
||||
import example1Dark from '../images/custom_thumb_example1-dark.png';
|
||||
import example2 from '../images/custom_thumb_example2.png';
|
||||
import example2Dark from '../images/custom_thumb_example2-dark.png';
|
||||
import { CartodiagramPluginConstructorOpts } from '../types';
|
||||
import { getLayerConfig } from '../util/controlPanelUtil';
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 59 KiB |
@@ -19,10 +19,10 @@
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core';
|
||||
import transformProps from './transformProps';
|
||||
import exampleUsa from './images/exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/exampleGermany-dark.jpg';
|
||||
import exampleUsa from './images/custom_thumb_exampleUsa.jpg';
|
||||
import exampleUsaDark from './images/custom_thumb_exampleUsa-dark.jpg';
|
||||
import exampleGermany from './images/custom_thumb_exampleGermany.jpg';
|
||||
import exampleGermanyDark from './images/custom_thumb_exampleGermany-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -21,8 +21,8 @@ import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
import controlPanel from './controlPanel';
|
||||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import thumbnail from './images/custom_thumb_thumbnail.png';
|
||||
import thumbnailDark from './images/custom_thumb_thumbnail-dark.png';
|
||||
|
||||
export default class PopKPIPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |