Compare commits

...

54 Commits

Author SHA1 Message Date
Evan
45d0cad2e4 chore(ci): pin setup-python to truthful version comment
The pinned commit a309ff8 for actions/setup-python resolves to release
tag v6.2.0, but the inline comment claimed `# v6` (the floating major
tag, which points at a different commit). zizmor's ref-version-mismatch
rule flags this mismatch because the comment misrepresents the exact
pinned version. Updated both occurrences to `# v6.2.0` so the comment
matches the pinned SHA.

Resolves code-scanning alert #2549

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:56:38 -07:00
Imad Helal
6bc77fecc2 feat(country-map): add cross-filters support (#35859)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-06-24 00:54:47 -07:00
dependabot[bot]
420a74b01e chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#41358)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 00:52:16 -07:00
dependabot[bot]
7ba59c2d79 chore(deps): bump @jsonforms/vanilla-renderers from 3.7.0 to 3.8.0 in /superset-frontend (#41367)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 00:51:53 -07:00
dependabot[bot]
b77c525d4b chore(deps-dev): bump storybook from 10.4.5 to 10.4.6 in /superset-frontend (#41368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 00:51:22 -07:00
dependabot[bot]
41ce9ca7d3 chore(deps-dev): bump @swc/plugin-emotion from 14.12.0 to 14.13.0 in /superset-frontend (#41377)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 00:51:06 -07:00
Abdul Rehman
c2fb94cedf perf(filters): cache column-values endpoint to skip DB on repeat requests (#40839) 2026-06-23 23:41:26 -07:00
yousoph
1d0866556f fix(sql_lab): serialize dict/list cell values as valid JSON strings (#41099)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 20:39:23 -07:00
Evan Rusackas
b4dfeef2fd fix(reports): add network timeouts so schedules can't hang forever (#41250)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-23 18:01:03 -07:00
Dinesh M
0ec6cae45d feat(Boxplot): Allow configuration of y-axis range (#24380)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: dinesh-zemoso <dinesh.mandava@zemosolabs.com>
2026-06-23 17:48:06 -07:00
Lukas Biermann
d6ede99861 fix(tags): tags api change tag_get_objects method to be aligned with api documentation (#29338)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 14:12:33 -07:00
Hans Yu
9b6d3ce775 fix(models): make naive datetime object timezone-aware before converting to unix timestamp (#39782)
Co-authored-by: Hans Yu <hans.yu@digits.schwarz>
2026-06-23 14:09:26 -07:00
yousoph
c1f4062af6 fix(sql-lab): normalize tabViewId in QUERY_EDITOR_SET_SQL reducer (#40983)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 13:28:20 -07:00
crabulous
3bc3f47d67 fix(dataset): import/export jinja template bug (#28790)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:25:49 -07:00
Durgaprasad M L
acb996a324 feat(mcp): support virtual dataset metrics and improve adhoc SQL metric discoverability (#40935)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:19:44 -07:00
innovark
c1d08bf27c fix(table): respect row limit with server pagination (#41024)
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-23 12:17:12 -07:00
Ayush Sharaf
d3d5297025 fix(reports): preserve dashboard state in tab permalinks (#39708)
Co-authored-by: Ayush Kumar Sharaf <sharaf@Ayushs-MacBook-Air.local>
Co-authored-by: Evan Rusackas <evan@preset.io>
Co-authored-by: Ayush Kumar Sharaf <ayush.sharaf@314ecorp.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:15:41 -07:00
sofiankhalfi-kosmos
b1470bd5a5 fix(i18n): correct french translations causing build errors (#34563)
Co-authored-by: sofiankhalfi-kosmos <sofiankhalfi-kosmos@users.noreply.github.com>
Co-authored-by: Sam Firke <sfirke@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:15:23 -07:00
peng weikang
18fea37e84 fix(SavedQueries): allow other admin users see "saved queries" (#20604) (#21769) 2026-06-23 12:14:48 -07:00
Evan Rusackas
1b71c105b7 docs(meta-db): warn that SUPERSET_META_DB_LIMIT truncates tables before joins (#41302)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 14:29:44 -04:00
Ville Brofeldt
b061b5d317 chore: fix lint on untouched files (#41333) 2026-06-23 11:29:19 -07:00
Evan Rusackas
386893f9f2 feat(security): record audit metadata on guest token issuance (#41305)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 11:25:44 -07:00
Evan Rusackas
c1787a67aa fix(extensions): log extension-init failures via the logger, not print() (#41304)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 11:25:33 -07:00
Evan Rusackas
dee5859599 fix(rls): reject empty or whitespace-only RLS clauses (#41297)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 11:24:38 -07:00
Evan Rusackas
1d3daf2ac8 fix(security): return generic error and log internally in RoleRestAPI.get_list (#41295)
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-23 11:24:26 -07:00
Elizabeth Thompson
9d56b1721d fix(models): use Series.iloc for positional access in post_process_df (#41344) 2026-06-23 11:22:22 -07:00
Ayush Anand
67182e255c fix(dashboard): prevent undo crash on new dashboard opened in edit mode (#41252) 2026-06-23 11:22:03 -07:00
Joe Li
e2c6dc3e1a fix(sqllab): shrink Template Parameters editor height and add outline (#41128)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:44:11 -07:00
Michael Shen
c539ae98ba fix(helm): enable graceful termination and overrides for celery worker (#41175)
Signed-off-by: Michael Shen <mishen@umich.edu>
2026-06-23 10:33:09 -07:00
Alexis
ca3c420412 fix(trino): ignore Iceberg $partitions metadata fields in partition detection (#41055)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-06-23 10:13:23 -07:00
Evan Rusackas
5e8a0c0244 fix(embedded): allow guest users to sort table columns in embedded dashboards (#41218)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-06-23 10:10:55 -07:00
dependabot[bot]
90fa31f305 chore(deps-dev): bump typescript-eslint from 8.61.0 to 8.61.1 in /superset-websocket (#41313)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 09:18:34 -07:00
Michael Shen
5731d0874a fix(docker): exec gunicorn in run-server.sh so it receives SIGTERM (#41173)
Signed-off-by: Michael Shen <mishen@umich.edu>
2026-06-23 09:17:59 -07:00
Evan Rusackas
66f5ab2d2f fix(ssh-tunnel): support ed25519 and ECDSA keys, not just RSA (#24180) (#40139)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-06-23 09:15:45 -07:00
Stepan
36b0ed023b fix(viz-date-control): Just use global DEFAULT_TIME_FORMAT instead of hardcoding 'smart_date' (#28708) 2026-06-23 08:53:39 -07:00
Enzo Martellucci
3ff90bd532 fix(big-number): respect extra_form_data.time_compare in Big Number with Time Comparison (#41342) 2026-06-23 17:05:42 +02:00
Beto Dealmeida
5d06438a07 fix(docker): restore working docker compose up for the dev stack (#41077) 2026-06-23 10:01:57 -04:00
dependabot[bot]
eb0d4dd601 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.61.0 to 8.61.1 in /superset-websocket (#41315)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 05:55:14 -07:00
dependabot[bot]
92109f0f99 chore(deps-dev): bump eslint-plugin-storybook from 10.4.4 to 10.4.5 in /superset-frontend (#41316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 05:55:11 -07:00
dependabot[bot]
9431381c3e chore(deps-dev): bump @storybook/addon-docs from 10.4.4 to 10.4.5 in /superset-frontend (#41326)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 05:55:07 -07:00
dependabot[bot]
b94f90e39e chore(deps-dev): bump @formatjs/intl-durationformat from 0.10.14 to 0.10.15 in /superset-frontend (#41332)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:51:20 -07:00
dependabot[bot]
714c5cd075 chore(deps-dev): bump oxlint from 1.69.0 to 1.70.0 in /superset-frontend (#41331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:46 -07:00
dependabot[bot]
c65c0951cf chore(deps-dev): bump storybook from 10.4.4 to 10.4.5 in /superset-frontend (#41330)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:42 -07:00
dependabot[bot]
ae5c08b993 chore(deps-dev): bump @playwright/test from 1.60.0 to 1.61.0 in /superset-frontend (#41327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:37 -07:00
dependabot[bot]
b9c61a079d chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 1.0.0 to 1.0.1 in /superset-frontend (#41322)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:27 -07:00
dependabot[bot]
2599bea0c2 chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#41321)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:07 -07:00
dependabot[bot]
6c70f3d275 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 8.61.0 to 8.61.1 in /superset-frontend (#41320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:50:02 -07:00
dependabot[bot]
da893462b8 chore(deps-dev): bump typescript-eslint from 8.61.0 to 8.61.1 in /docs (#41319)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:49:58 -07:00
dependabot[bot]
18853c6ecf chore(deps): bump actions/setup-java from 5.2.0 to 5.3.0 (#41317)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:49:53 -07:00
dependabot[bot]
8768e5be0f chore(deps-dev): bump @typescript-eslint/parser from 8.61.0 to 8.61.1 in /superset-websocket (#41312)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 02:49:46 -07:00
yousoph
133473d0f4 fix(explore): pre-populate SaveModal dashboard from chart metadata (#41181)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 00:54:49 -07:00
alex
5916ec4876 fix(plugin-chart-echarts): cross-filter horizontal bars on category not metric (#41104)
Signed-off-by: alex-poor <alex@karo.co.nz>
2026-06-22 20:29:29 -07:00
Harshit-Tiwary
36781fbf47 fix(i18n): wrap table access error message with gettext for translation (#38489)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:28:05 -07:00
Shaitan
215b207ae4 fix(sql): detect set operations and nested selects in subquery check (#38452)
Co-authored-by: sha174n <pedro.sousa@preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:27:32 -07:00
160 changed files with 4871 additions and 903 deletions

View File

@@ -42,7 +42,7 @@ runs:
fi
echo "python-version=$RESOLVED_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Python ${{ steps.set-python-version.outputs.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ steps.set-python-version.outputs.python-version }}
cache: ${{ inputs.cache }}

View File

@@ -31,7 +31,7 @@ jobs:
checks: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: true
ref: master
@@ -40,7 +40,7 @@ jobs:
uses: ./.github/actions/setup-supersetbot/
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.10"

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check and notify

View File

@@ -26,7 +26,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for file changes
@@ -58,7 +58,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

View File

@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: "Dependency Review"
@@ -51,7 +51,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: "Checkout Repository"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

View File

@@ -30,7 +30,7 @@ jobs:
docker: ${{ steps.check.outputs.docker }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for file changes
@@ -71,7 +71,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -177,7 +177,7 @@ jobs:
timeout-minutes: 30
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Free up disk space

View File

@@ -23,7 +23,7 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Note: registry-url is intentionally omitted. When set, actions/setup-node

View File

@@ -21,7 +21,7 @@ jobs:
run:
working-directory: superset-embedded-sdk
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6

View File

@@ -32,12 +32,12 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -27,7 +27,7 @@ jobs:
security-events: write
steps:
- name: Checkout Repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

View File

@@ -16,7 +16,7 @@ jobs:
issues: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -18,12 +18,12 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "temurin"
java-version: "11"

View File

@@ -21,7 +21,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "previous", "next"]') }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -33,7 +33,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# pulls all commits (needed for lerna / semantic release to correctly version)

View File

@@ -152,7 +152,7 @@ jobs:
- name: Checkout PR code (only if build needed)
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ steps.check.outputs.target_sha }}
persist-credentials: false

View File

@@ -41,7 +41,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
persist-credentials: false
@@ -71,7 +71,7 @@ jobs:
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: "zulu"
java-version: "21"

View File

@@ -28,7 +28,7 @@ jobs:
name: Link Checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Do not bump this linkinator-action version without opening
@@ -73,7 +73,7 @@ jobs:
working-directory: docs
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
@@ -112,7 +112,7 @@ jobs:
working-directory: docs
steps:
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

View File

@@ -38,7 +38,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for file changes
@@ -97,21 +97,21 @@ jobs:
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
@@ -207,21 +207,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge

View File

@@ -31,7 +31,7 @@ jobs:
working-directory: superset-extensions-cli
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -27,7 +27,7 @@ jobs:
should-run: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout Code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0
@@ -110,7 +110,7 @@ jobs:
id-token: write
steps:
- name: Checkout Code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.ref || github.ref_name }}
persist-credentials: true

View File

@@ -34,7 +34,7 @@ jobs:
frontend: ${{ steps.check.outputs.frontend }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for file changes
@@ -83,21 +83,21 @@ jobs:
# Conditional checkout based on context (same as Cypress workflow)
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge

View File

@@ -29,7 +29,7 @@ jobs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for file changes
@@ -72,7 +72,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
@@ -157,7 +157,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
@@ -207,7 +207,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for file changes
@@ -72,7 +72,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
@@ -127,7 +127,7 @@ jobs:
- 16379:6379
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -30,7 +30,7 @@ jobs:
python: ${{ steps.check.outputs.python }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check for file changes
@@ -55,7 +55,7 @@ jobs:
PYTHONPATH: ${{ github.workspace }}
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive
@@ -61,7 +61,7 @@ jobs:
pull-requests: read
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
submodules: recursive

View File

@@ -25,7 +25,7 @@ jobs:
timeout-minutes: 20
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Install dependencies

View File

@@ -38,7 +38,7 @@ jobs:
});
- name: "Checkout ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

View File

@@ -27,7 +27,7 @@ jobs:
# zizmor: ignore[artipacked] - required persisted credentials to push synced requirement changes back to remote
- name: Checkout source code
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: true

View File

@@ -54,7 +54,7 @@ jobs:
fail-fast: false
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0
@@ -120,7 +120,7 @@ jobs:
pull-requests: write
steps:
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
fetch-depth: 0

View File

@@ -32,7 +32,7 @@ jobs:
name: Generate Reports
steps:
- name: Checkout Repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

View File

@@ -72,20 +72,23 @@ services:
- -c
- |
url="http://host.docker.internal:9000/static/assets/manifest.json"
max_attempts=150 # ~5 minutes at 2s intervals
echo "Waiting for webpack dev server at $url..."
max_attempts=300 # ~10 minutes at 2s intervals; first build can be slow
echo "Waiting for webpack dev server at $$url..."
attempt=0
until curl -sf --max-time 5 -o /dev/null "$url"; do
attempt=$((attempt + 1))
if [ "$attempt" -ge "$max_attempts" ]; then
echo "ERROR: webpack dev server did not serve $url after $max_attempts attempts (~5 minutes)." >&2
until curl -sf --max-time 5 -H "Host: localhost" -o /dev/null "$$url"; do
attempt=$$((attempt + 1))
if [ "$$attempt" -ge "$$max_attempts" ]; then
echo "ERROR: webpack dev server did not serve $$url after $$max_attempts attempts." >&2
echo "Is the dev server running? With BUILD_SUPERSET_FRONTEND_IN_DOCKER=false you must start it on the host (e.g. 'npm run dev' in superset-frontend)." >&2
exit 1
fi
if [ $$((attempt % 15)) -eq 0 ]; then
echo "Still waiting for webpack dev server... ($$attempt/$$max_attempts)"
fi
sleep 2
done
echo "Webpack dev server is ready; starting nginx."
exec nginx -g 'daemon off;'
exec /docker-entrypoint.sh nginx -g 'daemon off;'
redis:
image: redis:7

View File

@@ -81,17 +81,19 @@ case "${1}" in
app)
echo "Starting web app (using development server)..."
# Environment-based debugger control for security
# Only enable Werkzeug interactive debugger when explicitly requested
# Modern Werkzeug (3.0+) includes PIN protection, but defense-in-depth approach
# Override FLASK_DEBUG so the effective state matches SUPERSET_DEBUG_ENABLED even
# when FLASK_DEBUG=true is inherited from docker/.env or .flaskenv
# Default to Flask debug mode in this dev compose entrypoint so the Talisman
# dev CSP (which permits 'unsafe-eval' required by React Refresh / HMR) is
# served. Operators can still set FLASK_DEBUG=false in docker/.env-local
# to exercise the production-like CSP and error handling.
: "${FLASK_DEBUG:=1}"
export FLASK_DEBUG
# Werkzeug's interactive debugger (/console) is a separate, security-sensitive
# feature and must be opted into explicitly via SUPERSET_DEBUG_ENABLED=true.
if [[ "${SUPERSET_DEBUG_ENABLED:-}" == "true" ]]; then
export FLASK_DEBUG=1
DEBUGGER_FLAG="--debugger"
echo " ⚠️ Werkzeug debugger enabled (requires PIN for /console access)"
else
export FLASK_DEBUG=0
DEBUGGER_FLAG="--no-debugger"
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
fi

View File

@@ -19,7 +19,7 @@
#
HYPHEN_SYMBOL='-'
gunicorn \
exec gunicorn \
--bind "${SUPERSET_BIND_ADDRESS:-0.0.0.0}:${SUPERSET_PORT:-8088}" \
--access-logfile "${ACCESS_LOG_FILE:-$HYPHEN_SYMBOL}" \
--error-logfile "${ERROR_LOG_FILE:-$HYPHEN_SYMBOL}" \

View File

@@ -109,7 +109,7 @@
"globals": "^17.6.0",
"prettier": "^3.8.4",
"typescript": "~6.0.3",
"typescript-eslint": "^8.61.0",
"typescript-eslint": "^8.61.1",
"webpack": "^5.107.2"
},
"browserslist": {

View File

@@ -1808,6 +1808,10 @@ If you enable DML in the meta database users will be able to run DML queries on
Second, you might want to change the value of `SUPERSET_META_DB_LIMIT`. The default value is 1000, and defines how many are read from each database before any aggregations and joins are executed. You can also set this value `None` if you only have small tables.
:::warning
`SUPERSET_META_DB_LIMIT` is applied to **each** underlying table *before* the in-memory join runs, not to the final result. If any table involved in a join has more rows than the limit, the meta database will read only the first `SUPERSET_META_DB_LIMIT` rows of that table, which means matching rows can be silently dropped and the join can return **incomplete or even empty** results with no error. If you join tables larger than the limit, raise `SUPERSET_META_DB_LIMIT` to comfortably exceed your largest joined table, or set it to `None` when working only with small tables, to get correct results.
:::
Additionally, you might want to restrict the databases to with the meta database has access to. This can be done in the database configuration, under "Advanced" -> "Other" -> "ENGINE PARAMETERS" and adding:
```json

View File

@@ -4932,110 +4932,110 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.61.0", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz#db20271974b94a3a54d3b9544e5f5b3481448400"
integrity sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==
"@typescript-eslint/eslint-plugin@8.61.1", "@typescript-eslint/eslint-plugin@^8.59.3":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz#6e4b7fee21f1983308e9e9b634ecbaf702c86006"
integrity sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/type-utils" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
"@typescript-eslint/scope-manager" "8.61.1"
"@typescript-eslint/type-utils" "8.61.1"
"@typescript-eslint/utils" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.61.0", "@typescript-eslint/parser@^8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.0.tgz#1afe73c9ccce16b7a26d6b95f9400b0ccc34af87"
integrity sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==
"@typescript-eslint/parser@8.61.1", "@typescript-eslint/parser@^8.61.0":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.61.1.tgz#881fba60b50636249cdeea2e547bf75715254c72"
integrity sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==
dependencies:
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
"@typescript-eslint/scope-manager" "8.61.1"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
debug "^4.4.3"
"@typescript-eslint/project-service@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.0.tgz#417a2feac32e8ebd336d63f068c3b42b736ea1ac"
integrity sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==
"@typescript-eslint/project-service@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.61.1.tgz#fcd9739964a40867eed55f1ac318d3909f24b4af"
integrity sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.61.0"
"@typescript-eslint/types" "^8.61.0"
"@typescript-eslint/tsconfig-utils" "^8.61.1"
"@typescript-eslint/types" "^8.61.1"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz#93c2520d05653fe65eb9ee98efc74fd0134a7852"
integrity sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==
"@typescript-eslint/scope-manager@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz#2479921a40fdb0afa18f5838fae6167264b417b2"
integrity sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==
dependencies:
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
"@typescript-eslint/tsconfig-utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz#05d6e3ff20001674ebcd22d03dac29ee448043ba"
integrity sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==
"@typescript-eslint/tsconfig-utils@^8.61.0":
"@typescript-eslint/tsconfig-utils@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz#ca88080e0cf191d49516d7f300b67aa090d2254f"
integrity sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==
"@typescript-eslint/type-utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz#50219b57e6b89cecfb1a15f093b15ec9ee019974"
integrity sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==
"@typescript-eslint/tsconfig-utils@^8.61.1":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.0.tgz#9440a673581c6d9de308c4d5803dd52ed5d71729"
integrity sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g==
"@typescript-eslint/type-utils@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz#8fa18f453ee140893b47d339d1a6b64cac9b08a1"
integrity sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==
dependencies:
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/utils" "8.61.1"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.0.tgz#0ddb46e012a4288292950bdd253db42f278ce64d"
integrity sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==
"@typescript-eslint/types@^8.61.0":
"@typescript-eslint/types@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.61.1.tgz#0c51f518e4e6848371a1c988e859d59eb7522d5a"
integrity sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==
"@typescript-eslint/typescript-estree@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz#98ca47260bbf627fc28f018b3a0abf00e3090690"
integrity sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==
"@typescript-eslint/types@^8.61.1":
version "8.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.0.tgz#601427c10203d9f0f34f0b3e474df735eb12b593"
integrity sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg==
"@typescript-eslint/typescript-estree@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz#febbe70365ac0bf7611262b61b338fc8797965c7"
integrity sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==
dependencies:
"@typescript-eslint/project-service" "8.61.0"
"@typescript-eslint/tsconfig-utils" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/visitor-keys" "8.61.0"
"@typescript-eslint/project-service" "8.61.1"
"@typescript-eslint/tsconfig-utils" "8.61.1"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/visitor-keys" "8.61.1"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.0.tgz#ed3546a052787e84ea6c5064d0919fc5eea8522f"
integrity sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==
"@typescript-eslint/utils@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.61.1.tgz#ffd1054de7dd33b7873cd6c6713ec6b0366316d3"
integrity sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.61.0"
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/scope-manager" "8.61.1"
"@typescript-eslint/types" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/visitor-keys@8.61.0":
version "8.61.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz#39b4e1ab8936d23bea973d39fd092f9aa21f275e"
integrity sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==
"@typescript-eslint/visitor-keys@8.61.1":
version "8.61.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz#546cf102b4efdb72a9a08e63a1b0d7d745eb66eb"
integrity sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==
dependencies:
"@typescript-eslint/types" "8.61.0"
"@typescript-eslint/types" "8.61.1"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -14502,15 +14502,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.61.0:
version "8.61.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.0.tgz#6927fb94f5f29623e370d33fd9fa61f15d6d996b"
integrity sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==
typescript-eslint@^8.61.1:
version "8.61.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.61.1.tgz#7c224a9a643b7f42d295c67a75c1e30fee8c3eaa"
integrity sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==
dependencies:
"@typescript-eslint/eslint-plugin" "8.61.0"
"@typescript-eslint/parser" "8.61.0"
"@typescript-eslint/typescript-estree" "8.61.0"
"@typescript-eslint/utils" "8.61.0"
"@typescript-eslint/eslint-plugin" "8.61.1"
"@typescript-eslint/parser" "8.61.1"
"@typescript-eslint/typescript-estree" "8.61.1"
"@typescript-eslint/utils" "8.61.1"
typescript@~6.0.3:
version "6.0.3"

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.17.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.17.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.17.0](https://img.shields.io/badge/Version-0.17.0-informational?style=flat-square)
![Version: 0.17.2](https://img.shields.io/badge/Version-0.17.2-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -216,6 +216,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
| supersetNode.extraContainers | list | `[]` | Launch additional containers into supersetNode pod |
| supersetNode.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
| supersetNode.initContainers | list | a container waiting for postgres | Init containers |
| supersetNode.lifecycle | object | `{}` | Container lifecycle hooks, e.g. a preStop sleep so the Service/Ingress stops routing to the pod before gunicorn receives SIGTERM |
| supersetNode.livenessProbe.failureThreshold | int | `3` | |
| supersetNode.livenessProbe.httpGet.path | string | `"/health"` | |
| supersetNode.livenessProbe.httpGet.port | string | `"http"` | |
@@ -248,6 +249,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
| supersetNode.startupProbe.successThreshold | int | `1` | |
| supersetNode.startupProbe.timeoutSeconds | int | `1` | |
| supersetNode.strategy | object | `{}` | |
| supersetNode.terminationGracePeriodSeconds | string | `nil` | Pod termination grace period (seconds). Set greater than GUNICORN_TIMEOUT so in-flight requests can drain before SIGKILL |
| supersetNode.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetNode deployments |
| supersetWebsockets.affinity | object | `{}` | Affinity to be added to supersetWebsockets deployment |
| supersetWebsockets.command | list | `[]` | |
@@ -311,6 +313,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
| supersetWorker.extraContainers | list | `[]` | Launch additional containers into supersetWorker pod |
| supersetWorker.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
| supersetWorker.initContainers | list | a container waiting for postgres and redis | Init container |
| supersetWorker.lifecycle | object | `{}` | Container lifecycle hooks for the worker pod |
| supersetWorker.livenessProbe.exec.command | list | a `celery inspect ping` command | Liveness probe command |
| supersetWorker.livenessProbe.failureThreshold | int | `3` | |
| supersetWorker.livenessProbe.initialDelaySeconds | int | `120` | |
@@ -331,6 +334,7 @@ On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverri
| supersetWorker.resources | object | `{}` | Resource settings for the supersetWorker pods - these settings overwrite might existing values from the global resources object defined above. |
| supersetWorker.startupProbe | object | `{}` | No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic) |
| supersetWorker.strategy | object | `{}` | |
| supersetWorker.terminationGracePeriodSeconds | string | `nil` | Pod termination grace period (seconds) for the worker pod so in-flight tasks can drain before SIGKILL |
| supersetWorker.topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to supersetWorker deployments |
| tolerations | list | `[]` | |
| topologySpreadConstraints | list | `[]` | TopologySpreadConstrains to be added to all deployments |

View File

@@ -134,6 +134,9 @@ spec:
{{- if .Values.supersetWorker.livenessProbe }}
livenessProbe: {{- .Values.supersetWorker.livenessProbe | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.supersetWorker.lifecycle }}
lifecycle: {{- .Values.supersetWorker.lifecycle | toYaml | nindent 12 }}
{{- end }}
resources:
{{- if .Values.supersetWorker.resources }}
{{- toYaml .Values.supersetWorker.resources | nindent 12 }}
@@ -170,6 +173,9 @@ spec:
{{- with .Values.tolerations }}
tolerations: {{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.supersetWorker.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.supersetWorker.terminationGracePeriodSeconds }}
{{- end }}
{{- if .Values.imagePullSecrets }}
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
{{- end }}

View File

@@ -144,6 +144,9 @@ spec:
{{- if .Values.supersetNode.livenessProbe }}
livenessProbe: {{- .Values.supersetNode.livenessProbe | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.supersetNode.lifecycle }}
lifecycle: {{- .Values.supersetNode.lifecycle | toYaml | nindent 12 }}
{{- end }}
resources:
{{- if .Values.supersetNode.resources }}
{{- toYaml .Values.supersetNode.resources | nindent 12 }}
@@ -180,6 +183,9 @@ spec:
{{- with .Values.tolerations }}
tolerations: {{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.supersetNode.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.supersetNode.terminationGracePeriodSeconds }}
{{- end }}
{{- if .Values.imagePullSecrets }}
imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }}
{{- end }}

View File

@@ -269,7 +269,7 @@ supersetNode:
command:
- "/bin/sh"
- "-c"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; /usr/bin/run-server.sh"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec /usr/bin/run-server.sh"
connections:
# -- Change in case of bringing your own redis and then also set redis.enabled:false
redis_host: "{{ .Release.Name }}-redis-headless"
@@ -369,6 +369,12 @@ supersetNode:
failureThreshold: 3
periodSeconds: 15
successThreshold: 1
# -- Container lifecycle hooks, e.g. a preStop sleep so the Service/Ingress
# stops routing to the pod before gunicorn receives SIGTERM
lifecycle: {}
# -- Pod termination grace period (seconds). Set greater than GUNICORN_TIMEOUT so
# in-flight requests can drain before SIGKILL
terminationGracePeriodSeconds: ~
# -- Resource settings for the supersetNode pods - these settings overwrite might existing values from the global resources object defined above.
resources: {}
# limits:
@@ -409,7 +415,7 @@ supersetWorker:
command:
- "/bin/sh"
- "-c"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app worker"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec celery --app=superset.tasks.celery_app:app worker"
# -- If true, forces deployment to reload on each upgrade
forceReload: false
# -- Init container
@@ -489,6 +495,10 @@ supersetWorker:
failureThreshold: 3
periodSeconds: 60
successThreshold: 1
# -- Container lifecycle hooks for the worker pod
lifecycle: {}
# -- Pod termination grace period (seconds) for the worker pod so in-flight tasks can drain before SIGKILL
terminationGracePeriodSeconds: ~
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
startupProbe: {}
# -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic)
@@ -513,7 +523,7 @@ supersetCeleryBeat:
command:
- "/bin/sh"
- "-c"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid --schedule /tmp/celerybeat-schedule"
- ". {{ .Values.configMountPath }}/superset_bootstrap.sh; exec celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid --schedule /tmp/celerybeat-schedule"
# -- If true, forces deployment to reload on each upgrade
forceReload: false
# -- List of init containers

View File

@@ -375,7 +375,6 @@ select = [
ignore = [
"S101",
"PT004", # Fixtures that don't return values - underscore prefix conflicts with pytest usage
"PT006",
"T201",
"N999",

File diff suppressed because it is too large Load Diff

View File

@@ -119,7 +119,7 @@
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.8.0",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
"@luma.gl/engine": "~9.2.5",
@@ -260,17 +260,17 @@
"@babel/types": "^7.29.7",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/jest": "^11.14.2",
"@formatjs/intl-durationformat": "^0.10.14",
"@formatjs/intl-durationformat": "^0.10.15",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@playwright/test": "^1.60.0",
"@playwright/test": "^1.61.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
"@storybook/addon-docs": "10.4.4",
"@storybook/addon-docs": "10.4.5",
"@storybook/addon-links": "10.4.4",
"@storybook/react-webpack5": "10.4.4",
"@storybook/test-runner": "0.24.4",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.15.41",
"@swc/plugin-emotion": "^14.12.0",
"@swc/plugin-emotion": "^14.13.0",
"@swc/plugin-transform-imports": "^12.5.0",
"@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^6.9.1",
@@ -296,7 +296,7 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/parser": "^8.61.0",
"babel-jest": "^30.4.1",
"babel-loader": "^10.1.1",
@@ -323,8 +323,8 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.0",
"eslint-plugin-storybook": "10.4.4",
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
"eslint-plugin-storybook": "10.4.5",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
"fetch-mock": "^12.6.0",
@@ -343,7 +343,7 @@
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
"open-cli": "^9.0.0",
"oxlint": "^1.69.0",
"oxlint": "^1.70.0",
"po2json": "^0.4.5",
"prettier": "3.8.4",
"prettier-plugin-packagejson": "^3.0.2",
@@ -355,7 +355,7 @@
"source-map": "^0.7.6",
"source-map-support": "^0.5.21",
"speed-measure-webpack-plugin": "^1.6.0",
"storybook": "10.4.4",
"storybook": "10.4.6",
"style-loader": "^4.0.0",
"swc-loader": "^0.2.7",
"terser-webpack-plugin": "^5.6.1",

View File

@@ -74,7 +74,10 @@ export function transformLinkUri(uri: string): string {
// "java\tscript:" or "java\x01script:") are ignored by browsers, so strip
// them before comparing against the blocklist.
// eslint-disable-next-line no-control-regex
const scheme = url.slice(0, colon).replace(/[\u0000-\u0020]/g, '').toLowerCase();
const scheme = url
.slice(0, colon)
.replace(/[\u0000-\u0020]/g, '')
.toLowerCase();
return DANGEROUS_LINK_PROTOCOLS.includes(scheme) ? '' : url;
}

View File

@@ -519,7 +519,8 @@ const Select = forwardRef(
handleSelectAll();
}}
>
{t('Select all')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
{t('Select all')}{' '}
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.selectable)})`}
</Button>
<Button
type="link"
@@ -536,7 +537,8 @@ const Select = forwardRef(
handleDeselectAll();
}}
>
{t('Clear')} {`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
{t('Clear')}{' '}
{`(${formatNumber('SMART_NUMBER', bulkSelectCounts.deselectable)})`}
</Button>
</StyledBulkActionsContainer>
),

View File

@@ -97,8 +97,11 @@ testWithAssets(
});
// At least one list item should contain a DD.MM.YYYY formatted date.
await expect(panel.locator('li').first()).toHaveText(/\d{2}\.\d{2}\.\d{4}/, {
timeout: TIMEOUT.API_RESPONSE,
});
await expect(panel.locator('li').first()).toHaveText(
/\d{2}\.\d{2}\.\d{4}/,
{
timeout: TIMEOUT.API_RESPONSE,
},
);
},
);

View File

@@ -182,10 +182,7 @@ testWithAssets(
// Now track POST /api/v1/chart/data requests around Clear All
const postsAfterClearAll: string[] = [];
const handler = (req: any) => {
if (
req.url().includes('/api/v1/chart/data') &&
req.method() === 'POST'
) {
if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') {
postsAfterClearAll.push(req.url());
}
};

View File

@@ -109,7 +109,12 @@ testWithAssets(
id: chartLayoutKey,
children: [],
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
meta: { chartId, width: 8, height: 60, sliceName: 'mixed_filter_repro' },
meta: {
chartId,
width: 8,
height: 60,
sliceName: 'mixed_filter_repro',
},
},
};
const jsonMetadata = {
@@ -130,9 +135,7 @@ testWithAssets(
defaultDataMask: {
filterState: { value: [FILTER_VALUE] },
extraFormData: {
filters: [
{ col: FILTER_COLUMN, op: 'IN', val: [FILTER_VALUE] },
],
filters: [{ col: FILTER_COLUMN, op: 'IN', val: [FILTER_VALUE] }],
},
},
cascadeParentIds: [],
@@ -158,15 +161,14 @@ testWithAssets(
const dashboardId: number = dashBody.result?.id ?? dashBody.id;
testAssets.trackDashboard(dashboardId);
await apiPut(page, `api/v1/chart/${chartId}`, { dashboards: [dashboardId] });
await apiPut(page, `api/v1/chart/${chartId}`, {
dashboards: [dashboardId],
});
// Capture the Mixed chart's data request (the one with two queries).
const twoQueryPayloads: any[] = [];
page.on('request', req => {
if (
req.url().includes('/api/v1/chart/data') &&
req.method() === 'POST'
) {
if (req.url().includes('/api/v1/chart/data') && req.method() === 'POST') {
try {
const body = req.postDataJSON();
if (body?.queries?.length === 2) {

View File

@@ -50,7 +50,10 @@ import {
getGuestToken,
} from '../../helpers/api/embedded';
import { apiPost, apiPut } from '../../helpers/api/requests';
import { apiPostDashboard, apiDeleteDashboard } from '../../helpers/api/dashboard';
import {
apiPostDashboard,
apiDeleteDashboard,
} from '../../helpers/api/dashboard';
import { apiDeleteChart } from '../../helpers/api/chart';
import { EmbeddedPage } from '../../pages/EmbeddedPage';
import { EMBEDDED } from '../../utils/constants';

View File

@@ -22,6 +22,7 @@ import {
ControlPanelConfig,
D3_FORMAT_DOCS,
D3_TIME_FORMAT_OPTIONS,
DEFAULT_TIME_FORMAT,
getStandardizedControls,
} from '@superset-ui/chart-controls';
@@ -145,7 +146,7 @@ const config: ControlPanelConfig = {
freeForm: true,
label: t('Time Format'),
renderTrigger: true,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},

View File

@@ -21,10 +21,12 @@
import d3 from 'd3';
import { extent as d3Extent } from 'd3-array';
import {
ValueFormatter,
getNumberFormatter,
getSequentialSchemeRegistry,
BinaryQueryObjectFilterClause,
CategoricalColorNamespace,
ContextMenuFilters,
DataMask,
ValueFormatter,
getSequentialSchemeRegistry,
} from '@superset-ui/core';
import countries, { countryOptions } from './countries';
@@ -65,9 +67,28 @@ interface CountryMapProps {
formatter: ValueFormatter;
colorScheme: string;
sliceId: number;
onContextMenu?: (
clientX: number,
clientY: number,
data: ContextMenuFilters,
) => void;
emitCrossFilters?: boolean;
setDataMask?: (dataMask: DataMask) => void;
filterState?: {
selectedValues?: string[];
extraFormData?: {
filters?: BinaryQueryObjectFilterClause[];
};
};
entity?: string;
}
const maps: Record<string, GeoData> = {};
// Store zoom state per chart instance using element as key to enable garbage collection
const zoomStates = new WeakMap<
HTMLElement,
{ scale: number; translate: [number, number] }
>();
function CountryMap(element: HTMLElement, props: CountryMapProps) {
const {
@@ -75,10 +96,15 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
width,
height,
country,
entity,
linearColorScheme,
formatter,
colorScheme,
sliceId,
filterState,
emitCrossFilters,
onContextMenu,
setDataMask,
} = props;
const container = element;
@@ -99,7 +125,15 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
? colorScale(d.country_id, sliceId)
: (linearColorScale(d.metric) ?? '');
});
const colorFn = (d: GeoFeature) => colorMap[d.properties.ISO] || 'none';
const colorFn = (feature: GeoFeature): string => {
if (!feature?.properties) return '#d9d9d9';
const iso = feature.properties.ISO;
return colorMap[iso] || '#d9d9d9';
};
// Check if dashboard is in edit mode
const isEditMode = container.closest('.dashboard--editing') !== null;
const path = d3.geo.path();
const div = d3.select(container);
@@ -112,6 +146,11 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
.attr('width', width)
.attr('height', height)
.attr('preserveAspectRatio', 'xMidYMid meet');
// Only set grab cursor if not in edit mode
if (!isEditMode) {
svg.style('cursor', 'grab');
}
const backgroundRect = svg
.append('rect')
.attr('class', 'background')
@@ -119,39 +158,64 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
.attr('height', height);
const g = svg.append('g');
const mapLayer = g.append('g').classed('map-layer', true);
// Add hover popup for tooltip
const hoverPopup = div.append('div').attr('class', 'hover-popup');
let centered: GeoFeature | null;
// Track mouse position to distinguish clicks from drags
let mousedownPos: { x: number; y: number } | null = null;
const clicked = function clicked(d: GeoFeature) {
const hasCenter = d && centered !== d;
let x: number;
let y: number;
let k: number;
const halfWidth = width / 2;
const halfHeight = height / 2;
// Cross-filter support
const getCrossFilterDataMask = (
source: GeoFeature,
): { dataMask: DataMask; isCurrentValueSelected: boolean } | undefined => {
if (!entity) return undefined;
if (hasCenter) {
const centroid = path.centroid(d);
[x, y] = centroid;
k = 4;
centered = d;
} else {
x = halfWidth;
y = halfHeight;
k = 1;
centered = null;
}
const selected = filterState?.selectedValues || [];
const iso = source?.properties?.ISO;
if (!iso) return undefined;
g.transition()
.duration(750)
.attr(
'transform',
`translate(${halfWidth},${halfHeight})scale(${k})translate(${-x},${-y})`,
);
const isSelected = selected.includes(iso);
const values = isSelected ? [] : [iso];
return {
dataMask: {
extraFormData: {
filters: values.length
? [{ col: entity, op: 'IN', val: values }]
: [],
},
filterState: {
value: values.length ? values : null,
selectedValues: values.length ? values : null,
},
},
isCurrentValueSelected: isSelected,
};
};
backgroundRect.on('click', clicked);
// Handle right-click context menu
const handleContextMenu = (feature: GeoFeature): void => {
const pointerEvent = d3.event;
if (typeof onContextMenu === 'function') {
pointerEvent?.preventDefault();
}
const iso = feature?.properties?.ISO;
if (!iso || typeof onContextMenu !== 'function' || !entity) return;
const drillVal = iso;
const drillToDetailFilters = [
{ col: entity, op: '==', val: drillVal, formattedVal: drillVal },
];
const drillByFilters = [{ col: entity, op: '==', val: drillVal }];
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: getCrossFilterDataMask(feature),
drillBy: { filters: drillByFilters, groupbyFieldName: 'entity' },
});
};
const getNameOfRegion = function getNameOfRegion(
feature: GeoFeature,
@@ -165,7 +229,7 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
return '';
};
const updatePopupPosition = () => {
const updatePopupPosition = (): void => {
const svgHeight = svg.node().getBoundingClientRect().height;
const [x, y] = d3.mouse(svg.node());
hoverPopup
@@ -175,34 +239,135 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
.classed('popup-at-bottom', y > (svgHeight * 2) / 3);
};
const mouseenter = function mouseenter(this: SVGPathElement, d: GeoFeature) {
const mouseenter = function mouseenter(
this: SVGPathElement,
d: GeoFeature,
): void {
// Darken color
let c: string = colorFn(d);
if (c !== 'none') {
if (c) {
c = d3.rgb(c).darker().toString();
}
d3.select(this).style('fill', c);
// Display information popup
const result = data.filter(
region => region.country_id === d.properties.ISO,
);
hoverPopup.style('display', 'block').html(
`<div><strong>${getNameOfRegion(d)}</strong><br>${result.length > 0 ? formatter(result[0].metric) : ''}</div>`,
);
const result = data.filter(r => r.country_id === d?.properties?.ISO);
const regionName = escapeHtml(getNameOfRegion(d));
const metricValue =
result.length > 0 ? escapeHtml(String(formatter(result[0].metric))) : '';
hoverPopup
.style('display', 'block')
.html(`<div><strong>${regionName}</strong><br>${metricValue}</div>`);
updatePopupPosition();
};
const mousemove = function mousemove() {
// Mouse move handler to update tooltip position
const mousemove = function mousemove(): void {
updatePopupPosition();
};
const mouseout = function mouseout(this: SVGPathElement) {
d3.select(this).style('fill', colorFn);
const mouseout = function mouseout(this: SVGPathElement): void {
d3.select(this).style('fill', (d: GeoFeature) => colorFn(d));
hoverPopup.style('display', 'none');
};
function drawMap(mapData: GeoData) {
// Only enable zoom if not in edit mode
if (!isEditMode) {
// Zoom with panning bounds
const zoom = d3.behavior
.zoom()
.scaleExtent([1, 4])
.on('zoomstart', () => {
svg.style('cursor', 'grabbing');
})
.on('zoom', () => {
const { translate, scale } = d3.event;
let [tx, ty] = translate;
const scaledW = width * scale;
const scaledH = height * scale;
const minX = Math.min(0, width - scaledW);
const maxX = 0;
const minY = Math.min(0, height - scaledH);
const maxY = 0;
tx = Math.max(Math.min(tx, maxX), minX);
ty = Math.max(Math.min(ty, maxY), minY);
// Sync D3's internal translate state with the clamped values so the
// next wheel/zoom event starts from the constrained position rather
// than the unclamped one (otherwise the view jumps).
zoom.translate([tx, ty]);
g.attr('transform', `translate(${tx}, ${ty}) scale(${scale})`);
const prev = zoomStates.get(element);
const changed =
!prev ||
prev.scale !== scale ||
prev.translate[0] !== tx ||
prev.translate[1] !== ty;
if (changed) {
zoomStates.set(element, { scale, translate: [tx, ty] });
}
})
.on('zoomend', () => {
svg.style('cursor', 'grab');
});
d3.select(svg.node()).call(zoom);
// Restore previous zoom state if it exists
const savedZoom = zoomStates.get(element);
if (savedZoom) {
const { scale, translate } = savedZoom;
zoom.scale(scale).translate(translate);
g.attr(
'transform',
`translate(${translate[0]}, ${translate[1]}) scale(${scale})`,
);
}
}
// Visual highlighting for selected regions
function highlightSelectedRegion(
selectedValues: string[] | null = null,
): void {
const selected = selectedValues || filterState?.selectedValues || [];
mapLayer
.selectAll('path.region')
.style('fill-opacity', (d: GeoFeature) => {
const iso = d?.properties?.ISO;
return selected.length === 0 || selected.includes(iso) ? 1 : 0.3;
})
.style('stroke', (d: GeoFeature) => {
const iso = d?.properties?.ISO;
return selected.includes(iso) ? '#222' : null;
})
.style('stroke-width', (d: GeoFeature) => {
const iso = d?.properties?.ISO;
return selected.includes(iso) ? '1.5px' : '0.5px';
});
}
// Click handler for cross-filters
const handleClick = (feature: GeoFeature): void => {
if (!entity || !emitCrossFilters || typeof setDataMask !== 'function') {
return;
}
const result = getCrossFilterDataMask(feature);
if (!result) return;
const { dataMask, isCurrentValueSelected } = result;
setDataMask(dataMask);
const iso = feature?.properties?.ISO;
const newSelection = isCurrentValueSelected || !iso ? [] : [iso];
highlightSelectedRegion(newSelection);
};
function drawMap(mapData: GeoData): void {
const { features } = mapData;
const center = d3.geo.centroid(mapData);
const scale = 100;
@@ -213,13 +378,11 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
.translate([width / 2, height / 2]);
path.projection(projection);
// Compute scale that fits container.
const bounds = path.bounds(mapData);
const hscale = (scale * width) / (bounds[1][0] - bounds[0][0]);
const vscale = (scale * height) / (bounds[1][1] - bounds[0][1]);
const newScale = hscale < vscale ? hscale : vscale;
const newScale = Math.min(hscale, vscale);
// Compute bounds and offset using the updated scale.
projection.scale(newScale);
const newBounds = path.bounds(mapData);
projection.translate([
@@ -227,20 +390,45 @@ function CountryMap(element: HTMLElement, props: CountryMapProps) {
height - (newBounds[0][1] + newBounds[1][1]) / 2,
]);
// Draw each province as a path
mapLayer
.selectAll('path')
.data(features)
const sel = mapLayer.selectAll('path.region').data(features);
sel
.enter()
.append('path')
.attr('d', path)
.attr('class', 'region')
.attr('vector-effect', 'non-scaling-stroke')
.attr('vector-effect', 'non-scaling-stroke');
// Apply attributes and event handlers to all elements (enter + update)
mapLayer
.selectAll('path.region')
.attr('d', path)
.style('fill', colorFn)
.on('mouseenter', mouseenter)
.on('mousemove', mousemove)
.on('mouseout', mouseout)
.on('click', clicked);
.on('contextmenu', handleContextMenu)
.on('mousedown', function mousedown() {
const pos = d3.mouse(svg.node());
mousedownPos = { x: pos[0], y: pos[1] };
})
.on('click', function click(feature: GeoFeature) {
if (mousedownPos) {
const pos = d3.mouse(svg.node());
const dx = Math.abs(pos[0] - mousedownPos.x);
const dy = Math.abs(pos[1] - mousedownPos.y);
const dragThreshold = 5;
if (dx < dragThreshold && dy < dragThreshold) {
handleClick(feature);
}
mousedownPos = null;
}
});
sel.exit().remove();
highlightSelectedRegion();
}
const map = maps[country];

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { t } from '@apache-superset/core/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
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';
@@ -49,6 +49,11 @@ const metadata = new ChartMetadata({
thumbnail,
thumbnailDark,
useLegacyApi: true,
behaviors: [
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
],
});
export default class CountryMapChartPlugin extends ChartPlugin {

View File

@@ -19,8 +19,18 @@
import { ChartProps, getValueFormatter } from '@superset-ui/core';
export default function transformProps(chartProps: ChartProps) {
const { width, height, formData, queriesData, datasource } = chartProps;
const {
width,
height,
formData,
queriesData,
datasource,
hooks = {},
filterState,
emitCrossFilters,
} = chartProps;
const {
entity,
linearColorScheme,
numberFormat,
currencyFormat,
@@ -49,6 +59,8 @@ export default function transformProps(chartProps: ChartProps) {
detectedCurrency,
);
const { onContextMenu, setDataMask } = hooks;
return {
width,
height,
@@ -59,5 +71,10 @@ export default function transformProps(chartProps: ChartProps) {
colorScheme,
sliceId,
formatter,
entity,
onContextMenu,
setDataMask,
emitCrossFilters,
filterState,
};
}

View File

@@ -133,10 +133,11 @@ describe('CountryMap (legacy d3)', () => {
expect(popup!).toHaveStyle({ display: 'none' });
});
test('shows tooltip on mouseenter/mousemove/mouseout', async () => {
test('emits a cross-filter data mask when a region is clicked', () => {
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
cb(null, mockMapData),
);
const setDataMask = jest.fn();
render(
<ReactCountryMap
@@ -147,19 +148,101 @@ describe('CountryMap (legacy d3)', () => {
linearColorScheme="bnbColors"
colorScheme=""
formatter={jest.fn().mockReturnValue('100')}
entity="country_code"
emitCrossFilters
setDataMask={setDataMask}
filterState={{ selectedValues: [] }}
/>,
);
const region = document.querySelector('path.region');
expect(region).not.toBeNull();
const popup = document.querySelector('.hover-popup');
expect(popup).not.toBeNull();
// A click is only treated as a selection when it follows a mousedown
// without dragging beyond the threshold (d3.mouse is mocked to a fixed
// position, so the down/up positions match).
fireEvent.mouseDown(region!);
fireEvent.click(region!);
fireEvent.mouseEnter(region!);
expect(popup!).toHaveStyle({ display: 'block' });
expect(setDataMask).toHaveBeenCalledTimes(1);
expect(setDataMask).toHaveBeenCalledWith(
expect.objectContaining({
extraFormData: {
filters: [{ col: 'country_code', op: 'IN', val: ['CAN'] }],
},
filterState: expect.objectContaining({ value: ['CAN'] }),
}),
);
});
fireEvent.mouseOut(region!);
expect(popup!).toHaveStyle({ display: 'none' });
test('does not emit a cross-filter when emitCrossFilters is disabled', () => {
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
cb(null, mockMapData),
);
const setDataMask = jest.fn();
render(
<ReactCountryMap
width={500}
height={300}
data={[{ country_id: 'CAN', metric: 100 }]}
country="canada"
linearColorScheme="bnbColors"
colorScheme=""
formatter={jest.fn().mockReturnValue('100')}
entity="country_code"
emitCrossFilters={false}
setDataMask={setDataMask}
filterState={{ selectedValues: [] }}
/>,
);
const region = document.querySelector('path.region');
fireEvent.mouseDown(region!);
fireEvent.click(region!);
expect(setDataMask).not.toHaveBeenCalled();
});
test('opens the context menu with drill-by keyed on the entity control', () => {
d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) =>
cb(null, mockMapData),
);
const onContextMenu = jest.fn();
render(
<ReactCountryMap
width={500}
height={300}
data={[{ country_id: 'CAN', metric: 100 }]}
country="canada"
linearColorScheme="bnbColors"
colorScheme=""
formatter={jest.fn().mockReturnValue('100')}
entity="country_code"
onContextMenu={onContextMenu}
filterState={{ selectedValues: [] }}
/>,
);
const region = document.querySelector('path.region');
expect(region).not.toBeNull();
fireEvent.contextMenu(region!, { clientX: 123, clientY: 45 });
expect(onContextMenu).toHaveBeenCalledTimes(1);
const [[clientX, clientY, payload]] = onContextMenu.mock.calls;
expect(clientX).toBe(123);
expect(clientY).toBe(45);
expect(payload.drillToDetail).toEqual([
{ col: 'country_code', op: '==', val: 'CAN', formattedVal: 'CAN' },
]);
// groupbyFieldName must be the form-data control key ('entity'), not the
// selected column value ('country_code'), so DrillByModal can map the
// selection back to the chart control.
expect(payload.drillBy).toEqual({
filters: [{ col: 'country_code', op: '==', val: 'CAN' }],
groupbyFieldName: 'entity',
});
});
});

View File

@@ -0,0 +1,76 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps } from '@superset-ui/core';
import transformProps from '../src/transformProps';
const onContextMenu = jest.fn();
const setDataMask = jest.fn();
const createProps = (formDataOverrides = {}, chartPropsOverrides = {}) =>
({
width: 800,
height: 600,
formData: {
entity: 'country_code',
linearColorScheme: 'bnbColors',
numberFormat: '.2f',
selectCountry: 'France',
colorScheme: '',
sliceId: 1,
metric: 'count',
...formDataOverrides,
},
queriesData: [{ data: [{ country_id: 'FRA', metric: 10 }] }],
datasource: { currencyFormats: {}, columnFormats: {} },
hooks: { onContextMenu, setDataMask },
filterState: { selectedValues: ['FRA'] },
emitCrossFilters: true,
...chartPropsOverrides,
}) as unknown as ChartProps;
test('forwards cross-filter hooks and state to the chart', () => {
const transformed = transformProps(createProps());
expect(transformed).toMatchObject({
width: 800,
height: 600,
entity: 'country_code',
onContextMenu,
setDataMask,
emitCrossFilters: true,
filterState: { selectedValues: ['FRA'] },
data: [{ country_id: 'FRA', metric: 10 }],
});
});
test('lowercases the selected country for map lookup', () => {
const transformed = transformProps(createProps());
expect(transformed.country).toBe('france');
});
test('passes a null country when none is selected', () => {
const transformed = transformProps(createProps({ selectCountry: undefined }));
expect(transformed.country).toBeNull();
});
test('defaults hooks to an empty object when none are provided', () => {
const transformed = transformProps(createProps({}, { hooks: undefined }));
expect(transformed.onContextMenu).toBeUndefined();
expect(transformed.setDataMask).toBeUndefined();
});

View File

@@ -25,6 +25,7 @@ import {
D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
DEFAULT_TIME_FORMAT,
getStandardizedControls,
} from '@superset-ui/chart-controls';
import OptionDescription from './OptionDescription';
@@ -154,7 +155,7 @@ const config: ControlPanelConfig = {
freeForm: true,
label: t('Date Time Format'),
renderTrigger: true,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},

View File

@@ -25,6 +25,7 @@ import {
D3_TIME_FORMAT_OPTIONS,
sections,
getStandardizedControls,
DEFAULT_TIME_FORMAT,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
@@ -78,7 +79,7 @@ const config: ControlPanelConfig = {
freeForm: true,
label: t('Date Time Format'),
renderTrigger: true,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},

View File

@@ -164,7 +164,8 @@ function WorldMap(element: HTMLElement, props: WorldMapProps): void {
processedData = filteredData.map(d => ({
...d,
radius: radiusScale(Math.sqrt(d.m2)),
fillColor: d.m1 != null ? colorFn(d.m1) ?? theme.colorBorder : theme.colorBorder,
fillColor:
d.m1 != null ? (colorFn(d.m1) ?? theme.colorBorder) : theme.colorBorder,
}));
}

View File

@@ -26,6 +26,7 @@ import {
D3_TIME_FORMAT_OPTIONS,
D3_FORMAT_DOCS,
D3_FORMAT_OPTIONS,
DEFAULT_TIME_FORMAT,
} from '@superset-ui/chart-controls';
/*
@@ -235,7 +236,7 @@ export const xAxisFormat: CustomControlItem = {
label: t('X Axis Format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
description: D3_FORMAT_DOCS,
},
};

View File

@@ -29,7 +29,7 @@ import {
import { isEmpty } from 'lodash';
export default function buildQuery(formData: QueryFormData) {
const { cols: groupby } = formData;
const { cols: groupby, extra_form_data } = formData;
const queryContextA = buildQueryContext(formData, baseQueryObject => {
const postProcessing: PostProcessingRule[] = [];
@@ -58,14 +58,24 @@ export default function buildQuery(formData: QueryFormData) {
timeOffsets = timeOffsets.concat(['inherit']);
}
}
if (
extra_form_data?.time_compare &&
!timeOffsets.includes(extra_form_data.time_compare)
) {
timeOffsets = [extra_form_data.time_compare];
}
return [
{
...baseQueryObject,
groupby,
post_processing: postProcessing,
time_offsets: isTimeComparison(formData, baseQueryObject)
? ensureIsArray(timeOffsets)
: [],
time_offsets:
isTimeComparison(formData, baseQueryObject) ||
extra_form_data?.time_compare
? ensureIsArray(timeOffsets)
: [],
},
];
});

View File

@@ -111,7 +111,11 @@ export default function transformProps(chartProps: ChartProps) {
const metrics = chartProps.datasource?.metrics || [];
const originalLabel = getOriginalLabel(metric, metrics);
const showMetricName = chartProps.rawFormData?.show_metric_name ?? false;
const timeComparison = ensureIsArray(chartProps.rawFormData?.time_compare)[0];
const dashboardTimeCompare = formData?.extraFormData?.time_compare;
const timeComparison =
dashboardTimeCompare ||
ensureIsArray(chartProps.rawFormData?.time_compare)[0];
const startDateOffset = chartProps.rawFormData?.start_date_offset;
const currentTimeRangeFilter = chartProps.rawFormData?.adhoc_filters?.filter(
(adhoc_filter: SimpleAdhocFilter) =>

View File

@@ -35,6 +35,7 @@ import {
ControlPanelState,
getTemporalColumns,
sharedControls,
DEFAULT_TIME_FORMAT,
} from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
@@ -153,12 +154,26 @@ const config: ControlPanelConfig = {
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
description: D3_FORMAT_DOCS,
},
},
],
['zoomable'],
[
{
name: 'y_axis_slider',
config: {
type: 'CheckboxControl',
label: t('Y-axis range slider'),
default: false,
renderTrigger: true,
description: t(
'Show a draggable slider to control the visible range of the Y-axis.',
),
},
},
],
],
},
],

View File

@@ -74,6 +74,7 @@ export default function transformProps(
yAxisTitlePosition,
sliceId,
zoomable,
yAxisSlider,
} = formData as BoxPlotQueryFormData;
const refs: Refs = {};
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
@@ -257,6 +258,28 @@ export default function transformProps(
convertInteger(yAxisTitleMargin),
convertInteger(xAxisTitleMargin),
);
const dataZoom = [
...(zoomable
? [
{
type: 'inside',
zoomOnMouseWheel: false,
moveOnMouseWheel: true,
},
]
: []),
...(yAxisSlider
? [
{
type: 'slider',
show: true,
yAxisIndex: [0],
// Adjust the axis window without dropping data points outside the range.
filterMode: 'none',
},
]
: []),
];
const echartOptions: EChartsCoreOption = {
grid: {
...defaultGrid,
@@ -298,15 +321,7 @@ export default function transformProps(
},
},
},
dataZoom: zoomable
? [
{
type: 'inside',
zoomOnMouseWheel: false,
moveOnMouseWheel: true,
},
]
: [],
dataZoom,
};
return {

View File

@@ -30,6 +30,7 @@ export type BoxPlotQueryFormData = QueryFormData & {
numberFormat?: string;
whiskerOptions?: BoxPlotFormDataWhiskerOptions;
xTickLayout?: BoxPlotFormXTickLayout;
yAxisSlider?: boolean;
} & TitleFormData;
export type BoxPlotFormDataWhiskerOptions =

View File

@@ -28,6 +28,7 @@ import {
D3_TIME_FORMAT_OPTIONS,
getStandardizedControls,
sharedControls,
DEFAULT_TIME_FORMAT,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
import { legendSection } from '../controls';
@@ -188,7 +189,7 @@ const config: ControlPanelConfig = {
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
description: D3_FORMAT_DOCS,
},
},

View File

@@ -33,6 +33,7 @@ import {
sharedControls,
ControlFormItemSpec,
getStandardizedControls,
DEFAULT_TIME_FORMAT,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
import { LabelPositionEnum } from '../types';
@@ -181,7 +182,7 @@ const config: ControlPanelConfig = {
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
description: D3_FORMAT_DOCS,
},
},

View File

@@ -26,6 +26,7 @@ import {
D3_FORMAT_OPTIONS,
D3_TIME_FORMAT_OPTIONS,
getStandardizedControls,
DEFAULT_TIME_FORMAT,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from './types';
@@ -132,7 +133,7 @@ const config: ControlPanelConfig = {
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
default: 'smart_date',
default: DEFAULT_TIME_FORMAT,
description: D3_FORMAT_DOCS,
},
},

View File

@@ -182,7 +182,6 @@ const config: ControlPanelConfig = {
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
visibility: ({ controls }: ControlPanelsContainerProps) =>
checkColumnType(

View File

@@ -396,3 +396,102 @@ test('does not emit cross-filter when no dimensions and time-based X-axis', asyn
expect(setDataMaskMock).not.toHaveBeenCalled();
}
});
// Test for issue #41102: horizontal bar cross-filter must use the category
// value, not the metric. For horizontal bars the data tuple is value-first
// (e.g. [100, 'Product A']), so relying on data[0] emitted the metric value.
test('emits cross-filter on the category value for a horizontal categorical bar', async () => {
const setDataMaskMock = jest.fn();
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
setDataMask: setDataMaskMock,
groupby: [], // No dimensions
xAxis: {
label: 'category_column',
type: AxisType.Category, // Categorical X-axis
},
};
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
const clickHandler = props.eventHandlers?.click;
if (clickHandler) {
clickHandler({
seriesName: 'Sales', // This is the metric name
data: [100, 'Product A'], // Horizontal: value first, category second
name: 'Product A',
dataIndex: 0,
});
await waitFor(
() => {
expect(setDataMaskMock).toHaveBeenCalled();
},
{ timeout: 500 },
);
// Must filter on the category ('Product A'), not the metric value (100)
const dataMaskCall = setDataMaskMock.mock.calls[0][0];
expect(dataMaskCall.extraFormData.filters).toEqual([
{
col: 'category_column',
op: 'IN',
val: ['Product A'],
},
]);
}
});
// Test for issue #41102: the context-menu ("Add cross-filter") path must also
// use the category value, not the metric, for a horizontal categorical bar.
test('context menu cross-filter uses the category value for a horizontal categorical bar', async () => {
const onContextMenuMock = jest.fn();
const propsWithHorizontalXAxis: TimeseriesChartTransformedProps = {
...defaultProps,
emitCrossFilters: true,
onContextMenu: onContextMenuMock,
groupby: [], // No dimensions
xAxis: {
label: 'category_column',
type: AxisType.Category, // Categorical X-axis
},
};
render(<EchartsTimeseries {...propsWithHorizontalXAxis} />);
const lastCall = mockEchart.mock.calls.at(-1);
expect(lastCall).toBeDefined();
const [props] = lastCall as [EchartsProps];
const contextMenuHandler = props.eventHandlers?.contextmenu;
expect(contextMenuHandler).toBeDefined();
if (contextMenuHandler) {
await contextMenuHandler({
seriesName: 'Sales', // This is the metric name
data: [100, 'Product A'], // Horizontal: value first, category second
name: 'Product A',
event: { stop: jest.fn(), event: { clientX: 10, clientY: 20 } },
});
await waitFor(() => {
expect(onContextMenuMock).toHaveBeenCalled();
});
// The cross-filter must use the category ('Product A'), not the metric (100)
const { crossFilter } = onContextMenuMock.mock.calls[0][2];
expect(crossFilter.dataMask.extraFormData.filters).toEqual([
{
col: 'category_column',
op: 'IN',
val: ['Product A'],
},
]);
}
});

View File

@@ -234,9 +234,12 @@ export default function EchartsTimeseries({
// Cross-filter by dimension (original behavior)
const { seriesName: name } = props;
handleChange(name);
} else if (canCrossFilterByXAxis && props.data?.[0] != null) {
// Cross-filter by X-axis value when no dimensions (issue #25334)
handleXAxisChange(props.data[0]);
} else if (canCrossFilterByXAxis && props.name != null) {
// Cross-filter by X-axis value when no dimensions (issue #25334).
// Use `name` (the category-axis value) instead of `data[0]`: for
// horizontal bars the data tuple is value-first, so `data[0]` would
// be the metric value rather than the category (issue #41102).
handleXAxisChange(props.name);
}
}, TIMER_DURATION);
},
@@ -318,8 +321,10 @@ export default function EchartsTimeseries({
let crossFilter;
if (hasDimensions) {
crossFilter = getCrossFilterDataMask(seriesName);
} else if (canCrossFilterByXAxis && data?.[0] != null) {
crossFilter = getXAxisCrossFilterDataMask(data[0]);
} else if (canCrossFilterByXAxis && eventParams.name != null) {
// Use `name` (the category-axis value), not `data[0]`, so horizontal
// bars cross-filter on the category and not the metric (issue #41102).
crossFilter = getXAxisCrossFilterDataMask(eventParams.name);
}
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {

View File

@@ -174,7 +174,6 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
visibility: ({ controls }: ControlPanelsContainerProps) =>
(isXAxis ? isVertical(controls) : isHorizontal(controls)) &&

View File

@@ -147,7 +147,6 @@ const config: ControlPanelConfig = {
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
visibility: ({ controls }: ControlPanelsContainerProps) =>
checkColumnType(

View File

@@ -113,7 +113,6 @@ const config: ControlPanelConfig = {
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
visibility: ({ controls }: ControlPanelsContainerProps) =>
checkColumnType(

View File

@@ -112,7 +112,6 @@ const config: ControlPanelConfig = {
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
visibility: ({ controls }: ControlPanelsContainerProps) =>
checkColumnType(

View File

@@ -164,7 +164,6 @@ const config: ControlPanelConfig = {
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`,
visibility: ({ controls }: ControlPanelsContainerProps) =>
checkColumnType(

View File

@@ -258,7 +258,6 @@ export const tooltipTimeFormatControl: ControlSetItem = {
config: {
...sharedControls.x_axis_time_format,
label: t('Tooltip time format'),
default: 'smart_date',
clearable: false,
},
};

View File

@@ -0,0 +1,73 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormData } from '@superset-ui/core';
import buildQuery from '../../../src/BigNumber/BigNumberPeriodOverPeriod/buildQuery';
describe('BigNumberPeriodOverPeriod buildQuery', () => {
const baseFormData: QueryFormData = {
datasource: '1__table',
viz_type: 'pop_kpi',
metric: 'count',
cols: [],
adhoc_filters: [
{
clause: 'WHERE',
subject: 'order_date',
operator: 'TEMPORAL_RANGE',
comparator: '2003-07-01 : 2004-01-01',
expressionType: 'SIMPLE',
},
],
};
test('flows extra_form_data.time_compare override into time_offsets', () => {
const queryContext = buildQuery({
...baseFormData,
extra_form_data: { time_compare: '1 year ago' },
});
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
});
test('requests offsets from the override even without the chart time_compare control', () => {
const queryContext = buildQuery({
...baseFormData,
time_compare: undefined,
extra_form_data: { time_compare: '1 year ago' },
});
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
});
test('does not duplicate the offset when it already matches time_compare', () => {
const queryContext = buildQuery({
...baseFormData,
time_compare: ['1 year ago'],
extra_form_data: { time_compare: '1 year ago' },
});
expect(queryContext.queries[0].time_offsets).toEqual(['1 year ago']);
});
test('omits time_offsets when neither the control nor the override is set', () => {
const queryContext = buildQuery(baseFormData);
expect(queryContext.queries[0].time_offsets).toEqual([]);
});
});

View File

@@ -71,6 +71,15 @@ describe('BoxPlot transformProps', () => {
theme: supersetTheme,
});
const buildChartProps = (formDataOverrides: Partial<SqlaFormData> = {}) =>
new ChartProps({
formData: { ...formData, ...formDataOverrides },
width: 800,
height: 600,
queriesData: chartProps.queriesData,
theme: supersetTheme,
}) as EchartsBoxPlotChartProps;
test('should transform chart props for viz', () => {
expect(transformProps(chartProps as EchartsBoxPlotChartProps)).toEqual(
expect.objectContaining({
@@ -125,4 +134,41 @@ describe('BoxPlot transformProps', () => {
}),
);
});
test('should add a vertical Y-axis slider to dataZoom when yAxisSlider is enabled', () => {
const { echartOptions } = transformProps(
buildChartProps({ yAxisSlider: true }),
);
expect((echartOptions as any).dataZoom).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'slider',
show: true,
yAxisIndex: [0],
filterMode: 'none',
}),
]),
);
});
test('should not add a Y-axis slider when yAxisSlider is disabled', () => {
const { echartOptions } = transformProps(
buildChartProps({ yAxisSlider: false }),
);
expect((echartOptions as any).dataZoom).not.toContainEqual(
expect.objectContaining({ type: 'slider' }),
);
});
test('should combine zoomable and yAxisSlider dataZoom entries', () => {
const { echartOptions } = transformProps(
buildChartProps({ zoomable: true, yAxisSlider: true }),
);
expect((echartOptions as any).dataZoom).toEqual(
expect.arrayContaining([
expect.objectContaining({ type: 'inside' }),
expect.objectContaining({ type: 'slider', yAxisIndex: [0] }),
]),
);
});
});

View File

@@ -54,7 +54,7 @@ export function getQueryMode(formData: TableChartFormData) {
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
}
const buildQuery: BuildQuery<TableChartFormData> = (
export const buildQuery: BuildQuery<TableChartFormData> = (
formData: TableChartFormData,
options,
) => {
@@ -217,6 +217,17 @@ const buildQuery: BuildQuery<TableChartFormData> = (
const moreProps: Partial<QueryObject> = {};
const ownState = options?.ownState ?? {};
// Server pagination sizing, shared between the per-page request below and
// the filter-change reset further down.
const pageSize =
Number(ownState.pageSize ?? formDataCopy.server_page_length) || 0;
const configuredRowLimit = Number(formDataCopy.row_limit) || 0;
// row_limit for the first page, capped by the configured row limit. Used
// when a filter change resets pagination back to page 0.
const firstPageRowLimit =
configuredRowLimit > 0
? Math.min(pageSize, configuredRowLimit)
: pageSize;
// Build Query flag to check if its for either download as csv, excel or json
const isDownloadQuery =
['csv', 'xlsx'].includes(formData?.result_format || '') ||
@@ -229,11 +240,24 @@ const buildQuery: BuildQuery<TableChartFormData> = (
}
if (!isDownloadQuery && formDataCopy.server_pagination) {
const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
const currentPage = ownState.currentPage ?? 0;
// Never page past the configured row limit. Clamping the page to the last
// one that still falls within the limit keeps the request inside the cap
// and avoids emitting row_limit: 0, which the backend treats as
// "no limit" rather than "no rows" (see helpers.py get_sqla_query).
const lastPage =
configuredRowLimit > 0 && pageSize > 0
? Math.max(Math.ceil(configuredRowLimit / pageSize) - 1, 0)
: Number(ownState.currentPage) || 0;
const currentPage = Math.min(Number(ownState.currentPage) || 0, lastPage);
const rowOffset = currentPage * pageSize;
const remainingRows =
configuredRowLimit > 0
? Math.max(configuredRowLimit - rowOffset, 0)
: pageSize;
moreProps.row_limit = pageSize;
moreProps.row_offset = currentPage * pageSize;
moreProps.row_limit =
configuredRowLimit > 0 ? Math.min(pageSize, remainingRows) : pageSize;
moreProps.row_offset = rowOffset;
}
// getting sort by in case of server pagination from own state
@@ -263,11 +287,19 @@ const buildQuery: BuildQuery<TableChartFormData> = (
JSON.stringify(options?.extras?.cachedChanges?.[formData.slice_id]) !==
JSON.stringify(queryObject.filters)
) {
queryObject = { ...queryObject, row_offset: 0 };
// Reset to the first page: restore the full first-page row_limit rather
// than carrying over the last page's capped value.
queryObject = {
...queryObject,
row_offset: 0,
row_limit: firstPageRowLimit,
};
const modifiedOwnState = {
...options?.ownState,
currentPage: 0,
pageSize: queryObject.row_limit ?? 0,
// Persist the user-selected page size, not the per-request row_limit,
// which may be capped to the remaining rows on the last page.
pageSize,
};
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
}

View File

@@ -17,7 +17,9 @@
* under the License.
*/
import { QueryMode, TimeGranularity, VizType } from '@superset-ui/core';
import buildQuery from '../src/buildQuery';
import buildQuery, {
buildQuery as buildQueryUncached,
} from '../src/buildQuery';
import { TableChartFormData } from '../src/types';
const basicFormData: TableChartFormData = {
@@ -278,6 +280,172 @@ describe('plugin-chart-table', () => {
expect(queries[0].filters?.some(f => f.op === 'ILIKE')).toBeFalsy();
});
test('uses user row limit when it is lower than server page size', () => {
const { queries } = buildQuery(
{
...baseFormDataWithServerPagination,
row_limit: 10,
server_page_length: 20,
slice_id: 101,
},
{
ownState: {
currentPage: 0,
pageSize: 20,
},
},
);
expect(queries[0]).toMatchObject({
row_limit: 10,
row_offset: 0,
});
});
test('limits server page size by remaining rows inside user row limit', () => {
const { queries } = buildQuery(
{
...baseFormDataWithServerPagination,
row_limit: 120,
server_page_length: 50,
slice_id: 102,
},
{
ownState: {
currentPage: 2,
pageSize: 50,
sortBy: [{ key: 'category', desc: true }],
},
},
);
expect(queries[0]).toMatchObject({
orderby: [['category', false]],
row_limit: 20,
row_offset: 100,
});
expect(queries[1]).toMatchObject({
is_rowcount: true,
row_limit: 120,
row_offset: 0,
});
});
test('clamps pages beyond the row limit instead of emitting row_limit: 0', () => {
const { queries } = buildQuery(
{
...baseFormDataWithServerPagination,
row_limit: 120,
server_page_length: 50,
slice_id: 103,
},
{
ownState: {
// Page 5 is well past the cap; offset would be 250 > 120, which
// previously made row_limit collapse to 0 ("no limit").
currentPage: 5,
pageSize: 50,
},
},
);
expect(queries[0].row_limit).not.toBe(0);
expect(queries[0]).toMatchObject({
row_limit: 20,
row_offset: 100,
});
});
test('restores the full first-page row limit after a filter change reset', () => {
// Uncached export lets us seed cachedChanges directly; the default
// export overrides extras with its own closure.
const { queries } = buildQueryUncached(
{
...baseFormDataWithServerPagination,
row_limit: 120,
server_page_length: 50,
slice_id: 104,
},
{
// User was on the capped last page (row_limit would be 20)...
ownState: {
currentPage: 2,
pageSize: 50,
},
// ...then an external filter changed, so the cached filters differ
// from the current ones and pagination resets to page 0.
extras: {
cachedChanges: {
104: [{ col: 'category', op: '==', val: 'previous' }],
},
},
},
);
expect(queries[0].row_limit).not.toBe(0);
expect(queries[0]).toMatchObject({
row_limit: 50,
row_offset: 0,
});
});
test('persists the user page size, not the capped limit, on filter reset', () => {
const setDataMask = jest.fn();
buildQueryUncached(
{
...baseFormDataWithServerPagination,
row_limit: 120,
server_page_length: 50,
slice_id: 106,
},
{
// On the capped last page, the per-request row_limit is 20.
ownState: {
currentPage: 2,
pageSize: 50,
},
extras: {
cachedChanges: {
106: [{ col: 'category', op: '==', val: 'previous' }],
},
},
hooks: { setDataMask, setCachedChanges: jest.fn() },
},
);
// The persisted page size must stay 50, not collapse to the capped 20.
expect(setDataMask).toHaveBeenCalledWith(
expect.objectContaining({
ownState: expect.objectContaining({
currentPage: 0,
pageSize: 50,
}),
}),
);
});
test('falls back to the page size when no row limit is configured', () => {
const { queries } = buildQuery(
{
...baseFormDataWithServerPagination,
row_limit: undefined,
server_page_length: 50,
slice_id: 105,
},
{
ownState: {
currentPage: 3,
pageSize: 50,
},
},
);
expect(queries[0]).toMatchObject({
row_limit: 50,
row_offset: 150,
});
});
});
});
});

View File

@@ -39,8 +39,10 @@ jest.mock('@superset-ui/core/components/Select/AsyncSelect', () => () => (
<div data-test="mock-async-select" />
));
jest.mock('src/core/editors', () => ({
EditorHost: ({ value }: { value: string }) => (
<div data-test="mock-async-ace-editor">{value}</div>
EditorHost: ({ value, height }: { value: string; height: string }) => (
<div data-test="mock-async-ace-editor" data-height={height}>
{value}
</div>
),
}));
@@ -79,6 +81,18 @@ describe('TemplateParamsEditor', () => {
});
});
test('renders the editor with a bounded height to avoid overflowing the popover', async () => {
const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters'));
await waitFor(() => {
expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
});
expect(getByTestId('mock-async-ace-editor')).toHaveAttribute(
'data-height',
'360px',
);
});
test('renders templateParams', async () => {
const { container, getByTestId } = setup();
fireEvent.click(getByText(container, 'Parameters'));

View File

@@ -30,10 +30,9 @@ import {
import { EditorHost } from 'src/core/editors';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
const StyledEditorHost = styled(EditorHost)`
&.ace_editor {
border: 1px solid ${({ theme }) => theme.colorBorder};
}
const EditorOutline = styled.div`
border: 1px solid ${({ theme }) => theme.colorBorder};
border-radius: ${({ theme }) => theme.borderRadius}px;
`;
const StyledParagraph = styled.p`
@@ -87,14 +86,16 @@ const TemplateParamsEditor = ({
</a>{' '}
{t('syntax.')}
</StyledParagraph>
<StyledEditorHost
id={`template-params-${queryEditorId}`}
height="800px"
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
language={language === 'yaml' ? 'yaml' : 'json'}
width="100%"
value={code}
/>
<EditorOutline>
<EditorHost
id={`template-params-${queryEditorId}`}
height="360px"
onChange={debounce(onChange, Constants.FAST_DEBOUNCE)}
language={language === 'yaml' ? 'yaml' : 'json'}
width="100%"
value={code}
/>
</EditorOutline>
</div>
);

View File

@@ -200,6 +200,27 @@ describe('sqlLabReducer', () => {
expect(newState.unsavedQueryEditor.sql).toBe(sql);
expect(newState.unsavedQueryEditor.id).toBe(qe!.id);
});
test('should set Sql when dispatched with tabViewId (backend persistence)', () => {
// Simulate SqllabBackendPersistence: queryEditor gets a tabViewId after save
const tabViewId = 'tab-view-42';
const migrateAction = {
type: actions.MIGRATE_QUERY_EDITOR,
oldQueryEditor: qe,
newQueryEditor: { ...qe!, tabViewId, inLocalStorage: false },
};
newState = sqlLabReducer(newState, migrateAction as SqlLabAction);
// Restore SQL using tabViewId (as restoreSql in QueryTable does)
const sql = 'SELECT restored_query FROM history';
const restoreAction = {
type: actions.QUERY_EDITOR_SET_SQL,
queryEditor: { id: tabViewId },
sql,
};
newState = sqlLabReducer(newState, restoreAction);
expect(newState.unsavedQueryEditor.sql).toBe(sql);
expect(newState.unsavedQueryEditor.id).toBe(qe!.id);
});
test('should not fail while setting queryLimit', () => {
const queryLimit = 101;
const action = {

View File

@@ -604,8 +604,20 @@ export default function sqlLabReducer(
},
[actions.QUERY_EDITOR_SET_SQL]() {
const { unsavedQueryEditor } = state;
const actionId = action.queryEditor!.id!;
// Skip the O(n) tabViewId scan on the common path (keystroke: actionId already
// matches the active editor's client-side id). Only scan when ids differ, which
// happens when restoring from history with a backend-assigned tabViewId.
const normalizedId =
unsavedQueryEditor?.id === actionId
? actionId
: ((
getFromArr(state.queryEditors, actionId, 'tabViewId') as
| QueryEditor
| undefined
)?.id ?? actionId);
if (
unsavedQueryEditor?.id === action.queryEditor!.id &&
unsavedQueryEditor?.id === normalizedId &&
unsavedQueryEditor.sql === action.sql
) {
return state;
@@ -618,7 +630,7 @@ export default function sqlLabReducer(
sql: action.sql ?? undefined,
...(action.queryId && { latestQueryId: action.queryId }),
},
action.queryEditor!.id!,
normalizedId,
),
};
},

View File

@@ -0,0 +1,160 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { AnyAction } from 'redux';
// eslint-disable-next-line import/named
import {
ActionCreators as UndoActionCreators,
StateWithHistory,
} from 'redux-undo';
import undoableLayoutReducer from 'src/dashboard/reducers/undoableDashboardLayout';
import { UPDATE_COMPONENTS } from 'src/dashboard/actions/dashboardLayout';
import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
import type { DashboardLayout } from 'src/dashboard/types';
import {
DASHBOARD_ROOT_ID,
DASHBOARD_GRID_ID,
DASHBOARD_HEADER_ID,
} from 'src/dashboard/util/constants';
import {
DASHBOARD_ROOT_TYPE,
DASHBOARD_GRID_TYPE,
DASHBOARD_HEADER_TYPE,
CHART_TYPE,
} from 'src/dashboard/util/componentTypes';
const reducer = undoableLayoutReducer;
// A minimal but valid dashboard layout always contains the root component.
const makeValidLayout = (
title = '[ untitled dashboard ]',
): DashboardLayout => ({
[DASHBOARD_ROOT_ID]: {
id: DASHBOARD_ROOT_ID,
type: DASHBOARD_ROOT_TYPE,
children: [DASHBOARD_GRID_ID],
meta: {},
},
[DASHBOARD_GRID_ID]: {
id: DASHBOARD_GRID_ID,
type: DASHBOARD_GRID_TYPE,
parents: [DASHBOARD_ROOT_ID],
children: [],
meta: {},
},
[DASHBOARD_HEADER_ID]: {
id: DASHBOARD_HEADER_ID,
type: DASHBOARD_HEADER_TYPE,
children: [],
meta: { text: title },
},
});
// The frontend locks redux-undo to 1.1.0, whose `clearHistory()` under
// `ignoreInitialState` resets `_latestUnfiltered` to null. That makes a rootless
// layout impossible to push onto `past` through normal layout actions, so the
// guard's corrupt-history precondition is seeded directly. `makeHistory` mirrors
// redux-undo's `StateWithHistory` shape — `past`/`present`/`future` is all that
// `undo()` needs to compute the previous state.
const makeHistory = (
past: DashboardLayout[],
present: DashboardLayout,
future: DashboardLayout[] = [],
): StateWithHistory<DashboardLayout> => ({ past, present, future });
const hydrate = (present: DashboardLayout): AnyAction => ({
type: HYDRATE_DASHBOARD,
data: { dashboardLayout: { present } },
});
test('hydrating a dashboard leaves an empty, disabled undo history', () => {
const initial = reducer(undefined, { type: '@@INIT' });
const state = reducer(initial, hydrate(makeValidLayout()));
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
// Hydration is not a user edit, so Undo (past) and Redo (future) start empty.
expect(state.past).toHaveLength(0);
expect(state.future).toHaveLength(0);
});
test('a layout edit is applied through the wrapped reducer', () => {
const hydrated = reducer(
reducer(undefined, { type: '@@INIT' }),
hydrate(makeValidLayout()),
);
const update: AnyAction = {
type: UPDATE_COMPONENTS,
payload: {
nextComponents: {
'CHART-1': { id: 'CHART-1', type: CHART_TYPE, children: [], meta: {} },
},
},
};
const state = reducer(hydrated, update);
expect(state.present['CHART-1']).toBeDefined();
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
});
test('re-hydrating a different dashboard clears the previous dashboard from the undo stack', () => {
// Simulates SPA navigation: dashboard A already has undo history when B opens.
const dashboardA = makeHistory(
[makeValidLayout('A v1')],
makeValidLayout('A v2'),
);
const state = reducer(dashboardA, hydrate(makeValidLayout('B')));
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
expect(state.past).toHaveLength(0);
expect(state.future).toHaveLength(0);
});
test('undo never reverts the layout to an invalid (rootless) state', () => {
// A rootless `{}` baseline sits at the head of `past`; a plain redux-undo
// undo() here would move it into `present` and crash rendering with
// `Cannot read properties of undefined (reading 'type')`.
const corrupt = makeHistory([{}], makeValidLayout());
const before = corrupt.present;
const state = reducer(corrupt, UndoActionCreators.undo());
// The guard rejects the transition: the valid layout is kept unchanged...
expect(state.present[DASHBOARD_ROOT_ID]).toBeDefined();
expect(state.present).toBe(before);
// ...and history is left intact, so undoLayoutAction() won't misread an
// emptied stack as a fully-reverted, clean dashboard.
expect(state.past).toHaveLength(1);
});
test('the guard does not interfere with a normal undo between valid layouts', () => {
const previous = makeValidLayout('previous');
const current = makeValidLayout('current');
const state = reducer(
makeHistory([previous], current),
UndoActionCreators.undo(),
);
// A valid -> valid undo proceeds normally.
expect(state.present).toBe(previous);
expect(state.past).toHaveLength(0);
expect(state.future).toHaveLength(1);
});

View File

@@ -18,8 +18,11 @@
*/
import { AnyAction, Reducer } from 'redux';
// eslint-disable-next-line import/named
import undoable, { StateWithHistory } from 'redux-undo';
import { UNDO_LIMIT } from '../util/constants';
import undoable, {
ActionCreators as UndoActionCreators,
StateWithHistory,
} from 'redux-undo';
import { DASHBOARD_ROOT_ID, UNDO_LIMIT } from '../util/constants';
import {
UPDATE_COMPONENTS,
DELETE_COMPONENT,
@@ -97,7 +100,7 @@ const layoutOnlyReducer: Reducer<DashboardLayout, AnyAction> = (
return dashboardLayout(state || {}, action);
};
const undoableReducer: Reducer<
const baseUndoableReducer: Reducer<
StateWithHistory<DashboardLayout>,
AnyAction
> = undoable(layoutOnlyReducer, {
@@ -107,4 +110,53 @@ const undoableReducer: Reducer<
ignoreInitialState: true,
});
/*
* A valid dashboard layout always contains the root component. Undo/redo must
* never leave `present` without it: a rootless layout renders the dashboard
* with no components and throws
* `TypeError: Cannot read properties of undefined (reading 'type')`. Such a
* state can arise whenever a rootless or empty layout reaches the undo history —
* e.g. an empty or partial hydration, or a tracked layout action dispatched
* before the dashboard has hydrated.
*/
const isValidLayout = (layout?: DashboardLayout): boolean =>
Boolean(layout && layout[DASHBOARD_ROOT_ID]);
/*
* Wraps the redux-undo reducer to keep the dashboard layout undo history sound:
*
* 1. Hydration establishes the baseline for the dashboard being opened. It is
* not a user edit and must never be undoable, so the history is reset on
* every HYDRATE_DASHBOARD. Doing this in the reducer — rather than relying
* solely on a follow-up clearDashboardHistory() dispatch from the page
* component — guarantees the Undo control starts disabled and that no layout
* from a previously edited dashboard lingers in the stack after navigation.
* 2. As defense in depth, undo/redo is never allowed to replace a valid layout
* with an invalid (rootless) one. Such a transition is rejected and the
* current valid layout is kept, so clicking Undo can never crash the
* dashboard. History is left untouched on rejection so callers that inspect
* it (e.g. undoLayoutAction) don't misread an emptied stack as a clean,
* fully-reverted dashboard and silently drop the unsaved-changes guard.
*/
const undoableReducer: Reducer<StateWithHistory<DashboardLayout>, AnyAction> = (
state,
action,
) => {
const nextState = baseUndoableReducer(state, action);
if (action.type === HYDRATE_DASHBOARD) {
return baseUndoableReducer(nextState, UndoActionCreators.clearHistory());
}
if (
state &&
isValidLayout(state.present) &&
!isValidLayout(nextState.present)
) {
return state;
}
return nextState;
};
export default undoableReducer;

View File

@@ -615,6 +615,172 @@ test('onTabChange correctly updates selectedTab via forceUpdate', () => {
});
});
const ownerUser = {
userId: 1,
username: 'testuser',
firstName: 'Test',
lastName: 'User',
isActive: true,
isAnonymous: false,
permissions: {},
roles: { Alpha: [['can_write', 'Dashboard']] as [string, string][] },
groups: [],
};
const makeMetadataDashboard = (id: number, title: string) => ({
id,
dashboard_title: title,
owners: [{ id: 1, first_name: 'Test', last_name: 'User' }],
extra_owners: [],
roles: [],
url: `/superset/dashboard/${id}/`,
slug: null,
thumbnail_url: null,
published: true,
changed_by_name: 'Test User',
changed_by: { id: 1, first_name: 'Test', last_name: 'User' },
changed_on: '2024-01-01',
charts: [],
});
test('pre-populates dashboard from metadata.dashboards when dashboardId prop is absent', async () => {
const dashboardId = 5;
const dashboardTitle = 'Chart Dashboard';
const myProps = {
...defaultProps,
dashboardId: null,
metadata: {
dashboards: [{ id: dashboardId, dashboard_title: dashboardTitle }],
owners: ['Test User'],
created_on_humanized: '2 days ago',
changed_on_humanized: '1 day ago',
},
user: ownerUser,
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
dispatch: jest.fn(),
addDangerToast: jest.fn(),
};
const component = new TestSaveModal(myProps);
const mockFull = makeMetadataDashboard(dashboardId, dashboardTitle);
component.loadDashboard = jest.fn().mockResolvedValue(mockFull);
component.loadTabs = jest.fn().mockResolvedValue([]);
const stateUpdates: any[] = [];
component.setState = jest.fn((update: any) => {
stateUpdates.push(update);
});
try {
sessionStorage.clear();
} catch (_) {
// ignore
}
await component.componentDidMount();
expect(component.loadDashboard).toHaveBeenCalledWith(dashboardId);
expect(stateUpdates).toContainEqual({
dashboard: { label: dashboardTitle, value: dashboardId },
});
expect(component.loadTabs).toHaveBeenCalledWith(dashboardId);
});
test('skips non-editable dashboards and picks the first editable one from metadata', async () => {
const editableId = 7;
const editableTitle = 'Editable Dashboard';
const myProps = {
...defaultProps,
dashboardId: null,
metadata: {
dashboards: [
{ id: 6, dashboard_title: 'Not Mine' },
{ id: editableId, dashboard_title: editableTitle },
],
owners: ['Test User'],
created_on_humanized: '2 days ago',
changed_on_humanized: '1 day ago',
},
user: ownerUser,
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
dispatch: jest.fn(),
addDangerToast: jest.fn(),
};
const component = new TestSaveModal(myProps);
const notMine = makeMetadataDashboard(6, 'Not Mine');
notMine.owners = [{ id: 99, first_name: 'Other', last_name: 'Owner' }];
const editable = makeMetadataDashboard(editableId, editableTitle);
component.loadDashboard = jest
.fn()
.mockImplementation((id: number) =>
Promise.resolve(id === 6 ? notMine : editable),
);
component.loadTabs = jest.fn().mockResolvedValue([]);
const stateUpdates: any[] = [];
component.setState = jest.fn((update: any) => {
stateUpdates.push(update);
});
try {
sessionStorage.clear();
} catch (_) {
// ignore
}
await component.componentDidMount();
expect(stateUpdates).toContainEqual({
dashboard: { label: editableTitle, value: editableId },
});
expect(component.loadTabs).toHaveBeenCalledWith(editableId);
});
test('does not use metadata fallback when dashboardId prop is set', async () => {
const propDashboardId = 3;
const propDashboardTitle = 'Prop Dashboard';
const myProps = {
...defaultProps,
dashboardId: propDashboardId,
metadata: {
dashboards: [{ id: 99, dashboard_title: 'Should Not Be Used' }],
owners: ['Test User'],
created_on_humanized: '2 days ago',
changed_on_humanized: '1 day ago',
},
user: ownerUser,
slice: { slice_id: 1, slice_name: 'My Chart', owners: [1] },
dispatch: jest.fn(),
addDangerToast: jest.fn(),
};
const component = new TestSaveModal(myProps);
const mockFull = makeMetadataDashboard(propDashboardId, propDashboardTitle);
component.loadDashboard = jest.fn().mockResolvedValue(mockFull);
component.loadTabs = jest.fn().mockResolvedValue([]);
const stateUpdates: any[] = [];
component.setState = jest.fn((update: any) => {
stateUpdates.push(update);
});
await component.componentDidMount();
expect(component.loadDashboard).toHaveBeenCalledWith(propDashboardId);
expect(component.loadDashboard).not.toHaveBeenCalledWith(99);
expect(stateUpdates).toContainEqual({
dashboard: { label: propDashboardTitle, value: propDashboardId },
});
});
test('chart placement logic finds row with available space', () => {
// Test case 1: Row has space (8 + 4 = 12 <= 12)
const positionJson1 = {

View File

@@ -54,7 +54,11 @@ import {
isUserAdmin,
} from 'src/dashboard/util/permissionUtils';
import { setSaveChartModalVisibility } from 'src/explore/actions/saveModalActions';
import { SaveActionType, ChartStatusType } from 'src/explore/types';
import {
SaveActionType,
ChartStatusType,
ExplorePageInitialData,
} from 'src/explore/types';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import {
removeChartState,
@@ -81,6 +85,7 @@ interface SaveModalProps extends RouteComponentProps {
isVisible: boolean;
dispatch: Dispatch;
theme: SupersetTheme;
metadata?: ExplorePageInitialData['metadata'];
}
type SaveModalState = {
@@ -162,6 +167,35 @@ class SaveModal extends Component<SaveModalProps, SaveModalState> {
t('An error occurred while loading dashboard information.'),
);
}
} else {
const metadataDashboards = this.props.metadata?.dashboards;
if (metadataDashboards?.length) {
// Fallback: the chart is already on one or more dashboards (from Explore API
// metadata). Pre-populate with the first dashboard the user can edit so the
// "Save & go to dashboard" button works out of the box.
try {
let editable: Dashboard | undefined;
for (const { id } of metadataDashboards) {
// eslint-disable-next-line no-await-in-loop
const result = await this.loadDashboard(id).catch(() => null);
if (result && canUserEditDashboard(result, this.props.user)) {
editable = result as Dashboard;
break;
}
}
if (editable) {
this.setState({
dashboard: {
label: editable.dashboard_title,
value: editable.id,
},
});
await this.loadTabs(editable.id);
}
} catch (error) {
logging.warn(error);
}
}
}
}
@@ -826,6 +860,7 @@ interface StateProps {
dashboards: any;
alert: any;
isVisible: boolean;
metadata?: ExplorePageInitialData['metadata'];
}
function mapStateToProps({
@@ -841,6 +876,7 @@ function mapStateToProps({
dashboards: saveModal.dashboards,
alert: saveModal.saveModalAlert,
isVisible: saveModal.isVisible,
metadata: explore.metadata,
};
}

View File

@@ -62,6 +62,7 @@ import { TagTypeEnum } from 'src/components/Tag/TagType';
import { loadTags } from 'src/components/Tag/utils';
import { Icons } from '@superset-ui/core/components/Icons';
import copyTextToClipboard from 'src/utils/copy';
import type Owner from 'src/types/Owner';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import SavedQueryPreviewModal from 'src/features/queries/SavedQueryPreviewModal';
import { findPermission } from 'src/utils/findPermission';
@@ -91,6 +92,15 @@ interface SavedQueryListProps {
};
}
type SavedQueryCellProps = {
row: {
original: SavedQueryObject & {
changed_by?: Owner | null;
created_by?: Owner | null;
};
};
};
const StyledTableLabel = styled.div`
.count {
margin-left: 5px;
@@ -435,12 +445,30 @@ function SavedQueryList({
changed_on_delta_humanized: changedOn,
},
},
}: any) => <ModifiedInfo user={changedBy} date={changedOn} />,
}: SavedQueryCellProps) => (
<ModifiedInfo user={changedBy ?? undefined} date={changedOn} />
),
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
size: 'xl',
id: 'changed_on_delta_humanized',
},
{
accessor: 'created_by.first_name',
Header: t('Created by'),
disableSortBy: true,
size: 'xl',
Cell: ({
row: {
original: { created_by: createdBy },
},
}: SavedQueryCellProps) =>
createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '',
},
{
accessor: 'created_by',
hidden: true,
},
{
Cell: ({ row: { original } }: any) => {
const handlePreview = () => {
@@ -589,6 +617,28 @@ function SavedQueryList({
),
paginate: true,
},
{
Header: t('Created by'),
key: 'created_by',
id: 'created_by',
input: 'select',
operator: FilterOperator.RelationOneMany,
unfilteredLabel: t('All'),
fetchSelects: createFetchRelated(
'saved_query',
'created_by',
createErrorHandler(errMsg =>
addDangerToast(
t(
'An error occurred while fetching created by values: %s',
errMsg,
),
),
),
user,
),
paginate: true,
},
],
[addDangerToast],
);

View File

@@ -25,8 +25,8 @@
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.3",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/parser": "^8.61.1",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-lodash": "^8.0.0",
@@ -37,7 +37,7 @@
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.0"
"typescript-eslint": "^8.61.1"
},
"engines": {
"node": "^24.16.0",
@@ -1844,17 +1844,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
"integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
"integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/type-utils": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/type-utils": "8.61.1",
"@typescript-eslint/utils": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1867,7 +1867,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/parser": "^8.61.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -1883,16 +1883,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1908,14 +1908,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
"integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
"integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.61.0",
"@typescript-eslint/types": "^8.61.0",
"@typescript-eslint/tsconfig-utils": "^8.61.1",
"@typescript-eslint/types": "^8.61.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1930,14 +1930,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
"integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
"integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0"
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1948,9 +1948,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
"integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
"integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1965,15 +1965,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
"integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
"integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/utils": "8.61.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1990,9 +1990,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
"integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
"integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2004,16 +2004,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
"integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
"integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.61.0",
"@typescript-eslint/tsconfig-utils": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"@typescript-eslint/project-service": "8.61.1",
"@typescript-eslint/tsconfig-utils": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2071,16 +2071,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
"integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
"integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0"
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2095,13 +2095,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
"integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
"integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/types": "8.61.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -6191,16 +6191,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
"integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
"integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0"
"@typescript-eslint/eslint-plugin": "8.61.1",
"@typescript-eslint/parser": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/utils": "8.61.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -7930,16 +7930,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
"integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
"integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/type-utils": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/type-utils": "8.61.1",
"@typescript-eslint/utils": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -7954,75 +7954,75 @@
}
},
"@typescript-eslint/parser": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"debug": "^4.4.3"
}
},
"@typescript-eslint/project-service": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
"integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
"integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
"dev": true,
"requires": {
"@typescript-eslint/tsconfig-utils": "^8.61.0",
"@typescript-eslint/types": "^8.61.0",
"@typescript-eslint/tsconfig-utils": "^8.61.1",
"@typescript-eslint/types": "^8.61.1",
"debug": "^4.4.3"
}
},
"@typescript-eslint/scope-manager": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
"integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
"integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0"
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1"
}
},
"@typescript-eslint/tsconfig-utils": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
"integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
"integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
"dev": true,
"requires": {}
},
"@typescript-eslint/type-utils": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
"integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
"integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/utils": "8.61.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
}
},
"@typescript-eslint/types": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
"integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
"integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
"integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
"integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
"dev": true,
"requires": {
"@typescript-eslint/project-service": "8.61.0",
"@typescript-eslint/tsconfig-utils": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"@typescript-eslint/project-service": "8.61.1",
"@typescript-eslint/tsconfig-utils": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/visitor-keys": "8.61.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -8057,24 +8057,24 @@
}
},
"@typescript-eslint/utils": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
"integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
"integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0"
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1"
}
},
"@typescript-eslint/visitor-keys": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
"integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
"integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
"dev": true,
"requires": {
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/types": "8.61.1",
"eslint-visitor-keys": "^5.0.0"
},
"dependencies": {
@@ -11024,15 +11024,15 @@
"dev": true
},
"typescript-eslint": {
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
"integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
"version": "8.61.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
"integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
"dev": true,
"requires": {
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0"
"@typescript-eslint/eslint-plugin": "8.61.1",
"@typescript-eslint/parser": "8.61.1",
"@typescript-eslint/typescript-estree": "8.61.1",
"@typescript-eslint/utils": "8.61.1"
}
},
"uglify-js": {

View File

@@ -33,8 +33,8 @@
"@types/lodash": "^4.17.24",
"@types/node": "^25.9.3",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.61.0",
"@typescript-eslint/parser": "^8.61.0",
"@typescript-eslint/eslint-plugin": "^8.61.1",
"@typescript-eslint/parser": "^8.61.1",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-lodash": "^8.0.0",
@@ -45,7 +45,7 @@
"ts-node": "^10.9.2",
"tscw-config": "^1.1.2",
"typescript": "^6.0.3",
"typescript-eslint": "^8.61.0"
"typescript-eslint": "^8.61.1"
},
"engines": {
"node": "^24.16.0",

View File

@@ -66,9 +66,9 @@ def cidr_func(req: AdvancedDataTypeRequest) -> AdvancedDataTypeResponse:
else:
resp["display_value"] = ", ".join(
map( # noqa: C417
lambda x: f"{x['start']} - {x['end']}"
if isinstance(x, dict)
else str(x),
lambda x: (
f"{x['start']} - {x['end']}" if isinstance(x, dict) else str(x)
),
resp["values"],
)
)

View File

@@ -95,9 +95,9 @@ def port_translation_func(req: AdvancedDataTypeRequest) -> AdvancedDataTypeRespo
else:
resp["display_value"] = ", ".join(
map( # noqa: C417
lambda x: f"{x['start']} - {x['end']}"
if isinstance(x, dict)
else str(x),
lambda x: (
f"{x['start']} - {x['end']}" if isinstance(x, dict) else str(x)
),
resp["values"],
)
)

Some files were not shown because too many files have changed in this diff Show More