mirror of
https://github.com/apache/superset.git
synced 2026-09-01 13:01:33 +00:00
Merge remote-tracking branch 'origin/master' into remove-legacy-viz-pipeline
Routine rebase; the only conflict was package-lock.json, regenerated via npm install against the already-cleanly-merged package.json. Everything else (package.json, plugin-chart-partition's package.json, superset/views/base.py, superset/views/core.py, tests/integration_tests/security_tests.py) merged automatically.
This commit is contained in:
Submodule .github/actions/chart-releaser-action deleted from a917fd15b2
@@ -23,7 +23,7 @@ on:
|
||||
|
||||
jobs:
|
||||
bump-python-package:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
actions: write
|
||||
contents: write
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ concurrency:
|
||||
jobs:
|
||||
check_db_migration_conflict:
|
||||
name: Check DB migration conflict
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -21,6 +22,10 @@ jobs:
|
||||
check-python-deps:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
@@ -46,7 +51,7 @@ jobs:
|
||||
- name: Login to Docker Hub
|
||||
if: steps.check.outputs.python
|
||||
continue-on-error: true
|
||||
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
||||
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Auto-approve Dependabot patch bumps
|
||||
|
||||
# Posts an approving review on Dependabot PRs that only bump a patch
|
||||
# version, using the same trigger/guard convention already proven to work
|
||||
# for Dependabot PRs in sync-requirements-for-python-dep-upgrade-pr.yml
|
||||
# (plain `pull_request` gets a working, write-capable GITHUB_TOKEN here
|
||||
# because Dependabot pushes branches directly to this repo, not a fork).
|
||||
#
|
||||
# This does NOT auto-merge anything - repo-wide auto-merge is disabled
|
||||
# (Settings > General > Pull Requests > "Allow auto-merge" is off), and
|
||||
# flipping that is a separate, repo-wide decision this workflow doesn't
|
||||
# make on its own. Branch protection also still requires 1 approving
|
||||
# review; this just means that review can already exist by the time a
|
||||
# human looks at the PR, for the (large majority of) ecosystems whose
|
||||
# files aren't matched by any CODEOWNERS pattern. One ecosystem - the npm
|
||||
# bump under .github/actions - matches the /.github/ CODEOWNERS entry, so
|
||||
# those PRs will still need a human owner's approval regardless of this
|
||||
# workflow; it posts a review there too, but that alone won't satisfy the
|
||||
# code-owner requirement.
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Cancel a superseded run if Dependabot pushes to the same PR again before
|
||||
# the previous run finished (matches the pattern used elsewhere in
|
||||
# superset-docs-verify.yml).
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
approve-patch-bump:
|
||||
name: Approve patch-level bump
|
||||
# Mirrors the guard in sync-requirements-for-python-dep-upgrade-pr.yml:
|
||||
# limited to (1) PRs authored by Dependabot and (2) the upstream repo,
|
||||
# since forked PRs don't get a write-capable token here anyway.
|
||||
if: >
|
||||
github.repository == 'apache/superset' &&
|
||||
github.event.pull_request.user.login == 'dependabot[bot]' &&
|
||||
github.event.pull_request.head.repo.fork == false
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write # to post the approving review via `gh pr review`
|
||||
steps:
|
||||
- name: Fetch Dependabot metadata
|
||||
id: metadata
|
||||
# This exact SHA is on ASF Infra's action allowlist
|
||||
# (apache/infrastructure-actions approved_patterns.yml) as of this
|
||||
# writing. Do not bump without opening an Infra ticket to allow
|
||||
# the new SHA first!
|
||||
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
||||
|
||||
- name: Approve patch-level bump
|
||||
if: steps.metadata.outputs.update-type == 'version-update:semver-patch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }}
|
||||
run: |
|
||||
gh pr review --approve "$PR_URL" \
|
||||
--body "Auto-approved: patch-level bump only ($DEPENDENCY_NAMES)."
|
||||
@@ -20,11 +20,12 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -47,8 +48,12 @@ jobs:
|
||||
python-dependency-liccheck:
|
||||
# NOTE: Configuration for liccheck lives in our pyproject.yml.
|
||||
# You cannot use a liccheck.ini file in this workflow.
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
setup_matrix:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
matrix_config: ${{ steps.set_matrix.outputs.matrix_config }}
|
||||
@@ -213,3 +213,14 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
docker compose -f docker-compose-image-tag.yml up superset-init --exit-code-from superset-init
|
||||
|
||||
actions-timeline:
|
||||
needs: [docker-build, docker-compose-image-tag]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
# gate on. Restrict to the canonical repo: forks cannot mint a valid OIDC
|
||||
# token for this package and must not publish.
|
||||
if: github.repository == 'apache/superset'
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # required for npm trusted publishing (OIDC)
|
||||
|
||||
@@ -16,7 +16,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
embedded-sdk-test:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
defaults:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
name: Generate Report
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
superbot-orglabel:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Label Merge Conflicts
|
||||
|
||||
# Sweeps every open PR and labels the ones GitHub reports as CONFLICTING with
|
||||
# `requires:rebase` (removing it once a rebase makes the PR mergeable again),
|
||||
# so the label can be used to filter the PR backlog for the ones that need a
|
||||
# rebase before they can be reviewed/merged.
|
||||
#
|
||||
# The action itself always re-checks *every* open PR via GraphQL on each run
|
||||
# regardless of what triggered it (see eps1lon/actions-label-merge-conflict's
|
||||
# sources/main.ts) - there's no way to scope it to "just this PR". The
|
||||
# project's own README suggests triggering on `push` (to the default branch)
|
||||
# plus `pull_request_target: [synchronize]`, but on a repo with Superset's PR
|
||||
# volume that combination would re-sweep the entire open-PR list on every
|
||||
# merge to master *and* every push to *any* open PR - many times an hour.
|
||||
# A schedule bounds that to a fixed, predictable cadence instead; adjust it
|
||||
# if 2 hours turns out to be too slow or too chatty in practice.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
# Avoid two full backlog sweeps racing (a manual workflow_dispatch landing
|
||||
# mid-schedule-tick, say); queue rather than cancel so an in-progress
|
||||
# paginated sweep always runs to completion.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
label-merge-conflicts:
|
||||
# Scheduled/dispatch workflows still run on forks that carry this file;
|
||||
# skip anywhere but the canonical repo.
|
||||
if: github.repository == 'apache/superset'
|
||||
name: Label Merge Conflicts
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write # to add/remove requires:rebase and need:merge
|
||||
steps:
|
||||
# ASF Infra allowlists this whole action via a wildcard
|
||||
# (eps1lon/actions-label-merge-conflict@*), so any pinned SHA/version
|
||||
# is already fine here - no Infra ticket needed for future bumps.
|
||||
- uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
|
||||
with:
|
||||
dirtyLabel: "requires:rebase"
|
||||
# A conflicting PR isn't actually ready to merge; strip that signal
|
||||
# if it was previously set so reviewers don't act on a stale one.
|
||||
removeOnDirtyLabel: "need:merge"
|
||||
repoToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Intentionally no commentOnDirty/commentOnClean: the label alone is
|
||||
# the signal (matches the label's existing description, and avoids
|
||||
# a one-time comment storm across the whole backlog on first run).
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||
with:
|
||||
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
jobs:
|
||||
latest-release:
|
||||
name: Add/update tag to new release
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ concurrency:
|
||||
jobs:
|
||||
license_check:
|
||||
name: License Check
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# 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.
|
||||
#
|
||||
# Mirror the Docker Hub service-container images that CI relies on into the
|
||||
# repository's GitHub Container Registry (GHCR) namespace.
|
||||
#
|
||||
# WHY: CI jobs declare Postgres/MySQL/Redis/Presto as `services:` containers
|
||||
# pulled anonymously from Docker Hub. Anonymous pulls share the runner's IP
|
||||
# rate limit, which causes intermittent timeouts / 429s / 502s on `master`
|
||||
# and same-repo PRs. The obvious fix — adding `credentials:` to the service
|
||||
# blocks — breaks fork PRs hard: forks can't read secrets, so the templated
|
||||
# username/password resolve to '' and GitHub rejects the workflow at parse
|
||||
# time ("Unexpected value ''"), failing every fork job at "Set up job".
|
||||
#
|
||||
# Mirroring to GHCR sidesteps both problems: public GHCR images are pulled
|
||||
# without Docker Hub's anonymous rate limit AND without any credentials, so
|
||||
# the consuming workflows need no `credentials:` block and forks work
|
||||
# unchanged.
|
||||
#
|
||||
# ONE-TIME BOOTSTRAP (maintainer, after this lands on the default branch):
|
||||
# 1. Run this workflow once (Actions tab → "Mirror service images to GHCR"
|
||||
# → Run workflow), or wait for the weekly schedule.
|
||||
# 2. In the org's Packages settings, set each mirrored package's visibility
|
||||
# to **public** (apache/superset → ci/postgres, ci/mysql, ci/redis,
|
||||
# ci/presto). Public visibility is what lets fork CI pull without auth.
|
||||
# 3. Only then merge the follow-up that repoints the `services.*.image`
|
||||
# refs at these GHCR copies and drops the `credentials:` blocks.
|
||||
#
|
||||
# NOTE: this mirrors only the images declared as `services:` containers (the
|
||||
# ones that broke forks). The `bde2020` hive-metastore image pulled via
|
||||
# `docker compose` in the Presto/Hive job is a separate path and is left for
|
||||
# a follow-up.
|
||||
|
||||
name: Mirror service images to GHCR
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Weekly, Monday 06:00 UTC — keeps the mirror fresh as upstream tags move.
|
||||
- cron: "0 6 * * 1"
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: mirror-service-images
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
# Never run on forks: they lack both the secrets and write access to the
|
||||
# apache GHCR namespace, so a scheduled run there would only ever fail.
|
||||
if: github.repository == 'apache/superset'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Keep this list in sync with the `services.*.image` refs in
|
||||
# superset-e2e.yml, superset-python-integrationtest.yml, and
|
||||
# superset-python-presto-hive.yml.
|
||||
image:
|
||||
- postgres:17-alpine
|
||||
- redis:7-alpine
|
||||
- mysql:8.0
|
||||
- starburstdata/presto:350-e.6
|
||||
steps:
|
||||
- name: Log in to Docker Hub (authenticated source pulls)
|
||||
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GHCR (push target)
|
||||
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Copy image to GHCR
|
||||
env:
|
||||
# Pass the matrix value through the environment rather than
|
||||
# interpolating it into the shell, to avoid template injection.
|
||||
SRC_IMAGE: ${{ matrix.image }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Destination keeps the image's short name (drop any namespace),
|
||||
# under a `ci/` prefix in this repo's GHCR namespace.
|
||||
name="${SRC_IMAGE##*/}"
|
||||
dst="ghcr.io/${GITHUB_REPOSITORY}/ci/${name}"
|
||||
echo "Mirroring docker.io/${SRC_IMAGE} -> ${dst}"
|
||||
# imagetools copies the full (multi-arch) manifest registry-to-
|
||||
# registry without a local pull/retag/push round trip.
|
||||
docker buildx imagetools create --tag "${dst}" "docker.io/${SRC_IMAGE}"
|
||||
echo "- \`docker.io/${SRC_IMAGE}\` → \`${dst}\`" >> "${GITHUB_STEP_SUMMARY}"
|
||||
@@ -17,7 +17,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
check-hold-label:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- name: Check for 'hold' label
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
lint-check:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
@@ -15,6 +15,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -188,3 +189,12 @@ jobs:
|
||||
echo "📖 More details here: https://superset.apache.org/docs/contributing/development#git-hooks"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
actions-timeline:
|
||||
needs: pre-commit
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -40,6 +41,10 @@ jobs:
|
||||
ports:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
@@ -30,6 +30,7 @@ concurrency:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
config:
|
||||
@@ -59,6 +60,10 @@ jobs:
|
||||
name: Build & Deploy
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
|
||||
@@ -340,3 +340,14 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "playwright-tests result: $RESULT (changes: $CHANGES)"
|
||||
|
||||
actions-timeline:
|
||||
needs: [cypress-matrix, playwright-tests, cypress-matrix-required, playwright-tests-required]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -11,6 +11,7 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
# cancel previous workflow jobs for PRs
|
||||
concurrency:
|
||||
@@ -69,3 +70,12 @@ jobs:
|
||||
with:
|
||||
name: superset-extensions-cli-coverage-html
|
||||
path: htmlcov/
|
||||
|
||||
actions-timeline:
|
||||
needs: test-superset-extensions-cli-package
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -201,3 +201,14 @@ jobs:
|
||||
run: |
|
||||
docker run --rm $TAG bash -c \
|
||||
"npm run build-storybook && npx playwright install-deps && npx playwright install chromium && npm run test-storybook:ci"
|
||||
|
||||
actions-timeline:
|
||||
needs: [report-coverage, lint-frontend, validate-frontend, test-storybook]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
# Serialize runs per PR without cancelling: when a first-time contributor's
|
||||
# queued runs are approved together, cancel-in-progress lets an older run
|
||||
@@ -19,8 +20,12 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
lint-test:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
@@ -38,6 +43,11 @@ jobs:
|
||||
with:
|
||||
install-superset: "false"
|
||||
|
||||
# Still vendored (not de-vendored like chart-releaser-action below): the
|
||||
# allowlisted helm/chart-testing-action@v2.8.0 depends internally on
|
||||
# astral-sh/setup-uv@v7.0.0, which isn't itself on the ASF Actions
|
||||
# allowlist (only v8.1.0+ are, at apache/infrastructure-actions'
|
||||
# actions.yml). Needs an INFRA request before this can de-vendor too.
|
||||
- name: Set up chart-testing
|
||||
uses: ./.github/actions/chart-testing-action
|
||||
|
||||
|
||||
@@ -95,13 +95,8 @@ jobs:
|
||||
# Return to the original branch
|
||||
git checkout local_gha_temp
|
||||
|
||||
- name: Fetch/list all tags
|
||||
run: |
|
||||
git submodule update
|
||||
cat .github/actions/chart-releaser-action/action.yml
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: ./.github/actions/chart-releaser-action
|
||||
uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0
|
||||
with:
|
||||
version: v1.6.0
|
||||
charts_dir: helm
|
||||
|
||||
@@ -170,3 +170,14 @@ jobs:
|
||||
${{ github.workspace }}/superset-frontend/playwright-results/
|
||||
${{ github.workspace }}/superset-frontend/test-results/
|
||||
name: playwright-experimental-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
|
||||
|
||||
actions-timeline:
|
||||
needs: playwright-tests-experimental
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -255,3 +255,14 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "test-postgres result: $RESULT"
|
||||
|
||||
actions-timeline:
|
||||
needs: [test-mysql, test-postgres, test-sqlite, test-postgres-required]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -158,3 +158,14 @@ jobs:
|
||||
verbose: true
|
||||
use_oidc: true
|
||||
slug: apache/superset
|
||||
|
||||
actions-timeline:
|
||||
needs: [test-postgres-presto, test-postgres-hive]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -101,7 +101,14 @@ jobs:
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
- name: Check unit-tests result
|
||||
env:
|
||||
RESULT: ${{ needs.unit-tests.result }}
|
||||
|
||||
@@ -153,3 +153,14 @@ jobs:
|
||||
- name: Fail if regression detected
|
||||
if: steps.regression.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
actions-timeline:
|
||||
needs: [frontend-check-translations, babel-extract]
|
||||
if: always()
|
||||
runs-on: ubuntu-26.04
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- uses: Kesin11/actions-timeline@7bf79990b7c09f5dfb570ac30b814ca597bd538e # v3.1.1
|
||||
with:
|
||||
expand-composite-actions: true
|
||||
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
app-checks:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
|
||||
continue-on-error: true
|
||||
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
|
||||
uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -32,7 +32,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
|
||||
@@ -11,7 +11,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
config:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
outputs:
|
||||
has-secrets: ${{ steps.check.outputs.has-secrets }}
|
||||
steps:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
process-and-upload:
|
||||
needs: config
|
||||
if: needs.config.outputs.has-secrets
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
name: Generate Reports
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
welcome:
|
||||
runs-on: ubuntu-26.04
|
||||
runs-on: ubuntu-slim
|
||||
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
[submodule ".github/actions/chart-testing-action"]
|
||||
path = .github/actions/chart-testing-action
|
||||
url = https://github.com/helm/chart-testing-action
|
||||
[submodule ".github/actions/chart-releaser-action"]
|
||||
path = .github/actions/chart-releaser-action
|
||||
url = https://github.com/helm/chart-releaser-action
|
||||
[submodule ".github/actions/github-action-push-to-another-repository"]
|
||||
path = .github/actions/github-action-push-to-another-repository
|
||||
url = https://github.com/cpina/github-action-push-to-another-repository
|
||||
|
||||
+1
-1
@@ -423,7 +423,7 @@ git push origin ${SUPERSET_VERSION}
|
||||
|
||||
### Publishing a Convenience Release to PyPI
|
||||
|
||||
Extract the release to the `/tmp` folder to build the PiPY release. Files in the `/tmp` folder will be automatically deleted by the OS.
|
||||
Extract the release to the `/tmp` folder to build the PyPI release. Files in the `/tmp` folder will be automatically deleted by the OS.
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/superset && cd /tmp/superset
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
@@ -15,8 +17,6 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Function to determine Python command
|
||||
get_python_command() {
|
||||
if command -v python3 &>/dev/null; then
|
||||
|
||||
@@ -38,7 +38,7 @@ RESET='\033[0m'
|
||||
echo -e "${GREEN}Updating package lists...${RESET}"
|
||||
apt-get update -qq
|
||||
|
||||
echo -e "${GREEN}Installing packages: $@${RESET}"
|
||||
echo -e "${GREEN}Installing packages: $*${RESET}"
|
||||
apt-get install -yqq --no-install-recommends "$@"
|
||||
|
||||
echo -e "${GREEN}Autoremoving unnecessary packages...${RESET}"
|
||||
|
||||
@@ -163,10 +163,10 @@ do
|
||||
# Iterate through the components of the version strings
|
||||
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
|
||||
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="greater"
|
||||
break
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="lesser"
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -112,12 +112,61 @@ USER superset
|
||||
CMD ["/app/docker/entrypoints/run-server.sh"]
|
||||
```
|
||||
|
||||
### Adding translations to a custom image
|
||||
|
||||
The pattern above, a small Dockerfile that just extends `FROM apache/superset:...`, can't add
|
||||
translations after the fact. By the time an official tag is published, its frontend and backend
|
||||
layers have already had non-English translation files stripped out unless `BUILD_TRANSLATIONS`
|
||||
was set at build time (see below), and there's no `superset/translations` source tree left in the
|
||||
final image to compile from.
|
||||
|
||||
To get translations into your own image, you need to build from the full Superset source (a
|
||||
clone or fork of this repo) rather than extend a published tag. The most efficient way to do this
|
||||
is to append your customizations as one more stage at the end of the repo's own `Dockerfile`, so
|
||||
Docker can reuse the cached upstream layers and only rebuild what your stage adds:
|
||||
|
||||
```Dockerfile
|
||||
# Append this to the end of the repo's Dockerfile
|
||||
# Keep this tag in sync with the branch/tag of the repo you cloned, so the
|
||||
# translation files built from source match the keys the runtime expects:
|
||||
FROM apache/superset:5.0.0 AS my-custom-image
|
||||
USER root
|
||||
|
||||
# Pull the translation files out of the earlier build stages (frontend
|
||||
# .json in `superset-node`, backend .mo in `python-translation-compiler`).
|
||||
# Those stages' own cleanup only matches single-character extensions, so
|
||||
# the source `.po` files can still be present here; strip them explicitly
|
||||
# so this stage only keeps the compiled translations.
|
||||
COPY --from=superset-node /app/superset/translations superset/translations
|
||||
COPY --from=python-translation-compiler /app/translations_mo superset/translations
|
||||
RUN find superset/translations -name '*.po' -delete
|
||||
|
||||
USER superset
|
||||
```
|
||||
|
||||
Then build with:
|
||||
|
||||
```bash
|
||||
docker build --target=my-custom-image --build-arg=BUILD_TRANSLATIONS=true -t mysuperset:5.0.0 .
|
||||
```
|
||||
|
||||
You can combine this with the database-driver/dependency pattern above by adding your own
|
||||
`RUN uv pip install ...` step before switching back to `USER superset`. See
|
||||
[issue #35959](https://github.com/apache/superset/issues/35959) for the discussion this pattern
|
||||
came out of, credit to the community for working it out.
|
||||
|
||||
## Key ARGs in Dockerfile
|
||||
|
||||
- `BUILD_TRANSLATIONS`: whether to build the translations into the image. For the
|
||||
frontend build this tells webpack to strip out all locales other than `en` from
|
||||
the `moment-timezone` library. For the backendthis skips compiling the
|
||||
`*.po` translation files
|
||||
- `BUILD_TRANSLATIONS`: whether to compile non-English translations into the image.
|
||||
When `true`, the frontend build converts the `*.po` files to locale JSON and the
|
||||
backend runs `pybabel compile` to produce `*.mo` files; both source `*.po` files
|
||||
are stripped afterward either way. When `false` (the default), those compile
|
||||
steps are skipped and only `en` ships. This only takes effect when building the image from source
|
||||
(`docker build` against this repo's own `Dockerfile`); it has no effect on a downstream
|
||||
Dockerfile that just extends an already-published tag, see
|
||||
"Adding translations to a custom image" above. Note that the backend `pybabel compile`
|
||||
step ignores its exit code, so a `.po` file with a compile error won't fail the build;
|
||||
check the build logs for `pybabel` warnings if a locale's backend strings aren't showing up.
|
||||
- `DEV_MODE`: whether to skip the frontend build, this is used by our `docker-compose` dev setup
|
||||
where we mount the local volume and build using `webpack` in `--watch` mode, meaning as you
|
||||
alter the code in the local file system, webpack, from within a docker image used for this
|
||||
|
||||
+6
-6
@@ -58,11 +58,11 @@
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^10.5.3",
|
||||
"@storybook/addon-docs": "^10.5.4",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.46",
|
||||
"antd": "^6.5.1",
|
||||
"baseline-browser-mapping": "^2.11.1",
|
||||
"antd": "^6.5.2",
|
||||
"baseline-browser-mapping": "^2.11.4",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.2",
|
||||
@@ -77,7 +77,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.2.0",
|
||||
"storybook": "^10.5.3",
|
||||
"storybook": "^10.5.4",
|
||||
"swagger-ui-react": "^5.32.11",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
@@ -95,11 +95,11 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.7.0",
|
||||
"globals": "^17.8.0",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"webpack": "^5.108.2"
|
||||
"webpack": "^5.109.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
+58
-65
@@ -3779,7 +3779,7 @@
|
||||
"@rc-component/virtual-list" "^1.2.0"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.10.0", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1":
|
||||
"@rc-component/trigger@^3.0.0", "@rc-component/trigger@^3.10.1", "@rc-component/trigger@^3.6.15", "@rc-component/trigger@^3.7.1":
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-3.10.1.tgz#cb28e1bc0745a2af6897dd7ec774f9b56dc88f86"
|
||||
integrity sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==
|
||||
@@ -3798,13 +3798,13 @@
|
||||
"@rc-component/util" "^1.11.1"
|
||||
clsx "^2.1.1"
|
||||
|
||||
"@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.11.1", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.7.0", "@rc-component/util@^1.9.0":
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.11.1.tgz#07d698908339c55648e4f974afa739345e65b483"
|
||||
integrity sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==
|
||||
"@rc-component/util@^1.10.1", "@rc-component/util@^1.11.0", "@rc-component/util@^1.11.1", "@rc-component/util@^1.12.0", "@rc-component/util@^1.2.0", "@rc-component/util@^1.2.1", "@rc-component/util@^1.3.0", "@rc-component/util@^1.4.0", "@rc-component/util@^1.7.0", "@rc-component/util@^1.9.0":
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.12.0.tgz#58e453585810bcb8a35ff1aafd5e01187457b86f"
|
||||
integrity sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==
|
||||
dependencies:
|
||||
is-mobile "^5.0.0"
|
||||
react-is "^18.2.0"
|
||||
react-is "^19.2.7"
|
||||
|
||||
"@rc-component/virtual-list@^1.0.1", "@rc-component/virtual-list@^1.2.0":
|
||||
version "1.2.0"
|
||||
@@ -4005,23 +4005,23 @@
|
||||
resolved "https://registry.yarnpkg.com/@standard-schema/utils/-/utils-0.3.0.tgz#3d5e608f16c2390c10528e98e59aef6bf73cae7b"
|
||||
integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==
|
||||
|
||||
"@storybook/addon-docs@^10.5.3":
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.3.tgz#b5666cf20361d85c98cc836b2bfcdbbb72af3e97"
|
||||
integrity sha512-MI1VDMSMQk78YxjIdt7WlrVOiA3TzTP00lRed1LeXh0fCvA9jxz9YXJI2+XigsLaxCSuOAEf/l35/GTLDMHD8A==
|
||||
"@storybook/addon-docs@^10.5.4":
|
||||
version "10.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-10.5.4.tgz#1906605d3dee2d86ff91bb79b444edb2961536fb"
|
||||
integrity sha512-2Z/x2pKEmXOCQjmttYzPuQBu9aWeMly8uEs3msrCTBLiHs/F7IlBFnMu0Z+T2Qvk0LEy8O93AlcPSP76aCcKjw==
|
||||
dependencies:
|
||||
"@mdx-js/react" "^3.0.0"
|
||||
"@storybook/csf-plugin" "10.5.3"
|
||||
"@storybook/csf-plugin" "10.5.4"
|
||||
"@storybook/icons" "^2.0.2"
|
||||
"@storybook/react-dom-shim" "10.5.3"
|
||||
"@storybook/react-dom-shim" "10.5.4"
|
||||
react "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
react-dom "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
ts-dedent "^2.0.0"
|
||||
|
||||
"@storybook/csf-plugin@10.5.3":
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.3.tgz#0818709c2761910f1e42710f6231f773eb97fb41"
|
||||
integrity sha512-mkPq6zru8fN5+46uC1cZEbKW2ws1hh9KvF4g4/Gu8pNbKnvqULPhk0/Bf0ZCtlr7zI7DvcFhyCy3dbvN+2n4Gw==
|
||||
"@storybook/csf-plugin@10.5.4":
|
||||
version "10.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-10.5.4.tgz#750cb3cb74693e482e48c945f1bd29252ab1dc52"
|
||||
integrity sha512-DSp5Z/eZlRnKq0KrKLJE6uoYf/Ysc+FP0Z5DVTGnOrie+z3tC0lNi9I4RB++EXkJeUDS9/4dxvJZVSWjhLlXxw==
|
||||
dependencies:
|
||||
unplugin "^2.3.5"
|
||||
|
||||
@@ -4035,10 +4035,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-2.1.0.tgz#edfc2450a39c5e780f28c6cbc49acd7bff59b41a"
|
||||
integrity sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==
|
||||
|
||||
"@storybook/react-dom-shim@10.5.3":
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.3.tgz#e176a6549aa02002e375cb66467a00c5403745f8"
|
||||
integrity sha512-eUWBsRRax5R3MDJVFs/CrFDF1bYS58AMB9tX02lLRuiZe6xy1cKh3CRFS+2xH571l0fNaXQ+7j69TOJ0fk2tmA==
|
||||
"@storybook/react-dom-shim@10.5.4":
|
||||
version "10.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-10.5.4.tgz#c92bb6d8b26aaa4523efa5a8b99178a0a76a3d52"
|
||||
integrity sha512-YdlppEOReg8MvTECRNuf79gu2zL83JqKDHIR/65eS0M6y+ue9pkpfjYo7hZVIcyOcRd9npBDXMdt2kC92bCuaA==
|
||||
|
||||
"@superset-ui/core@^0.20.4":
|
||||
version "0.20.4"
|
||||
@@ -5835,11 +5835,6 @@ acorn-dynamic-import@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
|
||||
integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
|
||||
|
||||
acorn-import-phases@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7"
|
||||
integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==
|
||||
|
||||
acorn-jsx@^5.0.0, acorn-jsx@^5.0.1, acorn-jsx@^5.3.2:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||
@@ -6028,10 +6023,10 @@ ansis@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7"
|
||||
integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==
|
||||
|
||||
antd@^6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.1.tgz#2623db1f3b0ae32a2e311ef20f2f1413934d0094"
|
||||
integrity sha512-VZVVF9zYI6S0NHqboVhCoY9Iiqj6dphW1NPB+sEaAf2HuIQ0haXWXj7ZvAXTRDzusktV6+cvvrSZEdRi4twATg==
|
||||
antd@^6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-6.5.2.tgz#d771211beddf539f37303f862df24e8aaf32b7c4"
|
||||
integrity sha512-ntYx0lr4Jq192QnBkDWkDqEeoberXZ34vSE9SgiP/0J6DY8O0pzR3bVZLBsdpCSguVkwjtEAP+QNeMN7LNAvgw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.1"
|
||||
"@ant-design/cssinjs" "^2.1.2"
|
||||
@@ -6073,9 +6068,9 @@ antd@^6.5.1:
|
||||
"@rc-component/tour" "~2.4.0"
|
||||
"@rc-component/tree" "~1.3.2"
|
||||
"@rc-component/tree-select" "~1.11.0"
|
||||
"@rc-component/trigger" "^3.10.0"
|
||||
"@rc-component/trigger" "^3.10.1"
|
||||
"@rc-component/upload" "~1.1.1"
|
||||
"@rc-component/util" "^1.11.1"
|
||||
"@rc-component/util" "^1.12.0"
|
||||
clsx "^2.1.1"
|
||||
dayjs "^1.11.11"
|
||||
scroll-into-view-if-needed "^3.1.0"
|
||||
@@ -6368,10 +6363,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.1, baseline-browser-mapping@^2.9.19:
|
||||
version "2.11.1"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz#390b4714558634093df77add4acca0c5c0c1605e"
|
||||
integrity sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==
|
||||
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.4, baseline-browser-mapping@^2.9.19:
|
||||
version "2.11.4"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz#3da1a877a8be2ec06495123ce994b43af58cc55e"
|
||||
integrity sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
@@ -8066,10 +8061,10 @@ encodeurl@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
enhanced-resolve@^5.22.2:
|
||||
version "5.24.1"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz#b2439adf5d31d7e4764de1f9ecf942d6cd3fc874"
|
||||
integrity sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==
|
||||
enhanced-resolve@^5.24.2:
|
||||
version "5.24.5"
|
||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz#b4dad3255b7545f07ba5535189868e9f85f47573"
|
||||
integrity sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
tapable "^2.3.3"
|
||||
@@ -9031,10 +9026,10 @@ globals@^14.0.0:
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
|
||||
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
|
||||
|
||||
globals@^17.7.0:
|
||||
version "17.7.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.7.0.tgz#553d55090b4dde8209ec2da42580d6e7e7d8b10d"
|
||||
integrity sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==
|
||||
globals@^17.8.0:
|
||||
version "17.8.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.8.0.tgz#a1f213a06adcd0eec38004c5cd39fef7af7e0830"
|
||||
integrity sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==
|
||||
|
||||
globalthis@^1.0.4:
|
||||
version "1.0.4"
|
||||
@@ -10431,11 +10426,6 @@ liquid-json@0.3.1:
|
||||
resolved "https://registry.yarnpkg.com/liquid-json/-/liquid-json-0.3.1.tgz#9155a18136d8a6b2615e5f16f9a2448ab6b50eea"
|
||||
integrity sha512-wUayTU8MS827Dam6MxgD72Ui+KOSF+u/eIqpatOtjnvgJ0+mnDq33uC2M7J0tPK+upe/DpUAuK4JUU89iBoNKQ==
|
||||
|
||||
loader-runner@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.2.tgz#9913d3a15971f8f635915e601fb5c9d495d918e9"
|
||||
integrity sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==
|
||||
|
||||
loader-utils@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
|
||||
@@ -13327,11 +13317,16 @@ react-is@^17.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-is@^18.0.0, react-is@^18.2.0:
|
||||
react-is@^18.0.0:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react-is@^19.2.7:
|
||||
version "19.2.8"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.8.tgz#09826f9fbc187bc668e3e5c62edc001f804d5018"
|
||||
integrity sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==
|
||||
|
||||
react-json-view-lite@^2.3.0:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz#c7ff011c7cc80e9900abc7aa4916c6a5c6d6c1c6"
|
||||
@@ -14620,10 +14615,10 @@ stop-iteration-iterator@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
internal-slot "^1.1.0"
|
||||
|
||||
storybook@^10.5.3:
|
||||
version "10.5.3"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.3.tgz#72ee7cc02e3b6353eeec9633c2367437cf2ac86e"
|
||||
integrity sha512-c8Wumu5qz0N2fnzWBxcPzUsY+8BpKBKChNyl4BEh9qhMV6KW587gL8il8emRB+4Hay+zMjDHA7cIeTkl4FKYuw==
|
||||
storybook@^10.5.4:
|
||||
version "10.5.4"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.5.4.tgz#3190fc10568053bd32f198ef164c045904cd2582"
|
||||
integrity sha512-bmLxPsxVSPnbeiZqYQpozyNOiJXfk+pf7WfHZflvPkwT6Y+rvYz3Cj/D6H4Kf2jHpuDNiMXBKO3yawLN2OWirg==
|
||||
dependencies:
|
||||
"@storybook/global" "^5.0.0"
|
||||
"@storybook/icons" "^2.0.2"
|
||||
@@ -15855,20 +15850,20 @@ webpack-merge@^6.0.1:
|
||||
flat "^5.0.2"
|
||||
wildcard "^2.0.1"
|
||||
|
||||
webpack-sources@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.0.tgz#87bf7f5801a4e985b1f1c92b64b9620a02f76d08"
|
||||
integrity sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==
|
||||
webpack-sources@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.1.tgz#76c2418486dcc02b2aa0694c104176c2858fe84a"
|
||||
integrity sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==
|
||||
|
||||
webpack-virtual-modules@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
||||
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
|
||||
|
||||
webpack@^5.108.2, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.108.4"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.108.4.tgz#141818a411662773a0bb32dc5536acc5409943b7"
|
||||
integrity sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w==
|
||||
webpack@^5.109.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.109.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.109.0.tgz#871d8eee5e2d5e6eaf5ec8d1a6db74ea65491030"
|
||||
integrity sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==
|
||||
dependencies:
|
||||
"@types/estree" "^1.0.8"
|
||||
"@types/json-schema" "^7.0.15"
|
||||
@@ -15876,22 +15871,20 @@ webpack@^5.108.2, webpack@^5.88.1, webpack@^5.95.0:
|
||||
"@webassemblyjs/wasm-edit" "^1.14.1"
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
acorn "^8.16.0"
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.28.1"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.22.2"
|
||||
enhanced-resolve "^5.24.2"
|
||||
es-module-lexer "^2.1.0"
|
||||
eslint-scope "5.1.1"
|
||||
events "^3.2.0"
|
||||
graceful-fs "^4.2.11"
|
||||
loader-runner "^4.3.2"
|
||||
mime-db "^1.54.0"
|
||||
minimizer-webpack-plugin "^5.6.1"
|
||||
neo-async "^2.6.2"
|
||||
schema-utils "^4.3.3"
|
||||
tapable "^2.3.0"
|
||||
watchpack "^2.5.2"
|
||||
webpack-sources "^3.5.0"
|
||||
webpack-sources "^3.5.1"
|
||||
|
||||
webpackbar@^7.0.0:
|
||||
version "7.0.0"
|
||||
|
||||
+10
-10
@@ -42,13 +42,13 @@ dependencies = [
|
||||
# ``google-auth`` 2.53+ dropped it, so Superset must declare it
|
||||
# explicitly to keep fresh ``pip install apache-superset`` working
|
||||
# without the ``base.txt`` lock file (#40962).
|
||||
"cachetools>=7.1.4, <8",
|
||||
"cachetools>=7.1.6, <8",
|
||||
"celery>=5.6.3, <6.0.0",
|
||||
"click>=8.4.2",
|
||||
"click-option-group",
|
||||
"colorama",
|
||||
"flask-cors>=6.0.5, <7.0",
|
||||
"croniter>=6.2.2",
|
||||
"croniter>=6.2.4",
|
||||
"cron-descriptor",
|
||||
"cryptography>=49.0.0, <50.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
@@ -62,7 +62,7 @@ dependencies = [
|
||||
"flask-session>=0.4.0, <1.0",
|
||||
"flask-wtf>=1.3.0, <2.0",
|
||||
"geopy",
|
||||
"greenlet<=3.5.3, >=3.5.3",
|
||||
"greenlet<=3.5.4, >=3.5.4",
|
||||
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
|
||||
"hashids>=1.3.1, <2",
|
||||
# holidays>=0.45 required for security fix
|
||||
@@ -90,7 +90,7 @@ dependencies = [
|
||||
"paramiko>=3.4.0, <4.0", # 4.0 removed DSSKey, still referenced by sshtunnel
|
||||
"pgsanity",
|
||||
"Pillow>=11.0.0, <13",
|
||||
"polyline>=2.0.0, <3.0",
|
||||
"polyline>=2.0.4, <3.0",
|
||||
"pydantic>=2.8.0",
|
||||
"pyparsing>=3.3.2, <4",
|
||||
"python-dateutil",
|
||||
@@ -122,14 +122,14 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
athena = ["pyathena[pandas]>=2, <4"]
|
||||
athena = ["pyathena[pandas]>=3.35.2, <4"]
|
||||
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
|
||||
bigquery = [
|
||||
"pandas-gbq>=0.35.0",
|
||||
"sqlalchemy-bigquery>=1.17.0",
|
||||
"google-cloud-bigquery>=3.42.2",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.4.2, <2.0"]
|
||||
clickhouse = ["clickhouse-connect>=1.6.0, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
crate = ["sqlalchemy-cratedb>=0.41.0, <1"]
|
||||
d1 = [
|
||||
@@ -139,7 +139,7 @@ d1 = [
|
||||
]
|
||||
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
|
||||
databricks = [
|
||||
"databricks-sql-connector>=4.2.6, <4.4.0",
|
||||
"databricks-sql-connector>=4.4.0, <4.5.0",
|
||||
"databricks-sqlalchemy==1.0.5",
|
||||
]
|
||||
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"]
|
||||
@@ -173,7 +173,7 @@ hana = ["hdbcli==2.29.25", "sqlalchemy_hana==3.0.3"]
|
||||
hive = [
|
||||
"pyhive[hive_pure_sasl]>=0.7.0",
|
||||
"tableschema",
|
||||
"thrift>=0.23.0, <1.0.0",
|
||||
"thrift>=0.24.0, <1.0.0",
|
||||
"thrift_sasl>=0.4.3, < 1.0.0",
|
||||
]
|
||||
impala = ["impyla>=0.24.0, <0.25"]
|
||||
@@ -196,7 +196,7 @@ playwright = ["playwright>=1.61.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.12"]
|
||||
presto = ["pyhive[presto]>=0.6.5"]
|
||||
trino = ["trino>=0.338.0"]
|
||||
prophet = ["prophet>=1.1.6, <2"]
|
||||
prophet = ["prophet>=1.3.0, <2"]
|
||||
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
|
||||
risingwave = ["sqlalchemy-risingwave"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
|
||||
@@ -206,7 +206,7 @@ sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
|
||||
spark = [
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
"tableschema",
|
||||
"thrift>=0.23.0, <1",
|
||||
"thrift>=0.24.0, <1",
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.8.9",
|
||||
|
||||
@@ -46,7 +46,7 @@ cachelib==0.13.0
|
||||
# via
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==7.1.4
|
||||
cachetools==7.1.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
cattrs==25.1.1
|
||||
# via requests-cache
|
||||
@@ -86,7 +86,7 @@ colorama==0.4.6
|
||||
# flask-appbuilder
|
||||
cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.2.2
|
||||
croniter==6.2.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==49.0.0
|
||||
# via
|
||||
@@ -166,7 +166,7 @@ google-auth==2.53.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# shillelagh
|
||||
greenlet==3.5.3
|
||||
greenlet==3.5.4
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
@@ -291,7 +291,7 @@ pillow==12.3.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
polyline==2.0.2
|
||||
polyline==2.0.4
|
||||
# via apache-superset (pyproject.toml)
|
||||
prison==0.2.1
|
||||
# via flask-appbuilder
|
||||
|
||||
@@ -101,7 +101,7 @@ cachelib==0.13.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-caching
|
||||
# flask-session
|
||||
cachetools==7.1.4
|
||||
cachetools==7.1.6
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -178,7 +178,7 @@ cron-descriptor==1.4.5
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
croniter==6.2.2
|
||||
croniter==6.2.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -377,7 +377,7 @@ googleapis-common-protos==1.66.0
|
||||
# via
|
||||
# google-api-core
|
||||
# grpcio-status
|
||||
greenlet==3.5.3
|
||||
greenlet==3.5.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -687,7 +687,7 @@ pluggy==1.5.0
|
||||
# via pytest
|
||||
polib==1.2.0
|
||||
# via apache-superset
|
||||
polyline==2.0.2
|
||||
polyline==2.0.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -703,7 +703,7 @@ prompt-toolkit==3.0.51
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# click-repl
|
||||
prophet==1.2.0
|
||||
prophet==1.3.0
|
||||
# via apache-superset
|
||||
proto-plus==1.25.0
|
||||
# via google-api-core
|
||||
|
||||
@@ -35,7 +35,7 @@ acquire_rat_jar () {
|
||||
wget --quiet ${URL} -O "$JAR_DL" && mv "$JAR_DL" "$JAR"
|
||||
else
|
||||
printf "You do not have curl or wget installed, please install rat manually.\n"
|
||||
exit -1
|
||||
exit 255
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -44,7 +44,7 @@ acquire_rat_jar () {
|
||||
# We failed to download
|
||||
rm "$JAR"
|
||||
printf "Our attempt to download rat locally to ${JAR} failed. Please install rat manually.\n"
|
||||
exit -1
|
||||
exit 255
|
||||
fi
|
||||
printf "Done downloading.\n"
|
||||
}
|
||||
|
||||
@@ -163,10 +163,10 @@ do
|
||||
# Iterate through the components of the version strings
|
||||
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
|
||||
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="greater"
|
||||
break
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
|
||||
compare_result="lesser"
|
||||
break
|
||||
fi
|
||||
|
||||
@@ -215,7 +215,7 @@ Common permissions you might need:
|
||||
|
||||
By default, the Embedded SDK creates an `iframe` element without a `referrerPolicy` value enforced. This means that a policy defined for `iframe` elements at the host app level would reflect to it.
|
||||
|
||||
This can be an issue as during the embedded enablement for a dashboard it's possible to specify which domain(s) are allowed to embed the dashboard, and this validation happens throuth the `Referrer` header. That said, in case the hosting app has a more restrictive policy that would omit this header, this validation would fail.
|
||||
This can be an issue as during the embedded enablement for a dashboard it's possible to specify which domain(s) are allowed to embed the dashboard, and this validation happens through the `Referrer` header. That said, in case the hosting app has a more restrictive policy that would omit this header, this validation would fail.
|
||||
|
||||
Use the `referrerPolicy` parameter in the `embedDashboard` method to specify [a particular policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy) that works for your implementation.
|
||||
|
||||
|
||||
Generated
+391
-820
File diff suppressed because it is too large
Load Diff
@@ -130,9 +130,9 @@
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^6.7.0",
|
||||
"@rjsf/core": "^6.7.1",
|
||||
"@rjsf/utils": "^6.6.2",
|
||||
"@rjsf/validator-ajv8": "^6.7.0",
|
||||
"@rjsf/validator-ajv8": "^6.7.1",
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
@@ -176,7 +176,7 @@
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^6.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.6",
|
||||
"fs-extra": "^11.4.0",
|
||||
"fuse.js": "^7.5.0",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
@@ -184,7 +184,7 @@
|
||||
"geostyler-openlayers-parser": "^5.7.1",
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.9.0",
|
||||
"google-auth-library": "^10.9.1",
|
||||
"immer": "^11.1.15",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
@@ -193,7 +193,7 @@
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.26.0",
|
||||
"mapbox-gl": "^3.27.0",
|
||||
"markdown-to-jsx": "^9.9.0",
|
||||
"match-sorter": "^8.3.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
@@ -265,17 +265,17 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.3",
|
||||
"@storybook/addon-links": "10.5.3",
|
||||
"@storybook/react-webpack5": "10.5.3",
|
||||
"@storybook/addon-docs": "10.5.4",
|
||||
"@storybook/addon-links": "10.5.4",
|
||||
"@storybook/react-webpack5": "10.5.4",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.46",
|
||||
"@swc/plugin-emotion": "^14.15.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^7.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
@@ -306,7 +306,7 @@
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"baseline-browser-mapping": "^2.11.1",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"concurrently": "^10.0.4",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.4",
|
||||
@@ -323,13 +323,13 @@
|
||||
"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.1",
|
||||
"eslint-plugin-storybook": "10.5.3",
|
||||
"eslint-plugin-storybook": "10.5.4",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||
"history": "^5.3.0",
|
||||
"html-webpack-plugin": "^5.6.7",
|
||||
"html-webpack-plugin": "^5.6.8",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^30.4.2",
|
||||
"jest-environment-jsdom": "^30.4.1",
|
||||
@@ -355,7 +355,7 @@
|
||||
"source-map": "^0.8.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.3",
|
||||
"storybook": "10.5.4",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.1",
|
||||
"swc-loader": "^0.2.7",
|
||||
@@ -365,7 +365,7 @@
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"wait-on": "^9.1.0",
|
||||
"webpack": "^5.108.4",
|
||||
"webpack": "^5.109.0",
|
||||
"webpack-bundle-analyzer": "^5.3.1",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.6.2",
|
||||
"chalk": "^6.0.0",
|
||||
"lodash-es": "^4.18.1",
|
||||
"yeoman-generator": "^8.2.2",
|
||||
"yosay": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.6",
|
||||
"fs-extra": "^11.4.0",
|
||||
"jest": "^30.4.2",
|
||||
"yeoman-test": "^11.6.0"
|
||||
},
|
||||
|
||||
@@ -93,9 +93,9 @@
|
||||
"typescript": "^5.0.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
|
||||
@@ -34,9 +34,9 @@
|
||||
"@ant-design/icons": "^5.6.1 || ^6.0.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
|
||||
@@ -92,9 +92,9 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.3.0",
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
ControlSubSectionHeader,
|
||||
D3_TIME_FORMAT_DOCS,
|
||||
DEFAULT_TIME_FORMAT,
|
||||
formatSelectOptions,
|
||||
getStandardizedControls,
|
||||
sharedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { showValueControl } from '../controls';
|
||||
@@ -245,6 +247,17 @@ const config: ControlPanelConfig = {
|
||||
multi: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
metric: getStandardizedControls().shiftMetric(),
|
||||
// Waterfall's `groupby` is a single-value control (multi: false;
|
||||
// buildQuery groups by the whole array but transformProps only reads
|
||||
// groupby[0]), so only one column should be taken off the queue here.
|
||||
// Using popAllColumns() would let a memorized multi-column breakdown
|
||||
// (e.g. from Table/Pivot) push extra dimensions into the SQL query
|
||||
// that never surface in the rendered chart.
|
||||
groupby: ensureIsArray(getStandardizedControls().shiftColumn()),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 { SqlaFormData } from '@superset-ui/core';
|
||||
|
||||
// Mock getStandardizedControls so we can assert the Waterfall control panel
|
||||
// actually consumes (shifts) the queued metric and column instead of
|
||||
// leaving them for the next viz-type switch to pick up again. Regression
|
||||
// test for https://github.com/apache/superset/issues/32835, where switching
|
||||
// away from and back to another chart type (e.g. Line) produced duplicate
|
||||
// metrics because Waterfall never drained the shared standardized-controls
|
||||
// queue.
|
||||
const mockShiftMetric = jest.fn(() => 'shiftedMetric');
|
||||
const mockShiftColumn = jest.fn(() => 'shiftedColumn');
|
||||
|
||||
jest.mock('@superset-ui/chart-controls', () => {
|
||||
const actual = jest.requireActual('@superset-ui/chart-controls');
|
||||
return {
|
||||
...actual,
|
||||
getStandardizedControls: jest.fn(() => ({
|
||||
shiftMetric: mockShiftMetric,
|
||||
shiftColumn: mockShiftColumn,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import controlPanel from '../../src/Waterfall/controlPanel';
|
||||
|
||||
test('formDataOverrides consumes a single metric and a single column from getStandardizedControls', () => {
|
||||
expect(controlPanel.formDataOverrides).toBeDefined();
|
||||
|
||||
const dummyFormData = { someProp: 'test' } as unknown as SqlaFormData;
|
||||
const newFormData = controlPanel.formDataOverrides!(dummyFormData);
|
||||
|
||||
// original properties are preserved
|
||||
expect(newFormData.someProp).toBe('test');
|
||||
|
||||
// only a single metric is taken (Waterfall only supports one metric),
|
||||
// leaving any remaining queued metrics for the next viz-type switch
|
||||
expect(newFormData.metric).toBe('shiftedMetric');
|
||||
expect(mockShiftMetric).toHaveBeenCalled();
|
||||
|
||||
// only a single column is taken for the (single-value) groupby control,
|
||||
// leaving any remaining queued columns for the next viz-type switch;
|
||||
// popping the whole queue here would let buildQuery group by columns
|
||||
// that transformProps (which only reads groupby[0]) never renders.
|
||||
expect(newFormData.groupby).toEqual(['shiftedColumn']);
|
||||
expect(mockShiftColumn).toHaveBeenCalled();
|
||||
});
|
||||
@@ -32,7 +32,7 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.7",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/jest": "^30.0.0",
|
||||
"jest": "^30.4.2"
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@math.gl/web-mercator": "^4.1.0",
|
||||
"mapbox-gl": "^3.26.0",
|
||||
"mapbox-gl": "^3.27.0",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"react-map-gl": "^8.1.1",
|
||||
"supercluster": "^8.0.1"
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react": "^15.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^8.2.0",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"lodash": "^4.18.1",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"ngeohash": "^0.6.3",
|
||||
"ngeohash": "^0.6.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-map-gl": "^8.1.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
|
||||
@@ -216,6 +216,12 @@ test('should render the error', async () => {
|
||||
.spyOn(SupersetClient, 'post')
|
||||
.mockRejectedValue(new Error('Something went wrong'));
|
||||
await waitForRender();
|
||||
// The error is wrapped in an Alert component with a stable headline and the
|
||||
// raw error text in the description — no more bare ``<pre>`` elements.
|
||||
expect(await screen.findByRole('alert')).toBeVisible();
|
||||
expect(
|
||||
await screen.findByText('Failed to load drill-to-detail rows'),
|
||||
).toBeVisible();
|
||||
expect(screen.getByText('Error: Something went wrong')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import BooleanCell from '@superset-ui/core/components/Table/cell-renderers/Boole
|
||||
import NullCell from '@superset-ui/core/components/Table/cell-renderers/NullCell';
|
||||
import TimeCell from '@superset-ui/core/components/Table/cell-renderers/TimeCell';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { getDatasourceSamples } from 'src/components/Chart/chartAction';
|
||||
import Table, {
|
||||
ColumnsType,
|
||||
@@ -362,13 +363,18 @@ export default function DrillDetailPane({
|
||||
if (responseError) {
|
||||
// Render error if page download failed
|
||||
tableContent = (
|
||||
<pre
|
||||
<div
|
||||
css={css`
|
||||
margin-top: ${theme.sizeUnit * 4}px;
|
||||
`}
|
||||
>
|
||||
{responseError}
|
||||
</pre>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={t('Failed to load drill-to-detail rows')}
|
||||
description={responseError}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (bootstrapping) {
|
||||
// Render loading if first page hasn't loaded
|
||||
|
||||
@@ -50,6 +50,7 @@ const DISABLED_REASONS = {
|
||||
DATABASE: t(
|
||||
'Drill to detail is disabled for this database. Change the database settings to enable it.',
|
||||
),
|
||||
DATASOURCE: t('Drill to detail is not available for this datasource type.'),
|
||||
NO_AGGREGATIONS: t(
|
||||
'Drill to detail is disabled because this chart does not group data by dimension value.',
|
||||
),
|
||||
@@ -116,6 +117,17 @@ export const useDrillDetailMenuItems = ({
|
||||
datasources[formData.datasource]?.database?.disable_drill_to_detail,
|
||||
);
|
||||
|
||||
// Capability flag on the datasource itself. Datasources that don't model
|
||||
// raw rows (e.g. semantic views) opt out via ``supports_drill_to_detail``
|
||||
// in the explore data payload.
|
||||
const datasourceSupportsDrillToDetail = useSelector<
|
||||
RootState,
|
||||
boolean | undefined
|
||||
>(
|
||||
({ datasources }) =>
|
||||
datasources[formData.datasource]?.supports_drill_to_detail,
|
||||
);
|
||||
|
||||
const openModal = useCallback(
|
||||
(filters: BinaryQueryObjectFilterClause[], event: MouseEvent) => {
|
||||
onClick(event);
|
||||
@@ -158,7 +170,10 @@ export const useDrillDetailMenuItems = ({
|
||||
|
||||
let drillDisabled;
|
||||
let drillByDisabled;
|
||||
if (drillToDetailDisabled) {
|
||||
if (datasourceSupportsDrillToDetail === false) {
|
||||
drillDisabled = DISABLED_REASONS.DATASOURCE;
|
||||
drillByDisabled = DISABLED_REASONS.DATASOURCE;
|
||||
} else if (drillToDetailDisabled) {
|
||||
drillDisabled = DISABLED_REASONS.DATABASE;
|
||||
drillByDisabled = DISABLED_REASONS.DATABASE;
|
||||
} else if (handlesDimensionContextMenu) {
|
||||
|
||||
+42
@@ -444,3 +444,45 @@ test('context menu renders <NULL> for null dimension values', async () => {
|
||||
await expectDrillToDetailByEnabled();
|
||||
await expectDrillToDetailByDimension(filterNull);
|
||||
});
|
||||
|
||||
const buildStateWithUnsupportedDatasource = () => {
|
||||
const baseState = getMockStoreWithNativeFilters().getState();
|
||||
const datasourceKey = defaultFormData.datasource as string;
|
||||
return {
|
||||
...baseState,
|
||||
datasources: {
|
||||
...baseState.datasources,
|
||||
[datasourceKey]: {
|
||||
...baseState.datasources[datasourceKey],
|
||||
supports_drill_to_detail: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
test('dropdown menu when datasource opts out via supports_drill_to_detail=false', async () => {
|
||||
cleanup();
|
||||
render(<MockRenderChart formData={defaultFormData} />, {
|
||||
useRouter: true,
|
||||
useRedux: true,
|
||||
initialState: buildStateWithUnsupportedDatasource(),
|
||||
});
|
||||
|
||||
await expectDrillToDetailDisabled(
|
||||
'Drill to detail is not available for this datasource type.',
|
||||
);
|
||||
await expectNoDrillToDetailBy();
|
||||
});
|
||||
|
||||
test('context menu when datasource opts out via supports_drill_to_detail=false', async () => {
|
||||
cleanup();
|
||||
render(<MockRenderChart formData={defaultFormData} isContextMenu />, {
|
||||
useRouter: true,
|
||||
useRedux: true,
|
||||
initialState: buildStateWithUnsupportedDatasource(),
|
||||
});
|
||||
|
||||
const message = 'Drill to detail is not available for this datasource type.';
|
||||
await expectDrillToDetailDisabled(message);
|
||||
await expectDrillToDetailByDisabled(message);
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ export function ErrorMessageWithStackTrace({
|
||||
fallback,
|
||||
compact,
|
||||
closable = true,
|
||||
errorMitigationFunction,
|
||||
}: Props) {
|
||||
// Check if a custom error message component was registered for this message
|
||||
if (error) {
|
||||
@@ -77,6 +78,7 @@ export function ErrorMessageWithStackTrace({
|
||||
error={error}
|
||||
source={source}
|
||||
subtitle={subtitle}
|
||||
errorMitigationFunction={errorMitigationFunction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import * as reduxHooks from 'react-redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore, Store } from 'redux';
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { act, render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { ErrorLevel, ErrorSource, ErrorTypeEnum } from '@superset-ui/core';
|
||||
import { reRunQuery } from 'src/SqlLab/actions/sqlLab';
|
||||
import { triggerQuery } from 'src/components/Chart/chartAction';
|
||||
@@ -166,15 +166,55 @@ describe('OAuth2RedirectMessage Component', () => {
|
||||
render(setup());
|
||||
|
||||
simulateBroadcastMessage({ tabId: 'tabId' });
|
||||
simulateStorageMessage({ tabId: 'tabId' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(reRunQuery).toHaveBeenCalledWith({ sql: 'SELECT * FROM table' });
|
||||
});
|
||||
expect(reRunQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('dispatches reRunQuery action when storage event has matching tab ID', async () => {
|
||||
render(setup());
|
||||
|
||||
simulateStorageMessage({ tabId: 'tabId' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(reRunQuery).toHaveBeenCalledWith({ sql: 'SELECT * FROM table' });
|
||||
});
|
||||
});
|
||||
|
||||
test('dispatches reRunQuery action when storage event has matching tab ID', async () => {
|
||||
render(setup());
|
||||
test('waits for the SQL Lab query before consuming the completion', async () => {
|
||||
const initialState = {
|
||||
sqlLab: {
|
||||
queries: {},
|
||||
queryEditors: [{ id: 'editor-id', latestQueryId: 'query-id' }],
|
||||
tabHistory: ['editor-id'],
|
||||
},
|
||||
explore: { slice: null },
|
||||
charts: {},
|
||||
dashboardInfo: {},
|
||||
};
|
||||
const delayedQueryStore = createStore(
|
||||
(state: typeof initialState = initialState, action) =>
|
||||
action.type === 'load-query'
|
||||
? {
|
||||
...state,
|
||||
sqlLab: {
|
||||
...state.sqlLab,
|
||||
queries: { 'query-id': { sql: 'SELECT * FROM table' } },
|
||||
},
|
||||
}
|
||||
: state,
|
||||
);
|
||||
render(setup({}, delayedQueryStore));
|
||||
|
||||
simulateBroadcastMessage({ tabId: 'tabId' });
|
||||
expect(reRunQuery).not.toHaveBeenCalled();
|
||||
|
||||
act(() => {
|
||||
delayedQueryStore.dispatch({ type: 'load-query' });
|
||||
});
|
||||
simulateStorageMessage({ tabId: 'tabId' });
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -234,4 +274,22 @@ describe('OAuth2RedirectMessage Component', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('runs scoped mitigation once instead of CRUD invalidation', async () => {
|
||||
const errorMitigationFunction = jest.fn();
|
||||
render(
|
||||
setup({
|
||||
source: 'crud' as ErrorSource,
|
||||
errorMitigationFunction,
|
||||
}),
|
||||
);
|
||||
|
||||
simulateBroadcastMessage({ tabId: 'tabId' });
|
||||
simulateStorageMessage({ tabId: 'tabId' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(errorMitigationFunction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(api.util.invalidateTags).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
|
||||
@@ -58,15 +58,16 @@ interface OAuth2RedirectExtra {
|
||||
*
|
||||
* After the token has been stored, the opened tab will broadcast a message to the
|
||||
* original tab and close itself. This component, running on the original tab, listens
|
||||
* on a same-origin BroadcastChannel and re-runs the query for the user once it
|
||||
* receives the success message — be it in SQL Lab, Explore, or a dashboard. Both tabs
|
||||
* share a "tab ID" (a UUID generated by the backend) which is echoed back through the
|
||||
* channel so the original tab only reacts to its own OAuth2 flow.
|
||||
* for same-origin BroadcastChannel and storage notifications and re-runs the query
|
||||
* for the user once it receives the success message — be it in SQL Lab, Explore, or
|
||||
* a dashboard. Both tabs share a "tab ID" (a UUID generated by the backend) which is
|
||||
* echoed back so the original tab only reacts to its own OAuth2 flow.
|
||||
*/
|
||||
export function OAuth2RedirectMessage({
|
||||
error,
|
||||
source,
|
||||
closable,
|
||||
errorMitigationFunction,
|
||||
}: ErrorMessageComponentProps<OAuth2RedirectExtra>) {
|
||||
const { extra, level } = error;
|
||||
|
||||
@@ -103,13 +104,17 @@ export function OAuth2RedirectMessage({
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const lastHandledTabIdRef = useRef<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const handleOAuthComplete = (tabId?: string) => {
|
||||
if (tabId !== extra.tab_id) {
|
||||
if (tabId !== extra.tab_id || tabId === lastHandledTabIdRef.current) {
|
||||
return;
|
||||
}
|
||||
if (source === 'sqllab' && query) {
|
||||
|
||||
if (errorMitigationFunction) {
|
||||
errorMitigationFunction();
|
||||
} else if (source === 'sqllab' && query) {
|
||||
dispatch(reRunQuery(query));
|
||||
} else if (source === 'explore') {
|
||||
dispatch(triggerQuery(true, chartId));
|
||||
@@ -123,7 +128,11 @@ export function OAuth2RedirectMessage({
|
||||
'Tables',
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
lastHandledTabIdRef.current = tabId;
|
||||
};
|
||||
|
||||
const channel =
|
||||
@@ -156,7 +165,16 @@ export function OAuth2RedirectMessage({
|
||||
window.removeEventListener('storage', handleStorage);
|
||||
channel?.close();
|
||||
};
|
||||
}, [source, extra.tab_id, dispatch, query, chartId, chartList, dashboardId]);
|
||||
}, [
|
||||
source,
|
||||
extra.tab_id,
|
||||
dispatch,
|
||||
query,
|
||||
chartId,
|
||||
chartList,
|
||||
dashboardId,
|
||||
errorMitigationFunction,
|
||||
]);
|
||||
|
||||
const body = (
|
||||
<p>
|
||||
|
||||
@@ -27,6 +27,7 @@ export type ErrorMessageComponentProps<ExtraType = Record<string, any> | null> =
|
||||
subtitle?: ReactNode;
|
||||
compact?: boolean;
|
||||
closable?: boolean;
|
||||
errorMitigationFunction?: () => void;
|
||||
};
|
||||
|
||||
export type ErrorMessageComponent = ComponentType<ErrorMessageComponentProps>;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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 { render, screen, fireEvent } from 'spec/helpers/testing-library';
|
||||
import DeleteComponentButton from './DeleteComponentButton';
|
||||
|
||||
test('exposes an accessible name without rendering visible label text', () => {
|
||||
render(<DeleteComponentButton onDelete={jest.fn()} />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Delete component' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Delete component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onDelete when clicked', () => {
|
||||
const onDelete = jest.fn();
|
||||
render(<DeleteComponentButton onDelete={onDelete} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Delete component' }));
|
||||
|
||||
expect(onDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
import { MouseEventHandler, FC } from 'react';
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import type { IconType } from '@superset-ui/core/components/Icons/types';
|
||||
import IconButton from './IconButton';
|
||||
@@ -33,6 +34,8 @@ const DeleteComponentButton: FC<DeleteComponentButtonProps> = ({
|
||||
}) => (
|
||||
<IconButton
|
||||
onClick={onDelete}
|
||||
label={t('Delete component')}
|
||||
hideVisibleLabel
|
||||
icon={<Icons.DeleteOutlined iconSize={iconSize ?? 'l'} />}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -73,3 +73,17 @@ test('renders the provided label', () => {
|
||||
|
||||
expect(screen.getByText('My Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hideVisibleLabel suppresses visible text but keeps the accessible name', () => {
|
||||
render(
|
||||
<IconButton
|
||||
icon={icon}
|
||||
onClick={jest.fn()}
|
||||
label="My Label"
|
||||
hideVisibleLabel
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('My Label')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'My Label' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import { styled, SupersetTheme } from '@apache-superset/core/theme';
|
||||
interface IconButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||
icon: JSX.Element;
|
||||
label?: string;
|
||||
hideVisibleLabel?: boolean;
|
||||
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||
disabled?: boolean;
|
||||
'data-test'?: string;
|
||||
@@ -63,6 +64,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
{
|
||||
icon,
|
||||
label,
|
||||
hideVisibleLabel,
|
||||
onClick,
|
||||
onKeyDown,
|
||||
disabled,
|
||||
@@ -75,6 +77,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
{...rest}
|
||||
ref={ref}
|
||||
type="button"
|
||||
aria-label={label}
|
||||
isDisabled={disabled}
|
||||
aria-disabled={disabled}
|
||||
data-test={dataTest}
|
||||
@@ -91,7 +94,7 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label && <StyledSpan>{label}</StyledSpan>}
|
||||
{label && !hideVisibleLabel && <StyledSpan>{label}</StyledSpan>}
|
||||
</StyledButton>
|
||||
),
|
||||
);
|
||||
|
||||
@@ -566,3 +566,31 @@ test('should pass filterState from dataMask to ChartContainer', () => {
|
||||
mockFilterState,
|
||||
);
|
||||
});
|
||||
|
||||
test('should pass chartStackTrace to ChartContainer so dashboard chart errors stay expandable', () => {
|
||||
// Regression guard for #31858: the dashboard chart wrapper stopped forwarding
|
||||
// the stack trace, so failed charts rendered a flat error with no "See more"
|
||||
// affordance while the same error in Explore stayed expandable.
|
||||
const stackTrace = 'Traceback (most recent call last): ValueError: boom';
|
||||
|
||||
setup(
|
||||
{},
|
||||
{
|
||||
...defaultState,
|
||||
charts: {
|
||||
...defaultState.charts,
|
||||
[queryId]: {
|
||||
...defaultState.charts[queryId],
|
||||
chartStatus: 'failed',
|
||||
chartAlert: 'Something went wrong',
|
||||
chartStackTrace: stackTrace,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(capturedChartContainerProps).toHaveProperty(
|
||||
'chartStackTrace',
|
||||
stackTrace,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -789,6 +789,7 @@ const Chart = (props: ChartProps) => {
|
||||
chartAlert={chart.chartAlert ?? undefined}
|
||||
chartId={props.id}
|
||||
chartStatus={chartStatus ?? undefined}
|
||||
chartStackTrace={chart.chartStackTrace ?? undefined}
|
||||
datasource={datasource}
|
||||
dashboardId={props.dashboardId}
|
||||
initialValues={EMPTY_OBJECT}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from 'spec/helpers/testing-library';
|
||||
import { fireEvent, render, screen } from 'spec/helpers/testing-library';
|
||||
|
||||
import BackgroundStyleDropdown from 'src/dashboard/components/menu/BackgroundStyleDropdown';
|
||||
import IconButton from 'src/dashboard/components/IconButton';
|
||||
@@ -200,6 +200,15 @@ test('should call deleteComponent when deleted', () => {
|
||||
expect(deleteComponent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('settings IconButton exposes an accessible name without visible label text', () => {
|
||||
setup({ component: columnWithoutChildren, editMode: true });
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Column settings' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Column settings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should pass its own width as availableColumnCount to children', () => {
|
||||
const { getByTestId } = setup();
|
||||
expect(getByTestId('mock-dashboard-component')).toHaveTextContent(
|
||||
|
||||
@@ -247,6 +247,8 @@ const Column = (props: ColumnProps) => {
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => handleChangeFocus(true)}
|
||||
label={t('Column settings')}
|
||||
hideVisibleLabel
|
||||
icon={<Icons.SettingOutlined iconSize="m" />}
|
||||
/>
|
||||
</HoverMenu>
|
||||
|
||||
@@ -145,7 +145,9 @@ describe('Header', () => {
|
||||
const deleteComponent = jest.fn();
|
||||
setup({ editMode: true, deleteComponent });
|
||||
|
||||
const trashButton = screen.getByRole('button', { name: 'delete' });
|
||||
const trashButton = screen.getByRole('button', {
|
||||
name: 'Delete component',
|
||||
});
|
||||
fireEvent.click(trashButton);
|
||||
|
||||
expect(deleteComponent).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -240,6 +240,15 @@ test('should call deleteComponent when deleted', () => {
|
||||
expect(deleteComponent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('settings IconButton exposes an accessible name without visible label text', () => {
|
||||
setup({ component: rowWithoutChildren, editMode: true });
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Row settings' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Row settings')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should pass appropriate availableColumnCount to children', () => {
|
||||
const { getByTestId } = setup();
|
||||
expect(getByTestId('mock-dashboard-component')).toHaveTextContent(
|
||||
|
||||
@@ -293,6 +293,8 @@ const Row = memo((props: RowProps) => {
|
||||
<DeleteComponentButton onDelete={handleDeleteComponent} />
|
||||
<IconButton
|
||||
onClick={() => handleChangeFocus(true)}
|
||||
label={t('Row settings')}
|
||||
hideVisibleLabel
|
||||
icon={<Icons.SettingOutlined iconSize="l" />}
|
||||
/>
|
||||
</HoverMenu>
|
||||
|
||||
@@ -232,6 +232,10 @@ export type Datasource = Dataset & {
|
||||
// Populated by the dashboard datasets API alongside ``type``; declared here
|
||||
// so callers can rely on structural typing instead of casting.
|
||||
datasource_type?: DatasourceType;
|
||||
/** False when the datasource can't return row samples (e.g. semantic views). */
|
||||
supports_samples?: boolean;
|
||||
/** False when the datasource can't answer drill-to-detail requests. */
|
||||
supports_drill_to_detail?: boolean;
|
||||
};
|
||||
export type DatasourcesState = {
|
||||
[key: string]: Datasource;
|
||||
|
||||
@@ -241,25 +241,44 @@ export const DataTablesPane = ({
|
||||
}
|
||||
}, [resultsTabFallback]);
|
||||
|
||||
// Hide the Samples tab for datasources that don't expose raw rows
|
||||
// (e.g. semantic views). The check is intentionally ``=== false`` so that
|
||||
// datasources from older backends that don't send the flag still show the
|
||||
// tab and preserve current behavior.
|
||||
const showSamplesTab = datasource?.supports_samples !== false;
|
||||
|
||||
// If the datasource swaps to one that doesn't support samples while the
|
||||
// Samples tab is active (e.g. the user picks a semantic view), the tab
|
||||
// disappears from ``tabItems`` and ``activeTabKey`` is orphaned. Fall back
|
||||
// to Results so the panel keeps rendering content.
|
||||
useEffect(() => {
|
||||
if (!showSamplesTab && activeTabKey === ResultTypes.Samples) {
|
||||
setActiveTabKey(ResultTypes.Results);
|
||||
}
|
||||
}, [showSamplesTab, activeTabKey]);
|
||||
const tabItems = [
|
||||
...queryResultsPanes,
|
||||
{
|
||||
key: ResultTypes.Samples,
|
||||
label: t('Samples'),
|
||||
children: (
|
||||
<StyledDiv>
|
||||
<SamplesPane
|
||||
datasource={datasource}
|
||||
queryFormData={queryFormData}
|
||||
queryForce={queryForce}
|
||||
isRequest={isRequest.samples}
|
||||
setForceQuery={setForceQuery}
|
||||
isVisible={ResultTypes.Samples === activeTabKey}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
</StyledDiv>
|
||||
),
|
||||
},
|
||||
...(showSamplesTab
|
||||
? [
|
||||
{
|
||||
key: ResultTypes.Samples,
|
||||
label: t('Samples'),
|
||||
children: (
|
||||
<StyledDiv>
|
||||
<SamplesPane
|
||||
datasource={datasource}
|
||||
queryFormData={queryFormData}
|
||||
queryForce={queryForce}
|
||||
isRequest={isRequest.samples}
|
||||
setForceQuery={setForceQuery}
|
||||
isVisible={ResultTypes.Samples === activeTabKey}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
</StyledDiv>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,6 +21,7 @@ import { t } from '@apache-superset/core/translation';
|
||||
import { ensureIsArray } from '@superset-ui/core';
|
||||
import { datasetLabelLower } from 'src/features/semanticLayers/label';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { GridTable } from 'src/components/GridTable';
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
|
||||
import { SamplesPaneProps } from '../types';
|
||||
|
||||
const Error = styled.pre`
|
||||
const ErrorAlertWrapper = styled.div`
|
||||
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
||||
`;
|
||||
|
||||
@@ -155,7 +156,14 @@ export const SamplesPane = ({
|
||||
rowLimitOptions={ROW_LIMIT_OPTIONS}
|
||||
onRowLimitChange={handleRowLimitChange}
|
||||
/>
|
||||
<Error>{responseError}</Error>
|
||||
<ErrorAlertWrapper>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={t('Failed to load samples')}
|
||||
description={responseError}
|
||||
/>
|
||||
</ErrorAlertWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+10
-2
@@ -27,13 +27,14 @@ import {
|
||||
QueryData,
|
||||
} from '@superset-ui/core';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Alert } from '@apache-superset/core/components';
|
||||
import { EmptyState, Loading } from '@superset-ui/core/components';
|
||||
import { getChartDataRequest } from 'src/components/Chart/chartAction';
|
||||
import { ResultsPaneProps, QueryResultInterface } from '../types';
|
||||
import { SingleQueryResultPane } from './SingleQueryResultPane';
|
||||
import { TableControls, ROW_LIMIT_OPTIONS } from './DataTableControls';
|
||||
|
||||
const Error = styled.pre`
|
||||
const ErrorAlertWrapper = styled.div`
|
||||
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
|
||||
`;
|
||||
|
||||
@@ -199,7 +200,14 @@ export const useResultsPane = ({
|
||||
isLoading={false}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
<Error>{responseError}</Error>
|
||||
<ErrorAlertWrapper>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={t('Failed to load results')}
|
||||
description={responseError}
|
||||
/>
|
||||
</ErrorAlertWrapper>
|
||||
</>
|
||||
);
|
||||
return Array(queryCount).fill(err);
|
||||
|
||||
+48
-1
@@ -19,7 +19,12 @@
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { FeatureFlag } from '@superset-ui/core';
|
||||
import * as copyUtils from 'src/utils/copy';
|
||||
import { render, screen, userEvent } from 'spec/helpers/testing-library';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
import { DataTablesPane } from '..';
|
||||
@@ -89,6 +94,48 @@ describe('DataTablesPane', () => {
|
||||
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Hides Samples tab when datasource opts out via supports_samples=false', async () => {
|
||||
const props = createDataTablesPaneProps(0);
|
||||
const propsWithoutSamples = {
|
||||
...props,
|
||||
datasource: { ...props.datasource, supports_samples: false },
|
||||
};
|
||||
render(<DataTablesPane {...propsWithoutSamples} />, { useRedux: true });
|
||||
expect(await screen.findByText('Results')).toBeVisible();
|
||||
expect(screen.queryByText('Samples')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Falls back to Results when active Samples tab disappears mid-session', async () => {
|
||||
// Regression for codeant Major finding on PR #41509: a datasource swap
|
||||
// that hides the Samples tab while it was the active tab used to leave
|
||||
// ``activeTabKey === 'samples'`` orphaned, rendering a blank panel.
|
||||
const props = createDataTablesPaneProps(0);
|
||||
const { rerender } = render(<DataTablesPane {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
// Open the panel and pick the Samples tab.
|
||||
userEvent.click(screen.getByLabelText('Expand data panel'));
|
||||
userEvent.click(await screen.findByText('Samples'));
|
||||
expect(await screen.findByLabelText('Collapse data panel')).toBeVisible();
|
||||
|
||||
// Swap to a datasource that doesn't support samples (e.g. a semantic
|
||||
// view). The Samples tab should disappear and the panel should land on
|
||||
// Results with content still rendered.
|
||||
rerender(
|
||||
<DataTablesPane
|
||||
{...props}
|
||||
datasource={{ ...props.datasource, supports_samples: false }}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Samples')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText('Results')).toBeVisible();
|
||||
// Panel stays expanded and renders Results content rather than going blank.
|
||||
expect(screen.getByLabelText('Collapse data panel')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should copy data table content correctly', async () => {
|
||||
fetchMock.post(
|
||||
'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D',
|
||||
|
||||
@@ -84,10 +84,14 @@ describe('SamplesPane', () => {
|
||||
const props = createSamplesPaneProps({
|
||||
datasourceId: 36,
|
||||
});
|
||||
const { findByText } = render(<SamplesPane {...props} />, {
|
||||
const { findByText, findByRole } = render(<SamplesPane {...props} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
// The error is now rendered inside an Alert component, with a clear
|
||||
// headline message and the raw error text as the description.
|
||||
expect(await findByRole('alert')).toBeVisible();
|
||||
expect(await findByText('Failed to load samples')).toBeVisible();
|
||||
expect(await findByText('Error: Bad request')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -49,6 +49,7 @@ export function CurrentCalendarFrame({ onChange, value }: FrameComponentProps) {
|
||||
wrap: true,
|
||||
}}
|
||||
size="large"
|
||||
value={value}
|
||||
onChange={(e: any) => {
|
||||
let newValue = e.target.value;
|
||||
newValue = newValue.trim();
|
||||
|
||||
+8
-2
@@ -16,9 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render } from 'spec/helpers/testing-library';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import { CurrentCalendarFrame } from '../components/CurrentCalendarFrame';
|
||||
import { CurrentWeek } from '../types';
|
||||
import { CurrentDay, CurrentWeek } from '../types';
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
@@ -33,3 +33,9 @@ test('returns null if value is not a valid CurrentRangeType', () => {
|
||||
);
|
||||
expect(container.childNodes.length).toBe(0);
|
||||
});
|
||||
|
||||
test('selects the radio button matching the current value', () => {
|
||||
render(<CurrentCalendarFrame onChange={mockOnChange} value={CurrentDay} />);
|
||||
|
||||
expect(screen.getByRole('radio', { name: CurrentDay })).toBeChecked();
|
||||
});
|
||||
|
||||
@@ -74,6 +74,18 @@ export type Datasource = Dataset & {
|
||||
schema?: string;
|
||||
is_sqllab_view?: boolean;
|
||||
extra?: string | object;
|
||||
/**
|
||||
* False when the datasource (e.g. a semantic view) doesn't model raw rows
|
||||
* and therefore can't return a row sample. Defaults to true on the server
|
||||
* side; missing here means the explore UI keeps current behavior.
|
||||
*/
|
||||
supports_samples?: boolean;
|
||||
/**
|
||||
* False when the datasource doesn't model raw rows and therefore can't
|
||||
* answer a drill-to-detail query. Tracked separately from
|
||||
* ``supports_samples`` so the two capabilities can diverge.
|
||||
*/
|
||||
supports_drill_to_detail?: boolean;
|
||||
};
|
||||
|
||||
export interface ExplorePageInitialData {
|
||||
|
||||
-1
@@ -35,6 +35,5 @@ export const Basic: StoryFn<typeof DatasetPanel> = args => (
|
||||
Basic.args = {
|
||||
tableName: 'example_table',
|
||||
loading: false,
|
||||
hasError: false,
|
||||
columnList: exampleColumns,
|
||||
};
|
||||
|
||||
-2
@@ -77,7 +77,6 @@ test('View Dataset opens a single-prefixed URL under a subdirectory deployment',
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="example_table"
|
||||
hasError={false}
|
||||
columnList={exampleColumns}
|
||||
loading={false}
|
||||
datasets={datasetWith(`${APP_ROOT}/explore/?datasource=1__table`)}
|
||||
@@ -97,7 +96,6 @@ test('View Dataset passes an external explore_url through unprefixed', async ()
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="example_table"
|
||||
hasError={false}
|
||||
columnList={exampleColumns}
|
||||
loading={false}
|
||||
datasets={datasetWith('https://external.example.com/custom-endpoint')}
|
||||
|
||||
+17
-29
@@ -17,10 +17,12 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import { ErrorTypeEnum } from '@superset-ui/core';
|
||||
import DatasetPanel, {
|
||||
REFRESHING,
|
||||
tableColumnDefinition,
|
||||
COLUMN_TITLE,
|
||||
ERROR_TITLE,
|
||||
} from 'src/features/datasets/AddDataset/DatasetPanel/DatasetPanel';
|
||||
import { exampleColumns, exampleDataset } from './fixtures';
|
||||
import { ITableColumn } from './types';
|
||||
@@ -31,8 +33,6 @@ import {
|
||||
SELECT_TABLE_TITLE,
|
||||
NO_COLUMNS_TITLE,
|
||||
NO_COLUMNS_DESCRIPTION,
|
||||
ERROR_TITLE,
|
||||
ERROR_DESCRIPTION,
|
||||
} from './MessageContent';
|
||||
|
||||
jest.mock(
|
||||
@@ -47,7 +47,7 @@ jest.mock(
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('DatasetPanel', () => {
|
||||
test('renders a blank state DatasetPanel', () => {
|
||||
render(<DatasetPanel hasError={false} columnList={[]} loading={false} />, {
|
||||
render(<DatasetPanel columnList={[]} loading={false} />, {
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
@@ -70,17 +70,9 @@ describe('DatasetPanel', () => {
|
||||
});
|
||||
|
||||
test('renders a no columns screen', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="Name"
|
||||
hasError={false}
|
||||
columnList={[]}
|
||||
loading={false}
|
||||
/>,
|
||||
{
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
render(<DatasetPanel tableName="Name" columnList={[]} loading={false} />, {
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
|
||||
expect(blankDatasetImg).toBeVisible();
|
||||
@@ -91,17 +83,9 @@ describe('DatasetPanel', () => {
|
||||
});
|
||||
|
||||
test('renders a loading screen', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="Name"
|
||||
hasError={false}
|
||||
columnList={[]}
|
||||
loading
|
||||
/>,
|
||||
{
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
render(<DatasetPanel tableName="Name" columnList={[]} loading />, {
|
||||
useRouter: true,
|
||||
});
|
||||
|
||||
const loadingIndicator = screen.getByTestId('loading-indicator');
|
||||
expect(loadingIndicator).toBeVisible();
|
||||
@@ -113,7 +97,12 @@ describe('DatasetPanel', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="Name"
|
||||
hasError
|
||||
error={{
|
||||
error_type: ErrorTypeEnum.GENERIC_BACKEND_ERROR,
|
||||
extra: null,
|
||||
level: 'error',
|
||||
message: 'Structured backend failure',
|
||||
}}
|
||||
columnList={[]}
|
||||
loading={false}
|
||||
/>,
|
||||
@@ -124,8 +113,9 @@ describe('DatasetPanel', () => {
|
||||
|
||||
const errorTitle = screen.getByText(ERROR_TITLE);
|
||||
expect(errorTitle).toBeVisible();
|
||||
const errorDescription = screen.getByText(ERROR_DESCRIPTION);
|
||||
const errorDescription = screen.getByText('Structured backend failure');
|
||||
expect(errorDescription).toBeVisible();
|
||||
expect(screen.getByTitle('Name')).toHaveStyle({ position: 'relative' });
|
||||
});
|
||||
|
||||
test('renders a table with columns displayed', async () => {
|
||||
@@ -133,7 +123,6 @@ describe('DatasetPanel', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName={tableName}
|
||||
hasError={false}
|
||||
columnList={exampleColumns}
|
||||
loading={false}
|
||||
/>,
|
||||
@@ -159,7 +148,6 @@ describe('DatasetPanel', () => {
|
||||
render(
|
||||
<DatasetPanel
|
||||
tableName="example_table"
|
||||
hasError={false}
|
||||
columnList={exampleColumns}
|
||||
loading={false}
|
||||
datasets={exampleDataset}
|
||||
|
||||
@@ -21,11 +21,13 @@ import { Alert } from '@apache-superset/core/components';
|
||||
import { css, styled } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { Loading } from '@superset-ui/core/components';
|
||||
import type { SupersetError } from '@superset-ui/core';
|
||||
import Table, {
|
||||
ColumnsType,
|
||||
TableSize,
|
||||
} from '@superset-ui/core/components/Table';
|
||||
import { DatasetObject } from 'src/features/datasets/AddDataset/types';
|
||||
import { ErrorMessageWithStackTrace } from 'src/components';
|
||||
import { openInNewTab, stripAppRoot } from 'src/utils/navigationUtils';
|
||||
import { ITableColumn } from './types';
|
||||
import MessageContent from './MessageContent';
|
||||
@@ -146,6 +148,10 @@ const TableScrollContainer = styled.div`
|
||||
right: 0;
|
||||
`;
|
||||
|
||||
const ErrorContainer = styled.div`
|
||||
padding: 0 ${({ theme }) => theme.sizeUnit * 6}px;
|
||||
`;
|
||||
|
||||
const StyledAlert = styled(Alert)`
|
||||
${({ theme }) => `
|
||||
border: 1px solid ${theme.colorInfoText};
|
||||
@@ -167,6 +173,7 @@ const StyledAlert = styled(Alert)`
|
||||
|
||||
export const REFRESHING = t('Refreshing columns');
|
||||
export const COLUMN_TITLE = t('Table columns');
|
||||
export const ERROR_TITLE = t('An Error Occurred');
|
||||
|
||||
const pageSizeOptions = ['5', '10', '15', '25'];
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
@@ -201,9 +208,13 @@ export interface IDatasetPanelProps {
|
||||
*/
|
||||
columnList: ITableColumn[];
|
||||
/**
|
||||
* Boolean indicating if there is an error state
|
||||
* Error returned while loading the table metadata
|
||||
*/
|
||||
hasError: boolean;
|
||||
error?: SupersetError;
|
||||
/**
|
||||
* Function used to retry loading the table metadata after error mitigation
|
||||
*/
|
||||
errorMitigationFunction?: () => void;
|
||||
/**
|
||||
* Boolean indicating if the component is in a loading state
|
||||
*/
|
||||
@@ -256,11 +267,11 @@ const DatasetPanel = ({
|
||||
tableName,
|
||||
columnList,
|
||||
loading,
|
||||
hasError,
|
||||
error,
|
||||
errorMitigationFunction,
|
||||
datasets,
|
||||
}: IDatasetPanelProps) => {
|
||||
const hasColumns = Boolean(columnList?.length > 0);
|
||||
const datasetNames = datasets?.map(dataset => dataset.table_name);
|
||||
const hasColumns = columnList.length > 0;
|
||||
const tableWithDataset = datasets?.find(
|
||||
dataset => dataset.table_name === tableName,
|
||||
);
|
||||
@@ -278,7 +289,19 @@ const DatasetPanel = ({
|
||||
);
|
||||
}
|
||||
if (!loading) {
|
||||
if (!loading && tableName && hasColumns && !hasError) {
|
||||
if (error) {
|
||||
component = (
|
||||
<ErrorContainer>
|
||||
<ErrorMessageWithStackTrace
|
||||
error={error}
|
||||
errorMitigationFunction={errorMitigationFunction}
|
||||
source="crud"
|
||||
subtitle={error.message}
|
||||
title={ERROR_TITLE}
|
||||
/>
|
||||
</ErrorContainer>
|
||||
);
|
||||
} else if (tableName && hasColumns) {
|
||||
component = (
|
||||
<>
|
||||
<StyledTitle title={COLUMN_TITLE}>{COLUMN_TITLE}</StyledTitle>
|
||||
@@ -312,13 +335,7 @@ const DatasetPanel = ({
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<MessageContent
|
||||
hasColumns={hasColumns}
|
||||
hasError={hasError}
|
||||
tableName={tableName}
|
||||
/>
|
||||
);
|
||||
component = <MessageContent tableName={tableName} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,11 +343,12 @@ const DatasetPanel = ({
|
||||
<>
|
||||
{tableName && (
|
||||
<>
|
||||
{datasetNames?.includes(tableName) &&
|
||||
renderExistingDatasetAlert(tableWithDataset)}
|
||||
{tableWithDataset && renderExistingDatasetAlert(tableWithDataset)}
|
||||
<StyledHeader
|
||||
position={
|
||||
!loading && hasColumns ? EPosition.RELATIVE : EPosition.ABSOLUTE
|
||||
!loading && (hasColumns || error)
|
||||
? EPosition.RELATIVE
|
||||
: EPosition.ABSOLUTE
|
||||
}
|
||||
title={tableName || ''}
|
||||
>
|
||||
|
||||
+122
-8
@@ -16,8 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, waitFor } from 'spec/helpers/testing-library';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import { act, render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { ErrorTypeEnum, SupersetClient } from '@superset-ui/core';
|
||||
import type { SupersetClientResponse } from '@superset-ui/core';
|
||||
import {
|
||||
DatabaseErrorMessage,
|
||||
getErrorMessageComponentRegistry,
|
||||
OAuth2RedirectMessage,
|
||||
} from 'src/components/ErrorMessage';
|
||||
import DatasetPanelWrapper from 'src/features/datasets/AddDataset/DatasetPanel';
|
||||
|
||||
jest.mock(
|
||||
@@ -29,17 +35,29 @@ jest.mock(
|
||||
),
|
||||
);
|
||||
|
||||
const errorMessageRegistry = getErrorMessageComponentRegistry();
|
||||
|
||||
afterEach(() => {
|
||||
errorMessageRegistry.remove(ErrorTypeEnum.GENERIC_BACKEND_ERROR);
|
||||
errorMessageRegistry.remove(ErrorTypeEnum.OAUTH2_REDIRECT);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const tableMetadataResponse = (
|
||||
name: string,
|
||||
columnName: string,
|
||||
): SupersetClientResponse => ({
|
||||
response: new Response(),
|
||||
json: {
|
||||
name,
|
||||
columns: [{ name: columnName, type: 'INTEGER', longType: 'INTEGER' }],
|
||||
},
|
||||
});
|
||||
|
||||
test('fetches table metadata for schema-less database without schema', async () => {
|
||||
const getSpy = jest.spyOn(SupersetClient, 'get').mockResolvedValue({
|
||||
json: {
|
||||
name: 'my_table',
|
||||
columns: [{ name: 'id', type: 'INTEGER', longType: 'INTEGER' }],
|
||||
},
|
||||
} as any);
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockResolvedValue(tableMetadataResponse('my_table', 'id'));
|
||||
|
||||
render(
|
||||
<DatasetPanelWrapper
|
||||
@@ -58,3 +76,99 @@ test('fetches table metadata for schema-less database without schema', async ()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('renders a fallback message for an unstructured metadata error', async () => {
|
||||
jest.spyOn(SupersetClient, 'get').mockRejectedValue({
|
||||
response: new Response('{}', {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
});
|
||||
errorMessageRegistry.registerValue(
|
||||
ErrorTypeEnum.GENERIC_BACKEND_ERROR,
|
||||
DatabaseErrorMessage,
|
||||
);
|
||||
|
||||
render(
|
||||
<DatasetPanelWrapper
|
||||
tableName="broken_table"
|
||||
dbId={1}
|
||||
database={{ supports_schemas: false }}
|
||||
/>,
|
||||
{ useRouter: true },
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText('Unable to load columns for the selected table.'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('retries only table metadata after matching OAuth completion', async () => {
|
||||
const oauthError = {
|
||||
error_type: ErrorTypeEnum.OAUTH2_REDIRECT,
|
||||
message: 'OAuth authorization is required.',
|
||||
extra: {
|
||||
url: 'https://example.com/authorize',
|
||||
tab_id: 'dataset-oauth-tab',
|
||||
},
|
||||
level: 'warning',
|
||||
};
|
||||
const getSpy = jest
|
||||
.spyOn(SupersetClient, 'get')
|
||||
.mockRejectedValueOnce({
|
||||
response: new Response(JSON.stringify({ errors: [oauthError] }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
})
|
||||
.mockResolvedValueOnce(tableMetadataResponse('oauth_table', 'oauth_id'));
|
||||
|
||||
errorMessageRegistry.registerValue(
|
||||
ErrorTypeEnum.OAUTH2_REDIRECT,
|
||||
OAuth2RedirectMessage,
|
||||
);
|
||||
|
||||
render(
|
||||
<DatasetPanelWrapper
|
||||
tableName="oauth_table"
|
||||
dbId={1}
|
||||
database={{ supports_schemas: false }}
|
||||
/>,
|
||||
{
|
||||
initialState: {
|
||||
charts: {},
|
||||
dashboardInfo: {},
|
||||
explore: {},
|
||||
sqlLab: {
|
||||
queries: {},
|
||||
queryEditors: [],
|
||||
tabHistory: [],
|
||||
},
|
||||
},
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
},
|
||||
);
|
||||
|
||||
const authorizationLink = await screen.findByRole('link', {
|
||||
name: /provide authorization/i,
|
||||
});
|
||||
expect(authorizationLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://example.com/authorize',
|
||||
);
|
||||
expect(getSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new StorageEvent('storage', {
|
||||
key: 'oauth2_auth_complete',
|
||||
newValue: JSON.stringify({ tabId: 'dataset-oauth-tab' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
expect(await screen.findByText('oauth_id')).toBeVisible();
|
||||
expect(getSpy).toHaveBeenCalledTimes(2);
|
||||
expect(getSpy.mock.calls[1]).toEqual(getSpy.mock.calls[0]);
|
||||
});
|
||||
|
||||
@@ -65,27 +65,17 @@ export const NO_COLUMNS_TITLE = t('No table columns');
|
||||
export const NO_COLUMNS_DESCRIPTION = t(
|
||||
'This database table does not contain any data. Please select a different table.',
|
||||
);
|
||||
export const ERROR_TITLE = t('An Error Occurred');
|
||||
export const ERROR_DESCRIPTION = t(
|
||||
'Unable to load columns for the selected table. Please select a different table.',
|
||||
);
|
||||
|
||||
interface MessageContentProps {
|
||||
hasError: boolean;
|
||||
tableName?: string | null;
|
||||
hasColumns: boolean;
|
||||
}
|
||||
|
||||
export const MessageContent = (props: MessageContentProps) => {
|
||||
const { hasError, tableName, hasColumns } = props;
|
||||
let currentImage: string | undefined = 'empty-dataset.svg';
|
||||
const { tableName } = props;
|
||||
let currentImage = 'empty-dataset.svg';
|
||||
let currentTitle = SELECT_TABLE_TITLE;
|
||||
let currentDescription = renderEmptyDescription();
|
||||
if (hasError) {
|
||||
currentTitle = ERROR_TITLE;
|
||||
currentDescription = <>{ERROR_DESCRIPTION}</>;
|
||||
currentImage = undefined;
|
||||
} else if (tableName && !hasColumns) {
|
||||
if (tableName) {
|
||||
currentImage = 'no-columns.svg';
|
||||
currentTitle = NO_COLUMNS_TITLE;
|
||||
currentDescription = <>{NO_COLUMNS_DESCRIPTION}</>;
|
||||
|
||||
@@ -16,9 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import {
|
||||
ErrorTypeEnum,
|
||||
getClientErrorObject,
|
||||
SupersetClient,
|
||||
} from '@superset-ui/core';
|
||||
import type { SupersetError } from '@superset-ui/core';
|
||||
import { logging } from '@apache-superset/core/utils';
|
||||
import { DatasetObject } from 'src/features/datasets/AddDataset/types';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
@@ -30,7 +35,7 @@ import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
|
||||
/**
|
||||
* Interface for the getTableMetadata API call
|
||||
*/
|
||||
interface IColumnProps {
|
||||
interface TableMetadataRequest {
|
||||
/**
|
||||
* Unique id of the database
|
||||
*/
|
||||
@@ -43,6 +48,10 @@ interface IColumnProps {
|
||||
* Name of the schema (optional for databases that don't support schemas)
|
||||
*/
|
||||
schema?: string | null;
|
||||
/**
|
||||
* Name of the catalog (optional for databases that don't support catalogs)
|
||||
*/
|
||||
catalog?: string | null;
|
||||
}
|
||||
|
||||
export interface IDatasetPanelWrapperProps {
|
||||
@@ -63,7 +72,7 @@ export interface IDatasetPanelWrapperProps {
|
||||
* The selected database object (used to check engine capabilities)
|
||||
*/
|
||||
database?: Partial<DatabaseObject> | null;
|
||||
setHasColumns?: Function;
|
||||
setHasColumns?: (hasColumns: boolean) => void;
|
||||
datasets?: DatasetObject[] | undefined;
|
||||
}
|
||||
|
||||
@@ -78,74 +87,131 @@ const DatasetPanelWrapper = ({
|
||||
}: IDatasetPanelWrapperProps) => {
|
||||
const [columnList, setColumnList] = useState<ITableColumn[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const tableNameRef = useRef(tableName);
|
||||
const [error, setError] = useState<SupersetError>();
|
||||
const requestIdRef = useRef(0);
|
||||
const currentRequestRef = useRef<TableMetadataRequest>();
|
||||
const supportsSchemas = database?.supports_schemas;
|
||||
|
||||
const getTableMetadata = async (props: IColumnProps) => {
|
||||
const { dbId, tableName, schema } = props;
|
||||
setLoading(true);
|
||||
setHasColumns?.(false);
|
||||
const path = `/api/v1/database/${dbId}/table_metadata/${toQueryString({
|
||||
name: tableName,
|
||||
catalog,
|
||||
schema,
|
||||
})}`;
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: path,
|
||||
});
|
||||
const getTableMetadata = useCallback(
|
||||
async (props: TableMetadataRequest) => {
|
||||
const { dbId, tableName, catalog, schema } = props;
|
||||
requestIdRef.current += 1;
|
||||
const requestId = requestIdRef.current;
|
||||
setLoading(true);
|
||||
setColumnList([]);
|
||||
setError(undefined);
|
||||
setHasColumns?.(false);
|
||||
const path = `/api/v1/database/${dbId}/table_metadata/${toQueryString({
|
||||
name: tableName,
|
||||
catalog,
|
||||
schema,
|
||||
})}`;
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: path,
|
||||
});
|
||||
|
||||
if (isIDatabaseTable(response?.json)) {
|
||||
const table: IDatabaseTable = response.json as IDatabaseTable;
|
||||
/**
|
||||
* The user is able to click other table columns while the http call for last selected table column is made
|
||||
* This check ensures we process the response that matches the last selected table name and ignore the others
|
||||
*/
|
||||
if (table.name === tableNameRef.current) {
|
||||
if (requestId !== requestIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = isIDatabaseTable(response?.json)
|
||||
? (response.json as IDatabaseTable)
|
||||
: undefined;
|
||||
if (table?.name === tableName) {
|
||||
setColumnList(table.columns);
|
||||
setHasColumns?.(table.columns.length > 0);
|
||||
setHasError(false);
|
||||
setError(undefined);
|
||||
} else {
|
||||
const message = t(
|
||||
'The API response from %s does not match the IDatabaseTable interface.',
|
||||
path,
|
||||
);
|
||||
setColumnList([]);
|
||||
setHasColumns?.(false);
|
||||
setError({
|
||||
error_type: ErrorTypeEnum.GENERIC_BACKEND_ERROR,
|
||||
extra: null,
|
||||
level: 'error',
|
||||
message,
|
||||
});
|
||||
addDangerToast(message);
|
||||
logging.error(message);
|
||||
}
|
||||
} else {
|
||||
setColumnList([]);
|
||||
setHasColumns?.(false);
|
||||
setHasError(true);
|
||||
addDangerToast(
|
||||
t(
|
||||
'The API response from %s does not match the IDatabaseTable interface.',
|
||||
path,
|
||||
),
|
||||
);
|
||||
logging.error(
|
||||
t(
|
||||
'The API response from %s does not match the IDatabaseTable interface.',
|
||||
path,
|
||||
),
|
||||
} catch (caughtError) {
|
||||
const clientError = await getClientErrorObject(
|
||||
caughtError as Parameters<typeof getClientErrorObject>[0],
|
||||
);
|
||||
|
||||
if (requestId === requestIdRef.current) {
|
||||
const parsedError = clientError.errors?.[0] ?? {
|
||||
error_type: ErrorTypeEnum.GENERIC_BACKEND_ERROR,
|
||||
extra: null,
|
||||
level: 'error' as const,
|
||||
message:
|
||||
clientError.error ||
|
||||
clientError.message ||
|
||||
clientError.statusText ||
|
||||
t('Unable to load columns for the selected table.'),
|
||||
};
|
||||
|
||||
setColumnList([]);
|
||||
setHasColumns?.(false);
|
||||
setError(parsedError);
|
||||
}
|
||||
} finally {
|
||||
if (requestId === requestIdRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setColumnList([]);
|
||||
setHasColumns?.(false);
|
||||
setHasError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
},
|
||||
[setHasColumns],
|
||||
);
|
||||
|
||||
const retryGetTableMetadata = useCallback(() => {
|
||||
if (currentRequestRef.current) {
|
||||
getTableMetadata(currentRequestRef.current);
|
||||
}
|
||||
};
|
||||
}, [getTableMetadata]);
|
||||
|
||||
useEffect(() => {
|
||||
tableNameRef.current = tableName;
|
||||
const schemaRequired = database?.supports_schemas !== false;
|
||||
const schemaRequired = supportsSchemas !== false;
|
||||
if (tableName && dbId && (schema || !schemaRequired)) {
|
||||
getTableMetadata({ tableName, dbId, schema: schema || undefined });
|
||||
const request = {
|
||||
tableName,
|
||||
dbId,
|
||||
catalog,
|
||||
schema: schema || undefined,
|
||||
};
|
||||
currentRequestRef.current = request;
|
||||
getTableMetadata(request);
|
||||
} else if (currentRequestRef.current) {
|
||||
currentRequestRef.current = undefined;
|
||||
requestIdRef.current += 1;
|
||||
setColumnList([]);
|
||||
setError(undefined);
|
||||
setHasColumns?.(false);
|
||||
setLoading(false);
|
||||
}
|
||||
// getTableMetadata is a const and should not be in dependency array
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tableName, dbId, schema, database]);
|
||||
|
||||
return () => {
|
||||
requestIdRef.current += 1;
|
||||
};
|
||||
}, [
|
||||
tableName,
|
||||
dbId,
|
||||
catalog,
|
||||
schema,
|
||||
supportsSchemas,
|
||||
getTableMetadata,
|
||||
setHasColumns,
|
||||
]);
|
||||
|
||||
return (
|
||||
<DatasetPanel
|
||||
columnList={columnList}
|
||||
hasError={hasError}
|
||||
error={error}
|
||||
errorMitigationFunction={retryGetTableMetadata}
|
||||
loading={loading}
|
||||
tableName={tableName}
|
||||
datasets={datasets}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user