mirror of
https://github.com/apache/superset.git
synced 2026-07-27 09:02:29 +00:00
Compare commits
128 Commits
feat/scatt
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490d284d75 | ||
|
|
6856d0fc67 | ||
|
|
8f8331f656 | ||
|
|
e54eccd5fb | ||
|
|
710037d3d2 | ||
|
|
90040fc1f6 | ||
|
|
ba0900d2b3 | ||
|
|
9a025267bd | ||
|
|
e3d3262e97 | ||
|
|
6c2ccc44bd | ||
|
|
e1ffa53d9b | ||
|
|
4443357f33 | ||
|
|
dd08c4f36c | ||
|
|
4f3a1a5058 | ||
|
|
9d6fbdceff | ||
|
|
8864b7a0a3 | ||
|
|
d4652460ee | ||
|
|
8c37d627aa | ||
|
|
5775bdbe5e | ||
|
|
c65bedf2d7 | ||
|
|
2a07f7967d | ||
|
|
b82ff27f9c | ||
|
|
83ea9a1025 | ||
|
|
e9f61c1e66 | ||
|
|
857a194c66 | ||
|
|
5ffdac1871 | ||
|
|
52552c8278 | ||
|
|
134cae07ae | ||
|
|
f3fa1c7d4f | ||
|
|
99841fb504 | ||
|
|
3a53c4ebc3 | ||
|
|
f7d0ec3a2c | ||
|
|
7eceebce1c | ||
|
|
3bdf134aaa | ||
|
|
65528a5b76 | ||
|
|
0792f98910 | ||
|
|
67c3fea0ce | ||
|
|
d2ea5e9a1a | ||
|
|
0b9a9c5d03 | ||
|
|
16de95f024 | ||
|
|
5b5e5cf82d | ||
|
|
542416ac6e | ||
|
|
4f017de532 | ||
|
|
a1142b0f91 | ||
|
|
4ee500e47b | ||
|
|
3ff5dbfe81 | ||
|
|
cfb04a6e79 | ||
|
|
49f4e84b48 | ||
|
|
921f75d544 | ||
|
|
bcf0361a91 | ||
|
|
5776aff50a | ||
|
|
14c96761e5 | ||
|
|
d41f0febae | ||
|
|
c43effa4a3 | ||
|
|
206fe7ab12 | ||
|
|
1049826bc1 | ||
|
|
6317113ee8 | ||
|
|
fdb9c152f0 | ||
|
|
2317d9cf91 | ||
|
|
df8bbb548a | ||
|
|
6b02a423ef | ||
|
|
f2848d951c | ||
|
|
cb319c9b81 | ||
|
|
cc8edd2231 | ||
|
|
c5f6d33d21 | ||
|
|
2e72080a16 | ||
|
|
6654318491 | ||
|
|
ab553c37ab | ||
|
|
873da606dd | ||
|
|
1dd3103c24 | ||
|
|
2926d6f6a9 | ||
|
|
67924ad35e | ||
|
|
ef459c21e8 | ||
|
|
3ed8889a01 | ||
|
|
17bd286ae9 | ||
|
|
32937f24d2 | ||
|
|
e075133a83 | ||
|
|
8d034ef65c | ||
|
|
641713f623 | ||
|
|
40d9e7d048 | ||
|
|
19159d58c8 | ||
|
|
894c0191d6 | ||
|
|
49b4243129 | ||
|
|
ac41f61d4a | ||
|
|
f50b7b7154 | ||
|
|
be5744c431 | ||
|
|
8c5d465a65 | ||
|
|
c5935b6904 | ||
|
|
10ff470702 | ||
|
|
1c0f259e38 | ||
|
|
b8f3b768d4 | ||
|
|
6e1954fd51 | ||
|
|
248f6e1064 | ||
|
|
46c5e30533 | ||
|
|
b71b58e821 | ||
|
|
1392fbc9b2 | ||
|
|
a086b2eae9 | ||
|
|
788165b5bb | ||
|
|
60a7c18204 | ||
|
|
db1959b9b6 | ||
|
|
936b8a84ae | ||
|
|
37ef2f381e | ||
|
|
cae61b51e1 | ||
|
|
9c08240fdc | ||
|
|
072ff01406 | ||
|
|
8b1ef3a4cb | ||
|
|
d62dbdb694 | ||
|
|
2ddea33bb6 | ||
|
|
15d7d36580 | ||
|
|
ab8e23d874 | ||
|
|
1b882c1f47 | ||
|
|
8329481c2e | ||
|
|
c95c8086f5 | ||
|
|
cfac785541 | ||
|
|
b97a7f5f94 | ||
|
|
e5c418a187 | ||
|
|
34ebe3d22a | ||
|
|
0dd06f3b7b | ||
|
|
40a13883ea | ||
|
|
f2967264d2 | ||
|
|
706a65cb02 | ||
|
|
f9378baba8 | ||
|
|
e1a265447f | ||
|
|
f999afaac0 | ||
|
|
1a862476e1 | ||
|
|
df34b5d277 | ||
|
|
b362d36019 | ||
|
|
666b6805c4 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,6 +1,6 @@
|
||||
# Notify all committers of DB migration changes, per SIP-59
|
||||
|
||||
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe
|
||||
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe @rusackas
|
||||
|
||||
# Notify some committers of changes in the components
|
||||
|
||||
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -70,6 +70,10 @@ updates:
|
||||
- "@storybook/*"
|
||||
- "storybook"
|
||||
- "eslint-plugin-storybook"
|
||||
ag-grid:
|
||||
patterns:
|
||||
- "ag-grid-react"
|
||||
- "ag-grid-community"
|
||||
open-pull-requests-limit: 30
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
|
||||
4
.github/workflows/bump-python-package.yml
vendored
4
.github/workflows/bump-python-package.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
checks: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: master
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
2
.github/workflows/check-python-deps.yml
vendored
2
.github/workflows/check-python-deps.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check and notify
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -58,13 +58,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
4
.github/workflows/dependency-review.yml
vendored
4
.github/workflows/dependency-review.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Dependency Review"
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
docker: ${{ steps.check.outputs.docker }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Free up disk space
|
||||
|
||||
2
.github/workflows/embedded-sdk-release.yml
vendored
2
.github/workflows/embedded-sdk-release.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Note: registry-url is intentionally omitted. When set, actions/setup-node
|
||||
|
||||
2
.github/workflows/embedded-sdk-test.yml
vendored
2
.github/workflows/embedded-sdk-test.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
|
||||
2
.github/workflows/generate-FOSSA-report.yml
vendored
2
.github/workflows/generate-FOSSA-report.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -46,4 +46,4 @@ jobs:
|
||||
run: bash .github/workflows/github-action-validator.sh
|
||||
|
||||
- name: Check for security issues on GHA workflows
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
|
||||
|
||||
2
.github/workflows/issue_creation.yml
vendored
2
.github/workflows/issue_creation.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
|
||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
|
||||
2
.github/workflows/latest-release-tag.yml
vendored
2
.github/workflows/latest-release-tag.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/license-check.yml
vendored
2
.github/workflows/license-check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/pr-lint.yml
vendored
2
.github/workflows/pr-lint.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
24
.github/workflows/pre-commit.yml
vendored
24
.github/workflows/pre-commit.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
|
||||
- name: Install helm-docs
|
||||
run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
|
||||
@@ -81,7 +81,6 @@ jobs:
|
||||
id: changed_files
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -95,7 +94,15 @@ jobs:
|
||||
# Resolve the commit to diff against.
|
||||
base=""
|
||||
if [ "${EVENT_NAME}" = "pull_request" ]; then
|
||||
base="${BASE_SHA}"
|
||||
# HEAD is the PR merge commit, so its first parent is the current
|
||||
# tip of the base branch. github.event.pull_request.base.sha is
|
||||
# NOT that: GitHub freezes it at PR creation, so on a long-lived
|
||||
# PR it points at the original branch point and the diff picks up
|
||||
# all of the base branch's churn since then — thousands of paths,
|
||||
# enough for the CHANGED_FILES env var below to exceed the
|
||||
# kernel's per-variable size limit and kill the step with
|
||||
# "Argument list too long" before bash even starts.
|
||||
base="$(git rev-parse HEAD^1 2>/dev/null || true)"
|
||||
elif [ -n "${BEFORE_SHA:-}" ] && \
|
||||
[ "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]; then
|
||||
base="${BEFORE_SHA}"
|
||||
@@ -114,6 +121,15 @@ jobs:
|
||||
# Files present in HEAD that changed since the base (drop deletions).
|
||||
files="$(git diff --name-only --diff-filter=ACMRT "${base}...HEAD")"
|
||||
|
||||
# Env vars have a hard per-variable size limit (E2BIG at step
|
||||
# start). A PR that legitimately touches thousands of files is
|
||||
# better served by --all-files anyway.
|
||||
if [ "$(printf '%s' "${files}" | wc -c)" -gt 100000 ]; then
|
||||
echo "::notice::Changed-file list too large to pass via env; falling back to --all-files."
|
||||
echo "mode=all" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${files}" ]; then
|
||||
echo "mode=none" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# pulls all commits (needed for lerna / semantic release to correctly version)
|
||||
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: "Checkout release tag: ${{ needs.config.outputs.latest-release }}"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ needs.config.outputs.latest-release }}
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/showtime-trigger.yml
vendored
2
.github/workflows/showtime-trigger.yml
vendored
@@ -153,7 +153,7 @@ jobs:
|
||||
|
||||
- name: Checkout PR code (only if build needed)
|
||||
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.target_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/superset-app-cli.yml
vendored
2
.github/workflows/superset-app-cli.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/superset-docs-deploy.yml
vendored
2
.github/workflows/superset-docs-deploy.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/superset-docs-verify.yml
vendored
6
.github/workflows/superset-docs-verify.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
name: Link Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
# Do not bump this linkinator-action version without opening
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
14
.github/workflows/superset-e2e.yml
vendored
14
.github/workflows/superset-e2e.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -96,21 +96,21 @@ jobs:
|
||||
# Conditional checkout based on context
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -206,21 +206,21 @@ jobs:
|
||||
# Conditional checkout based on context (same as Cypress workflow)
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
working-directory: superset-extensions-cli
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
4
.github/workflows/superset-frontend.yml
vendored
4
.github/workflows/superset-frontend.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
should-run: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/superset-helm-release.yml
vendored
2
.github/workflows/superset-helm-release.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref_name }}
|
||||
persist-credentials: true
|
||||
|
||||
8
.github/workflows/superset-playwright.yml
vendored
8
.github/workflows/superset-playwright.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
frontend: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -83,21 +83,21 @@ jobs:
|
||||
# Conditional checkout based on context (same as Cypress workflow)
|
||||
- name: Checkout for push or pull_request event
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
- name: Checkout using ref (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
submodules: recursive
|
||||
- name: Checkout using PR ID (workflow_dispatch)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
python: ${{ steps.check.outputs.python }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
SQLALCHEMY_WARN_20: "1"
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
4
.github/workflows/superset-translations.yml
vendored
4
.github/workflows/superset-translations.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
2
.github/workflows/superset-websocket.yml
vendored
2
.github/workflows/superset-websocket.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js
|
||||
|
||||
2
.github/workflows/supersetbot.yml
vendored
2
.github/workflows/supersetbot.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
});
|
||||
|
||||
- name: "Checkout ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
# zizmor: ignore[artipacked] - required persisted credentials to push synced requirement changes back to remote
|
||||
- name: Checkout source code
|
||||
if: ${{ steps.dependabot-metadata.outputs.package-ecosystem == 'pip' }}
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: true
|
||||
|
||||
4
.github/workflows/tag-release.yml
vendored
4
.github/workflows/tag-release.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -126,7 +126,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/tech-debt.yml
vendored
2
.github/workflows/tech-debt.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
name: Generate Reports
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -57,6 +57,7 @@ local_config.py
|
||||
/superset_text.yml
|
||||
superset.egg-info/
|
||||
superset/bin/supersetc
|
||||
superset/extensions/.reload_trigger
|
||||
tmp
|
||||
rat-results.txt
|
||||
superset/app/
|
||||
|
||||
@@ -144,7 +144,7 @@ repos:
|
||||
git fetch --no-recurse-submodules origin "$TARGET_BRANCH" 2>/dev/null || true
|
||||
fi
|
||||
BASE=$(git merge-base origin/"$TARGET_BRANCH" HEAD 2>/dev/null) || BASE="HEAD"
|
||||
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD 2>/dev/null | grep '^superset/.*\.py$' || true)
|
||||
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD 2>/dev/null | grep '^superset/.*\.py$' | grep -v '^superset/migrations/' || true)
|
||||
if [ -n "$files" ]; then
|
||||
pylint --rcfile=.pylintrc --load-plugins=superset.extensions.pylint --reports=no $files
|
||||
else
|
||||
|
||||
@@ -168,6 +168,11 @@ categories:
|
||||
url: https://www.now.vn/
|
||||
contributors: ["@davidkohcw"]
|
||||
|
||||
- name: Pattern
|
||||
url: https://www.pattern.com
|
||||
contributors: ["@alephys26", "@hladush", "@sanketjadhavSF"]
|
||||
logo: pattern_logo.png
|
||||
|
||||
- name: Qunar
|
||||
url: https://www.qunar.com/
|
||||
contributors: ["@flametest"]
|
||||
|
||||
68
UPDATING.md
68
UPDATING.md
@@ -64,6 +64,48 @@ Update your environment (virtualenv, Docker base image, CI configuration, etc.)
|
||||
Python 3.11+ before upgrading. The `apache/superset-cache:3.10-slim-trixie` and
|
||||
`py310` Docker image variants are no longer published.
|
||||
|
||||
### `from_dttm` and `to_dttm` Jinja template variables removed
|
||||
|
||||
The `{{ from_dttm }}` and `{{ to_dttm }}` Jinja template variables, deprecated since
|
||||
v5.0, have been removed from the Jinja context and are no longer available in virtual
|
||||
dataset SQL or custom SQL expressions.
|
||||
|
||||
Replace usages with the `get_time_filter()` function. For example:
|
||||
|
||||
```sql
|
||||
-- Before
|
||||
SELECT * FROM tbl
|
||||
WHERE dttm_col > '{{ from_dttm }}' AND dttm_col < '{{ to_dttm }}'
|
||||
|
||||
-- After
|
||||
{% set tf = get_time_filter("dttm_col") %}
|
||||
SELECT * FROM tbl
|
||||
WHERE dttm_col > {{ tf.from_expr }} AND dttm_col <= {{ tf.to_expr }}
|
||||
```
|
||||
|
||||
Note that `from_expr` and `to_expr` are already fully-formatted SQL expressions (e.g.
|
||||
`TO_TIMESTAMP('2024-01-01', ...)`) — do not wrap them in single quotes.
|
||||
|
||||
### Removed deck.gl JavaScript tooltip/data-mutator controls and ENABLE_JAVASCRIPT_CONTROLS
|
||||
|
||||
The `ENABLE_JAVASCRIPT_CONTROLS` feature flag and the deck.gl chart controls it gated
|
||||
(`js_tooltip`, `js_onclick_href`, `js_data_mutator`, and the GeoJSON layer's label/icon
|
||||
JavaScript-mode generators) have been removed. These controls let users write arbitrary
|
||||
JavaScript, sandboxed via Node's `vm` module, to customize deck.gl tooltips, click
|
||||
behavior, and data transforms; the flag defaulted off and the feature saw negligible use.
|
||||
|
||||
The deck.gl "Extra data for JS" control (`js_columns`) has also been removed. It only
|
||||
ever existed to feed extra columns into the JavaScript controls above; deck.gl's
|
||||
built-in field-based tooltips and cross-filtering already pull in any columns they need
|
||||
via `tooltip_contents`/`cross_filter_column`, so this control had no remaining purpose.
|
||||
Any chart layer whose "Advanced" control panel section only contained this control no
|
||||
longer has an "Advanced" section.
|
||||
|
||||
Any saved charts with these fields set will simply ignore them going forward and fall back
|
||||
to deck.gl's built-in field-based tooltips (`tooltip_contents`/`tooltip_template`) and
|
||||
native click/cross-filter behavior. No migration is required; the fields are dropped
|
||||
silently on next save.
|
||||
|
||||
### Owners, dashboard roles, and RLS roles replaced by Subjects
|
||||
|
||||
Superset now uses subject-based access assignments for dashboards, charts, datasets,
|
||||
@@ -246,6 +288,22 @@ The pivot table chart's `First` and `Last` aggregations now return the first and
|
||||
|
||||
The `error` and `response` parameters of the `retryDelay` and `retryOn` callbacks in `FetchRetryOptions` (exported from `@superset-ui/core`) are now typed `Error | null` and `Response | null` to match the actual call-site signature provided by `fetch-retry`. Because these parameter types are contravariant, consumers who typed their callbacks with the non-nullable `(attempt: number, error: Error, response: Response) => number` will get a TypeScript compile error. Widen your callback signatures to accept `Error | null` / `Response | null`.
|
||||
|
||||
### Pivot Table totals are now computed by the database (per-metric "Aggregation function" control removed)
|
||||
|
||||
Pivot Table subtotals and grand totals are now computed by the database at each
|
||||
rollup level instead of re-aggregating the already-aggregated cell values on the
|
||||
client. This fixes long-standing incorrect totals for non-additive metrics
|
||||
(ratios such as `SUM(a)/SUM(b)`, `COUNT_DISTINCT`, `AVG`, percentiles, etc.),
|
||||
which previously summed the displayed cell values.
|
||||
|
||||
As a result the per-table **"Aggregation function"** control (which let you pick
|
||||
how totals were aggregated client-side, e.g. Sum/Average/Count) has been
|
||||
removed: totals now always reflect the metric's own definition evaluated at the
|
||||
total's granularity. For additive metrics (`SUM`/`COUNT`/`MIN`/`MAX`) the result
|
||||
is unchanged. Saved charts that set `aggregateFunction` will ignore it; no
|
||||
migration is required. If you previously relied on a plain sum-of-cells total
|
||||
for a non-additive metric, that specific behavior is no longer available.
|
||||
|
||||
### `thumbnail_url` removed from dashboard list API response
|
||||
|
||||
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
|
||||
@@ -552,6 +610,12 @@ The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WH
|
||||
|
||||
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
### Engine spec capability flag: `supports_offset`
|
||||
|
||||
A new `BaseEngineSpec.supports_offset` attribute (default `True`) indicates whether a database engine supports the SQL `OFFSET` clause. Engines that do not support `OFFSET` — such as Elasticsearch SQL and OpenDistro — opt out by setting it to `False`, and Superset uses each engine's cursor API to paginate drill-to-detail samples instead of emitting `OFFSET`. Downstream forks maintaining custom engine specs may set the flag to `False` (and implement `fetch_data_with_cursor`) to avoid crashes when paginated drill-to-detail queries are run against engines without `OFFSET` support.
|
||||
|
||||
**Note on deep-pagination cost:** Cursor-based engines (including Elasticsearch and OpenDistro) are forward-only, so reaching page `N` of a drill-to-detail view issues `N` round trips to the cluster. Deep pagination is therefore linear in page number; users paginating into the hundreds or thousands will notice added latency compared to `OFFSET`-capable engines.
|
||||
|
||||
### Granular Export Controls
|
||||
|
||||
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
|
||||
@@ -588,6 +652,10 @@ Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to se
|
||||
|
||||
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
|
||||
|
||||
### Kenya Country Map: Updated Administrative Divisions
|
||||
|
||||
The Kenya country map has been updated to reflect the 47 counties established under Kenya's 2010 constitution, replacing the outdated 8-province boundaries from the Natural Earth dataset. County keys now use ISO 3166-2:KE codes (`KE-01` through `KE-47`), replacing the former province codes (`KE-110`, `KE-200`, ..., `KE-800`). Dashboards that join on the old province codes will need to re-key their datasets to use the new county codes.
|
||||
|
||||
### MCP Tool Observability
|
||||
|
||||
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
|
||||
|
||||
@@ -34,6 +34,7 @@ x-superset-volumes: &superset-volumes
|
||||
- superset_home:/app/superset_home
|
||||
- ./tests:/app/tests
|
||||
- superset_data:/app/data
|
||||
- ./local_extensions:/app/local_extensions
|
||||
x-common-build: &common-build
|
||||
context: .
|
||||
target: ${SUPERSET_BUILD_TARGET:-dev} # can use `dev` (default) or `lean`
|
||||
|
||||
@@ -98,7 +98,9 @@ case "${1}" in
|
||||
echo " 🔒 Werkzeug debugger disabled (set SUPERSET_DEBUG_ENABLED=true to enable)"
|
||||
fi
|
||||
|
||||
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 --exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*"
|
||||
flask run -p $PORT --reload $DEBUGGER_FLAG --host=0.0.0.0 \
|
||||
--extra-files "/app/superset/extensions/.reload_trigger" \
|
||||
--exclude-patterns "*/node_modules/*:*/.venv/*:*/build/*:*/__pycache__/*:*/superset-frontend/*:*/superset/__init__.py"
|
||||
;;
|
||||
app-gunicorn)
|
||||
echo "Starting web app..."
|
||||
|
||||
@@ -149,10 +149,10 @@ database engine on a separate host or container.
|
||||
|
||||
Superset supports the following database engines/versions:
|
||||
|
||||
| Database Engine | Supported Versions |
|
||||
| ----------------------------------------- | ---------------------------------------- |
|
||||
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X |
|
||||
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
|
||||
| Database Engine | Supported Versions |
|
||||
| ----------------------------------------- | ---------------------------------------------- |
|
||||
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X, 17.X |
|
||||
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
|
||||
|
||||
Use the following database drivers and connection strings:
|
||||
|
||||
|
||||
@@ -25,6 +25,33 @@ DECKGL_BASE_MAP = [
|
||||
]
|
||||
```
|
||||
|
||||
[Yandex Tiles API](https://yandex.ru/maps-api/docs/tiles-api/request.html) can be used as a custom tile provider. The URL must include the `apikey` from the [Yandex Developer Dashboard](https://developer.tech.yandex.ru/services), tile coordinates (`x`, `y`, and `z`), and the required map layer parameters.
|
||||
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
[
|
||||
"tile://https://tiles.api-maps.yandex.ru/v1/tiles/"
|
||||
"?apikey=YOUR_YANDEX_API_KEY"
|
||||
"&lang=ru_RU"
|
||||
"&x={x}&y={y}&z={z}"
|
||||
"&l=map"
|
||||
"&projection=web_mercator"
|
||||
"&maptype=map",
|
||||
"Yandex Maps - Web Mercator",
|
||||
],
|
||||
[
|
||||
"tile://https://tiles.api-maps.yandex.ru/v1/tiles/"
|
||||
"?apikey=YOUR_YANDEX_API_KEY"
|
||||
"&lang=ru_RU"
|
||||
"&x={x}&y={y}&z={z}"
|
||||
"&l=map"
|
||||
"&projection=wgs84_mercator"
|
||||
"&maptype=map",
|
||||
"Yandex Maps - WGS84 Mercator",
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
Default values are:
|
||||
```python
|
||||
DECKGL_BASE_MAP = [
|
||||
@@ -56,6 +83,7 @@ CORS_OPTIONS: dict[Any, Any] = {
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
"https://tiles.api-maps.yandex.ru",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -72,6 +100,7 @@ TALISMAN_CONFIG = {
|
||||
"https://tile.openstreetmap.org",
|
||||
"https://tile.osm.ch",
|
||||
"https://your_personal_url/{z}/{x}/{y}.png",
|
||||
"https://tiles.api-maps.yandex.ru",
|
||||
],
|
||||
...
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ made available in the Jinja context:
|
||||
|
||||
- `columns`: columns which to group by in the query
|
||||
- `filter`: filters applied in the query
|
||||
- `from_dttm`: start `datetime` value from the selected time range (`None` if undefined). **Note:** Only available in virtual datasets when a time range filter is applied in Explore/Chart views—not available in standalone SQL Lab queries. (deprecated beginning in version 5.0, use `get_time_filter` instead)
|
||||
- `to_dttm`: end `datetime` value from the selected time range (`None` if undefined). **Note:** Only available in virtual datasets when a time range filter is applied in Explore/Chart views—not available in standalone SQL Lab queries. (deprecated beginning in version 5.0, use `get_time_filter` instead)
|
||||
- `groupby`: columns which to group by in the query (deprecated)
|
||||
- `metrics`: aggregate expressions in the query
|
||||
- `row_limit`: row limit of the query
|
||||
@@ -49,85 +47,26 @@ made available in the Jinja context:
|
||||
- `time_column`: temporal column of the query (`None` if undefined)
|
||||
- `time_grain`: selected time grain (`None` if undefined)
|
||||
|
||||
For example, to add a time range to a virtual dataset, you can write the following:
|
||||
For example, to add a time range to a virtual dataset, use the `get_time_filter` macro:
|
||||
|
||||
```sql
|
||||
{% set tf = get_time_filter("dttm_col") %}
|
||||
SELECT *
|
||||
FROM tbl
|
||||
WHERE dttm_col > '{{ from_dttm }}' and dttm_col < '{{ to_dttm }}'
|
||||
WHERE dttm_col > {{ tf.from_expr }} AND dttm_col <= {{ tf.to_expr }}
|
||||
```
|
||||
|
||||
You can also use [Jinja's logic](https://jinja.palletsprojects.com/en/2.11.x/templates/#tests)
|
||||
to make your query robust to clearing the timerange filter:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM tbl
|
||||
WHERE (
|
||||
{% if from_dttm is not none %}
|
||||
dttm_col > '{{ from_dttm }}' AND
|
||||
{% endif %}
|
||||
{% if to_dttm is not none %}
|
||||
dttm_col < '{{ to_dttm }}' AND
|
||||
{% endif %}
|
||||
1 = 1
|
||||
)
|
||||
```
|
||||
|
||||
The `1 = 1` at the end ensures a value is present for the `WHERE` clause even when
|
||||
the time filter is not set. For many database engines, this could be replaced with `true`.
|
||||
|
||||
Note that the Jinja parameters are called within _double_ brackets in the query and with
|
||||
_single_ brackets in the logic blocks.
|
||||
|
||||
### Understanding Context Availability
|
||||
|
||||
Some Jinja variables like `from_dttm`, `to_dttm`, and `filter` are **only available when a chart or dashboard provides them**. They are populated from:
|
||||
Some Jinja variables like `filter` are **only available when a chart or dashboard provides them**. They are populated from:
|
||||
|
||||
- Time range filters applied in Explore/Chart views
|
||||
- Dashboard native filters
|
||||
- Filter components
|
||||
|
||||
**These variables are NOT available in standalone SQL Lab queries** because there's no filter context. If you try to use `{{ from_dttm }}` directly in SQL Lab, you'll get an "undefined parameter" error.
|
||||
|
||||
#### Testing Time-Filtered Queries in SQL Lab
|
||||
|
||||
To test queries that use time variables in SQL Lab, you have several options:
|
||||
|
||||
**Option 1: Use Jinja defaults (recommended)**
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM tbl
|
||||
WHERE dttm_col > '{{ from_dttm | default("2024-01-01", true) }}'
|
||||
AND dttm_col < '{{ to_dttm | default("2024-12-31", true) }}'
|
||||
```
|
||||
|
||||
**Option 2: Use SQL Lab Parameters**
|
||||
|
||||
Set parameters in the SQL Lab UI (Parameters menu):
|
||||
```json
|
||||
{
|
||||
"from_dttm": "2024-01-01",
|
||||
"to_dttm": "2024-12-31"
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Use `{% set %}` for testing**
|
||||
|
||||
```sql
|
||||
{% set from_dttm = "2024-01-01" %}
|
||||
{% set to_dttm = "2024-12-31" %}
|
||||
SELECT *
|
||||
FROM tbl
|
||||
WHERE dttm_col > '{{ from_dttm }}' AND dttm_col < '{{ to_dttm }}'
|
||||
```
|
||||
|
||||
:::tip
|
||||
When you save a SQL Lab query as a virtual dataset and use it in a chart with time filters,
|
||||
the actual filter values will override any defaults or test values you set.
|
||||
:::
|
||||
|
||||
To add custom functionality to the Jinja context, you need to overload the default Jinja
|
||||
context in your environment by defining the `JINJA_CONTEXT_ADDONS` in your superset configuration
|
||||
(`superset_config.py`). Objects referenced in this dictionary are made available for users to use
|
||||
@@ -479,10 +418,13 @@ Here's a concrete example:
|
||||
|
||||
### Time Filter
|
||||
|
||||
The `{{ get_time_filter() }}` macro returns the time filter applied to a specific column. This is useful if you want
|
||||
to handle time filters inside the virtual dataset, as by default the time filter is placed on the outer query. This can
|
||||
considerably improve performance, as many databases and query engines are able to optimize the query better
|
||||
if the temporal filter is placed on the inner query, as opposed to the outer query.
|
||||
The `{{ get_time_filter() }}` macro is the standard way to reference the selected time range
|
||||
in a virtual dataset. It returns a `TimeFilter` object whose `from_expr` and `to_expr`
|
||||
properties are fully-formatted SQL expressions for the start and end of the range — use them
|
||||
directly in your `WHERE` clause without quoting.
|
||||
|
||||
Placing the filter inside the virtual dataset (rather than relying on it being applied to
|
||||
the outer query) also gives many database engines the opportunity to optimize the query better.
|
||||
|
||||
The macro takes the following parameters:
|
||||
|
||||
|
||||
@@ -163,10 +163,6 @@ The architecture leverages Webpack's Module Federation to enable dynamic loading
|
||||
Extensions configure Webpack to expose their entry points:
|
||||
|
||||
```javascript
|
||||
externalsType: 'window',
|
||||
externals: {
|
||||
'@apache-superset/core': 'superset',
|
||||
},
|
||||
plugins: [
|
||||
new ModuleFederationPlugin({
|
||||
name: 'my_extension',
|
||||
@@ -178,6 +174,7 @@ plugins: [
|
||||
react: { singleton: true, import: false },
|
||||
'react-dom': { singleton: true, import: false },
|
||||
antd: { singleton: true, import: false },
|
||||
'@apache-superset/core': { singleton: true, import: false },
|
||||
},
|
||||
}),
|
||||
]
|
||||
@@ -187,9 +184,7 @@ This configuration does several important things:
|
||||
|
||||
**`exposes`** - Declares which modules are available to the host application. Superset always loads extensions by requesting the `./index` module from the remote container — this is a fixed convention, not a configurable value. Extensions must expose exactly `'./index': './src/index.tsx'` and place all API registrations (views, commands, menus, editors, event listeners) in that file. The module is executed as a side effect when the extension loads, so any call to `views.registerView`, `commands.registerCommand`, etc. made at the top level of `index.tsx` will run automatically.
|
||||
|
||||
**`externals` and `externalsType`** - Tell Webpack that when the extension imports `@apache-superset/core`, it should use `window.superset` at runtime instead of bundling its own copy. This ensures extensions use the host's implementation of shared packages.
|
||||
|
||||
**`shared`** - Prevents duplication of common libraries like React and Ant Design. The `singleton: true` setting ensures only one instance of each library exists, avoiding version conflicts and reducing bundle size.
|
||||
**`shared`** - Prevents duplication of common libraries like React and Ant Design, and, for `@apache-superset/core`, is the mechanism that gives each extension an isolated context (see below). The `singleton: true` setting ensures only one *logical* instance of each library exists — for `react`/`react-dom`/`antd` that means the host's actual instance is reused; for `@apache-superset/core` it means the extension's container defers to whatever module the host's loader supplies for it at init time, which is not the same object for every extension (see [Runtime Resolution](#runtime-resolution)).
|
||||
|
||||
### Runtime Resolution
|
||||
|
||||
@@ -200,25 +195,30 @@ The following diagram illustrates the module loading process:
|
||||
Here's what happens at runtime:
|
||||
|
||||
1. **Extension Registration**: When an extension is registered, Superset stores its remote entry URL
|
||||
2. **Dynamic Loading**: When the extension is activated, the host fetches the remote entry file
|
||||
3. **Module Resolution**: The extension imports `@apache-superset/core`, which resolves to `window.superset`
|
||||
4. **Execution**: The extension code runs with access to the host's APIs and shared dependencies
|
||||
2. **Dynamic Loading**: When the extension is activated, the host fetches the remote entry file, then initializes Module Federation sharing (`__webpack_init_sharing__`) and looks up the extension's container on `window`
|
||||
3. **Per-Extension Scope Injection**: Before calling `container.init()`, the loader builds a **per-container copy** of the webpack share scope in which the `@apache-superset/core` entry is replaced with a synthetic module — a copy of the host's own `window.superset` implementations with `extensions.getContext` bound to that one extension's isolated context object. Because Module Federation caches the resolved module per container, every import of `@apache-superset/core` inside that extension resolves to this pre-bound copy for the lifetime of the container, and because each container gets its own independent scope object, extensions loading in parallel cannot see each other's context.
|
||||
4. **Execution**: The extension code runs with access to the host's APIs (via its per-extension `@apache-superset/core` instance) and shared dependencies (`react`, `react-dom`, `antd`)
|
||||
|
||||
### Host API Setup
|
||||
|
||||
On the Superset side, the APIs are mapped to `window.superset` during application bootstrap:
|
||||
On the Superset side, the real implementations backing `@apache-superset/core` (`commands`, `views`, `menus`, `extensions`, etc.) are assigned onto `window.superset` during application bootstrap, once the current user is known:
|
||||
|
||||
```typescript
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
import * as supersetCore from '@apache-superset/core';
|
||||
import { commands, views, menus, extensions /* ... */ } from 'src/core';
|
||||
|
||||
export default function setupExtensionsAPI() {
|
||||
window.superset = {
|
||||
...supersetCore,
|
||||
};
|
||||
}
|
||||
window.superset = {
|
||||
...supersetCore,
|
||||
commands,
|
||||
views,
|
||||
menus,
|
||||
extensions,
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
This function runs before any extensions are loaded, ensuring the APIs are available when extensions import from `@apache-superset/core`.
|
||||
This runs before any extensions are loaded. `window.superset` is not itself what an extension's `@apache-superset/core` import resolves to — it is the source object the loader reads from when building each extension's per-container scoped instance (see [Runtime Resolution](#runtime-resolution)), so every extension ends up sharing the same underlying `commands`/`views`/`menus` singletons but with its own `extensions.getContext()`.
|
||||
|
||||
### Benefits
|
||||
|
||||
|
||||
@@ -47,13 +47,14 @@ Extension developers have access to pre-built UI components via `@apache-superse
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[Quick Start](./quick-start.md)** - Build your first extension with a complete walkthrough
|
||||
- **[Architecture](./architecture.md)** - Design principles and system overview
|
||||
- **[Dependencies](./dependencies.md)** - Managing dependencies and understanding API stability
|
||||
- **[Contribution Types](./contribution-types.md)** - Available extension points
|
||||
- **[Development](./development.md)** - Project structure, APIs, and development workflow
|
||||
- **[Deployment](./deployment.md)** - Packaging and deploying extensions
|
||||
- **[MCP Integration](./mcp.md)** - Adding AI agent capabilities using extensions
|
||||
- **[Security](./security.md)** - Security considerations and best practices
|
||||
- **[Tasks](./tasks.md)** - Framework for creating and managing long running tasks
|
||||
- **[Community Extensions](./registry.md)** - Browse extensions shared by the community
|
||||
- **[Quick Start](./quick-start)** - Build your first extension with a complete walkthrough
|
||||
- **[Architecture](./architecture)** - Design principles and system overview
|
||||
- **[Dependencies](./dependencies)** - Managing dependencies and understanding API stability
|
||||
- **[Contribution Types](./contribution-types)** - Available extension points
|
||||
- **[Development](./development)** - Project structure, APIs, and development workflow
|
||||
- **[Deployment](./deployment)** - Packaging and deploying extensions
|
||||
- **[MCP Integration](./mcp)** - Adding AI agent capabilities using extensions
|
||||
- **[Security](./security)** - Security considerations and best practices
|
||||
- **[Storage](./storage)** - Managed storage API for persisting extension data
|
||||
- **[Tasks](./tasks)** - Framework for creating and managing long running tasks
|
||||
- **[Community Extensions](./registry)** - Browse extensions shared by the community
|
||||
|
||||
@@ -223,7 +223,7 @@ The `@apache-superset/core` package must be listed in both `peerDependencies` (t
|
||||
|
||||
**`frontend/webpack.config.js`**
|
||||
|
||||
The webpack configuration requires specific settings for Module Federation. Key settings include `externalsType: "window"` and `externals` to map `@apache-superset/core` to `window.superset` at runtime, `import: false` for shared modules to use the host's React instead of bundling a separate copy, and `remoteEntry.[contenthash].js` for cache busting.
|
||||
The webpack configuration requires specific settings for Module Federation. `@apache-superset/core` is declared as a `shared` singleton alongside `react`/`react-dom`/`antd` — Superset injects a per-extension instance of the package into the container's share scope at load time, so the extension's own webpack config only needs to mark it `shared` with `singleton: true`, not resolve it itself. `import: false` on each shared entry means the extension doesn't bundle its own copy and instead uses the instance the host (or, for `@apache-superset/core`, the loader) provides. `remoteEntry.[contenthash].js` gives the built remote entry file a content hash for cache busting.
|
||||
|
||||
**Convention**: Superset always loads extensions by requesting the `./index` module from the Module Federation container. The `exposes` entry must be exactly `'./index': './src/index.tsx'` — do not rename or add additional entries. All API registrations must be reachable from that file. See [Architecture](./architecture.md#module-federation) for a full explanation.
|
||||
|
||||
@@ -255,11 +255,6 @@ module.exports = (env, argv) => {
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.jsx'],
|
||||
},
|
||||
// Map @apache-superset/core imports to window.superset at runtime
|
||||
externalsType: 'window',
|
||||
externals: {
|
||||
'@apache-superset/core': 'superset',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -292,6 +287,10 @@ module.exports = (env, argv) => {
|
||||
requiredVersion: packageConfig.peerDependencies['antd'],
|
||||
import: false,
|
||||
},
|
||||
'@apache-superset/core': {
|
||||
singleton: true,
|
||||
import: false, // Resolved to a per-extension instance by the loader
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
@@ -498,7 +497,7 @@ Here's what happens when your extension loads:
|
||||
1. **Superset starts**: Reads `manifest.json` from the `.supx` bundle and loads the backend entrypoint
|
||||
2. **Backend registration**: `entrypoint.py` imports your API class, triggering the [`@api`](https://github.com/apache/superset/blob/master/superset-core/src/superset_core/rest_api/decorators.py) decorator to register it automatically
|
||||
3. **Frontend loads**: When SQL Lab opens, Superset fetches the remote entry file
|
||||
4. **Module Federation**: Webpack loads your extension module and resolves `@apache-superset/core` to `window.superset`
|
||||
4. **Module Federation**: Webpack loads your extension module; the extension's `@apache-superset/core` import resolves to a per-extension instance injected into its container's share scope by the loader
|
||||
5. **Registration**: The module executes at load time, calling `views.registerView` to register your panel
|
||||
6. **Rendering**: When the user opens your panel, React renders `<HelloWorldPanel />`
|
||||
7. **API call**: The component fetches data from `/extensions/my-org/hello-world/message`
|
||||
|
||||
465
docs/developer_docs/extensions/storage.md
Normal file
465
docs/developer_docs/extensions/storage.md
Normal file
@@ -0,0 +1,465 @@
|
||||
---
|
||||
title: Storage
|
||||
sidebar_position: 8
|
||||
---
|
||||
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# Storage
|
||||
|
||||
Superset Extensions have access to a managed storage API for persisting data. The storage system provides multiple tiers with different persistence characteristics, allowing extensions to choose the right storage for their needs.
|
||||
|
||||
Each extension receives its own isolated storage namespace. When Superset loads your extension, it binds storage to your extension's unique identifier, ensuring data privacy—two extensions using the same key will never collide, and extensions cannot access each other's data.
|
||||
|
||||
Storage is always accessed through the extension context via `getContext()`. This binding to the context is what ties every operation to the current extension's namespace.
|
||||
|
||||
## Storage Tiers
|
||||
|
||||
| Tier | Storage Type | Context Property | Use Case |
|
||||
| ---- | ----------------- | ------------------------------------------ | -------------------------------------- |
|
||||
| 1 | Browser storage | `ctx.storage.local`, `ctx.storage.session` | UI state, wizard progress, draft forms |
|
||||
| 2 | Server-side cache | `ctx.storage.ephemeral` | Job progress, temporary results |
|
||||
| 3 | Database | `ctx.storage.persistent` | Saved artifacts, durable config |
|
||||
|
||||
## Tier 1: Local State
|
||||
|
||||
Browser-based storage that persists on the user's device. Use this for UI state and settings that don't need to sync across devices.
|
||||
|
||||
### Why Use the API Instead of localStorage Directly?
|
||||
|
||||
You might wonder why extensions should use `ctx.storage.local` instead of directly accessing `window.localStorage`. The managed API provides several benefits:
|
||||
|
||||
- **Automatic namespacing**: Each extension's data is isolated. Two extensions using the same key name won't collide.
|
||||
- **User isolation**: By default, data is scoped to the current user, preventing data leakage between users on shared devices.
|
||||
- **Clean uninstall**: When an extension is uninstalled, all its data can be cleanly removed using prefix-based deletion.
|
||||
- **Future sandboxing**: The async API is designed for a future sandboxed execution model where extensions run in isolated contexts without direct DOM access.
|
||||
- **Consistent patterns**: The same API shape works across all storage tiers, making it easy to switch between them.
|
||||
|
||||
### localState
|
||||
|
||||
Data persists across browser sessions until explicitly deleted or the user clears browser storage.
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
// Save sidebar state
|
||||
await ctx.storage.local.set('sidebar_collapsed', true);
|
||||
|
||||
// Retrieve it later
|
||||
const isCollapsed = await ctx.storage.local.get('sidebar_collapsed');
|
||||
|
||||
// Remove it
|
||||
await ctx.storage.local.remove('sidebar_collapsed');
|
||||
```
|
||||
|
||||
### sessionState
|
||||
|
||||
Data is cleared when the browser tab is closed. Use for transient state within a single session.
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
// Save wizard progress (lost when tab closes)
|
||||
await ctx.storage.session.set('wizard_step', 3);
|
||||
await ctx.storage.session.set('unsaved_form', { name: 'Draft' });
|
||||
|
||||
// Retrieve on page reload within same tab
|
||||
const step = await ctx.storage.session.get('wizard_step');
|
||||
```
|
||||
|
||||
Tier 1 has no `shared` accessor. Browser storage is per-device, so a "shared" value would only be visible to other users of that same browser, not to other users of the extension generally — see `.shared` on [Tier 2](#tier-2-ephemeral-state) or [Tier 3](#tier-3-persistent-state) for storage that's actually shared across users.
|
||||
|
||||
### When to Use Tier 1
|
||||
|
||||
- UI state (sidebar collapsed, panel sizes)
|
||||
- Recently used items
|
||||
- Draft form values
|
||||
- Any data acceptable to lose if user clears browser
|
||||
|
||||
### Limitations
|
||||
|
||||
- Per-browser, per-device (not shared across devices)
|
||||
- Subject to browser storage quotas (~5-10 MB)
|
||||
- Not accessible from backend code
|
||||
|
||||
## Tier 2: Ephemeral State
|
||||
|
||||
Server-side cache storage with automatic TTL expiration. Use for temporary data that needs to be shared between frontend and backend, or persist across page reloads.
|
||||
|
||||
TTL is required for every `set` call. The maximum allowed TTL is controlled by `MAX_TTL` in the server configuration.
|
||||
|
||||
### Frontend Usage
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
// Store with a 5-minute TTL
|
||||
await ctx.storage.ephemeral.set('job_progress', { pct: 42, status: 'running' }, { ttl: 300 });
|
||||
|
||||
// Retrieve
|
||||
const progress = await ctx.storage.ephemeral.get('job_progress');
|
||||
|
||||
// Remove
|
||||
await ctx.storage.ephemeral.remove('job_progress');
|
||||
```
|
||||
|
||||
### Backend Usage
|
||||
|
||||
```python
|
||||
from superset_core.extensions.context import get_context
|
||||
from superset_core.extensions.storage.ephemeral import EphemeralSetOptions
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
# Store job progress with a 5-minute TTL
|
||||
ctx.storage.ephemeral.set(
|
||||
'job_progress', {'pct': 42, 'status': 'running'}, EphemeralSetOptions(ttl=300)
|
||||
)
|
||||
|
||||
# Retrieve
|
||||
progress = ctx.storage.ephemeral.get('job_progress')
|
||||
|
||||
# Remove
|
||||
ctx.storage.ephemeral.remove('job_progress')
|
||||
```
|
||||
|
||||
### Shared State
|
||||
|
||||
For data that needs to be visible to all users:
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
await ctx.storage.ephemeral.shared.set('shared_result', { data: [1, 2, 3] }, { ttl: 3600 });
|
||||
const result = await ctx.storage.ephemeral.shared.get('shared_result');
|
||||
```
|
||||
|
||||
```python
|
||||
from superset_core.extensions.context import get_context
|
||||
from superset_core.extensions.storage.ephemeral import EphemeralSetOptions
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
ctx.storage.ephemeral.shared.set(
|
||||
'shared_result', {'data': [1, 2, 3]}, EphemeralSetOptions(ttl=3600)
|
||||
)
|
||||
result = ctx.storage.ephemeral.shared.get('shared_result')
|
||||
```
|
||||
|
||||
### When to Use Tier 2
|
||||
|
||||
- Background job progress indicators
|
||||
- Cross-request intermediate state
|
||||
- Query result previews
|
||||
- Temporary computation results
|
||||
- Any data that can be recomputed if lost
|
||||
|
||||
### Limitations
|
||||
|
||||
- Not guaranteed to survive server restarts
|
||||
- Subject to cache eviction under memory pressure
|
||||
- TTL-based expiration (data disappears after timeout)
|
||||
- TTL is required on every `set` call
|
||||
|
||||
## Tier 3: Persistent State
|
||||
|
||||
Database-backed storage that survives server restarts, cache evictions, and browser clears. Use for any data that must not be lost.
|
||||
|
||||
### Frontend Usage
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
// Store a saved SQL snippet
|
||||
await ctx.storage.persistent.set('snippet:top_customers', { sql: 'SELECT ...' });
|
||||
|
||||
// Retrieve
|
||||
const snippet = await ctx.storage.persistent.get('snippet:top_customers');
|
||||
|
||||
// Remove
|
||||
await ctx.storage.persistent.remove('snippet:top_customers');
|
||||
```
|
||||
|
||||
### Backend Usage
|
||||
|
||||
```python
|
||||
from superset_core.extensions.context import get_context
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
# Store a saved SQL snippet
|
||||
ctx.storage.persistent.set('snippet:top_customers', {'sql': 'SELECT ...'})
|
||||
|
||||
# Retrieve
|
||||
snippet = ctx.storage.persistent.get('snippet:top_customers')
|
||||
|
||||
# Remove
|
||||
ctx.storage.persistent.remove('snippet:top_customers')
|
||||
```
|
||||
|
||||
### Shared State
|
||||
|
||||
For data that should be visible to all users of the extension:
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
await ctx.storage.persistent.shared.set('global_config', { version: 2 });
|
||||
const config = await ctx.storage.persistent.shared.get('global_config');
|
||||
```
|
||||
|
||||
```python
|
||||
from superset_core.extensions.context import get_context
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
ctx.storage.persistent.shared.set('global_config', {'version': 2})
|
||||
config = ctx.storage.persistent.shared.get('global_config')
|
||||
```
|
||||
|
||||
### Listing Entries
|
||||
|
||||
Both the frontend and backend accessors support listing entries in the caller's scope, without needing to know every key in advance. `page` and `pageSize`/`page_size` are required on every call — `list()` always returns one page of results, never the whole result set, so there's no default that could make that fact easy to miss. Check the returned `count` (total entries matching the scope/filters, across all pages) against `pageSize`/`page_size` to know whether more pages exist.
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
// Filter by resource
|
||||
const result = await ctx.storage.persistent.list({
|
||||
resourceType: 'dashboard',
|
||||
resourceUuid: '1234-uuid',
|
||||
page: 0,
|
||||
pageSize: 25,
|
||||
});
|
||||
result.entries.forEach(entry => {
|
||||
console.log(entry.key, entry.value, entry.codec);
|
||||
});
|
||||
console.log(result.count); // total matching entries across all pages
|
||||
|
||||
// Shared (global) scope
|
||||
const shared = await ctx.storage.persistent.shared.list({ page: 0, pageSize: 10 });
|
||||
```
|
||||
|
||||
```python
|
||||
from superset_core.extensions.context import get_context
|
||||
from superset_core.extensions.storage.persistent import PersistentListOptions
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
# Filter by resource
|
||||
result = ctx.storage.persistent.list(
|
||||
PersistentListOptions(
|
||||
resource_type="dashboard",
|
||||
resource_uuid="1234-uuid",
|
||||
page=0,
|
||||
page_size=25,
|
||||
)
|
||||
)
|
||||
for entry in result.entries:
|
||||
print(entry.key, entry.value, entry.codec)
|
||||
print(result.count) # total matching entries across all pages
|
||||
|
||||
# Shared (global) scope
|
||||
shared = ctx.storage.persistent.shared.list(PersistentListOptions(page=0, page_size=10))
|
||||
```
|
||||
|
||||
`resource_type`/`resource_uuid` filter to entries linked to one resource — since a single resource can have several keyed entries (e.g. `"layout"`, `"notes"`), this is how an extension enumerates all of them without tracking key names itself.
|
||||
|
||||
There is no fixed limit on `page_size`, but the combined size of a requested page's values is checked against `MAX_LIST_PAYLOAD_SIZE` (see [Quotas](#quotas)) before any row's value is read from the database — a page that would exceed it is rejected rather than silently truncated, so reduce `page_size` and retry if that happens.
|
||||
|
||||
Backend `list()` decodes every entry's value unconditionally. The REST API backing the frontend's `list()` call is more restrictive: an entry written with a codec that isn't safe to decode over the wire (e.g. `pickle`) comes back with `value: null` (its `codec` is still reported) rather than being decoded.
|
||||
|
||||
### Enumerating and Managing Storage from the Backend
|
||||
|
||||
`ctx.storage.persistent.list()` covers the common case of listing an extension's own entries. Backend extension code that needs lower-level bulk operations — for example, a cleanup routine that removes storage linked to resources that no longer exist, using filters `list()` doesn't expose — can use `superset_core.extensions.storage.dao.ExtensionStorageDAO` instead:
|
||||
|
||||
```python
|
||||
from superset_core.extensions.storage.dao import ExtensionStorageDAO
|
||||
|
||||
# All entries this extension has linked to a given resource type
|
||||
entries = ExtensionStorageDAO.filter_by(resource_type="my-resource-type")
|
||||
|
||||
# Bulk-delete rows for resources that no longer exist
|
||||
orphaned = [e for e in entries if e.resource_uuid not in my_extension_live_resource_ids()]
|
||||
ExtensionStorageDAO.delete(orphaned)
|
||||
```
|
||||
|
||||
`ExtensionStorageDAO` is automatically scoped to the calling extension's own rows — `extension_id` is never a parameter you pass, so there is no way to reach another extension's storage through this API. It supports the standard DAO operations `find_all`, `find_one_or_none`, `filter_by`, `query`, `update`, and `delete`. `create()` is not supported — it's a raw insert with no upsert dedup, quota check, or locking against the key `.set()` writes to, so use `ctx.storage.persistent.set()` to write a value instead.
|
||||
|
||||
### When to Use Tier 3
|
||||
|
||||
- Extension configuration that must survive restarts
|
||||
- User-specific saved artifacts that need to roam across devices and browsers (e.g. saved SQL snippets, annotations)
|
||||
- Any data where loss is unacceptable
|
||||
|
||||
### Limitations
|
||||
|
||||
- Higher latency than Tiers 1–2 (database round-trip per operation)
|
||||
- Subject to the per-value size limit configured via `EXTENSIONS_PERSISTENT_STORAGE.MAX_VALUE_SIZE` (see [Quotas](#quotas))
|
||||
- Subject to the per-extension quota configured via `EXTENSIONS_PERSISTENT_STORAGE.QUOTA_PER_EXTENSION` (see [Quotas](#quotas))
|
||||
- Requires a database migration when first deployed
|
||||
|
||||
## Working with Binary Data
|
||||
|
||||
Tiers 2 and 3 accept a `codec` option describing how `value` is encoded — `"json"` (the default) for JSON-native values, or `"binary"` for raw bytes. `list()` and `get()` report which codec an entry was written with via `entry.codec`.
|
||||
|
||||
### Backend
|
||||
|
||||
Backend code talks to the storage DAO directly and works with real Python `bytes` — no extra flag is needed:
|
||||
|
||||
```python
|
||||
from superset_core.extensions.context import get_context
|
||||
from superset_core.extensions.storage.persistent import PersistentSetOptions
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
with open('logo.png', 'rb') as f:
|
||||
png_bytes = f.read()
|
||||
|
||||
ctx.storage.persistent.set('logo', png_bytes, PersistentSetOptions(codec='binary'))
|
||||
|
||||
stored = ctx.storage.persistent.get('logo') # raw bytes back, unchanged
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
The frontend SDK talks to the backend over a JSON REST API, which has no byte type — a binary value must be base64-encoded to travel in a JSON request body. There is no way for the SDK to infer this from a JS value's type, so it is never detected automatically: set `isBinary: true` explicitly whenever `value` is binary.
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47 /* ... */]);
|
||||
await ctx.storage.persistent.set('logo', pngBytes, { isBinary: true });
|
||||
```
|
||||
|
||||
`get()` returns a binary entry's value exactly as stored on the wire — a base64 string, not a `Uint8Array` — so decode it yourself:
|
||||
|
||||
```typescript
|
||||
const base64 = await ctx.storage.persistent.get<string>('logo');
|
||||
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
|
||||
```
|
||||
|
||||
`list()` entries report `isBinary` alongside `codec`, so you know which entries need decoding before you read `value`:
|
||||
|
||||
```typescript
|
||||
const result = await ctx.storage.persistent.list({ page: 0, pageSize: 10 });
|
||||
result.entries.forEach(entry => {
|
||||
const value = entry.isBinary
|
||||
? Uint8Array.from(atob(entry.value as string), c => c.charCodeAt(0))
|
||||
: entry.value;
|
||||
});
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
All storage keys are automatically namespaced:
|
||||
|
||||
| Scope | Key Pattern |
|
||||
| ---------------------- | -------------------------------------------------- |
|
||||
| User-scoped | `superset-ext:{extension_id}:user:{user_id}:{key}` |
|
||||
| Shared (Tiers 2 and 3) | `superset-ext:{extension_id}:shared:{key}` |
|
||||
|
||||
This ensures:
|
||||
|
||||
- Extensions cannot accidentally access each other's data
|
||||
- Users cannot see other users' data (by default)
|
||||
- Clean prefix-based deletion on uninstall
|
||||
|
||||
## Configuration
|
||||
|
||||
### Tier 2: Ephemeral Storage
|
||||
|
||||
Administrators can configure the server-side cache backend in `superset_config.py`:
|
||||
|
||||
```python
|
||||
EXTENSIONS_EPHEMERAL_STORAGE = {
|
||||
# Use Redis for better performance in production
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
"CACHE_REDIS_URL": "redis://localhost:6379/2",
|
||||
"MAX_TTL": 604800, # 7 days maximum TTL
|
||||
"MAX_VALUE_SIZE": 100 * 1024, # 100 KB maximum per stored value
|
||||
}
|
||||
```
|
||||
|
||||
For development, the default `SupersetMetastoreCache` stores data in the metadata database.
|
||||
|
||||
### Tier 3: Persistent Storage
|
||||
|
||||
Tier 3 values are stored in the `extension_storage` database table. Values are stored unencrypted by default; encryption is opt-in per write, available via a `PersistentSetOptions`-shaped `encrypt` option:
|
||||
|
||||
```typescript
|
||||
import { extensions } from '@apache-superset/core';
|
||||
|
||||
const ctx = extensions.getContext();
|
||||
|
||||
await ctx.storage.persistent.set('api_token', 'sk-...', { encrypt: true });
|
||||
const token = await ctx.storage.persistent.get('api_token');
|
||||
```
|
||||
|
||||
```python
|
||||
from superset_core.extensions.context import get_context
|
||||
from superset_core.extensions.storage.persistent import PersistentSetOptions
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
ctx.storage.persistent.set('api_token', 'sk-...', PersistentSetOptions(encrypt=True))
|
||||
token = ctx.storage.persistent.get('api_token')
|
||||
```
|
||||
|
||||
Encryption reuses Superset's existing `EncryptedType` (from `sqlalchemy-utils`) rather than a separate mechanism — the same infrastructure used for database connection credentials. The key is not configured directly: user-scoped values are encrypted with a key derived per-user via HMAC-SHA256 from the deployment's `SECRET_KEY`, so ciphertext for one user cannot be decrypted as another's; shared/global values (written via `.shared`) use `SECRET_KEY` directly. The encryption engine (AES-CBC by default) can be changed for the whole deployment via the existing `SQLALCHEMY_ENCRYPTED_FIELD_ENGINE` config, the same setting that controls encryption for database credentials elsewhere in Superset — there is no separate key list or rotation mechanism specific to extension storage. When `SECRET_KEY` is rotated, extension storage rows are re-encrypted by the existing `SecretsMigrator` tooling alongside database credentials, with no extension-specific steps required.
|
||||
|
||||
### Quotas
|
||||
|
||||
Persistent storage is subject to per-value, per-list-page, and per-extension size limits, configured in `superset_config.py`:
|
||||
|
||||
```python
|
||||
EXTENSIONS_PERSISTENT_STORAGE = {
|
||||
# Maximum total bytes an extension may store across all of its rows
|
||||
# (global, every user's, and shared). Defaults to 100 MB.
|
||||
"QUOTA_PER_EXTENSION": 100 * 1024 * 1024,
|
||||
# Maximum size (in bytes) of a single stored value. Defaults to 1 MB.
|
||||
"MAX_VALUE_SIZE": 1024 * 1024,
|
||||
# Maximum combined value size (in bytes) that a single list() page may
|
||||
# return. Defaults to 10 MB.
|
||||
"MAX_LIST_PAYLOAD_SIZE": 10 * 1024 * 1024,
|
||||
}
|
||||
```
|
||||
|
||||
A write that would push the extension's total stored size over `QUOTA_PER_EXTENSION` is rejected — the frontend SDK's `set()` call rejects with an HTTP 413 response, and backend code calling `ctx.storage.persistent.set()` sees the corresponding exception raised. Overwriting an existing key nets out that key's own current size against usage first, so replacing a value with one of the same or smaller size never spuriously fails even when the extension is already near its quota.
|
||||
|
||||
A write whose encoded value exceeds `MAX_VALUE_SIZE` is rejected outright, independently of the total quota.
|
||||
|
||||
A `list()` call whose requested page would return more than `MAX_LIST_PAYLOAD_SIZE` combined bytes of value data is rejected — reduce `page`/`pageSize` and retry. This check runs against the page's stored sizes before any value is read from the database, so an oversized request fails fast. Because `list()`'s response is consumed as JSON in the browser, raising `MAX_LIST_PAYLOAD_SIZE` substantially above the default risks client-side memory and parse-time issues — treat the default as already generous rather than a floor to build up from.
|
||||
@@ -146,7 +146,9 @@ The `where_in` filter converts the list to SQL format: `('value1', 'value2', 'va
|
||||
|
||||
### Time Filters
|
||||
|
||||
For charts with time range filters:
|
||||
`get_time_filter()` is the standard way to reference the selected time range in a virtual dataset.
|
||||
It returns a `TimeFilter` object with `from_expr` and `to_expr` — fully-formatted SQL expressions
|
||||
that should be used directly in `WHERE` clauses without quoting.
|
||||
|
||||
| Macro | Description |
|
||||
|-------|-------------|
|
||||
@@ -198,17 +200,9 @@ GROUP BY category
|
||||
|
||||
## Testing Templates in SQL Lab
|
||||
|
||||
Some variables like `from_dttm` and `filter_values()` only work when filters are applied from dashboards or charts. To test in SQL Lab:
|
||||
Some variables like `filter_values()` only work when filters are applied from dashboards or charts. To test in SQL Lab:
|
||||
|
||||
**Option 1: Use defaults**
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM orders
|
||||
WHERE date >= '{{ from_dttm | default("2024-01-01", true) }}'
|
||||
```
|
||||
|
||||
**Option 2: Set test parameters**
|
||||
**Option 1: Set test parameters**
|
||||
|
||||
Add to the Parameters menu:
|
||||
|
||||
@@ -220,7 +214,7 @@ Add to the Parameters menu:
|
||||
}
|
||||
```
|
||||
|
||||
**Option 3: Use `{% set %}`**
|
||||
**Option 2: Use `{% set %}`**
|
||||
|
||||
```sql
|
||||
{% set start_date = "2024-01-01" %}
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
"@swc/core": "^1.15.43",
|
||||
"antd": "^6.5.1",
|
||||
"baseline-browser-mapping": "^2.10.43",
|
||||
"caniuse-lite": "^1.0.30001805",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.2",
|
||||
"js-yaml": "^5.2.0",
|
||||
"js-yaml": "^5.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -82,12 +82,14 @@ const sidebars = {
|
||||
items: [
|
||||
'extensions/extension-points/sqllab',
|
||||
'extensions/extension-points/editors',
|
||||
'extensions/extension-points/chat',
|
||||
],
|
||||
},
|
||||
'extensions/development',
|
||||
'extensions/deployment',
|
||||
'extensions/mcp',
|
||||
'extensions/security',
|
||||
'extensions/storage',
|
||||
'extensions/tasks',
|
||||
'extensions/registry',
|
||||
],
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"generated": "2026-04-17T17:08:35.132Z",
|
||||
"statistics": {
|
||||
"totalDatabases": 80,
|
||||
"withDocumentation": 80,
|
||||
"withConnectionString": 80,
|
||||
"withDrivers": 28,
|
||||
"totalDatabases": 81,
|
||||
"withDocumentation": 81,
|
||||
"withConnectionString": 81,
|
||||
"withDrivers": 29,
|
||||
"withAuthMethods": 5,
|
||||
"supportsJoins": 74,
|
||||
"supportsSubqueries": 77,
|
||||
"supportsJoins": 75,
|
||||
"supportsSubqueries": 78,
|
||||
"supportsDynamicSchema": 29,
|
||||
"supportsCatalog": 20,
|
||||
"averageScore": 54,
|
||||
"averageScore": 53,
|
||||
"maxScore": 201,
|
||||
"byCategory": {
|
||||
"Cloud - AWS": [
|
||||
@@ -20,6 +20,7 @@
|
||||
],
|
||||
"Query Engines": [
|
||||
"Amazon Athena",
|
||||
"Apache DataFusion",
|
||||
"Apache Drill",
|
||||
"Apache Hive",
|
||||
"Apache Impala",
|
||||
@@ -94,6 +95,7 @@
|
||||
"Vertica"
|
||||
],
|
||||
"Apache Projects": [
|
||||
"Apache DataFusion",
|
||||
"Apache Doris",
|
||||
"Apache Drill",
|
||||
"Apache Druid",
|
||||
@@ -108,6 +110,7 @@
|
||||
"Databricks Interactive Cluster"
|
||||
],
|
||||
"Open Source": [
|
||||
"Apache DataFusion",
|
||||
"Apache Doris",
|
||||
"Apache Drill",
|
||||
"Apache Druid",
|
||||
@@ -9698,6 +9701,97 @@
|
||||
"supports_dynamic_schema": true,
|
||||
"supports_catalog": true,
|
||||
"supports_dynamic_catalog": true
|
||||
},
|
||||
"Apache DataFusion": {
|
||||
"time_grains": {
|
||||
"SECOND": true,
|
||||
"FIVE_SECONDS": true,
|
||||
"THIRTY_SECONDS": true,
|
||||
"MINUTE": true,
|
||||
"FIVE_MINUTES": true,
|
||||
"TEN_MINUTES": true,
|
||||
"FIFTEEN_MINUTES": true,
|
||||
"THIRTY_MINUTES": true,
|
||||
"HALF_HOUR": false,
|
||||
"HOUR": true,
|
||||
"SIX_HOURS": false,
|
||||
"DAY": true,
|
||||
"WEEK": true,
|
||||
"WEEK_STARTING_SUNDAY": false,
|
||||
"WEEK_STARTING_MONDAY": false,
|
||||
"WEEK_ENDING_SATURDAY": false,
|
||||
"WEEK_ENDING_SUNDAY": false,
|
||||
"MONTH": true,
|
||||
"QUARTER": true,
|
||||
"QUARTER_YEAR": false,
|
||||
"YEAR": true
|
||||
},
|
||||
"module": "superset.db_engine_specs.datafusion",
|
||||
"limit_method": 1,
|
||||
"limit_clause": true,
|
||||
"joins": true,
|
||||
"subqueries": true,
|
||||
"alias_in_select": true,
|
||||
"alias_in_orderby": true,
|
||||
"time_groupby_inline": false,
|
||||
"alias_to_source_column": false,
|
||||
"order_by_not_in_select": true,
|
||||
"expressions_in_orderby": false,
|
||||
"cte_in_subquery": true,
|
||||
"max_column_name": null,
|
||||
"sql_comments": true,
|
||||
"escaped_colons": true,
|
||||
"masked_encrypted_extra": false,
|
||||
"column_type_mapping": false,
|
||||
"function_names": false,
|
||||
"user_impersonation": false,
|
||||
"file_upload": false,
|
||||
"get_extra_table_metadata": false,
|
||||
"dbapi_exception_mapping": false,
|
||||
"custom_errors": false,
|
||||
"dynamic_schema": false,
|
||||
"catalog": false,
|
||||
"dynamic_catalog": false,
|
||||
"ssh_tunneling": true,
|
||||
"query_cancelation": false,
|
||||
"get_metrics": false,
|
||||
"where_latest_partition": false,
|
||||
"expand_data": false,
|
||||
"query_cost_estimation": false,
|
||||
"sql_validation": false,
|
||||
"score": 24,
|
||||
"max_score": 201,
|
||||
"documentation": {
|
||||
"description": "DataFusion is a highly performant query engine",
|
||||
"logo": "datafusion.png",
|
||||
"homepage_url": "https://datafusion.apache.org/",
|
||||
"categories": [
|
||||
"Query Engines",
|
||||
"Open Source",
|
||||
"Apache Projects"
|
||||
],
|
||||
"pypi_packages": [
|
||||
"flightsql-dbapi"
|
||||
],
|
||||
"connection_string": "datafusion://host:port",
|
||||
"drivers": [
|
||||
{
|
||||
"name": "Arrow Flight SQL (Recommended)",
|
||||
"pypi_package": "flightsql-dbapi",
|
||||
"connection_string": "datafusion://host:port",
|
||||
"is_recommended": true
|
||||
}
|
||||
],
|
||||
"category": "Apache Projects"
|
||||
},
|
||||
"engine": "datafusion",
|
||||
"engine_name": "Apache DataFusion",
|
||||
"engine_aliases": [],
|
||||
"default_driver": "flightsql",
|
||||
"supports_file_upload": false,
|
||||
"supports_dynamic_schema": false,
|
||||
"supports_catalog": false,
|
||||
"supports_dynamic_catalog": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
docs/static/.htaccess
vendored
3
docs/static/.htaccess
vendored
@@ -28,8 +28,9 @@ RewriteRule ^(.*)$ https://superset.apache.org/$1 [R=301,L]
|
||||
# - *.googleapis.com, *.google.com, *.gstatic.com: Google Calendar embed, kapa.ai reCAPTCHA - all of these loaded with user consent, following policy laid out in https://privacy.apache.org/faq/committers.html
|
||||
# - github.com, *.github.com, *.githubusercontent.com: GitHub user-attachment images in docs (apex github.com serves user-attachments/* assets). Discussed/resolved in this thread: https://issues.apache.org/jira/browse/INFRA-25701?filter=-2 (DPA in place with GitHub)
|
||||
# - *.algolia.net, *.algolianet.com: Algolia DocSearch. Approved here: https://privacy.apache.org/faq/committers.html
|
||||
# - *.run.app: Google-owned domain (Google Cloud Run) used by the kapa.ai widget backend - only reached after explicit user acceptance when opening the widget, allowable per the policy laid out in https://privacy.apache.org/faq/committers.html
|
||||
# See: https://infra.apache.org/tools/csp.html
|
||||
SetEnv CSP_PROJECT_DOMAINS "widget.kapa.ai https://*.googleapis.com/ https://*.google.com/ https://*.gstatic.com/ https://github.com/ https://*.github.com/ https://*.githubusercontent.com/ https://*.algolia.net/ https://*.algolianet.com/"
|
||||
SetEnv CSP_PROJECT_DOMAINS "widget.kapa.ai https://*.googleapis.com/ https://*.google.com/ https://*.gstatic.com/ https://github.com/ https://*.github.com/ https://*.githubusercontent.com/ https://*.algolia.net/ https://*.algolianet.com/ https://*.run.app/"
|
||||
|
||||
# REDIRECTS
|
||||
|
||||
|
||||
6
docs/static/feature-flags.json
vendored
6
docs/static/feature-flags.json
vendored
@@ -405,12 +405,6 @@
|
||||
"default": true,
|
||||
"lifecycle": "deprecated",
|
||||
"description": "Enable drill-to-detail functionality in charts"
|
||||
},
|
||||
{
|
||||
"name": "ENABLE_JAVASCRIPT_CONTROLS",
|
||||
"default": false,
|
||||
"lifecycle": "deprecated",
|
||||
"description": "Allow JavaScript in chart controls. WARNING: XSS security vulnerability!"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
docs/static/img/databases/datafusion.png
vendored
Normal file
BIN
docs/static/img/databases/datafusion.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/static/img/logos/pattern_logo.png
vendored
Normal file
BIN
docs/static/img/logos/pattern_logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
@@ -6394,9 +6394,9 @@ binary-extensions@^2.0.0:
|
||||
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
|
||||
|
||||
body-parser@~1.20.5:
|
||||
version "1.20.5"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.5.tgz#303c8c34423d1d6fa799bc764e93c1e4dc6ebf64"
|
||||
integrity sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==
|
||||
version "1.20.6"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.6.tgz#60c789c78e0992d906da0a29d71ae01d15c1ed76"
|
||||
integrity sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==
|
||||
dependencies:
|
||||
bytes "~3.1.2"
|
||||
content-type "~1.0.5"
|
||||
@@ -6602,10 +6602,10 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001805:
|
||||
version "1.0.30001805"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz#78d5d5968a69b7ff81af87a96d7ddc7ea6670b1e"
|
||||
integrity sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001806:
|
||||
version "1.0.30001806"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz#1bc8e502b723fa393455dfbedd5ccec0c29bb74e"
|
||||
integrity sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -7963,9 +7963,9 @@ domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@^3.3.3, dompurify@^3.4.11:
|
||||
version "3.4.11"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.11.tgz#29c8ba496475f279ef4015784068452fb14a0680"
|
||||
integrity sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==
|
||||
version "3.4.12"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.12.tgz#6fa2265e9bbdce882c4ace4107626051b448ffa8"
|
||||
integrity sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
@@ -8682,9 +8682,9 @@ fast-safe-stringify@^2.0.7:
|
||||
integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
|
||||
|
||||
fast-uri@^3.0.1:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.3.tgz#f695a40f006aba505631573a0021ddb21194ad11"
|
||||
integrity sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.4.tgz#3b3daf9ce68f41f956df0b505132c0cfce9ec7af"
|
||||
integrity sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.20.1"
|
||||
@@ -10155,10 +10155,10 @@ js-yaml@=4.2.0:
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
js-yaml@^5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.2.1.tgz#bb3f2e87295ebfe6ef0a75e17c62834daeff9ce2"
|
||||
integrity sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==
|
||||
js-yaml@^5.2.2:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-5.2.2.tgz#497bfe63f0b0db11c7bbc5ce8bc568e836c8b08c"
|
||||
integrity sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==
|
||||
dependencies:
|
||||
argparse "^2.0.1"
|
||||
|
||||
@@ -11681,10 +11681,10 @@ mz@^2.7.0:
|
||||
object-assign "^4.0.1"
|
||||
thenify-all "^1.0.0"
|
||||
|
||||
nanoid@^3.3.12:
|
||||
version "3.3.15"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316"
|
||||
integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==
|
||||
nanoid@^3.3.16:
|
||||
version "3.3.16"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c"
|
||||
integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
@@ -12974,11 +12974,11 @@ postcss-zindex@^6.0.2:
|
||||
integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==
|
||||
|
||||
postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.33, postcss@^8.5.4:
|
||||
version "8.5.16"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28"
|
||||
integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==
|
||||
version "8.5.23"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.23.tgz#3493550116f478487298301d2c2e8dc5a56e6594"
|
||||
integrity sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==
|
||||
dependencies:
|
||||
nanoid "^3.3.12"
|
||||
nanoid "^3.3.16"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
@@ -14889,9 +14889,9 @@ svg-parser@^2.0.4:
|
||||
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
|
||||
|
||||
svgo@^3.0.2, svgo@^3.2.0:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.3.tgz#8246aee0b08791fde3b0ed22b5661b471fadf58e"
|
||||
integrity sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.4.tgz#fd2aa10ff585b3bd2b83ce3602f5582bc0718bb5"
|
||||
integrity sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==
|
||||
dependencies:
|
||||
commander "^7.2.0"
|
||||
css-select "^5.1.0"
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.22.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.22.3 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
@@ -131,7 +131,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|
||||
| httproute.hostnames | list | `[]` | Hostnames that match against the HTTP Host header (templated) |
|
||||
| httproute.labels | object | `{}` | Additional labels to add to the HTTPRoute |
|
||||
| httproute.parentRefs | list | `[]` | Gateways this HTTPRoute attaches to |
|
||||
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. |
|
||||
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. When both `supersetMcp.enabled` and `supersetMcp.httproute.enabled` are true, an extra rule routing `supersetMcp.httproute.path` to the `-mcp` service is also appended. |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.repository | string | `"apachesuperset.docker.scarf.sh/apache/superset"` | |
|
||||
| image.tag | string | `nil` | |
|
||||
@@ -176,7 +176,10 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|
||||
| redis | object | see `values.yaml` | Configuration values for the Redis dependency. ref: https://github.com/bitnami/charts/blob/master/bitnami/redis More documentation can be found here: https://artifacthub.io/packages/helm/bitnami/redis |
|
||||
| resources | object | `{}` | |
|
||||
| runAsUser | int | `0` | User ID directive. This user must have enough permissions to run the bootstrap script Running containers as root is not recommended in production. Change this to another UID - e.g. 1000 to be more secure |
|
||||
| secretEnv | object | `{"create":true}` | Specify rather or not helm should create the secret described in `secret-env.yaml` template |
|
||||
| secretConfig | object | `{"annotations":{}}` | Specify whether Helm should apply additional settings to a secret containing configuration files |
|
||||
| secretConfig.annotations | object | `{}` | Annotations to be added to the secret which contains configuration files |
|
||||
| secretEnv | object | `{"annotations":{},"create":true}` | Specify rather or not helm should create the secret described in `secret-env.yaml` template |
|
||||
| secretEnv.annotations | object | `{}` | Annotations to be added to the secret |
|
||||
| secretEnv.create | bool | `true` | Change to false in order to support externally created secret (Binami "Sealed Secrets" for Kubernetes or External Secrets Operator) note: when externally creating the secret, the chart still expects to pull values from a secret with the name of the release defaults to `release-name-superset-env` - full logic located in _helpers.tpl file: `define "superset.fullname"` |
|
||||
| service.annotations | object | `{}` | |
|
||||
| service.loadBalancerIP | string | `nil` | |
|
||||
@@ -263,6 +266,8 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|
||||
| supersetMcp.extraContainers | list | `[]` | Launch additional containers into supersetMcp pods |
|
||||
| supersetMcp.extraInitContainers | list | `[]` | Extra init containers appended after supersetMcp initContainers |
|
||||
| supersetMcp.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
|
||||
| supersetMcp.httproute.enabled | bool | `false` | If true, the MCP server will be exposed via the HTTPRoute /mcp subpath |
|
||||
| supersetMcp.httproute.path | string | `"/mcp"` | |
|
||||
| supersetMcp.ingress.enabled | bool | `false` | If true, the MCP server will be exposed via the ingress /mcp subpath |
|
||||
| supersetMcp.ingress.path | string | `"/mcp"` | |
|
||||
| supersetMcp.ingress.pathType | string | `"Prefix"` | |
|
||||
@@ -361,6 +366,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|
||||
| supersetWebsockets.affinity | object | `{}` | Affinity to be added to supersetWebsockets deployment |
|
||||
| supersetWebsockets.command | list | `[]` | |
|
||||
| supersetWebsockets.config | object | see `values.yaml` | The config.json to pass to the server, see https://github.com/apache/superset/tree/master/superset-websocket Note that the configuration can also read from environment variables (which will have priority), see https://github.com/apache/superset/blob/master/superset-websocket/src/config.ts for a list of supported variables |
|
||||
| supersetWebsockets.configAnnotations | object | `{}` | Annotations to be added to the secret which contains config.json |
|
||||
| supersetWebsockets.containerSecurityContext | object | `{}` | |
|
||||
| supersetWebsockets.deploymentAdditionalPodSpec | object | `{}` | Custom pod spec to be added to supersetWebsockets deployment |
|
||||
| supersetWebsockets.deploymentAnnotations | object | `{}` | |
|
||||
|
||||
@@ -80,4 +80,16 @@ spec:
|
||||
type: PathPrefix
|
||||
value: {{ .Values.supersetWebsockets.ingress.path }}
|
||||
{{- end }}
|
||||
{{- if and .Values.supersetMcp.enabled .Values.supersetMcp.httproute.enabled }}
|
||||
- backendRefs:
|
||||
- group: ''
|
||||
kind: Service
|
||||
name: {{ $fullName }}-mcp
|
||||
port: {{ .Values.supersetMcp.service.port }}
|
||||
weight: 1
|
||||
matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: {{ .Values.supersetMcp.httproute.path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -24,6 +24,9 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
{{- if .Values.secretEnv.annotations }}
|
||||
annotations: {{- toYaml .Values.secretEnv.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
DB_HOST: {{ include "superset.db.host" . | quote }}
|
||||
|
||||
@@ -24,6 +24,9 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
{{- if .Values.secretConfig.annotations }}
|
||||
annotations: {{- toYaml .Values.secretConfig.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
superset_config.py: |
|
||||
|
||||
@@ -25,6 +25,9 @@ metadata:
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
{{- if .Values.supersetWebsockets.configAnnotations }}
|
||||
annotations: {{- toYaml .Values.supersetWebsockets.configAnnotations | nindent 4 }}
|
||||
{{- end }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
config.json: |
|
||||
|
||||
92
helm/superset/tests/httproute_test.yaml
Normal file
92
helm/superset/tests/httproute_test.yaml
Normal file
@@ -0,0 +1,92 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
suite: HTTPRoute
|
||||
templates:
|
||||
- httproute.yaml
|
||||
tests:
|
||||
- it: routes the general Superset path
|
||||
set:
|
||||
httproute.enabled: true
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 1
|
||||
- equal:
|
||||
path: spec.rules[0].backendRefs[0].name
|
||||
value: RELEASE-NAME-superset
|
||||
- equal:
|
||||
path: spec.rules[0].backendRefs[0].port
|
||||
value: 8088
|
||||
- equal:
|
||||
path: spec.rules[0].matches[0].path.value
|
||||
value: /
|
||||
|
||||
- it: routes the WebSocket path when WebSockets are enabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetWebsockets.enabled: true
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 2
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].name
|
||||
value: RELEASE-NAME-superset-ws
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].port
|
||||
value: 8080
|
||||
- equal:
|
||||
path: spec.rules[1].matches[0].path.value
|
||||
value: /ws
|
||||
|
||||
- it: does not route the WebSocket path when WebSockets are disabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetWebsockets.enabled: false
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 1
|
||||
|
||||
- it: routes the MCP path when MCP HTTPRoute is enabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetMcp.enabled: true
|
||||
supersetMcp.httproute.enabled: true
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 2
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].name
|
||||
value: RELEASE-NAME-superset-mcp
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].port
|
||||
value: 5008
|
||||
- equal:
|
||||
path: spec.rules[1].matches[0].path.value
|
||||
value: /mcp
|
||||
|
||||
- it: does not route MCP when MCP HTTPRoute is disabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetMcp.enabled: true
|
||||
supersetMcp.httproute.enabled: false
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 1
|
||||
@@ -61,6 +61,8 @@ secretEnv:
|
||||
# -- Change to false in order to support externally created secret (Binami "Sealed Secrets" for Kubernetes or External Secrets Operator)
|
||||
# note: when externally creating the secret, the chart still expects to pull values from a secret with the name of the release defaults to `release-name-superset-env` - full logic located in _helpers.tpl file: `define "superset.fullname"`
|
||||
create: true
|
||||
# -- Annotations to be added to the secret
|
||||
annotations: {}
|
||||
|
||||
# -- Specify service account name to be used
|
||||
serviceAccountName: ~
|
||||
@@ -82,6 +84,11 @@ bootstrapScript: |
|
||||
# Note: this secret must have the key superset_config.py in it and can include other files as well
|
||||
configFromSecret: '{{ template "superset.fullname" . }}-config'
|
||||
|
||||
# -- Specify whether Helm should apply additional settings to a secret containing configuration files
|
||||
secretConfig:
|
||||
# -- Annotations to be added to the secret which contains configuration files
|
||||
annotations: {}
|
||||
|
||||
# -- The name of the secret which we will use to populate env vars in deployed pods
|
||||
# This can be useful for secret keys, etc.
|
||||
envFromSecret: '{{ template "superset.fullname" . }}-env'
|
||||
@@ -91,13 +98,13 @@ envFromSecrets: []
|
||||
# -- Extra environment variables that will be passed into pods
|
||||
extraEnv: {}
|
||||
# Different gunicorn settings, refer to the gunicorn documentation
|
||||
# https://docs.gunicorn.org/en/stable/settings.html#
|
||||
# https://gunicorn.org/reference/settings/
|
||||
# These variables are used as Flags at the gunicorn startup
|
||||
# https://github.com/apache/superset/blob/master/docker/run-server.sh#L22
|
||||
# https://github.com/apache/superset/blob/master/docker/entrypoints/run-server.sh#L22
|
||||
# Extend timeout to allow long running queries.
|
||||
# GUNICORN_TIMEOUT: 300
|
||||
# Increase the gunicorn worker amount, can improve performance drastically
|
||||
# See: https://docs.gunicorn.org/en/stable/design.html#how-many-workers
|
||||
# See: https://gunicorn.org/design/#how-many-workers
|
||||
# SERVER_WORKER_AMOUNT: 4
|
||||
# WORKER_MAX_REQUESTS: 0
|
||||
# WORKER_MAX_REQUESTS_JITTER: 0
|
||||
@@ -270,7 +277,9 @@ httproute:
|
||||
# per rule to leave room for traffic splitting (defaults to 1). When
|
||||
# `supersetWebsockets.enabled` is true, an extra rule routing
|
||||
# `supersetWebsockets.ingress.path` to the `-ws` service is appended
|
||||
# automatically, mirroring the ingress behavior.
|
||||
# automatically, mirroring the ingress behavior. When both `supersetMcp.enabled` and
|
||||
# `supersetMcp.httproute.enabled` are true, an extra rule routing
|
||||
# `supersetMcp.httproute.path` to the `-mcp` service is also appended.
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
@@ -892,6 +901,8 @@ supersetWebsockets:
|
||||
"jwtSecret": "CHANGE-ME",
|
||||
"jwtCookieName": "async-token",
|
||||
}
|
||||
# -- Annotations to be added to the secret which contains config.json
|
||||
configAnnotations: {}
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
@@ -978,7 +989,6 @@ supersetWebsockets:
|
||||
# -- Set priorityClassName for supersetWebsockets pods
|
||||
priorityClassName: ~
|
||||
|
||||
|
||||
supersetMcp:
|
||||
# -- Enables the Superset MCP Server. To expose it via the shared ingress at /mcp,
|
||||
# also set supersetMcp.ingress.enabled=true.
|
||||
@@ -1006,6 +1016,10 @@ supersetMcp:
|
||||
enabled: false
|
||||
path: /mcp
|
||||
pathType: Prefix
|
||||
httproute:
|
||||
# -- If true, the MCP server will be exposed via the HTTPRoute /mcp subpath
|
||||
enabled: false
|
||||
path: /mcp
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
|
||||
@@ -54,7 +54,7 @@ dependencies = [
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <4.0.0",
|
||||
"flask-appbuilder>=5.2.2, <6.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-caching>=2.4.1, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
"flask-login>=0.6.0, < 1.0",
|
||||
@@ -92,7 +92,7 @@ dependencies = [
|
||||
"Pillow>=11.0.0, <13",
|
||||
"polyline>=2.0.0, <3.0",
|
||||
"pydantic>=2.8.0",
|
||||
"pyparsing>=3.0.6, <4",
|
||||
"pyparsing>=3.3.2, <4",
|
||||
"python-dateutil",
|
||||
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
|
||||
"pygeohash",
|
||||
@@ -106,7 +106,7 @@ dependencies = [
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
"simplejson>=4.1.1",
|
||||
"slack_sdk>=3.43.0, <4",
|
||||
"sqlalchemy>=1.4, <2",
|
||||
"sqlalchemy>=1.4.43, <2", # 1.4.43 adds the python-oracledb (oracle+oracledb) dialect
|
||||
"sqlalchemy-continuum>=1.6.0, <2.0.0",
|
||||
"sqlalchemy-utils>=0.42.1, <0.43", # expanding lowerbound to work with pydoris
|
||||
"sqlglot>=30.12.0, <31",
|
||||
@@ -127,7 +127,7 @@ 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.1",
|
||||
"google-cloud-bigquery>=3.42.2",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.4.2, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
@@ -139,10 +139,11 @@ d1 = [
|
||||
]
|
||||
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
|
||||
databricks = [
|
||||
"databricks-sql-connector==4.2.6",
|
||||
"databricks-sql-connector>=4.2.6, <4.4.0",
|
||||
"databricks-sqlalchemy==1.0.5",
|
||||
]
|
||||
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
|
||||
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"]
|
||||
db2 = ["ibm-db-sa<=0.4.4, >=0.4.4"]
|
||||
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
|
||||
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
|
||||
drill = ["sqlalchemy-drill>=1.1.10, <2"]
|
||||
@@ -188,7 +189,7 @@ ocient = [
|
||||
"shapely",
|
||||
"geojson",
|
||||
]
|
||||
oracle = ["oracledb>=2.0.0, <5"]
|
||||
oracle = ["oracledb>=4.0.2, <5"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
|
||||
pinot = ["pinotdb>=5.0.0, <10.0.0"]
|
||||
playwright = ["playwright>=1.61.0, <2"]
|
||||
@@ -200,7 +201,7 @@ redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
|
||||
risingwave = ["sqlalchemy-risingwave"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"]
|
||||
sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
|
||||
spark = [
|
||||
"pyhive[hive_pure_sasl]>=0.7",
|
||||
@@ -209,7 +210,7 @@ spark = [
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.8.9",
|
||||
"taos-ws-py>=0.6.9"
|
||||
"taos-ws-py>=0.7.0"
|
||||
]
|
||||
teradata = ["teradatasql>=20.0.0.62"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
@@ -226,7 +227,7 @@ development = [
|
||||
"docker",
|
||||
"flask-testing",
|
||||
"freezegun",
|
||||
"grpcio>=1.81.1",
|
||||
"grpcio>=1.82.1",
|
||||
"openapi-spec-validator",
|
||||
"parameterized",
|
||||
"pip",
|
||||
|
||||
@@ -32,15 +32,13 @@ filterwarnings =
|
||||
error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The ``declarative_base\(\)`` function is now available:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The Engine.execute\(\) method is considered legacy:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The legacy calling style of select\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The "whens" argument to case:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"User" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
|
||||
@@ -28,6 +28,8 @@ babel==2.17.0
|
||||
# via flask-babel
|
||||
backoff==2.2.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
backports-zstd==1.6.0
|
||||
# via flask-compress
|
||||
bcrypt==4.3.0
|
||||
# via paramiko
|
||||
billiard==4.2.1
|
||||
@@ -128,9 +130,9 @@ flask-appbuilder==5.2.2
|
||||
# apache-superset-core
|
||||
flask-babel==3.1.0
|
||||
# via flask-appbuilder
|
||||
flask-caching==2.3.1
|
||||
flask-caching==2.4.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-compress==1.17
|
||||
flask-compress==1.24
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-cors==6.0.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -244,7 +246,7 @@ msgpack==1.2.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
msgspec==0.19.0
|
||||
# via flask-session
|
||||
nh3==0.3.5
|
||||
nh3==0.3.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
numexpr==2.10.2
|
||||
# via -r requirements/base.in
|
||||
@@ -285,7 +287,7 @@ parsedatetime==2.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
pgsanity==0.2.9
|
||||
# via apache-superset (pyproject.toml)
|
||||
pillow==12.2.0
|
||||
pillow==12.3.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
@@ -300,17 +302,17 @@ pyarrow==24.0.0
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
pyasn1==0.6.3
|
||||
pyasn1==0.6.4
|
||||
# via pyasn1-modules
|
||||
pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.11.7
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
pydantic-core==2.33.2
|
||||
pydantic-core==2.46.4
|
||||
# via pydantic
|
||||
pygeohash==3.2.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -327,7 +329,7 @@ pyopenssl==26.3.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# shillelagh
|
||||
pyparsing==3.2.3
|
||||
pyparsing==3.3.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
pysocks==1.7.1
|
||||
# via urllib3
|
||||
@@ -408,7 +410,7 @@ sqlalchemy==1.4.54
|
||||
# shillelagh
|
||||
# sqlalchemy-continuum
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-continuum==1.6.0
|
||||
sqlalchemy-continuum==1.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
sqlalchemy-utils==0.42.1
|
||||
# via
|
||||
@@ -443,7 +445,7 @@ typing-extensions==4.16.0
|
||||
# selenium
|
||||
# shillelagh
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
typing-inspection==0.4.2
|
||||
# via pydantic
|
||||
tzdata==2025.2
|
||||
# via
|
||||
@@ -496,5 +498,3 @@ xlsxwriter==3.2.9
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# pandas
|
||||
zstandard==0.23.0
|
||||
# via flask-compress
|
||||
|
||||
@@ -64,6 +64,10 @@ backoff==2.2.1
|
||||
# apache-superset
|
||||
backports-tarfile==1.2.0
|
||||
# via jaraco-context
|
||||
backports-zstd==1.6.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-compress
|
||||
bcrypt==4.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -272,11 +276,11 @@ flask-babel==3.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-appbuilder
|
||||
flask-caching==2.3.1
|
||||
flask-caching==2.4.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
flask-compress==1.17
|
||||
flask-compress==1.24
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -337,7 +341,7 @@ geopy==2.4.1
|
||||
# apache-superset
|
||||
gevent==26.4.0
|
||||
# via apache-superset
|
||||
google-api-core==2.23.0
|
||||
google-api-core==2.33.0
|
||||
# via
|
||||
# google-cloud-bigquery
|
||||
# google-cloud-core
|
||||
@@ -358,7 +362,7 @@ google-auth-oauthlib==1.2.1
|
||||
# via
|
||||
# pandas-gbq
|
||||
# pydata-google-auth
|
||||
google-cloud-bigquery==3.42.1
|
||||
google-cloud-bigquery==3.42.2
|
||||
# via
|
||||
# apache-superset
|
||||
# pandas-gbq
|
||||
@@ -382,7 +386,7 @@ greenlet==3.5.3
|
||||
# sqlalchemy
|
||||
griffelib==2.0.2
|
||||
# via fastmcp-slim
|
||||
grpcio==1.81.1
|
||||
grpcio==1.83.0
|
||||
# via
|
||||
# apache-superset
|
||||
# google-api-core
|
||||
@@ -570,7 +574,7 @@ msgspec==0.19.0
|
||||
# flask-session
|
||||
mysqlclient==2.2.8
|
||||
# via apache-superset
|
||||
nh3==0.3.5
|
||||
nh3==0.3.6
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -665,7 +669,7 @@ pgsanity==0.2.9
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
pillow==12.2.0
|
||||
pillow==12.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -724,7 +728,7 @@ pyarrow==24.0.0
|
||||
# apache-superset-core
|
||||
# db-dtypes
|
||||
# pandas-gbq
|
||||
pyasn1==0.6.3
|
||||
pyasn1==0.6.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pyasn1-modules
|
||||
@@ -738,7 +742,7 @@ pycparser==2.22
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# cffi
|
||||
pydantic==2.11.7
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -747,7 +751,7 @@ pydantic==2.11.7
|
||||
# mcp
|
||||
# openapi-pydantic
|
||||
# pydantic-settings
|
||||
pydantic-core==2.33.2
|
||||
pydantic-core==2.46.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pydantic
|
||||
@@ -791,7 +795,7 @@ pyopenssl==26.3.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# google-auth
|
||||
# shillelagh
|
||||
pyparsing==3.2.3
|
||||
pyparsing==3.3.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -987,7 +991,7 @@ sqlalchemy==1.4.54
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.17.0
|
||||
# via apache-superset
|
||||
sqlalchemy-continuum==1.6.0
|
||||
sqlalchemy-continuum==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -1066,7 +1070,7 @@ typing-extensions==4.16.0
|
||||
# shillelagh
|
||||
# starlette
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# mcp
|
||||
@@ -1168,7 +1172,4 @@ zope-event==5.0
|
||||
zope-interface==5.4.0
|
||||
# via gevent
|
||||
zstandard==0.23.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-compress
|
||||
# trino
|
||||
# via trino
|
||||
|
||||
@@ -93,7 +93,7 @@ def find_models(module: ModuleType) -> list[type[Model]]: # noqa: C901
|
||||
# where the current model is out-of-sync with the existing table after a
|
||||
# downgrade
|
||||
sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
|
||||
engine = create_engine(sqlalchemy_uri)
|
||||
engine = create_engine(sqlalchemy_uri, future=True)
|
||||
Base = automap_base() # noqa: N806
|
||||
Base.prepare(engine, reflect=True)
|
||||
seen = set()
|
||||
|
||||
119
superset-core/src/superset_core/extensions/context.py
Normal file
119
superset-core/src/superset_core/extensions/context.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Extension Context API for superset-core extensions.
|
||||
|
||||
Provides access to the current extension's context, including metadata
|
||||
and scoped resources like storage. Extensions call `get_context()` to
|
||||
access their context during execution.
|
||||
|
||||
The context is set by the host (Superset) during extension loading and
|
||||
is only available within extension code.
|
||||
|
||||
Usage:
|
||||
from superset_core.extensions.context import get_context
|
||||
|
||||
def setup():
|
||||
ctx = get_context()
|
||||
|
||||
# Access extension metadata
|
||||
print(f"Running {ctx.extension.displayName} v{ctx.extension.version}")
|
||||
|
||||
# Access extension-scoped storage
|
||||
ctx.storage.ephemeral.set("lastRun", time.time(), EphemeralSetOptions(ttl=3600))
|
||||
data = ctx.storage.ephemeral.get("cachedData")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset_core.extensions.storage.ephemeral import EphemeralStateAccessor
|
||||
from superset_core.extensions.storage.persistent import PersistentStateAccessor
|
||||
from superset_core.extensions.types import BaseExtension
|
||||
|
||||
|
||||
class ExtensionStorage(Protocol):
|
||||
"""Extension-scoped storage accessor for all available tiers."""
|
||||
|
||||
@property
|
||||
def ephemeral(self) -> "EphemeralStateAccessor":
|
||||
"""Server-side cache (Redis/Memcached) with TTL."""
|
||||
...
|
||||
|
||||
@property
|
||||
def persistent(self) -> "PersistentStateAccessor":
|
||||
"""Database-backed persistent storage."""
|
||||
...
|
||||
|
||||
|
||||
class ExtensionContext(Protocol):
|
||||
"""
|
||||
Context object providing extension-specific resources.
|
||||
|
||||
This context is only available during extension execution.
|
||||
Calling `get_context()` outside of an extension will raise an error.
|
||||
"""
|
||||
|
||||
@property
|
||||
def extension(self) -> "BaseExtension":
|
||||
"""Metadata about the current extension."""
|
||||
...
|
||||
|
||||
@property
|
||||
def storage(self) -> ExtensionStorage:
|
||||
"""Extension-scoped storage across all available tiers."""
|
||||
...
|
||||
|
||||
|
||||
def get_context() -> ExtensionContext:
|
||||
"""
|
||||
Get the current extension's context.
|
||||
|
||||
This function returns the context for the currently executing extension,
|
||||
providing access to extension metadata and scoped resources like storage.
|
||||
|
||||
Host implementations will replace this function during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
:returns: The current extension's context.
|
||||
:raises RuntimeError: If called outside of an extension context.
|
||||
|
||||
Example:
|
||||
from superset_core.extensions.context import get_context
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
# Access extension metadata
|
||||
print(f"Extension: {ctx.extension.id}")
|
||||
print(f"Version: {ctx.extension.version}")
|
||||
|
||||
# Access extension-scoped storage
|
||||
ctx.storage.ephemeral.set("tempData", data, EphemeralSetOptions(ttl=3600))
|
||||
value = ctx.storage.ephemeral.get("tempData")
|
||||
|
||||
# Access shared (cross-user) storage
|
||||
ctx.storage.ephemeral.shared.set(
|
||||
"globalCounter", count, EphemeralSetOptions(ttl=3600)
|
||||
)
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"get_context() must be called within an extension context. "
|
||||
"This function is replaced by the host during extension loading."
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Storage API for superset-core extensions.
|
||||
|
||||
Provides storage tiers for extensions with different persistence characteristics.
|
||||
Storage is accessed via `ctx.storage` from `get_context()`.
|
||||
|
||||
Tier 1 - Local State (Frontend Only):
|
||||
- local: Browser localStorage - persists across sessions
|
||||
- session: Browser sessionStorage - cleared on tab close
|
||||
These are frontend-only and cannot be imported in backend code.
|
||||
|
||||
Tier 2 - Ephemeral State (Server Cache):
|
||||
- ephemeral: Short-lived KV storage backed by server-side cache
|
||||
- Supports TTL, not guaranteed to survive server restarts
|
||||
- Use for temporary state like job progress or intermediate results
|
||||
|
||||
Tier 3 - Persistent State (Database):
|
||||
- persistent: Durable KV storage backed by database table
|
||||
- Survives server restarts, supports encryption and resource linking
|
||||
- Use for user preferences, extension config, per-resource settings
|
||||
|
||||
All tiers follow the same API pattern:
|
||||
- User-scoped by default (private to current user)
|
||||
- `shared` accessor for data visible to all users
|
||||
|
||||
For backend code that needs to query, enumerate, or bulk-manage its own
|
||||
Tier 3 rows rather than get/set a single key at a time (for example, a
|
||||
cleanup job pruning rows linked to resources that no longer exist), see
|
||||
`superset_core.extensions.storage.dao.ExtensionStorageDAO`.
|
||||
|
||||
Usage:
|
||||
from superset_core.extensions.context import get_context
|
||||
from superset_core.extensions.storage.ephemeral import EphemeralSetOptions
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
# Tier 2: Ephemeral state
|
||||
ctx.storage.ephemeral.get('preference')
|
||||
ctx.storage.ephemeral.set('preference', 'compact', EphemeralSetOptions(ttl=3600))
|
||||
|
||||
# Tier 2: Shared ephemeral state
|
||||
ctx.storage.ephemeral.shared.get('job_progress')
|
||||
ctx.storage.ephemeral.shared.set(
|
||||
'job_progress', {'pct': 42}, EphemeralSetOptions(ttl=3600)
|
||||
)
|
||||
|
||||
# Tier 3: Persistent state
|
||||
ctx.storage.persistent.get('config')
|
||||
ctx.storage.persistent.set('config', {'version': 2})
|
||||
|
||||
# Tier 3: DAO-style access to this extension's own rows
|
||||
from superset_core.extensions.storage.dao import ExtensionStorageDAO
|
||||
|
||||
entries = ExtensionStorageDAO.filter_by(resource_type='my-resource-type')
|
||||
"""
|
||||
|
||||
from superset_core.extensions.storage import (
|
||||
dao, # noqa: F401
|
||||
ephemeral, # noqa: F401
|
||||
models, # noqa: F401
|
||||
persistent, # noqa: F401
|
||||
)
|
||||
70
superset-core/src/superset_core/extensions/storage/dao.py
Normal file
70
superset-core/src/superset_core/extensions/storage/dao.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Persistent storage DAO API for superset-core extensions (Tier 3 Storage).
|
||||
|
||||
Provides direct DAO-style access to an extension's own Tier 3 persistent
|
||||
storage rows, for backend code that needs to query, enumerate, or bulk-manage
|
||||
entries rather than get/set a single key at a time (see
|
||||
`superset_core.extensions.storage.persistent` for that simpler shape).
|
||||
|
||||
Every query and mutation made through this DAO is automatically scoped to
|
||||
the extension currently executing — `extension_id` is never a parameter the
|
||||
caller supplies, so an extension cannot read or modify another extension's
|
||||
storage through this DAO. The scoping is resolved from the ambient
|
||||
extension context (the same context `get_context()` reads from), so this DAO
|
||||
can only be used from within extension backend code, not host code.
|
||||
|
||||
Usage:
|
||||
from superset_core.extensions.storage.dao import ExtensionStorageDAO
|
||||
|
||||
# All entries linked to a specific resource this extension created
|
||||
entries = ExtensionStorageDAO.filter_by(
|
||||
resource_type="my-resource-type",
|
||||
resource_uuid=resource_uuid,
|
||||
)
|
||||
|
||||
# Bulk-delete rows for a resource that no longer exists
|
||||
ExtensionStorageDAO.delete(entries)
|
||||
"""
|
||||
|
||||
from superset_core.common.daos import BaseDAO
|
||||
from superset_core.extensions.storage.models import ExtensionStorageEntry
|
||||
|
||||
|
||||
class ExtensionStorageDAO(BaseDAO[ExtensionStorageEntry]):
|
||||
"""
|
||||
Abstract DAO for an extension's own Tier 3 persistent storage rows.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
Inherits `find_all`, `find_one_or_none`, `filter_by`, `query`, `update`,
|
||||
and `delete` from `BaseDAO`, every one restricted to the calling
|
||||
extension's own rows. `create()` is overridden by the host
|
||||
implementation to always raise: it is a raw insert with no upsert
|
||||
dedup, quota check, or locking against the key
|
||||
`superset_core.extensions.storage.persistent`'s `set()` writes to, so
|
||||
use that (or the ambient `ctx.storage.persistent.set()` accessor) to
|
||||
write a value instead.
|
||||
"""
|
||||
|
||||
# Class variables that will be set by host implementation
|
||||
model_cls = None
|
||||
base_filter = None
|
||||
id_column_name = "id"
|
||||
162
superset-core/src/superset_core/extensions/storage/ephemeral.py
Normal file
162
superset-core/src/superset_core/extensions/storage/ephemeral.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Ephemeral State API for superset-core extensions (Tier 2 Storage).
|
||||
|
||||
Provides short-lived KV storage backed by the configured server-side cache
|
||||
backend (Redis, Memcached, or filesystem). Automatically expires based on TTL.
|
||||
Not guaranteed to survive server restarts.
|
||||
|
||||
Host implementations will replace the EphemeralState class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
Cache keys are namespaced automatically:
|
||||
- User-scoped (default): superset-ext:{extension_id}:user:{user_id}:{key}
|
||||
- Shared (global): superset-ext:{extension_id}:shared:{key}
|
||||
|
||||
Usage (via extension context - preferred):
|
||||
from superset_core.extensions.context import get_context
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
# User-scoped state (default - private to current user)
|
||||
ctx.storage.ephemeral.get('preference')
|
||||
ctx.storage.ephemeral.set('preference', 'compact', EphemeralSetOptions(ttl=3600))
|
||||
ctx.storage.ephemeral.remove('preference')
|
||||
|
||||
# Shared state (explicit opt-in - visible to all users)
|
||||
ctx.storage.ephemeral.shared.get('job_progress')
|
||||
ctx.storage.ephemeral.shared.set(
|
||||
'job_progress', {'pct': 42}, EphemeralSetOptions(ttl=3600)
|
||||
)
|
||||
ctx.storage.ephemeral.shared.remove('job_progress')
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, ClassVar, Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EphemeralSetOptions:
|
||||
"""
|
||||
Options for an ephemeral state `set` call.
|
||||
|
||||
`ttl` is required (no default): every write must have an explicit
|
||||
expiration, matching the frontend `EphemeralSetOptions` and the REST API.
|
||||
"""
|
||||
|
||||
ttl: int
|
||||
#: Name of the codec used to encode `value`, e.g. "json" (default).
|
||||
codec: str = "json"
|
||||
|
||||
|
||||
class EphemeralStateAccessor(Protocol):
|
||||
"""Protocol for scoped ephemeral state access."""
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
"""Get a value from ephemeral state."""
|
||||
...
|
||||
|
||||
def set(self, key: str, value: Any, options: EphemeralSetOptions) -> None:
|
||||
"""Set a value in ephemeral state with TTL."""
|
||||
...
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
"""Remove a value from ephemeral state."""
|
||||
...
|
||||
|
||||
|
||||
class _UnconfiguredEphemeralStateAccessor:
|
||||
"""Placeholder for `.shared` before host replacement.
|
||||
|
||||
Raises the same `NotImplementedError` as the other stub methods, rather
|
||||
than a bare `AttributeError`, if accessed before the host wires up the
|
||||
concrete implementation.
|
||||
"""
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
def set(self, key: str, value: Any, options: EphemeralSetOptions) -> None:
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
|
||||
class EphemeralState:
|
||||
"""
|
||||
Tier 2 ephemeral state storage for extensions.
|
||||
|
||||
Backed by the configured server-side cache (Redis, Memcached, or filesystem).
|
||||
Data expires based on TTL and is not guaranteed to survive server restarts.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
All operations are user-scoped by default (private to the current user).
|
||||
Use `.shared` to access state that is visible to all users.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get(key: str) -> Any:
|
||||
"""
|
||||
Get a value from user-scoped ephemeral state.
|
||||
|
||||
Data is automatically scoped to the current authenticated user.
|
||||
Other users cannot see or modify this data.
|
||||
|
||||
:param key: The key to retrieve.
|
||||
:returns: The stored value, or None if not found or expired.
|
||||
"""
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
@staticmethod
|
||||
def set(key: str, value: Any, options: EphemeralSetOptions) -> None:
|
||||
"""
|
||||
Set a value in user-scoped ephemeral state with TTL.
|
||||
|
||||
Data is automatically scoped to the current authenticated user.
|
||||
Other users cannot see or modify this data.
|
||||
|
||||
:param key: The key to store.
|
||||
:param value: The value to store, encoded with `options.codec`
|
||||
(default "json"). The encoded value must not exceed
|
||||
MAX_VALUE_SIZE from config.
|
||||
:param options: `EphemeralSetOptions`, e.g. `ttl=3600`. Required —
|
||||
`ttl` must not exceed MAX_TTL from config. `codec="pickle"`
|
||||
stores a value that isn't JSON-serializable.
|
||||
"""
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
@staticmethod
|
||||
def remove(key: str) -> None:
|
||||
"""
|
||||
Remove a value from user-scoped ephemeral state.
|
||||
|
||||
Data is automatically scoped to the current authenticated user.
|
||||
|
||||
:param key: The key to remove.
|
||||
"""
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
#: Shared (global) ephemeral state accessor.
|
||||
#: Data stored via this accessor is visible to all users of the extension.
|
||||
#: WARNING: Do not store user-specific or sensitive data here.
|
||||
#: Host implementations will replace this during initialization.
|
||||
shared: ClassVar[EphemeralStateAccessor] = _UnconfiguredEphemeralStateAccessor()
|
||||
74
superset-core/src/superset_core/extensions/storage/models.py
Normal file
74
superset-core/src/superset_core/extensions/storage/models.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Persistent storage model API for superset-core extensions (Tier 3 Storage).
|
||||
|
||||
Provides the model class backing `superset_core.extensions.storage.dao.
|
||||
ExtensionStorageDAO`, for extension backend code that needs to enumerate or
|
||||
bulk-manage its own persistent storage rows (for example, a periodic cleanup
|
||||
job pruning rows linked to resources that no longer exist), rather than the
|
||||
single-key get/set/remove shape of `persistent`.
|
||||
|
||||
Usage:
|
||||
from superset_core.extensions.storage.models import ExtensionStorageEntry
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from superset_core.common.models import CoreModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ExtensionStorageEntry(CoreModel):
|
||||
"""
|
||||
Abstract model interface for a single Tier 3 persistent storage row.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
`value` is the raw stored payload (encrypted at rest when `is_encrypted`
|
||||
is set), encoded with the codec named by `codec`; decoding/decryption is
|
||||
handled by `superset_core.extensions.storage.dao.ExtensionStorageDAO`,
|
||||
not by reading this field directly.
|
||||
"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
# Type hints for expected column attributes
|
||||
id: int
|
||||
uuid: UUID
|
||||
extension_id: str
|
||||
user_fk: int | None
|
||||
resource_type: str | None
|
||||
resource_uuid: str | None
|
||||
key: str
|
||||
value: bytes
|
||||
value_size: int
|
||||
codec: str
|
||||
is_encrypted: bool
|
||||
|
||||
# Audit fields
|
||||
created_on: "datetime | None"
|
||||
changed_on: "datetime | None"
|
||||
created_by_fk: int | None
|
||||
changed_by_fk: int | None
|
||||
243
superset-core/src/superset_core/extensions/storage/persistent.py
Normal file
243
superset-core/src/superset_core/extensions/storage/persistent.py
Normal file
@@ -0,0 +1,243 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Persistent State API for superset-core extensions (Tier 3 Storage).
|
||||
|
||||
Provides durable KV storage backed by a dedicated database table.
|
||||
Data survives server restarts, cache evictions, and browser clears.
|
||||
Suitable for user preferences, saved state, and any data that must not be lost.
|
||||
|
||||
Host implementations will replace the PersistentState class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
Database keys are namespaced automatically:
|
||||
- User-scoped (default): (extension_id, user_id, key)
|
||||
- Shared (global): (extension_id, null, key)
|
||||
|
||||
Usage (via extension context - preferred):
|
||||
from superset_core.extensions.context import get_context
|
||||
|
||||
ctx = get_context()
|
||||
|
||||
# User-scoped state (default - private to current user)
|
||||
ctx.storage.persistent.get('preferences')
|
||||
ctx.storage.persistent.set('preferences', {'theme': 'dark'})
|
||||
ctx.storage.persistent.remove('preferences')
|
||||
|
||||
# Shared state (explicit opt-in - visible to all users)
|
||||
ctx.storage.persistent.shared.get('global_config')
|
||||
ctx.storage.persistent.shared.set('global_config', {'version': 2})
|
||||
ctx.storage.persistent.shared.remove('global_config')
|
||||
|
||||
# Encrypted at rest
|
||||
ctx.storage.persistent.set(
|
||||
'api_token', 'sk-...', PersistentSetOptions(encrypt=True)
|
||||
)
|
||||
|
||||
# Listing entries (page and page_size are required)
|
||||
result = ctx.storage.persistent.list(PersistentListOptions(page=0, page_size=10))
|
||||
for entry in result.entries:
|
||||
print(entry.key, entry.value)
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, ClassVar, Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PersistentSetOptions:
|
||||
"""
|
||||
Options for a persistent state `set` call.
|
||||
|
||||
NOTE: This is intentionally minimal for the initial implementation.
|
||||
Additional options can be added here later without changing the `set`
|
||||
signature on `PersistentStateAccessor`/`PersistentState`.
|
||||
"""
|
||||
|
||||
encrypt: bool = False
|
||||
#: Name of the codec used to encode `value`, e.g. "json" (default).
|
||||
codec: str = "json"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PersistentListOptions:
|
||||
"""Options for a persistent state `list` call.
|
||||
|
||||
`page` and `page_size` are required (no default): `list` returns one
|
||||
page of a caller's entries, not the whole result set, and a default
|
||||
would let that fact go unnoticed at the call site. Check the returned
|
||||
`PersistentListResult.count` against `page_size` to know whether more
|
||||
pages exist.
|
||||
|
||||
`resource_type`/`resource_uuid` are optional filters; omitting them
|
||||
lists every entry in the caller's scope (global or user-scoped,
|
||||
depending on whether `list` is called via `.shared` or directly).
|
||||
"""
|
||||
|
||||
#: Zero-indexed page number.
|
||||
page: int
|
||||
#: Entries per page. There is no fixed ceiling on this value, but a
|
||||
#: page whose combined value size exceeds MAX_LIST_PAYLOAD_SIZE from
|
||||
#: config is rejected — reduce page_size and retry if that happens.
|
||||
page_size: int
|
||||
resource_type: str | None = None
|
||||
resource_uuid: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PersistentListEntry:
|
||||
"""A single entry returned by a persistent state `list` call."""
|
||||
|
||||
key: str
|
||||
value: Any
|
||||
#: Name of the codec `value` was encoded with, e.g. "json" or "pickle".
|
||||
codec: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PersistentListResult:
|
||||
"""Result of a persistent state `list` call."""
|
||||
|
||||
entries: list[PersistentListEntry] = field(default_factory=list)
|
||||
#: Total number of entries matching the given scope/filters, across all
|
||||
#: pages — not just the number returned in `entries`.
|
||||
count: int = 0
|
||||
|
||||
|
||||
class PersistentStateAccessor(Protocol):
|
||||
"""Protocol for scoped persistent state access."""
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
"""Get a value from persistent state."""
|
||||
...
|
||||
|
||||
def set(
|
||||
self, key: str, value: Any, options: PersistentSetOptions | None = None
|
||||
) -> None:
|
||||
"""Set a value in persistent state."""
|
||||
...
|
||||
|
||||
def list(self, options: PersistentListOptions) -> PersistentListResult:
|
||||
"""List entries in persistent state."""
|
||||
...
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
"""Remove a value from persistent state."""
|
||||
...
|
||||
|
||||
|
||||
class _UnconfiguredPersistentStateAccessor:
|
||||
"""Placeholder for `.shared` before host replacement.
|
||||
|
||||
Raises the same `NotImplementedError` as the other stub methods, rather
|
||||
than a bare `AttributeError`, if accessed before the host wires up the
|
||||
concrete implementation.
|
||||
"""
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
def set(
|
||||
self, key: str, value: Any, options: PersistentSetOptions | None = None
|
||||
) -> None:
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
def list(self, options: PersistentListOptions) -> PersistentListResult:
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
|
||||
class PersistentState:
|
||||
"""
|
||||
Tier 3 persistent state storage for extensions.
|
||||
|
||||
Backed by a dedicated database table. Data survives server restarts,
|
||||
cache evictions, and browser clears.
|
||||
|
||||
Host implementations will replace this class during initialization
|
||||
with a concrete implementation providing actual functionality.
|
||||
|
||||
All operations are user-scoped by default (private to the current user).
|
||||
Use `.shared` to access state that is visible to all users.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get(key: str) -> Any:
|
||||
"""
|
||||
Get a value from user-scoped persistent state.
|
||||
|
||||
Data is automatically scoped to the current authenticated user.
|
||||
Other users cannot see or modify this data.
|
||||
|
||||
:param key: The key to retrieve.
|
||||
:returns: The stored value, or None if not found.
|
||||
"""
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
@staticmethod
|
||||
def set(key: str, value: Any, options: PersistentSetOptions | None = None) -> None:
|
||||
"""
|
||||
Set a value in user-scoped persistent state.
|
||||
|
||||
Data is automatically scoped to the current authenticated user.
|
||||
Other users cannot see or modify this data.
|
||||
Data persists indefinitely until explicitly removed.
|
||||
|
||||
:param key: The key to store.
|
||||
:param value: The value to store, encoded with `options.codec`
|
||||
(default "json"). The encoded value must not exceed
|
||||
MAX_VALUE_SIZE from config.
|
||||
:param options: Optional `PersistentSetOptions`, e.g. `encrypt=True`
|
||||
to store the value encrypted at rest, or `codec="pickle"` to
|
||||
store a value that isn't JSON-serializable.
|
||||
"""
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
@staticmethod
|
||||
def list(options: PersistentListOptions) -> PersistentListResult:
|
||||
"""
|
||||
List entries in user-scoped persistent state.
|
||||
|
||||
Data is automatically scoped to the current authenticated user.
|
||||
Other users' entries are never returned.
|
||||
|
||||
:param options: `PersistentListOptions`, e.g.
|
||||
`PersistentListOptions(page=0, page_size=10)`.
|
||||
:returns: `PersistentListResult` with the page's entries and the
|
||||
total count across all pages.
|
||||
"""
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
@staticmethod
|
||||
def remove(key: str) -> None:
|
||||
"""
|
||||
Remove a value from user-scoped persistent state.
|
||||
|
||||
Data is automatically scoped to the current authenticated user.
|
||||
|
||||
:param key: The key to remove.
|
||||
"""
|
||||
raise NotImplementedError("Class will be replaced during initialization")
|
||||
|
||||
#: Shared (global) persistent state accessor.
|
||||
#: Data stored via this accessor is visible to all users of the extension.
|
||||
#: WARNING: Do not store user-specific or sensitive data here.
|
||||
#: Host implementations will replace this during initialization.
|
||||
shared: ClassVar[PersistentStateAccessor] = _UnconfiguredPersistentStateAccessor()
|
||||
@@ -25,7 +25,7 @@ Two distinct schemas:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field # noqa: I001
|
||||
from pydantic import BaseModel, computed_field, Field # noqa: I001
|
||||
|
||||
from superset_core.extensions.constants import (
|
||||
DISPLAY_NAME_PATTERN,
|
||||
@@ -78,6 +78,11 @@ class BaseExtension(BaseModel):
|
||||
description="Permissions required by this extension",
|
||||
)
|
||||
|
||||
@computed_field(description="Composite extension ID (publisher.name)") # type: ignore[prop-decorator]
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return f"{self.publisher}.{self.name}"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ExtensionConfig (extension.json)
|
||||
@@ -137,11 +142,6 @@ class Manifest(BaseExtension):
|
||||
This file is generated by the build tool from extension.json.
|
||||
"""
|
||||
|
||||
id: str = Field(
|
||||
...,
|
||||
description="Composite extension ID (publisher.name)",
|
||||
min_length=1,
|
||||
)
|
||||
frontend: ManifestFrontend | None = Field(
|
||||
default=None,
|
||||
description="Frontend manifest",
|
||||
|
||||
26
superset-embedded-sdk/package-lock.json
generated
26
superset-embedded-sdk/package-lock.json
generated
@@ -2662,9 +2662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
|
||||
"version": "1.1.16",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3042,9 +3042,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
||||
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3937,9 +3937,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4114,9 +4114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"version": "8.5.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
||||
"integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4134,7 +4134,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"nanoid": "^3.3.16",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
||||
@@ -151,9 +151,6 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
|
||||
|
||||
extension = ExtensionConfig.model_validate(extension_data)
|
||||
|
||||
# Generate composite ID from publisher and name
|
||||
composite_id = f"{extension.publisher}.{extension.name}"
|
||||
|
||||
frontend: ManifestFrontend | None = None
|
||||
if remote_entry:
|
||||
frontend = ManifestFrontend(
|
||||
@@ -173,7 +170,6 @@ def build_manifest(cwd: Path, remote_entry: str | None) -> Manifest:
|
||||
backend = ManifestBackend(entrypoint=entrypoint)
|
||||
|
||||
return Manifest(
|
||||
id=composite_id,
|
||||
publisher=extension.publisher,
|
||||
name=extension.name,
|
||||
displayName=extension.displayName,
|
||||
|
||||
@@ -25,10 +25,6 @@ module.exports = (env, argv) => {
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
||||
},
|
||||
externalsType: "window",
|
||||
externals: {
|
||||
"@apache-superset/core": "superset",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -61,6 +57,10 @@ module.exports = (env, argv) => {
|
||||
requiredVersion: packageConfig.peerDependencies["antd"],
|
||||
import: false,
|
||||
},
|
||||
"@apache-superset/core": {
|
||||
singleton: true,
|
||||
import: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
},
|
||||
fallback: {
|
||||
tty: false,
|
||||
vm: require.resolve('vm-browserify')
|
||||
vm: false,
|
||||
}
|
||||
},
|
||||
plugins: [...config.plugins, ...filteredPlugins],
|
||||
|
||||
@@ -97,7 +97,7 @@ export const databasesPage = {
|
||||
infoAlert: '.ant-alert',
|
||||
serviceAccountInput: '[name="credentials_info"]',
|
||||
connectionStep: {
|
||||
modal: '.ant-modal-content',
|
||||
modal: '.ant-modal-container',
|
||||
modalBody: '.ant-modal-body',
|
||||
stepTitle: '.css-7x6kk > h4',
|
||||
helperBottom: '.helper-bottom',
|
||||
@@ -261,7 +261,7 @@ export const datasetsList = {
|
||||
},
|
||||
},
|
||||
deleteDatasetModal: {
|
||||
modal: '.ant-modal-content',
|
||||
modal: '.ant-modal-container',
|
||||
deleteInput: dataTestLocator('delete-modal-input'),
|
||||
deleteButton: dataTestLocator('modal-confirm-button'),
|
||||
text: '.css-kxmt87',
|
||||
@@ -474,7 +474,7 @@ export const exploreView = {
|
||||
},
|
||||
chartAreaItem: '.nv-legend-text',
|
||||
viewQueryModal: {
|
||||
container: '.ant-modal-content',
|
||||
container: '.ant-modal-container',
|
||||
closeButton: 'button.ant-modal-close',
|
||||
},
|
||||
embedCodeModal: {
|
||||
@@ -482,7 +482,7 @@ export const exploreView = {
|
||||
textfield: dataTestLocator('embed-code-textarea'),
|
||||
},
|
||||
saveModal: {
|
||||
modal: '.ant-modal-content',
|
||||
modal: '.ant-modal-container',
|
||||
chartNameInput: dataTestLocator('new-chart-name'),
|
||||
dashboardNameInput: '.ant-select-input',
|
||||
addToDashboardInput: dataTestLocator(
|
||||
@@ -578,7 +578,7 @@ export const exploreView = {
|
||||
},
|
||||
},
|
||||
editDatasetModal: {
|
||||
container: '.ant-modal-content',
|
||||
container: '.ant-modal-container',
|
||||
datasetTabsContainer: dataTestLocator('edit-dataset-tabs'),
|
||||
saveButton: dataTestLocator('datasource-modal-save'),
|
||||
metricsTab: {
|
||||
@@ -617,12 +617,12 @@ export const dashboardView = {
|
||||
closeButton: dataTestLocator('close-button'),
|
||||
},
|
||||
saveModal: {
|
||||
modal: '.ant-modal-content',
|
||||
modal: '.ant-modal-container',
|
||||
dashboardNameInput: '.ant-input',
|
||||
saveButton: dataTestLocator('modal-save-dashboard-button'),
|
||||
},
|
||||
dashboardProperties: {
|
||||
modal: '.ant-modal-content',
|
||||
modal: '.ant-modal-container',
|
||||
dashboardTitleInput: dataTestLocator('dashboard-title-input'),
|
||||
modalButton: '[type="button"]',
|
||||
},
|
||||
|
||||
6611
superset-frontend/cypress-base/package-lock.json
generated
6611
superset-frontend/cypress-base/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
"chai": "^4.5.0",
|
||||
"cy-verify-downloads": "^0.2.5",
|
||||
"cypress-fail-on-console-error": "^4.0.3",
|
||||
"nanoid": "^5.0.9",
|
||||
"nanoid": "^6.0.0",
|
||||
"querystringify": "^2.2.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"rison": "^0.1.1"
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
// timezone for unit tests
|
||||
process.env.TZ = 'America/New_York';
|
||||
module.exports = {
|
||||
// [/\\] matches both path separators so the suite also collects on
|
||||
// native Windows, where jest hands the regex backslash-separated paths.
|
||||
testRegex:
|
||||
'\\/superset-frontend\\/(spec|src|plugins|packages|tools)\\/.*(_spec|\\.test)\\.[jt]sx?$',
|
||||
'[/\\\\]superset-frontend[/\\\\](spec|src|plugins|packages|tools)[/\\\\].*(_spec|\\.test)\\.[jt]sx?$',
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|geojson)$': '<rootDir>/spec/__mocks__/mockExportObject.js',
|
||||
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/spec/__mocks__/mockExportString.js',
|
||||
|
||||
@@ -215,6 +215,7 @@
|
||||
"jsx-a11y/no-noninteractive-tabindex": "error",
|
||||
"jsx-a11y/no-redundant-roles": "error",
|
||||
"jsx-a11y/no-static-element-interactions": "error",
|
||||
"jsx-a11y/prefer-tag-over-role": "error",
|
||||
"jsx-a11y/role-has-required-aria-props": "error",
|
||||
"jsx-a11y/role-supports-aria-props": "error",
|
||||
"jsx-a11y/scope": "error",
|
||||
|
||||
1528
superset-frontend/package-lock.json
generated
1528
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -166,8 +166,8 @@
|
||||
"@visx/scale": "^4.0.0",
|
||||
"@visx/tooltip": "^4.0.0",
|
||||
"@visx/xychart": "^4.0.0",
|
||||
"ag-grid-community": "36.0.0",
|
||||
"ag-grid-react": "36.0.0",
|
||||
"ag-grid-community": "36.0.1",
|
||||
"ag-grid-react": "36.0.1",
|
||||
"antd": "^6.5.1",
|
||||
"chrono-node": "^2.10.0",
|
||||
"classnames": "^2.2.5",
|
||||
@@ -179,7 +179,7 @@
|
||||
"echarts": "^6.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.6",
|
||||
"fuse.js": "^7.4.2",
|
||||
"fuse.js": "^7.5.0",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
@@ -187,7 +187,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.9.0",
|
||||
"immer": "^11.1.11",
|
||||
"immer": "^11.1.15",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -201,7 +201,7 @@
|
||||
"memoize-one": "^6.0.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.16",
|
||||
"nanoid": "^6.0.0",
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
@@ -263,13 +263,13 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.17",
|
||||
"@formatjs/intl-durationformat": "^0.10.18",
|
||||
"@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.0",
|
||||
"@storybook/addon-links": "10.5.0",
|
||||
"@storybook/react-webpack5": "10.5.0",
|
||||
"@storybook/addon-docs": "10.5.2",
|
||||
"@storybook/addon-links": "10.5.2",
|
||||
"@storybook/react-webpack5": "10.5.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.43",
|
||||
@@ -300,7 +300,7 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.64.0",
|
||||
"@typescript-eslint/parser": "^8.63.0",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
@@ -325,7 +325,7 @@
|
||||
"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.0",
|
||||
"eslint-plugin-storybook": "10.5.2",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -344,7 +344,7 @@
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.6.1",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.73.0",
|
||||
"oxlint": "^1.74.0",
|
||||
"po2json": "^0.4.5",
|
||||
"postcss-styled-syntax": "^0.7.2",
|
||||
"prettier": "3.9.5",
|
||||
@@ -357,7 +357,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.0",
|
||||
"storybook": "10.5.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
@@ -366,10 +366,9 @@
|
||||
"tsx": "^4.23.1",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"wait-on": "^9.0.10",
|
||||
"webpack": "^5.108.4",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
"webpack-bundle-analyzer": "^5.3.1",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
"webpack-manifest-plugin": "^6.0.1",
|
||||
@@ -416,7 +415,7 @@
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"fast-xml-parser": "^5.8.0",
|
||||
"fast-uri": "^3.1.3",
|
||||
"fast-uri": "^3.1.4",
|
||||
"jest-mock": "^30.4.0",
|
||||
"jest-runtime": "^30.4.0",
|
||||
"@jest/globals": "^30.4.0",
|
||||
@@ -424,6 +423,7 @@
|
||||
"jest-util": "^30.4.0",
|
||||
"jest-circus": "^30.4.0",
|
||||
"jest-environment-node": "^30.4.0",
|
||||
"typescript-json-schema": "^0.68.0",
|
||||
"@babel/eslint-parser": {
|
||||
"eslint": "$eslint"
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ src/
|
||||
├── extensions/
|
||||
├── menus/
|
||||
├── sqlLab/
|
||||
├── storage/
|
||||
├── theme/
|
||||
├── translation/
|
||||
├── utils/
|
||||
|
||||
@@ -73,6 +73,10 @@
|
||||
"./testing": {
|
||||
"types": "./lib/testing.d.ts",
|
||||
"default": "./lib/testing.js"
|
||||
},
|
||||
"./storage": {
|
||||
"types": "./lib/storage/index.d.ts",
|
||||
"default": "./lib/storage/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@@ -104,7 +108,7 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"nanoid": "^5.0.9",
|
||||
"nanoid": "*",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
|
||||
@@ -227,8 +227,6 @@ export interface Extension {
|
||||
id: string;
|
||||
/** Human-readable name of the extension */
|
||||
name: string;
|
||||
/** URL or path to the extension's remote entry point */
|
||||
remoteEntry: string;
|
||||
/** Version of the extension */
|
||||
version: string;
|
||||
}
|
||||
|
||||
@@ -24,9 +24,80 @@
|
||||
* including querying extension metadata and monitoring extension lifecycle events.
|
||||
* Extensions can use this API to discover other extensions and react to changes
|
||||
* in the extension ecosystem.
|
||||
*
|
||||
* Extensions can access their own context via `getContext()`, which provides:
|
||||
* - Extension metadata (id, name, version, etc.)
|
||||
* - Extension-scoped storage (localStorage, sessionStorage, ephemeral cache)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { extensions } from '@apache-superset/core';
|
||||
*
|
||||
* // Get the current extension's context
|
||||
* const ctx = extensions.getContext();
|
||||
*
|
||||
* // Access extension metadata
|
||||
* console.log(`Running ${ctx.extension.name} v${ctx.extension.version}`);
|
||||
*
|
||||
* // Access extension-scoped storage
|
||||
* await ctx.storage.local.set('preference', { theme: 'dark' });
|
||||
* await ctx.storage.ephemeral.set('cache', data, { ttl: 300 });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { Extension } from '../common';
|
||||
import { ExtensionStorage } from '../storage';
|
||||
|
||||
/**
|
||||
* Context object providing extension-specific resources.
|
||||
*
|
||||
* This context is only available during extension execution.
|
||||
* Calling `getContext()` outside of an extension will throw an error.
|
||||
*/
|
||||
export interface ExtensionContext {
|
||||
/**
|
||||
* Metadata about the current extension.
|
||||
*/
|
||||
extension: Extension;
|
||||
|
||||
/**
|
||||
* Extension-scoped storage across all tiers.
|
||||
* All keys are automatically namespaced to prevent collisions.
|
||||
*/
|
||||
storage: ExtensionStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current extension's context.
|
||||
*
|
||||
* This function returns the context for the currently executing extension,
|
||||
* providing access to extension metadata and scoped resources like storage.
|
||||
*
|
||||
* @returns The current extension's context.
|
||||
* @throws Error if called outside of an extension context.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { extensions } from '@apache-superset/core';
|
||||
*
|
||||
* const ctx = extensions.getContext();
|
||||
*
|
||||
* // Access extension metadata
|
||||
* console.log(`Extension: ${ctx.extension.id}`);
|
||||
* console.log(`Version: ${ctx.extension.version}`);
|
||||
*
|
||||
* // Access extension-scoped storage
|
||||
* await ctx.storage.local.set('userPref', { sidebar: 'collapsed' });
|
||||
* const pref = await ctx.storage.local.get('userPref');
|
||||
*
|
||||
* // Use ephemeral storage with TTL
|
||||
* await ctx.storage.ephemeral.set('tempData', data, { ttl: 3600 });
|
||||
*
|
||||
* // Access shared (cross-user) storage
|
||||
* await ctx.storage.ephemeral.shared.set('globalCounter', count, { ttl: 3600 });
|
||||
* ```
|
||||
*/
|
||||
export declare function getContext(): ExtensionContext;
|
||||
|
||||
/**
|
||||
* Get an extension by its full identifier in the form of: `publisher.name`.
|
||||
|
||||
@@ -30,4 +30,5 @@ export * as contributions from './contributions';
|
||||
export * as theme from './theme';
|
||||
export * as translation from './translation';
|
||||
export * as components from './components';
|
||||
export * as storage from './storage';
|
||||
export * as utils from './utils';
|
||||
|
||||
218
superset-frontend/packages/superset-core/src/storage/index.ts
Normal file
218
superset-frontend/packages/superset-core/src/storage/index.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileoverview Shared types for extension storage APIs.
|
||||
*
|
||||
* These types are shared across all storage tiers (local, session, ephemeral,
|
||||
* persistent) to ensure a consistent API pattern.
|
||||
*/
|
||||
|
||||
/**
|
||||
* JSON-compatible value type.
|
||||
* These are the only values that can be safely serialized/deserialized via JSON.
|
||||
*/
|
||||
export type JsonValue =
|
||||
string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
||||
/**
|
||||
* Base interface for a storage accessor.
|
||||
* All storage tiers implement this interface for both user-scoped and shared access.
|
||||
*/
|
||||
export interface StorageAccessor {
|
||||
/**
|
||||
* Get a value from storage.
|
||||
*
|
||||
* @param key The key to retrieve.
|
||||
* @returns The stored value, or null if not found.
|
||||
*/
|
||||
get<T = JsonValue>(key: string): Promise<T | null>;
|
||||
|
||||
/**
|
||||
* Set a value in storage.
|
||||
*
|
||||
* @param key The key to store.
|
||||
* @param value The value to store (must be JSON-serializable).
|
||||
*/
|
||||
set<T = JsonValue>(key: string, value: T): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove a value from storage.
|
||||
*
|
||||
* @param key The key to remove.
|
||||
*/
|
||||
remove(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options required when setting ephemeral (server-cached) state.
|
||||
*/
|
||||
export interface EphemeralSetOptions {
|
||||
ttl: number;
|
||||
/** Name of the codec used to encode the value, e.g. "json" (default). */
|
||||
codec?: string;
|
||||
/** Whether `value` is binary data. */
|
||||
isBinary?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options available when setting persistent (database-backed) state.
|
||||
*/
|
||||
export interface PersistentSetOptions {
|
||||
encrypt?: boolean;
|
||||
/** Name of the codec used to encode the value, e.g. "json" (default). */
|
||||
codec?: string;
|
||||
/** Whether `value` is binary data. */
|
||||
isBinary?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for listing persistent (database-backed) state entries.
|
||||
*
|
||||
* `page` and `pageSize` are required (no default): `list` returns one page
|
||||
* of a caller's entries, not the whole result set, and a default would let
|
||||
* that fact go unnoticed at the call site. Check the returned
|
||||
* `PersistentListResult.count` against `pageSize` to know whether more
|
||||
* pages exist.
|
||||
*
|
||||
* `resourceType`/`resourceUuid` are optional filters; omitting them lists
|
||||
* every entry in the caller's scope (global or user-scoped, depending on
|
||||
* whether `list` is called via `.shared` or directly).
|
||||
*/
|
||||
export interface PersistentListOptions {
|
||||
/** Zero-indexed page number. */
|
||||
page: number;
|
||||
/**
|
||||
* Entries per page. There is no fixed ceiling on this value, but a page
|
||||
* whose combined value size exceeds MAX_LIST_PAYLOAD_SIZE from config is
|
||||
* rejected — reduce pageSize and retry if that happens.
|
||||
*/
|
||||
pageSize: number;
|
||||
resourceType?: string;
|
||||
resourceUuid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single entry returned by a persistent state `list` call.
|
||||
*/
|
||||
export interface PersistentListEntry<T = JsonValue> {
|
||||
key: string;
|
||||
/**
|
||||
* The stored value. When `isBinary` is true, this is a base64 string
|
||||
* that must be decoded to get the actual value.
|
||||
*/
|
||||
value: T | null;
|
||||
/** Name of the codec `value` was encoded with, e.g. "json" or "binary". */
|
||||
codec: string;
|
||||
/** Whether the stored value is binary data. */
|
||||
isBinary: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a persistent state `list` call.
|
||||
*/
|
||||
export interface PersistentListResult<T = JsonValue> {
|
||||
entries: PersistentListEntry<T>[];
|
||||
/**
|
||||
* Total number of entries matching the given scope/filters, across all
|
||||
* pages — not just the number returned in `entries`.
|
||||
*/
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage accessor for ephemeral state, requiring TTL on every set.
|
||||
*/
|
||||
export interface EphemeralStorageAccessor {
|
||||
get<T = JsonValue>(key: string): Promise<T | null>;
|
||||
set<T = JsonValue>(
|
||||
key: string,
|
||||
value: T,
|
||||
options: EphemeralSetOptions,
|
||||
): Promise<void>;
|
||||
remove(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage tier for ephemeral state with TTL-required set operations.
|
||||
*/
|
||||
export interface EphemeralStorageTier extends EphemeralStorageAccessor {
|
||||
shared: EphemeralStorageAccessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage accessor for persistent state, supporting optional encryption.
|
||||
*/
|
||||
export interface PersistentStorageAccessor {
|
||||
get<T = JsonValue>(key: string): Promise<T | null>;
|
||||
set<T = JsonValue>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?: PersistentSetOptions,
|
||||
): Promise<void>;
|
||||
list<T = JsonValue>(
|
||||
options: PersistentListOptions,
|
||||
): Promise<PersistentListResult<T>>;
|
||||
remove(key: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage tier for persistent (database-backed) state.
|
||||
*/
|
||||
export interface PersistentStorageTier extends PersistentStorageAccessor {
|
||||
shared: PersistentStorageAccessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension-scoped storage accessor for all available tiers.
|
||||
*
|
||||
* All storage tiers are automatically namespaced to the current extension,
|
||||
* preventing key collisions between extensions.
|
||||
*/
|
||||
export interface ExtensionStorage {
|
||||
/**
|
||||
* Browser localStorage - persists across browser sessions.
|
||||
* Data is scoped to the current extension and user. There is no
|
||||
* `.shared` accessor for this tier: browser storage is per-device, so a
|
||||
* "shared" value would only be visible to other users of the same
|
||||
* browser, not to other users of the extension generally.
|
||||
*/
|
||||
local: StorageAccessor;
|
||||
|
||||
/**
|
||||
* Browser sessionStorage - cleared when the tab closes.
|
||||
* Data is scoped to the current extension and user. There is no
|
||||
* `.shared` accessor for this tier; see `local` above.
|
||||
*/
|
||||
session: StorageAccessor;
|
||||
|
||||
/**
|
||||
* Server-side cache (Redis/Memcached) with TTL.
|
||||
* Data is scoped to the current extension and user.
|
||||
* Use `.shared` for data visible to all users.
|
||||
*/
|
||||
ephemeral: EphemeralStorageTier;
|
||||
|
||||
/**
|
||||
* Durable database-backed storage (Tier 3).
|
||||
* Data survives server restarts and cache evictions.
|
||||
* Use `.shared` for data visible to all users.
|
||||
*/
|
||||
persistent: PersistentStorageTier;
|
||||
}
|
||||
@@ -16,9 +16,21 @@
|
||||
* specific language governing permissions and limitationsxw
|
||||
* under the License.
|
||||
*/
|
||||
import { PostProcessingProphet, getXAxisLabel } from '@superset-ui/core';
|
||||
import {
|
||||
PostProcessingProphet,
|
||||
TimeGranularity,
|
||||
getXAxisColumn,
|
||||
getXAxisLabel,
|
||||
isAdhocColumn,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
// Fallback grain used only when no time grain can be resolved from the form
|
||||
// data, query object, or x-axis column. Matches the `time_grain_sqla` control
|
||||
// default in sharedControls so forecasting stays functional rather than failing
|
||||
// with an opaque backend error.
|
||||
const DEFAULT_TIME_GRAIN = TimeGranularity.DAY;
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export const prophetOperator: PostProcessingFactory<PostProcessingProphet> = (
|
||||
formData,
|
||||
@@ -26,10 +38,25 @@ export const prophetOperator: PostProcessingFactory<PostProcessingProphet> = (
|
||||
) => {
|
||||
const xAxisLabel = getXAxisLabel(formData);
|
||||
if (formData.forecastEnabled && xAxisLabel) {
|
||||
// The effective time grain can live in several places depending on how the
|
||||
// chart was configured. Prefer, in order:
|
||||
// 1. the grain popover on an adhoc x-axis column (generic x-axis),
|
||||
// 2. the grain resolved onto the query object's extras (picks up
|
||||
// dashboard-applied grains and the panel control),
|
||||
// 3. the `time_grain_sqla` panel control on the form data directly.
|
||||
// Fall back to a daily grain so a saved/dashboard chart with the grain
|
||||
// cleared to "None" still forecasts instead of raising a backend error.
|
||||
const xAxisColumn = getXAxisColumn(formData);
|
||||
const timeGrain =
|
||||
(isAdhocColumn(xAxisColumn) &&
|
||||
(xAxisColumn.timeGrain as TimeGranularity)) ||
|
||||
queryObject.extras?.time_grain_sqla ||
|
||||
formData.time_grain_sqla ||
|
||||
DEFAULT_TIME_GRAIN;
|
||||
return {
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: formData.time_grain_sqla,
|
||||
time_grain: timeGrain,
|
||||
periods: parseInt(formData.forecastPeriods, 10),
|
||||
confidence_interval: parseFloat(formData.forecastInterval),
|
||||
yearly_seasonality: formData.forecastSeasonalityYearly,
|
||||
|
||||
@@ -43,6 +43,11 @@ const queryObject: QueryObject = {
|
||||
granularity: 'P1Y',
|
||||
};
|
||||
|
||||
// A chart whose Time Grain control was cleared to "None": form_data has no
|
||||
// `time_grain_sqla` key at all (SC-113749).
|
||||
const formDataWithoutGrain: SqlaFormData = { ...formData };
|
||||
delete formDataWithoutGrain.time_grain_sqla;
|
||||
|
||||
test('should skip prophetOperator', () => {
|
||||
expect(prophetOperator(formData, queryObject)).toEqual(undefined);
|
||||
});
|
||||
@@ -137,3 +142,103 @@ test('should do prophetOperator over adhoc column', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should fall back to a daily grain when no time grain is resolvable', () => {
|
||||
// Regression for SC-113749: a saved/dashboard chart with the Time Grain
|
||||
// control cleared to "None" has no `time_grain_sqla` in form_data. Prior to
|
||||
// the fix this emitted `time_grain: undefined`, which `JSON.stringify` drops,
|
||||
// causing the backend `prophet()` call to raise a raw `TypeError`.
|
||||
expect(
|
||||
prophetOperator(
|
||||
{
|
||||
...formDataWithoutGrain,
|
||||
granularity_sqla: 'time_column',
|
||||
forecastEnabled: true,
|
||||
forecastPeriods: '3',
|
||||
forecastInterval: '5',
|
||||
forecastSeasonalityYearly: true,
|
||||
forecastSeasonalityWeekly: false,
|
||||
forecastSeasonalityDaily: false,
|
||||
},
|
||||
{ ...queryObject, extras: {} },
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: 'P1D',
|
||||
periods: 3.0,
|
||||
confidence_interval: 5.0,
|
||||
yearly_seasonality: true,
|
||||
weekly_seasonality: false,
|
||||
daily_seasonality: false,
|
||||
index: DTTM_ALIAS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should resolve the time grain from the adhoc x-axis column', () => {
|
||||
// With the generic x-axis, the grain lives on the column's popover
|
||||
// (`timeGrain`) rather than the `time_grain_sqla` panel control.
|
||||
expect(
|
||||
prophetOperator(
|
||||
{
|
||||
...formDataWithoutGrain,
|
||||
x_axis: {
|
||||
label: 'ds',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: 'ds',
|
||||
timeGrain: 'P1M',
|
||||
},
|
||||
forecastEnabled: true,
|
||||
forecastPeriods: '3',
|
||||
forecastInterval: '5',
|
||||
forecastSeasonalityYearly: true,
|
||||
forecastSeasonalityWeekly: false,
|
||||
forecastSeasonalityDaily: false,
|
||||
},
|
||||
{ ...queryObject, extras: {} },
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: 'P1M',
|
||||
periods: 3.0,
|
||||
confidence_interval: 5.0,
|
||||
yearly_seasonality: true,
|
||||
weekly_seasonality: false,
|
||||
daily_seasonality: false,
|
||||
index: 'ds',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should resolve the time grain from the query object extras', () => {
|
||||
// Dashboard-applied grains (e.g. via native filters) land in
|
||||
// `queryObject.extras.time_grain_sqla` even when form_data has none.
|
||||
expect(
|
||||
prophetOperator(
|
||||
{
|
||||
...formDataWithoutGrain,
|
||||
granularity_sqla: 'time_column',
|
||||
forecastEnabled: true,
|
||||
forecastPeriods: '3',
|
||||
forecastInterval: '5',
|
||||
forecastSeasonalityYearly: true,
|
||||
forecastSeasonalityWeekly: false,
|
||||
forecastSeasonalityDaily: false,
|
||||
},
|
||||
{ ...queryObject, extras: { time_grain_sqla: 'P1W' } },
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: 'P1W',
|
||||
periods: 3.0,
|
||||
confidence_interval: 5.0,
|
||||
yearly_seasonality: true,
|
||||
weekly_seasonality: false,
|
||||
daily_seasonality: false,
|
||||
index: DTTM_ALIAS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user