mirror of
https://github.com/apache/superset.git
synced 2026-05-05 07:54:17 +00:00
Compare commits
219 Commits
fix/check-
...
6.1.0rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68bbc68cce | ||
|
|
7a558f7ceb | ||
|
|
3b7ddf88e9 | ||
|
|
232a555d55 | ||
|
|
ba6ec40fed | ||
|
|
0648f78785 | ||
|
|
129fcb5451 | ||
|
|
6048e2f2e1 | ||
|
|
1a537420dc | ||
|
|
4e9a6db9b3 | ||
|
|
02e6d671b4 | ||
|
|
3cbc83449f | ||
|
|
954902db1d | ||
|
|
8b9a74a3d4 | ||
|
|
2dca313e41 | ||
|
|
b196e11af2 | ||
|
|
b3c2965eaf | ||
|
|
b424683c74 | ||
|
|
119f173009 | ||
|
|
89231addf4 | ||
|
|
04565fa232 | ||
|
|
ea040d3136 | ||
|
|
1afa47bf80 | ||
|
|
94dc92df02 | ||
|
|
c85d515af3 | ||
|
|
18a8f6cb04 | ||
|
|
9e313ba82f | ||
|
|
305927df7b | ||
|
|
bd7b53e341 | ||
|
|
3f157d9ca1 | ||
|
|
15fa218529 | ||
|
|
9499ccf52c | ||
|
|
2b4e9909db | ||
|
|
2c163576a7 | ||
|
|
33531088d6 | ||
|
|
c14f042185 | ||
|
|
af0f8176e4 | ||
|
|
81ecb7f42b | ||
|
|
4e16086a9b | ||
|
|
c78a599ca6 | ||
|
|
1d9dbe0697 | ||
|
|
0d5c7cb9b0 | ||
|
|
dd9724daae | ||
|
|
1f7838367f | ||
|
|
3fdbbb6e7e | ||
|
|
59d67fb786 | ||
|
|
e2d6afa38d | ||
|
|
252044702a | ||
|
|
f35654179f | ||
|
|
8a915c4783 | ||
|
|
f0d7945ae9 | ||
|
|
32bd0caec3 | ||
|
|
7ee70d9aba | ||
|
|
ccfe29e83f | ||
|
|
f59eaff400 | ||
|
|
25fb118883 | ||
|
|
09292ba228 | ||
|
|
e4ced6ad82 | ||
|
|
07c548100d | ||
|
|
23759b9a02 | ||
|
|
6017b1a3b5 | ||
|
|
6848970d0d | ||
|
|
a8daa0d4dd | ||
|
|
04ca466c77 | ||
|
|
374cc592ac | ||
|
|
e9ca6e04d5 | ||
|
|
fcd2bac21d | ||
|
|
c41894ac0b | ||
|
|
1e4ad1ba52 | ||
|
|
1423387010 | ||
|
|
685d9a0652 | ||
|
|
b9b48fb1e4 | ||
|
|
4764eea1dc | ||
|
|
bc8690187f | ||
|
|
dcca02dd16 | ||
|
|
53d427aa79 | ||
|
|
a8ff6e463b | ||
|
|
a881c20598 | ||
|
|
096ea4ee42 | ||
|
|
7eb702e99e | ||
|
|
9689739e1a | ||
|
|
4a10bf4ca7 | ||
|
|
42fe0f755a | ||
|
|
b0adfc64c7 | ||
|
|
4f9ec243b3 | ||
|
|
bf399b9f97 | ||
|
|
b1eb6ac7c9 | ||
|
|
0f676a8e67 | ||
|
|
d4832fac72 | ||
|
|
81dbfad95a | ||
|
|
389897bb9c | ||
|
|
0f4184db2f | ||
|
|
5d8d170093 | ||
|
|
5a348c0dae | ||
|
|
fa6801a525 | ||
|
|
f8368404cf | ||
|
|
27c9d806f1 | ||
|
|
0bff78e45b | ||
|
|
2ca54b418f | ||
|
|
6cff944b7f | ||
|
|
ec9ffc4c31 | ||
|
|
ddb285b4c3 | ||
|
|
850c33fa6b | ||
|
|
c853d4df63 | ||
|
|
bda02a3fdc | ||
|
|
f1f757b5c5 | ||
|
|
bf8e927f2a | ||
|
|
875d0c062f | ||
|
|
e08c305396 | ||
|
|
df2a0416eb | ||
|
|
2aaf935db8 | ||
|
|
54cb0c1fb4 | ||
|
|
807505a89a | ||
|
|
f968c5cc78 | ||
|
|
c512e30b17 | ||
|
|
9fa0b68575 | ||
|
|
e2d103f9a5 | ||
|
|
8d47bd7f42 | ||
|
|
bd552209e7 | ||
|
|
686fa44a5f | ||
|
|
77df7c2add | ||
|
|
6a866335b6 | ||
|
|
26c3021b66 | ||
|
|
bd8b02c1c0 | ||
|
|
446358749e | ||
|
|
1e7d781354 | ||
|
|
9619fa2156 | ||
|
|
c4af1cbca7 | ||
|
|
9a35a4f43a | ||
|
|
a5d4348fb1 | ||
|
|
dd1f946962 | ||
|
|
27206fc892 | ||
|
|
861ce50473 | ||
|
|
1d170f0da1 | ||
|
|
a33c2a9c0e | ||
|
|
ac8d6b0c53 | ||
|
|
de42d4a986 | ||
|
|
11b510ecad | ||
|
|
a16f0c81b5 | ||
|
|
155f55dbe1 | ||
|
|
deeedef0dc | ||
|
|
967e78a716 | ||
|
|
a541d69019 | ||
|
|
493f6c0aed | ||
|
|
c26d2de616 | ||
|
|
6d1a1b1863 | ||
|
|
68cc13cfbf | ||
|
|
935226c736 | ||
|
|
9239db5a32 | ||
|
|
cf493a928a | ||
|
|
67b2bf0646 | ||
|
|
af54788b5e | ||
|
|
0915e675df | ||
|
|
884f8f7a35 | ||
|
|
da5f968a8e | ||
|
|
e9150afba6 | ||
|
|
519e7c58de | ||
|
|
1f94c88025 | ||
|
|
d7633752bd | ||
|
|
c2b3d36435 | ||
|
|
c68dee2caf | ||
|
|
30690aaf7b | ||
|
|
25915f016b | ||
|
|
b9259db772 | ||
|
|
9277f85d65 | ||
|
|
f7e4fe9d0f | ||
|
|
2e09934bb5 | ||
|
|
435e405263 | ||
|
|
54b313c659 | ||
|
|
e2495119a3 | ||
|
|
87a63a81e2 | ||
|
|
b95e556840 | ||
|
|
134e77b7c2 | ||
|
|
5adf07aef3 | ||
|
|
9b6c4486c0 | ||
|
|
4ce0627c73 | ||
|
|
530ad25c90 | ||
|
|
f11a4834d2 | ||
|
|
50ff9fdb3e | ||
|
|
a83ff0057c | ||
|
|
cffca7367c | ||
|
|
e855fbf71d | ||
|
|
1726169c1a | ||
|
|
8b20d95366 | ||
|
|
41371e4816 | ||
|
|
185c0eb3bf | ||
|
|
4a153a0ec3 | ||
|
|
aaaead02f1 | ||
|
|
4baedcc794 | ||
|
|
825b284492 | ||
|
|
33b0b6b3bb | ||
|
|
e42f6c3a1c | ||
|
|
b3eaa5ad01 | ||
|
|
e41653faff | ||
|
|
39bdccd7ee | ||
|
|
ba2cfac373 | ||
|
|
d170decfc5 | ||
|
|
b791979c37 | ||
|
|
3ad8d484d7 | ||
|
|
343e7f33af | ||
|
|
3521504b05 | ||
|
|
f63c0d4caa | ||
|
|
cfcaaa9db4 | ||
|
|
224e9ea411 | ||
|
|
ec8177fe15 | ||
|
|
88195cdb54 | ||
|
|
a3dba466cb | ||
|
|
1354dc6881 | ||
|
|
249c21655f | ||
|
|
224a922341 | ||
|
|
a9b24da0a2 | ||
|
|
ab64ad7ac7 | ||
|
|
d266146bbb | ||
|
|
10415fe8be | ||
|
|
d42caf744f | ||
|
|
d8e346d52d | ||
|
|
8d7a36df5a | ||
|
|
77c7f9b5e8 | ||
|
|
6ebaf5919f |
6
.github/actions/setup-docker/action.yml
vendored
6
.github/actions/setup-docker/action.yml
vendored
@@ -26,16 +26,16 @@ runs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ inputs.build == 'true' }}
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ inputs.build == 'true' }}
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Try to login to DockerHub
|
||||
if: ${{ inputs.login-to-dockerhub == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ inputs.dockerhub-user }}
|
||||
password: ${{ inputs.dockerhub-token }}
|
||||
|
||||
4
.github/workflows/bump-python-package.yml
vendored
4
.github/workflows/bump-python-package.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: master
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-supersetbot/
|
||||
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
|
||||
2
.github/workflows/cancel_duplicates.yml
vendored
2
.github/workflows/cancel_duplicates.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
if: steps.check_queued.outputs.count >= 20
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Cancel duplicate workflow runs
|
||||
if: steps.check_queued.outputs.count >= 20
|
||||
|
||||
2
.github/workflows/check-python-deps.yml
vendored
2
.github/workflows/check-python-deps.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: Check and notify
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
|
||||
6
.github/workflows/claude.yml
vendored
6
.github/workflows/claude.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Comment access denied
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const message = `👋 Hi @${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}!
|
||||
@@ -71,12 +71,12 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
uses: anthropics/claude-code-action@beta
|
||||
uses: anthropics/claude-code-action@28f83620103c48a57093dcc2837eec89e036bb9f # beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
timeout_minutes: "60"
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Check for file changes
|
||||
id: check
|
||||
|
||||
6
.github/workflows/dependency-review.yml
vendored
6
.github/workflows/dependency-review.yml
vendored
@@ -27,9 +27,9 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail-on-severity: critical
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
|
||||
25
.github/workflows/docker.yml
vendored
25
.github/workflows/docker.yml
vendored
@@ -14,7 +14,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
setup_matrix:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
@@ -40,9 +39,8 @@ jobs:
|
||||
IMAGE_TAG: apache/superset:GHA-${{ matrix.build_preset }}-${{ github.run_id }}
|
||||
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -91,7 +89,7 @@ jobs:
|
||||
# in the context of push (using multi-platform build), we need to pull the image locally
|
||||
- name: Docker pull
|
||||
if: github.event_name == 'push' && (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker)
|
||||
run: docker pull $IMAGE_TAG
|
||||
run: docker pull $IMAGE_TAG
|
||||
|
||||
- name: Print docker stats
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker
|
||||
@@ -101,23 +99,6 @@ jobs:
|
||||
docker images $IMAGE_TAG
|
||||
docker history $IMAGE_TAG
|
||||
|
||||
# Scan for vulnerabilities in built container image after pushes to mainline branch.
|
||||
- name: Run Trivy container image vulnerabity scan
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'lean'
|
||||
uses: aquasecurity/trivy-action@97e0b3872f55f89b95b2f65b3dbab56962816478 # v0.34.2
|
||||
with:
|
||||
image-ref: ${{ env.IMAGE_TAG }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
vuln-type: 'os'
|
||||
severity: 'CRITICAL,HIGH'
|
||||
ignore-unfixed: true
|
||||
- name: Upload Trivy scan results to GitHub Security tab
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'lean'
|
||||
uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
|
||||
with:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
- name: docker-compose sanity check
|
||||
if: (steps.check.outputs.python || steps.check.outputs.frontend || steps.check.outputs.docker) && matrix.build_preset == 'dev'
|
||||
shell: bash
|
||||
@@ -134,7 +115,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Check for file changes
|
||||
|
||||
4
.github/workflows/embedded-sdk-release.yml
vendored
4
.github/workflows/embedded-sdk-release.yml
vendored
@@ -28,8 +28,8 @@ jobs:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
4
.github/workflows/embedded-sdk-test.yml
vendored
4
.github/workflows/embedded-sdk-test.yml
vendored
@@ -18,8 +18,8 @@ jobs:
|
||||
run:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
2
.github/workflows/ephemeral-env-pr-close.yml
vendored
2
.github/workflows/ephemeral-env-pr-close.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Comment (success)
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
|
||||
28
.github/workflows/ephemeral-env.yml
vendored
28
.github/workflows/ephemeral-env.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
- name: Get event SHA
|
||||
id: get-sha
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
core.setOutput("sha", prSha);
|
||||
|
||||
- name: Looking for feature flags in PR description
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
id: eval-feature-flags
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
return results;
|
||||
|
||||
- name: Reply with confirmation comment
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ needs.ephemeral-env-label.outputs.sha }} : ${{steps.get-sha.outputs.sha}} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ needs.ephemeral-env-label.outputs.sha }}
|
||||
persist-credentials: false
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
--extra-flags "--build-arg INCLUDE_CHROMIUM=false"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
uses: aws-actions/amazon-ecr-login@c962da2960ed15f492addc26fffa274485265950 # v2
|
||||
|
||||
- name: Load, tag and push image to ECR
|
||||
id: push-image
|
||||
@@ -220,12 +220,12 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@v2
|
||||
uses: aws-actions/amazon-ecr-login@c962da2960ed15f492addc26fffa274485265950 # v2
|
||||
|
||||
- name: Check target image exists in ECR
|
||||
id: check-image
|
||||
@@ -248,7 +248,7 @@ jobs:
|
||||
|
||||
- name: Fail on missing container image
|
||||
if: steps.check-image.outcome == 'failure'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
@@ -263,7 +263,7 @@ jobs:
|
||||
|
||||
- name: Fill in the new image ID in the Amazon ECS task definition
|
||||
id: task-def
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||
uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1
|
||||
with:
|
||||
task-definition: .github/workflows/ecs-task-definition.json
|
||||
container-name: superset-ci
|
||||
@@ -296,7 +296,7 @@ jobs:
|
||||
--tags key=pr,value=$PR_NUMBER key=github_user,value=${{ github.actor }}
|
||||
- name: Deploy Amazon ECS task definition
|
||||
id: deploy-task
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
||||
uses: aws-actions/amazon-ecs-deploy-task-definition@cbf54ec46642b86ff78c2f5793da6746954cf8ff # v2
|
||||
with:
|
||||
task-definition: ${{ steps.task-def.outputs.task-definition }}
|
||||
service: pr-${{ github.event.inputs.issue_number || github.event.pull_request.number }}-service
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
echo "ip=$(aws ec2 describe-network-interfaces --network-interface-ids ${{ steps.get-eni.outputs.eni }} | jq -r '.NetworkInterfaces | first | .Association.PublicIp')" >> $GITHUB_OUTPUT
|
||||
- name: Comment (success)
|
||||
if: ${{ success() }}
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
});
|
||||
- name: Comment (failure)
|
||||
if: ${{ failure() }}
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
|
||||
4
.github/workflows/generate-FOSSA-report.yml
vendored
4
.github/workflows/generate-FOSSA-report.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: "11"
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
||||
2
.github/workflows/issue_creation.yml
vendored
2
.github/workflows/issue_creation.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
4
.github/workflows/license-check.yml
vendored
4
.github/workflows/license-check.yml
vendored
@@ -15,12 +15,12 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '11'
|
||||
|
||||
2
.github/workflows/no-hold-label.yml
vendored
2
.github/workflows/no-hold-label.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for 'hold' label
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/pr-lint.yml
vendored
2
.github/workflows/pr-lint.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
6
.github/workflows/pre-commit.yml
vendored
6
.github/workflows/pre-commit.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
python-version: ["current", "previous", "next"]
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
echo "HOMEBREW_REPOSITORY=$HOMEBREW_REPOSITORY" >>"${GITHUB_ENV}"
|
||||
brew install norwoodj/tap/helm-docs
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
yarn install --immutable
|
||||
|
||||
- name: Cache pre-commit environments
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
name: Bump version and publish package(s)
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
# pulls all commits (needed for lerna / semantic release to correctly version)
|
||||
fetch-depth: 0
|
||||
@@ -42,13 +42,13 @@ jobs:
|
||||
|
||||
- name: Install Node.js
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
- name: Cache npm
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
with:
|
||||
path: ~/.npm # npm cache files are stored in `~/.npm` on Linux/macOS
|
||||
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
|
||||
- name: Cache npm
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/cache@v5
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
id: npm-cache # use this to check for `cache-hit` (`steps.npm-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.npm-cache-dir-path.outputs.dir }}
|
||||
|
||||
4
.github/workflows/showtime-trigger.yml
vendored
4
.github/workflows/showtime-trigger.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
steps:
|
||||
- name: Security Check - Authorize Maintainers Only
|
||||
id: auth
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -147,7 +147,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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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
@@ -37,7 +37,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
|
||||
16
.github/workflows/superset-docs-deploy.yml
vendored
16
.github/workflows/superset-docs-deploy.yml
vendored
@@ -38,21 +38,21 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: Setup Python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
- uses: actions/setup-java@v5
|
||||
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '21'
|
||||
distribution: "zulu"
|
||||
java-version: "21"
|
||||
- name: Install Graphviz
|
||||
run: sudo apt-get install -y graphviz
|
||||
- name: Compute Entity Relationship diagram (ERD)
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics (if triggered by integration tests)
|
||||
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
|
||||
uses: dawidd6/action-download-artifact@v16
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
path: docs/src/data/
|
||||
- name: Try to download latest diagnostics (for push/dispatch triggers)
|
||||
if: github.event_name != 'workflow_run'
|
||||
uses: dawidd6/action-download-artifact@v16
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
|
||||
19
.github/workflows/superset-docs-verify.yml
vendored
19
.github/workflows/superset-docs-verify.yml
vendored
@@ -24,10 +24,10 @@ jobs:
|
||||
name: Link Checking
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# an ASF Infra ticket to allow the new version first!
|
||||
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
|
||||
- uses: JustinBeckwith/linkinator-action@af984b9f30f63e796ae2ea5be5e07cb587f1bbd9 # v2.3
|
||||
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
|
||||
with:
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
@@ -67,14 +67,14 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
@@ -98,25 +98,26 @@ jobs:
|
||||
working-directory: docs
|
||||
steps:
|
||||
- name: "Checkout PR head: ${{ github.event.workflow_run.head_sha }}"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
node-version-file: "./docs/.nvmrc"
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install --check-cache
|
||||
- name: Download database diagnostics from integration tests
|
||||
uses: dawidd6/action-download-artifact@v16
|
||||
uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
|
||||
with:
|
||||
workflow: superset-python-integrationtest.yml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: database-diagnostics
|
||||
path: docs/src/data/
|
||||
if_no_artifact_found: "warning"
|
||||
- name: Use fresh diagnostics
|
||||
run: |
|
||||
if [ -f "src/data/databases-diagnostics.json" ]; then
|
||||
|
||||
20
.github/workflows/superset-e2e.yml
vendored
20
.github/workflows/superset-e2e.yml
vendored
@@ -69,21 +69,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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: ${{ github.workspace }}/superset-frontend/cypress-base/cypress/screenshots
|
||||
@@ -186,21 +186,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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
@@ -259,7 +259,7 @@ jobs:
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Playwright Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
working-directory: superset-extensions-cli
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.check.outputs.superset-extensions-cli
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: superset-extensions-cli
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
- name: Upload HTML coverage report
|
||||
if: steps.check.outputs.superset-extensions-cli
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: superset-extensions-cli-coverage-html
|
||||
path: htmlcov/
|
||||
|
||||
20
.github/workflows/superset-frontend.yml
vendored
20
.github/workflows/superset-frontend.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
should-run: ${{ steps.check.outputs.frontend }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
- name: Upload Docker Image Artifact
|
||||
if: steps.check.outputs.frontend
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: docker-image
|
||||
path: docker-image.tar.gz
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: docker-image
|
||||
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
"npm run test -- --coverage --shard=${{ matrix.shard }}/8 --coverageReporters=json"
|
||||
|
||||
- name: Upload Coverage Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: coverage-artifacts-${{ matrix.shard }}
|
||||
path: superset-frontend/coverage
|
||||
@@ -103,14 +103,14 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
- name: Download Coverage Artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
pattern: coverage-artifacts-*
|
||||
path: coverage/
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
run: npx nyc merge coverage/ merged-output/coverage-summary.json
|
||||
|
||||
- name: Upload Code Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
flags: javascript
|
||||
use_oidc: true
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: docker-image
|
||||
|
||||
@@ -166,7 +166,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: docker-image
|
||||
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Download Docker Image Artifact
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||
with:
|
||||
name: docker-image
|
||||
|
||||
|
||||
4
.github/workflows/superset-helm-lint.yml
vendored
4
.github/workflows/superset-helm-lint.yml
vendored
@@ -16,14 +16,14 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
|
||||
with:
|
||||
version: v3.16.4
|
||||
|
||||
|
||||
6
.github/workflows/superset-helm-release.yml
vendored
6
.github/workflows/superset-helm-release.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref_name }}
|
||||
persist-credentials: true
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
|
||||
with:
|
||||
version: v3.5.4
|
||||
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
CR_RELEASE_NAME_TEMPLATE: "superset-helm-chart-{{ .Version }}"
|
||||
|
||||
- name: Open Pull Request
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const branchName = '${{ env.branch_name }}';
|
||||
|
||||
10
.github/workflows/superset-playwright.yml
vendored
10
.github/workflows/superset-playwright.yml
vendored
@@ -60,21 +60,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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
run: playwright_testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
SAFE_APP_ROOT=${APP_ROOT//\//_}
|
||||
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
|
||||
- name: Upload Playwright Artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
if: failure()
|
||||
with:
|
||||
path: |
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
flags: python,mysql
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
"
|
||||
- name: Upload database diagnostics artifact
|
||||
if: steps.check.outputs.python
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: database-diagnostics
|
||||
path: databases-diagnostics.json
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
flags: python,postgres
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
flags: python,sqlite
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
run: |
|
||||
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
flags: python,presto
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
- 16379:6379
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
pip install -e .[hive]
|
||||
./scripts/python_tests.sh -m 'chart_data_flow or sql_json_flow'
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
flags: python,hive
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
run: |
|
||||
pytest --durations-min=0.5 --cov=superset/sql/ ./tests/unit_tests/sql/ --cache-clear --cov-fail-under=100
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
flags: python,unit
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
10
.github/workflows/superset-translations.yml
vendored
10
.github/workflows/superset-translations.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install dependencies
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -62,6 +62,10 @@ jobs:
|
||||
- name: Setup Python
|
||||
if: steps.check.outputs.python
|
||||
uses: ./.github/actions/setup-backend/
|
||||
|
||||
- name: Install msgcat
|
||||
run: sudo apt update && sudo apt install gettext
|
||||
|
||||
- name: Test babel extraction
|
||||
if: steps.check.outputs.python
|
||||
run: ./scripts/translations/babel_update.sh
|
||||
|
||||
2
.github/workflows/superset-websocket.yml
vendored
2
.github/workflows/superset-websocket.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install dependencies
|
||||
|
||||
4
.github/workflows/supersetbot.yml
vendored
4
.github/workflows/supersetbot.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- name: Quickly add thumbs up!
|
||||
if: github.event_name == 'issue_comment' && contains(github.event.comment.body, '@supersetbot')
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
});
|
||||
|
||||
- name: "Checkout ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
8
.github/workflows/tag-release.yml
vendored
8
.github/workflows/tag-release.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
build: "true"
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -107,12 +107,12 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
4
.github/workflows/tech-debt.yml
vendored
4
.github/workflows/tech-debt.yml
vendored
@@ -30,10 +30,10 @@ jobs:
|
||||
name: Generate Reports
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -133,6 +133,7 @@ CLAUDE.local.md
|
||||
PROJECT.md
|
||||
.aider*
|
||||
.claude_rc*
|
||||
.claude/settings.local.json
|
||||
.env.local
|
||||
oxc-custom-build/
|
||||
*.code-workspace
|
||||
|
||||
@@ -50,3 +50,4 @@ under the License.
|
||||
- [4.1.4](./CHANGELOG/4.1.4.md)
|
||||
- [5.0.0](./CHANGELOG/5.0.0.md)
|
||||
- [6.0.0](./CHANGELOG/6.0.0.md)
|
||||
- [6.1.0](./CHANGELOG/6.1.0.md)
|
||||
|
||||
1545
CHANGELOG/6.1.0.md
Normal file
1545
CHANGELOG/6.1.0.md
Normal file
File diff suppressed because it is too large
Load Diff
132
UPDATING.md
132
UPDATING.md
@@ -24,46 +24,12 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
## 6.1.0
|
||||
|
||||
### ClickHouse minimum driver version bump
|
||||
|
||||
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.
|
||||
|
||||
### MCP Tool Observability
|
||||
|
||||
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
|
||||
|
||||
**Two-layer instrumentation:**
|
||||
1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table)
|
||||
2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution
|
||||
|
||||
**Action naming convention:**
|
||||
- Tool-level logs: `mcp_tool_call` (via middleware)
|
||||
- Sub-operation logs: `mcp.{tool_name}.{operation}` (e.g., `mcp.generate_chart.validation`, `mcp.execute_sql.query_execution`)
|
||||
|
||||
**Querying MCP logs:**
|
||||
```sql
|
||||
-- Top slowest MCP operations
|
||||
SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms
|
||||
FROM logs
|
||||
WHERE action LIKE 'mcp.%'
|
||||
GROUP BY action
|
||||
ORDER BY avg_ms DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- MCP tool success rate
|
||||
SELECT
|
||||
json_extract(curated_payload, '$.tool') as tool,
|
||||
COUNT(*) as total_calls,
|
||||
SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) as successful,
|
||||
ROUND(100.0 * SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate
|
||||
FROM logs
|
||||
WHERE action = 'mcp_tool_call'
|
||||
GROUP BY tool
|
||||
ORDER BY total_calls DESC;
|
||||
```
|
||||
|
||||
**Security note:** Sensitive parameters (passwords, API keys, tokens) are automatically redacted in logs as `[REDACTED]`.
|
||||
|
||||
### Distributed Coordination Backend
|
||||
|
||||
A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-based backend for real-time coordination features in Superset. This backend enables:
|
||||
@@ -75,6 +41,7 @@ A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-b
|
||||
The distributed coordination is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments.
|
||||
|
||||
Example configuration in `superset_config.py`:
|
||||
|
||||
```python
|
||||
DISTRIBUTED_COORDINATION_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
@@ -89,9 +56,11 @@ See `superset/config.py` for complete configuration options.
|
||||
### WebSocket config for GAQ with Docker
|
||||
|
||||
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
|
||||
|
||||
- Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward)
|
||||
- Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example:
|
||||
``` yaml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
superset-websocket:
|
||||
volumes:
|
||||
@@ -104,7 +73,9 @@ services:
|
||||
### Example Data Loading Improvements
|
||||
|
||||
#### New Directory Structure
|
||||
|
||||
Examples are now organized by name with data and configs co-located:
|
||||
|
||||
```
|
||||
superset/examples/
|
||||
├── _shared/ # Shared database & metadata configs
|
||||
@@ -116,31 +87,12 @@ superset/examples/
|
||||
└── ...
|
||||
```
|
||||
|
||||
#### Simplified Parquet-based Loading
|
||||
- Auto-discovery: create `superset/examples/my_dataset/data.parquet` to add a new example
|
||||
- Parquet is an Apache project format: compressed (~27% smaller), self-describing schema
|
||||
- YAML configs define datasets, charts, and dashboards declaratively
|
||||
- Removed Python-based data generation from individual example files
|
||||
|
||||
#### Test Data Reorganization
|
||||
- Moved `big_data.py` to `superset/cli/test_loaders.py` - better reflects its purpose as a test utility
|
||||
- Fixed inverted logic for `--load-test-data` flag (now correctly includes .test.yaml files when flag is set)
|
||||
- Clarified CLI flags:
|
||||
- `--force` / `-f`: Force reload even if tables exist
|
||||
- `--only-metadata` / `-m`: Create table metadata without loading data
|
||||
- `--load-test-data` / `-t`: Include test dashboards and .test.yaml configs
|
||||
- `--load-big-data` / `-b`: Generate synthetic stress-test data
|
||||
|
||||
#### Bug Fixes
|
||||
- Fixed numpy array serialization for PostgreSQL (converts complex types to JSON strings)
|
||||
- Fixed KeyError for `allow_csv_upload` field in database configs (now optional with default)
|
||||
- Fixed test data loading logic that was incorrectly filtering files
|
||||
|
||||
### MCP Service
|
||||
|
||||
The MCP (Model Context Protocol) service enables AI assistants and automation tools to interact programmatically with Superset.
|
||||
|
||||
#### New Features
|
||||
|
||||
- MCP service infrastructure with FastMCP framework
|
||||
- Tools for dashboards, charts, datasets, SQL Lab, and instance metadata
|
||||
- Optional dependency: install with `pip install apache-superset[fastmcp]`
|
||||
@@ -150,6 +102,7 @@ The MCP (Model Context Protocol) service enables AI assistants and automation to
|
||||
#### New Configuration Options
|
||||
|
||||
**Development** (single-user, local testing):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_DEV_USERNAME = "admin" # User for MCP authentication
|
||||
@@ -158,6 +111,7 @@ MCP_SERVICE_PORT = 5008
|
||||
```
|
||||
|
||||
**Production** (JWT-based, multi-user):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
@@ -203,12 +157,14 @@ superset mcp run --port 5008 --use-factory-config
|
||||
The MCP service runs as a **separate process** from the Superset web server.
|
||||
|
||||
**Important**:
|
||||
|
||||
- Requires same Python environment and configuration as Superset
|
||||
- Shares database connections with main Superset app
|
||||
- Can be scaled independently from web server
|
||||
- Requires `fastmcp` package (optional dependency)
|
||||
|
||||
**Installation**:
|
||||
|
||||
```bash
|
||||
# Install with MCP support
|
||||
pip install apache-superset[fastmcp]
|
||||
@@ -222,6 +178,7 @@ Use systemd, supervisord, or Kubernetes to manage the MCP service process.
|
||||
See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
|
||||
**Security**:
|
||||
|
||||
- Development: Uses `MCP_DEV_USERNAME` for single-user access
|
||||
- Production: **MUST** configure JWT authentication
|
||||
- See `superset/mcp_service/SECURITY.md` for details
|
||||
@@ -234,14 +191,50 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
- Developer Guide: `superset/mcp_service/CLAUDE.md`
|
||||
- Quick Start: `superset/mcp_service/README.md`
|
||||
|
||||
---
|
||||
### MCP Tool Observability
|
||||
|
||||
- [35621](https://github.com/apache/superset/pull/35621): The default hash algorithm has changed from MD5 to SHA-256 for improved security and FedRAMP compliance. This affects cache keys for thumbnails, dashboard digests, chart digests, and filter option names. Existing cached data will be invalidated upon upgrade. To opt out of this change and maintain backward compatibility, set `HASH_ALGORITHM = "md5"` in your `superset_config.py`.
|
||||
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
|
||||
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
|
||||
|
||||
**Two-layer instrumentation:**
|
||||
|
||||
1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table)
|
||||
2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution
|
||||
|
||||
**Action naming convention:**
|
||||
|
||||
- Tool-level logs: `mcp_tool_call` (via middleware)
|
||||
- Sub-operation logs: `mcp.{tool_name}.{operation}` (e.g., `mcp.generate_chart.validation`, `mcp.execute_sql.query_execution`)
|
||||
|
||||
**Querying MCP logs:**
|
||||
|
||||
```sql
|
||||
-- Top slowest MCP operations
|
||||
SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms
|
||||
FROM logs
|
||||
WHERE action LIKE 'mcp.%'
|
||||
GROUP BY action
|
||||
ORDER BY avg_ms DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- MCP tool success rate
|
||||
SELECT
|
||||
json_extract(curated_payload, '$.tool') as tool,
|
||||
COUNT(*) as total_calls,
|
||||
SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) as successful,
|
||||
ROUND(100.0 * SUM(CASE WHEN json_extract(curated_payload, '$.success') = 'true' THEN 1 ELSE 0 END) / COUNT(*), 2) as success_rate
|
||||
FROM logs
|
||||
WHERE action = 'mcp_tool_call'
|
||||
GROUP BY tool
|
||||
ORDER BY total_calls DESC;
|
||||
```
|
||||
|
||||
**Security note:** Sensitive parameters (passwords, API keys, tokens) are automatically redacted in logs as `[REDACTED]`.
|
||||
|
||||
### APP_NAME configuration
|
||||
|
||||
### Breaking Changes
|
||||
- [37370](https://github.com/apache/superset/pull/37370): The `APP_NAME` configuration variable no longer controls the browser window/tab title or other frontend branding. Application names should now be configured using the theme system with the `brandAppName` token. The `APP_NAME` config is still used for backend contexts (MCP service, logs, etc.) and serves as a fallback if `brandAppName` is not set.
|
||||
- **Migration:**
|
||||
|
||||
```python
|
||||
# Before (Superset 5.x)
|
||||
APP_NAME = "My Custom App"
|
||||
@@ -260,16 +253,22 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
APP_NAME = "My Custom App"
|
||||
# But you should migrate to THEME_DEFAULT.token.brandAppName
|
||||
```
|
||||
|
||||
- **Note:** For dark mode, set the same tokens in `THEME_DARK` configuration.
|
||||
|
||||
### CUSTOM_FONT_URLS configuration
|
||||
|
||||
- [36317](https://github.com/apache/superset/pull/36317): The `CUSTOM_FONT_URLS` configuration option has been removed. Use the new per-theme `fontUrls` token in `THEME_DEFAULT` or database-managed themes instead.
|
||||
- **Before:**
|
||||
|
||||
```python
|
||||
CUSTOM_FONT_URLS = [
|
||||
"https://fonts.example.com/myfont.css",
|
||||
]
|
||||
```
|
||||
|
||||
- **After:**
|
||||
|
||||
```python
|
||||
THEME_DEFAULT = {
|
||||
"token": {
|
||||
@@ -281,7 +280,13 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
}
|
||||
```
|
||||
|
||||
### Other
|
||||
|
||||
- [35621](https://github.com/apache/superset/pull/35621): The default hash algorithm has changed from MD5 to SHA-256 for improved security and FedRAMP compliance. This affects cache keys for thumbnails, dashboard digests, chart digests, and filter option names. Existing cached data will be invalidated upon upgrade. To opt out of this change and maintain backward compatibility, set `HASH_ALGORITHM = "md5"` in your `superset_config.py`.
|
||||
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
|
||||
|
||||
## 6.0.0
|
||||
|
||||
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
|
||||
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
|
||||
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
|
||||
@@ -290,8 +295,8 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
|
||||
- Custom colors are no longer supported to maintain consistency with Ant Design components
|
||||
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
|
||||
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
|
||||
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
|
||||
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
|
||||
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
|
||||
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
|
||||
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
|
||||
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
|
||||
@@ -681,7 +686,6 @@ Note: Pillow is now a required dependency (previously optional) to support image
|
||||
|
||||
- [11509](https://github.com/apache/superset/pull/12491): Dataset metadata updates check user ownership, only owners or an Admin are allowed.
|
||||
- Security simplification (SIP-19), the following permission domains were simplified:
|
||||
|
||||
- [12072](https://github.com/apache/superset/pull/12072): `Query` with `can_read`, `can_write`
|
||||
- [12036](https://github.com/apache/superset/pull/12036): `Database` with `can_read`, `can_write`.
|
||||
- [12012](https://github.com/apache/superset/pull/12036): `Dashboard` with `can_read`, `can_write`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: MCP Server Deployment & Authentication
|
||||
hide_title: true
|
||||
sidebar_position: 9
|
||||
sidebar_position: 14
|
||||
version: 1
|
||||
---
|
||||
|
||||
@@ -30,6 +30,10 @@ Superset includes a built-in [Model Context Protocol (MCP)](https://modelcontext
|
||||
|
||||
This guide covers how to run, secure, and deploy the MCP server.
|
||||
|
||||
:::tip Looking for user docs?
|
||||
See **[Using AI with Superset](/user-docs/using-superset/using-ai-with-superset)** for a guide on what AI can do with Superset and how to connect your AI client.
|
||||
:::
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["AI Client<br/>(Claude, ChatGPT, etc.)"] -- "MCP protocol<br/>(HTTP + JSON-RPC)" --> B["MCP Server<br/>(:5008/mcp)"]
|
||||
@@ -668,12 +672,13 @@ MCP_CSRF_CONFIG = {
|
||||
- **Secrets management** -- Store `MCP_JWT_SECRET`, database credentials, and API keys in environment variables or a secrets manager, never in config files committed to version control
|
||||
- **Scoped tokens** -- Use `MCP_REQUIRED_SCOPES` to limit what operations a token can perform
|
||||
- **Network isolation** -- In Kubernetes, restrict MCP pod network policies to only allow traffic from your AI client endpoints
|
||||
- Review the **[Security documentation](./security)** for additional extension security guidance
|
||||
- Review the **[Security documentation](/developer-docs/extensions/security)** for additional extension security guidance
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **[MCP Integration](./mcp)** -- Build custom MCP tools and prompts via Superset extensions
|
||||
- **[Security](./security)** -- Security best practices for extensions
|
||||
- **[Deployment](./deployment)** -- Package and deploy Superset extensions
|
||||
- **[Using AI with Superset](/user-docs/using-superset/using-ai-with-superset)** -- What AI can do with Superset and how to get started
|
||||
- **[MCP Integration](/developer-docs/extensions/mcp)** -- Build custom MCP tools and prompts via Superset extensions
|
||||
- **[Security](/developer-docs/extensions/security)** -- Security best practices for extensions
|
||||
- **[Deployment](/developer-docs/extensions/deployment)** -- Package and deploy Superset extensions
|
||||
@@ -47,10 +47,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the CSRF token](./api/get-the-csrf-token) | `/api/v1/security/csrf_token/` |
|
||||
| `POST` | [Get a guest token](./api/get-a-guest-token) | `/api/v1/security/guest_token/` |
|
||||
| `POST` | [Create security login](./api/create-security-login) | `/api/v1/security/login` |
|
||||
| `POST` | [Create security refresh](./api/create-security-refresh) | `/api/v1/security/refresh` |
|
||||
| `GET` | [Get the CSRF token](/developer-docs/api/get-the-csrf-token) | `/api/v1/security/csrf_token/` |
|
||||
| `POST` | [Get a guest token](/developer-docs/api/get-a-guest-token) | `/api/v1/security/guest_token/` |
|
||||
| `POST` | [Create security login](/developer-docs/api/create-security-login) | `/api/v1/security/login` |
|
||||
| `POST` | [Create security refresh](/developer-docs/api/create-security-refresh) | `/api/v1/security/refresh` |
|
||||
|
||||
---
|
||||
|
||||
@@ -63,32 +63,32 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete dashboards](./api/bulk-delete-dashboards) | `/api/v1/dashboard/` |
|
||||
| `GET` | [Get a list of dashboards](./api/get-a-list-of-dashboards) | `/api/v1/dashboard/` |
|
||||
| `POST` | [Create a new dashboard](./api/create-a-new-dashboard) | `/api/v1/dashboard/` |
|
||||
| `GET` | [Get metadata information about this API resource (dashboard--info)](./api/get-metadata-information-about-this-api-resource-dashboard-info) | `/api/v1/dashboard/_info` |
|
||||
| `GET` | [Get a dashboard detail information](./api/get-a-dashboard-detail-information) | `/api/v1/dashboard/{id_or_slug}` |
|
||||
| `GET` | [Get a dashboard's chart definitions.](./api/get-a-dashboard-s-chart-definitions) | `/api/v1/dashboard/{id_or_slug}/charts` |
|
||||
| `POST` | [Create a copy of an existing dashboard](./api/create-a-copy-of-an-existing-dashboard) | `/api/v1/dashboard/{id_or_slug}/copy/` |
|
||||
| `GET` | [Get dashboard's datasets](./api/get-dashboard-s-datasets) | `/api/v1/dashboard/{id_or_slug}/datasets` |
|
||||
| `DELETE` | [Delete a dashboard's embedded configuration](./api/delete-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `GET` | [Get the dashboard's embedded configuration](./api/get-the-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `POST` | [Set a dashboard's embedded configuration](./api/set-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `PUT` | [Update dashboard by id_or_slug embedded](./api/update-dashboard-by-id-or-slug-embedded) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `GET` | [Get dashboard's tabs](./api/get-dashboard-s-tabs) | `/api/v1/dashboard/{id_or_slug}/tabs` |
|
||||
| `DELETE` | [Delete a dashboard](./api/delete-a-dashboard) | `/api/v1/dashboard/{pk}` |
|
||||
| `PUT` | [Update a dashboard](./api/update-a-dashboard) | `/api/v1/dashboard/{pk}` |
|
||||
| `POST` | [Compute and cache a screenshot (dashboard-pk-cache-dashboard-screenshot)](./api/compute-and-cache-a-screenshot-dashboard-pk-cache-dashboard-screenshot) | `/api/v1/dashboard/{pk}/cache_dashboard_screenshot/` |
|
||||
| `PUT` | [Update colors configuration for a dashboard.](./api/update-colors-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/colors` |
|
||||
| `DELETE` | [Remove the dashboard from the user favorite list](./api/remove-the-dashboard-from-the-user-favorite-list) | `/api/v1/dashboard/{pk}/favorites/` |
|
||||
| `POST` | [Mark the dashboard as favorite for the current user](./api/mark-the-dashboard-as-favorite-for-the-current-user) | `/api/v1/dashboard/{pk}/favorites/` |
|
||||
| `PUT` | [Update native filters configuration for a dashboard.](./api/update-native-filters-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/filters` |
|
||||
| `GET` | [Get a computed screenshot from cache (dashboard-pk-screenshot-digest)](./api/get-a-computed-screenshot-from-cache-dashboard-pk-screenshot-digest) | `/api/v1/dashboard/{pk}/screenshot/{digest}/` |
|
||||
| `GET` | [Get dashboard's thumbnail](./api/get-dashboard-s-thumbnail) | `/api/v1/dashboard/{pk}/thumbnail/{digest}/` |
|
||||
| `GET` | [Download multiple dashboards as YAML files](./api/download-multiple-dashboards-as-yaml-files) | `/api/v1/dashboard/export/` |
|
||||
| `GET` | [Check favorited dashboards for current user](./api/check-favorited-dashboards-for-current-user) | `/api/v1/dashboard/favorite_status/` |
|
||||
| `POST` | [Import dashboard(s) with associated charts/datasets/databases](./api/import-dashboard-s-with-associated-charts-datasets-databases) | `/api/v1/dashboard/import/` |
|
||||
| `GET` | [Get related fields data (dashboard-related-column-name)](./api/get-related-fields-data-dashboard-related-column-name) | `/api/v1/dashboard/related/{column_name}` |
|
||||
| `DELETE` | [Bulk delete dashboards](/developer-docs/api/bulk-delete-dashboards) | `/api/v1/dashboard/` |
|
||||
| `GET` | [Get a list of dashboards](/developer-docs/api/get-a-list-of-dashboards) | `/api/v1/dashboard/` |
|
||||
| `POST` | [Create a new dashboard](/developer-docs/api/create-a-new-dashboard) | `/api/v1/dashboard/` |
|
||||
| `GET` | [Get metadata information about this API resource (dashboard--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-dashboard-info) | `/api/v1/dashboard/_info` |
|
||||
| `GET` | [Get a dashboard detail information](/developer-docs/api/get-a-dashboard-detail-information) | `/api/v1/dashboard/{id_or_slug}` |
|
||||
| `GET` | [Get a dashboard's chart definitions.](/developer-docs/api/get-a-dashboard-s-chart-definitions) | `/api/v1/dashboard/{id_or_slug}/charts` |
|
||||
| `POST` | [Create a copy of an existing dashboard](/developer-docs/api/create-a-copy-of-an-existing-dashboard) | `/api/v1/dashboard/{id_or_slug}/copy/` |
|
||||
| `GET` | [Get dashboard's datasets](/developer-docs/api/get-dashboard-s-datasets) | `/api/v1/dashboard/{id_or_slug}/datasets` |
|
||||
| `DELETE` | [Delete a dashboard's embedded configuration](/developer-docs/api/delete-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `GET` | [Get the dashboard's embedded configuration](/developer-docs/api/get-the-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `POST` | [Set a dashboard's embedded configuration](/developer-docs/api/set-a-dashboard-s-embedded-configuration) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `PUT` | [Update dashboard by id_or_slug embedded](/developer-docs/api/update-dashboard-by-id-or-slug-embedded) | `/api/v1/dashboard/{id_or_slug}/embedded` |
|
||||
| `GET` | [Get dashboard's tabs](/developer-docs/api/get-dashboard-s-tabs) | `/api/v1/dashboard/{id_or_slug}/tabs` |
|
||||
| `DELETE` | [Delete a dashboard](/developer-docs/api/delete-a-dashboard) | `/api/v1/dashboard/{pk}` |
|
||||
| `PUT` | [Update a dashboard](/developer-docs/api/update-a-dashboard) | `/api/v1/dashboard/{pk}` |
|
||||
| `POST` | [Compute and cache a screenshot (dashboard-pk-cache-dashboard-screenshot)](/developer-docs/api/compute-and-cache-a-screenshot-dashboard-pk-cache-dashboard-screenshot) | `/api/v1/dashboard/{pk}/cache_dashboard_screenshot/` |
|
||||
| `PUT` | [Update colors configuration for a dashboard.](/developer-docs/api/update-colors-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/colors` |
|
||||
| `DELETE` | [Remove the dashboard from the user favorite list](/developer-docs/api/remove-the-dashboard-from-the-user-favorite-list) | `/api/v1/dashboard/{pk}/favorites/` |
|
||||
| `POST` | [Mark the dashboard as favorite for the current user](/developer-docs/api/mark-the-dashboard-as-favorite-for-the-current-user) | `/api/v1/dashboard/{pk}/favorites/` |
|
||||
| `PUT` | [Update native filters configuration for a dashboard.](/developer-docs/api/update-native-filters-configuration-for-a-dashboard) | `/api/v1/dashboard/{pk}/filters` |
|
||||
| `GET` | [Get a computed screenshot from cache (dashboard-pk-screenshot-digest)](/developer-docs/api/get-a-computed-screenshot-from-cache-dashboard-pk-screenshot-digest) | `/api/v1/dashboard/{pk}/screenshot/{digest}/` |
|
||||
| `GET` | [Get dashboard's thumbnail](/developer-docs/api/get-dashboard-s-thumbnail) | `/api/v1/dashboard/{pk}/thumbnail/{digest}/` |
|
||||
| `GET` | [Download multiple dashboards as YAML files](/developer-docs/api/download-multiple-dashboards-as-yaml-files) | `/api/v1/dashboard/export/` |
|
||||
| `GET` | [Check favorited dashboards for current user](/developer-docs/api/check-favorited-dashboards-for-current-user) | `/api/v1/dashboard/favorite_status/` |
|
||||
| `POST` | [Import dashboard(s) with associated charts/datasets/databases](/developer-docs/api/import-dashboard-s-with-associated-charts-datasets-databases) | `/api/v1/dashboard/import/` |
|
||||
| `GET` | [Get related fields data (dashboard-related-column-name)](/developer-docs/api/get-related-fields-data-dashboard-related-column-name) | `/api/v1/dashboard/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -97,26 +97,26 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete charts](./api/bulk-delete-charts) | `/api/v1/chart/` |
|
||||
| `GET` | [Get a list of charts](./api/get-a-list-of-charts) | `/api/v1/chart/` |
|
||||
| `POST` | [Create a new chart](./api/create-a-new-chart) | `/api/v1/chart/` |
|
||||
| `GET` | [Get metadata information about this API resource (chart--info)](./api/get-metadata-information-about-this-api-resource-chart-info) | `/api/v1/chart/_info` |
|
||||
| `DELETE` | [Delete a chart](./api/delete-a-chart) | `/api/v1/chart/{pk}` |
|
||||
| `GET` | [Get a chart detail information](./api/get-a-chart-detail-information) | `/api/v1/chart/{pk}` |
|
||||
| `PUT` | [Update a chart](./api/update-a-chart) | `/api/v1/chart/{pk}` |
|
||||
| `GET` | [Compute and cache a screenshot (chart-pk-cache-screenshot)](./api/compute-and-cache-a-screenshot-chart-pk-cache-screenshot) | `/api/v1/chart/{pk}/cache_screenshot/` |
|
||||
| `GET` | [Return payload data response for a chart](./api/return-payload-data-response-for-a-chart) | `/api/v1/chart/{pk}/data/` |
|
||||
| `DELETE` | [Remove the chart from the user favorite list](./api/remove-the-chart-from-the-user-favorite-list) | `/api/v1/chart/{pk}/favorites/` |
|
||||
| `POST` | [Mark the chart as favorite for the current user](./api/mark-the-chart-as-favorite-for-the-current-user) | `/api/v1/chart/{pk}/favorites/` |
|
||||
| `GET` | [Get a computed screenshot from cache (chart-pk-screenshot-digest)](./api/get-a-computed-screenshot-from-cache-chart-pk-screenshot-digest) | `/api/v1/chart/{pk}/screenshot/{digest}/` |
|
||||
| `GET` | [Get chart thumbnail](./api/get-chart-thumbnail) | `/api/v1/chart/{pk}/thumbnail/{digest}/` |
|
||||
| `POST` | [Return payload data response for the given query (chart-data)](./api/return-payload-data-response-for-the-given-query-chart-data) | `/api/v1/chart/data` |
|
||||
| `GET` | [Return payload data response for the given query (chart-data-cache-key)](./api/return-payload-data-response-for-the-given-query-chart-data-cache-key) | `/api/v1/chart/data/{cache_key}` |
|
||||
| `GET` | [Download multiple charts as YAML files](./api/download-multiple-charts-as-yaml-files) | `/api/v1/chart/export/` |
|
||||
| `GET` | [Check favorited charts for current user](./api/check-favorited-charts-for-current-user) | `/api/v1/chart/favorite_status/` |
|
||||
| `POST` | [Import chart(s) with associated datasets and databases](./api/import-chart-s-with-associated-datasets-and-databases) | `/api/v1/chart/import/` |
|
||||
| `GET` | [Get related fields data (chart-related-column-name)](./api/get-related-fields-data-chart-related-column-name) | `/api/v1/chart/related/{column_name}` |
|
||||
| `PUT` | [Warm up the cache for the chart](./api/warm-up-the-cache-for-the-chart) | `/api/v1/chart/warm_up_cache` |
|
||||
| `DELETE` | [Bulk delete charts](/developer-docs/api/bulk-delete-charts) | `/api/v1/chart/` |
|
||||
| `GET` | [Get a list of charts](/developer-docs/api/get-a-list-of-charts) | `/api/v1/chart/` |
|
||||
| `POST` | [Create a new chart](/developer-docs/api/create-a-new-chart) | `/api/v1/chart/` |
|
||||
| `GET` | [Get metadata information about this API resource (chart--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-chart-info) | `/api/v1/chart/_info` |
|
||||
| `DELETE` | [Delete a chart](/developer-docs/api/delete-a-chart) | `/api/v1/chart/{pk}` |
|
||||
| `GET` | [Get a chart detail information](/developer-docs/api/get-a-chart-detail-information) | `/api/v1/chart/{pk}` |
|
||||
| `PUT` | [Update a chart](/developer-docs/api/update-a-chart) | `/api/v1/chart/{pk}` |
|
||||
| `GET` | [Compute and cache a screenshot (chart-pk-cache-screenshot)](/developer-docs/api/compute-and-cache-a-screenshot-chart-pk-cache-screenshot) | `/api/v1/chart/{pk}/cache_screenshot/` |
|
||||
| `GET` | [Return payload data response for a chart](/developer-docs/api/return-payload-data-response-for-a-chart) | `/api/v1/chart/{pk}/data/` |
|
||||
| `DELETE` | [Remove the chart from the user favorite list](/developer-docs/api/remove-the-chart-from-the-user-favorite-list) | `/api/v1/chart/{pk}/favorites/` |
|
||||
| `POST` | [Mark the chart as favorite for the current user](/developer-docs/api/mark-the-chart-as-favorite-for-the-current-user) | `/api/v1/chart/{pk}/favorites/` |
|
||||
| `GET` | [Get a computed screenshot from cache (chart-pk-screenshot-digest)](/developer-docs/api/get-a-computed-screenshot-from-cache-chart-pk-screenshot-digest) | `/api/v1/chart/{pk}/screenshot/{digest}/` |
|
||||
| `GET` | [Get chart thumbnail](/developer-docs/api/get-chart-thumbnail) | `/api/v1/chart/{pk}/thumbnail/{digest}/` |
|
||||
| `POST` | [Return payload data response for the given query (chart-data)](/developer-docs/api/return-payload-data-response-for-the-given-query-chart-data) | `/api/v1/chart/data` |
|
||||
| `GET` | [Return payload data response for the given query (chart-data-cache-key)](/developer-docs/api/return-payload-data-response-for-the-given-query-chart-data-cache-key) | `/api/v1/chart/data/{cache_key}` |
|
||||
| `GET` | [Download multiple charts as YAML files](/developer-docs/api/download-multiple-charts-as-yaml-files) | `/api/v1/chart/export/` |
|
||||
| `GET` | [Check favorited charts for current user](/developer-docs/api/check-favorited-charts-for-current-user) | `/api/v1/chart/favorite_status/` |
|
||||
| `POST` | [Import chart(s) with associated datasets and databases](/developer-docs/api/import-chart-s-with-associated-datasets-and-databases) | `/api/v1/chart/import/` |
|
||||
| `GET` | [Get related fields data (chart-related-column-name)](/developer-docs/api/get-related-fields-data-chart-related-column-name) | `/api/v1/chart/related/{column_name}` |
|
||||
| `PUT` | [Warm up the cache for the chart](/developer-docs/api/warm-up-the-cache-for-the-chart) | `/api/v1/chart/warm_up_cache` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -125,24 +125,24 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete datasets](./api/bulk-delete-datasets) | `/api/v1/dataset/` |
|
||||
| `GET` | [Get a list of datasets](./api/get-a-list-of-datasets) | `/api/v1/dataset/` |
|
||||
| `POST` | [Create a new dataset](./api/create-a-new-dataset) | `/api/v1/dataset/` |
|
||||
| `GET` | [Get metadata information about this API resource (dataset--info)](./api/get-metadata-information-about-this-api-resource-dataset-info) | `/api/v1/dataset/_info` |
|
||||
| `DELETE` | [Delete a dataset](./api/delete-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `GET` | [Get a dataset](./api/get-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `PUT` | [Update a dataset](./api/update-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `DELETE` | [Delete a dataset column](./api/delete-a-dataset-column) | `/api/v1/dataset/{pk}/column/{column_id}` |
|
||||
| `DELETE` | [Delete a dataset metric](./api/delete-a-dataset-metric) | `/api/v1/dataset/{pk}/metric/{metric_id}` |
|
||||
| `PUT` | [Refresh and update columns of a dataset](./api/refresh-and-update-columns-of-a-dataset) | `/api/v1/dataset/{pk}/refresh` |
|
||||
| `GET` | [Get charts and dashboards count associated to a dataset](./api/get-charts-and-dashboards-count-associated-to-a-dataset) | `/api/v1/dataset/{pk}/related_objects` |
|
||||
| `GET` | [Get distinct values from field data (dataset-distinct-column-name)](./api/get-distinct-values-from-field-data-dataset-distinct-column-name) | `/api/v1/dataset/distinct/{column_name}` |
|
||||
| `POST` | [Duplicate a dataset](./api/duplicate-a-dataset) | `/api/v1/dataset/duplicate` |
|
||||
| `GET` | [Download multiple datasets as YAML files](./api/download-multiple-datasets-as-yaml-files) | `/api/v1/dataset/export/` |
|
||||
| `POST` | [Retrieve a table by name, or create it if it does not exist](./api/retrieve-a-table-by-name-or-create-it-if-it-does-not-exist) | `/api/v1/dataset/get_or_create/` |
|
||||
| `POST` | [Import dataset(s) with associated databases](./api/import-dataset-s-with-associated-databases) | `/api/v1/dataset/import/` |
|
||||
| `GET` | [Get related fields data (dataset-related-column-name)](./api/get-related-fields-data-dataset-related-column-name) | `/api/v1/dataset/related/{column_name}` |
|
||||
| `PUT` | [Warm up the cache for each chart powered by the given table](./api/warm-up-the-cache-for-each-chart-powered-by-the-given-table) | `/api/v1/dataset/warm_up_cache` |
|
||||
| `DELETE` | [Bulk delete datasets](/developer-docs/api/bulk-delete-datasets) | `/api/v1/dataset/` |
|
||||
| `GET` | [Get a list of datasets](/developer-docs/api/get-a-list-of-datasets) | `/api/v1/dataset/` |
|
||||
| `POST` | [Create a new dataset](/developer-docs/api/create-a-new-dataset) | `/api/v1/dataset/` |
|
||||
| `GET` | [Get metadata information about this API resource (dataset--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-dataset-info) | `/api/v1/dataset/_info` |
|
||||
| `DELETE` | [Delete a dataset](/developer-docs/api/delete-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `GET` | [Get a dataset](/developer-docs/api/get-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `PUT` | [Update a dataset](/developer-docs/api/update-a-dataset) | `/api/v1/dataset/{pk}` |
|
||||
| `DELETE` | [Delete a dataset column](/developer-docs/api/delete-a-dataset-column) | `/api/v1/dataset/{pk}/column/{column_id}` |
|
||||
| `DELETE` | [Delete a dataset metric](/developer-docs/api/delete-a-dataset-metric) | `/api/v1/dataset/{pk}/metric/{metric_id}` |
|
||||
| `PUT` | [Refresh and update columns of a dataset](/developer-docs/api/refresh-and-update-columns-of-a-dataset) | `/api/v1/dataset/{pk}/refresh` |
|
||||
| `GET` | [Get charts and dashboards count associated to a dataset](/developer-docs/api/get-charts-and-dashboards-count-associated-to-a-dataset) | `/api/v1/dataset/{pk}/related_objects` |
|
||||
| `GET` | [Get distinct values from field data (dataset-distinct-column-name)](/developer-docs/api/get-distinct-values-from-field-data-dataset-distinct-column-name) | `/api/v1/dataset/distinct/{column_name}` |
|
||||
| `POST` | [Duplicate a dataset](/developer-docs/api/duplicate-a-dataset) | `/api/v1/dataset/duplicate` |
|
||||
| `GET` | [Download multiple datasets as YAML files](/developer-docs/api/download-multiple-datasets-as-yaml-files) | `/api/v1/dataset/export/` |
|
||||
| `POST` | [Retrieve a table by name, or create it if it does not exist](/developer-docs/api/retrieve-a-table-by-name-or-create-it-if-it-does-not-exist) | `/api/v1/dataset/get_or_create/` |
|
||||
| `POST` | [Import dataset(s) with associated databases](/developer-docs/api/import-dataset-s-with-associated-databases) | `/api/v1/dataset/import/` |
|
||||
| `GET` | [Get related fields data (dataset-related-column-name)](/developer-docs/api/get-related-fields-data-dataset-related-column-name) | `/api/v1/dataset/related/{column_name}` |
|
||||
| `PUT` | [Warm up the cache for each chart powered by the given table](/developer-docs/api/warm-up-the-cache-for-each-chart-powered-by-the-given-table) | `/api/v1/dataset/warm_up_cache` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -151,37 +151,37 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a list of databases](./api/get-a-list-of-databases) | `/api/v1/database/` |
|
||||
| `POST` | [Create a new database](./api/create-a-new-database) | `/api/v1/database/` |
|
||||
| `GET` | [Get metadata information about this API resource (database--info)](./api/get-metadata-information-about-this-api-resource-database-info) | `/api/v1/database/_info` |
|
||||
| `DELETE` | [Delete a database](./api/delete-a-database) | `/api/v1/database/{pk}` |
|
||||
| `GET` | [Get a database](./api/get-a-database) | `/api/v1/database/{pk}` |
|
||||
| `PUT` | [Change a database](./api/change-a-database) | `/api/v1/database/{pk}` |
|
||||
| `GET` | [Get all catalogs from a database](./api/get-all-catalogs-from-a-database) | `/api/v1/database/{pk}/catalogs/` |
|
||||
| `GET` | [Get a database connection info](./api/get-a-database-connection-info) | `/api/v1/database/{pk}/connection` |
|
||||
| `GET` | [Get function names supported by a database](./api/get-function-names-supported-by-a-database) | `/api/v1/database/{pk}/function_names/` |
|
||||
| `GET` | [Get charts and dashboards count associated to a database](./api/get-charts-and-dashboards-count-associated-to-a-database) | `/api/v1/database/{pk}/related_objects/` |
|
||||
| `GET` | [The list of the database schemas where to upload information](./api/the-list-of-the-database-schemas-where-to-upload-information) | `/api/v1/database/{pk}/schemas_access_for_file_upload/` |
|
||||
| `GET` | [Get all schemas from a database](./api/get-all-schemas-from-a-database) | `/api/v1/database/{pk}/schemas/` |
|
||||
| `GET` | [Get database select star for table (database-pk-select-star-table-name)](./api/get-database-select-star-for-table-database-pk-select-star-table-name) | `/api/v1/database/{pk}/select_star/{table_name}/` |
|
||||
| `GET` | [Get database select star for table (database-pk-select-star-table-name-schema-name)](./api/get-database-select-star-for-table-database-pk-select-star-table-name-schema-name) | `/api/v1/database/{pk}/select_star/{table_name}/{schema_name}/` |
|
||||
| `DELETE` | [Delete a SSH tunnel](./api/delete-a-ssh-tunnel) | `/api/v1/database/{pk}/ssh_tunnel/` |
|
||||
| `POST` | [Re-sync all permissions for a database connection](./api/re-sync-all-permissions-for-a-database-connection) | `/api/v1/database/{pk}/sync_permissions/` |
|
||||
| `GET` | [Get table extra metadata (database-pk-table-extra-table-name-schema-name)](./api/get-table-extra-metadata-database-pk-table-extra-table-name-schema-name) | `/api/v1/database/{pk}/table_extra/{table_name}/{schema_name}/` |
|
||||
| `GET` | [Get table metadata](./api/get-table-metadata) | `/api/v1/database/{pk}/table_metadata/` |
|
||||
| `GET` | [Get table extra metadata (database-pk-table-metadata-extra)](./api/get-table-extra-metadata-database-pk-table-metadata-extra) | `/api/v1/database/{pk}/table_metadata/extra/` |
|
||||
| `GET` | [Get database table metadata](./api/get-database-table-metadata) | `/api/v1/database/{pk}/table/{table_name}/{schema_name}/` |
|
||||
| `GET` | [Get a list of tables for given database](./api/get-a-list-of-tables-for-given-database) | `/api/v1/database/{pk}/tables/` |
|
||||
| `POST` | [Upload a file to a database table](./api/upload-a-file-to-a-database-table) | `/api/v1/database/{pk}/upload/` |
|
||||
| `POST` | [Validate arbitrary SQL](./api/validate-arbitrary-sql) | `/api/v1/database/{pk}/validate_sql/` |
|
||||
| `GET` | [Get names of databases currently available](./api/get-names-of-databases-currently-available) | `/api/v1/database/available/` |
|
||||
| `GET` | [Download database(s) and associated dataset(s) as a zip file](./api/download-database-s-and-associated-dataset-s-as-a-zip-file) | `/api/v1/database/export/` |
|
||||
| `POST` | [Import database(s) with associated datasets](./api/import-database-s-with-associated-datasets) | `/api/v1/database/import/` |
|
||||
| `GET` | [Receive personal access tokens from OAuth2](./api/receive-personal-access-tokens-from-oauth2) | `/api/v1/database/oauth2/` |
|
||||
| `GET` | [Get related fields data (database-related-column-name)](./api/get-related-fields-data-database-related-column-name) | `/api/v1/database/related/{column_name}` |
|
||||
| `POST` | [Test a database connection](./api/test-a-database-connection) | `/api/v1/database/test_connection/` |
|
||||
| `POST` | [Upload a file and returns file metadata](./api/upload-a-file-and-returns-file-metadata) | `/api/v1/database/upload_metadata/` |
|
||||
| `POST` | [Validate database connection parameters](./api/validate-database-connection-parameters) | `/api/v1/database/validate_parameters/` |
|
||||
| `GET` | [Get a list of databases](/developer-docs/api/get-a-list-of-databases) | `/api/v1/database/` |
|
||||
| `POST` | [Create a new database](/developer-docs/api/create-a-new-database) | `/api/v1/database/` |
|
||||
| `GET` | [Get metadata information about this API resource (database--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-database-info) | `/api/v1/database/_info` |
|
||||
| `DELETE` | [Delete a database](/developer-docs/api/delete-a-database) | `/api/v1/database/{pk}` |
|
||||
| `GET` | [Get a database](/developer-docs/api/get-a-database) | `/api/v1/database/{pk}` |
|
||||
| `PUT` | [Change a database](/developer-docs/api/change-a-database) | `/api/v1/database/{pk}` |
|
||||
| `GET` | [Get all catalogs from a database](/developer-docs/api/get-all-catalogs-from-a-database) | `/api/v1/database/{pk}/catalogs/` |
|
||||
| `GET` | [Get a database connection info](/developer-docs/api/get-a-database-connection-info) | `/api/v1/database/{pk}/connection` |
|
||||
| `GET` | [Get function names supported by a database](/developer-docs/api/get-function-names-supported-by-a-database) | `/api/v1/database/{pk}/function_names/` |
|
||||
| `GET` | [Get charts and dashboards count associated to a database](/developer-docs/api/get-charts-and-dashboards-count-associated-to-a-database) | `/api/v1/database/{pk}/related_objects/` |
|
||||
| `GET` | [The list of the database schemas where to upload information](/developer-docs/api/the-list-of-the-database-schemas-where-to-upload-information) | `/api/v1/database/{pk}/schemas_access_for_file_upload/` |
|
||||
| `GET` | [Get all schemas from a database](/developer-docs/api/get-all-schemas-from-a-database) | `/api/v1/database/{pk}/schemas/` |
|
||||
| `GET` | [Get database select star for table (database-pk-select-star-table-name)](/developer-docs/api/get-database-select-star-for-table-database-pk-select-star-table-name) | `/api/v1/database/{pk}/select_star/{table_name}/` |
|
||||
| `GET` | [Get database select star for table (database-pk-select-star-table-name-schema-name)](/developer-docs/api/get-database-select-star-for-table-database-pk-select-star-table-name-schema-name) | `/api/v1/database/{pk}/select_star/{table_name}/{schema_name}/` |
|
||||
| `DELETE` | [Delete a SSH tunnel](/developer-docs/api/delete-a-ssh-tunnel) | `/api/v1/database/{pk}/ssh_tunnel/` |
|
||||
| `POST` | [Re-sync all permissions for a database connection](/developer-docs/api/re-sync-all-permissions-for-a-database-connection) | `/api/v1/database/{pk}/sync_permissions/` |
|
||||
| `GET` | [Get table extra metadata (database-pk-table-extra-table-name-schema-name)](/developer-docs/api/get-table-extra-metadata-database-pk-table-extra-table-name-schema-name) | `/api/v1/database/{pk}/table_extra/{table_name}/{schema_name}/` |
|
||||
| `GET` | [Get table metadata](/developer-docs/api/get-table-metadata) | `/api/v1/database/{pk}/table_metadata/` |
|
||||
| `GET` | [Get table extra metadata (database-pk-table-metadata-extra)](/developer-docs/api/get-table-extra-metadata-database-pk-table-metadata-extra) | `/api/v1/database/{pk}/table_metadata/extra/` |
|
||||
| `GET` | [Get database table metadata](/developer-docs/api/get-database-table-metadata) | `/api/v1/database/{pk}/table/{table_name}/{schema_name}/` |
|
||||
| `GET` | [Get a list of tables for given database](/developer-docs/api/get-a-list-of-tables-for-given-database) | `/api/v1/database/{pk}/tables/` |
|
||||
| `POST` | [Upload a file to a database table](/developer-docs/api/upload-a-file-to-a-database-table) | `/api/v1/database/{pk}/upload/` |
|
||||
| `POST` | [Validate arbitrary SQL](/developer-docs/api/validate-arbitrary-sql) | `/api/v1/database/{pk}/validate_sql/` |
|
||||
| `GET` | [Get names of databases currently available](/developer-docs/api/get-names-of-databases-currently-available) | `/api/v1/database/available/` |
|
||||
| `GET` | [Download database(s) and associated dataset(s) as a zip file](/developer-docs/api/download-database-s-and-associated-dataset-s-as-a-zip-file) | `/api/v1/database/export/` |
|
||||
| `POST` | [Import database(s) with associated datasets](/developer-docs/api/import-database-s-with-associated-datasets) | `/api/v1/database/import/` |
|
||||
| `GET` | [Receive personal access tokens from OAuth2](/developer-docs/api/receive-personal-access-tokens-from-oauth2) | `/api/v1/database/oauth2/` |
|
||||
| `GET` | [Get related fields data (database-related-column-name)](/developer-docs/api/get-related-fields-data-database-related-column-name) | `/api/v1/database/related/{column_name}` |
|
||||
| `POST` | [Test a database connection](/developer-docs/api/test-a-database-connection) | `/api/v1/database/test_connection/` |
|
||||
| `POST` | [Upload a file and returns file metadata](/developer-docs/api/upload-a-file-and-returns-file-metadata) | `/api/v1/database/upload_metadata/` |
|
||||
| `POST` | [Validate database connection parameters](/developer-docs/api/validate-database-connection-parameters) | `/api/v1/database/validate_parameters/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -192,7 +192,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Assemble Explore related information in a single endpoint](./api/assemble-explore-related-information-in-a-single-endpoint) | `/api/v1/explore/` |
|
||||
| `GET` | [Assemble Explore related information in a single endpoint](/developer-docs/api/assemble-explore-related-information-in-a-single-endpoint) | `/api/v1/explore/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -201,12 +201,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the bootstrap data for SqlLab page](./api/get-the-bootstrap-data-for-sqllab-page) | `/api/v1/sqllab/` |
|
||||
| `POST` | [Estimate the SQL query execution cost](./api/estimate-the-sql-query-execution-cost) | `/api/v1/sqllab/estimate/` |
|
||||
| `POST` | [Execute a SQL query](./api/execute-a-sql-query) | `/api/v1/sqllab/execute/` |
|
||||
| `GET` | [Export the SQL query results to a CSV](./api/export-the-sql-query-results-to-a-csv) | `/api/v1/sqllab/export/{client_id}/` |
|
||||
| `POST` | [Format SQL code](./api/format-sql-code) | `/api/v1/sqllab/format_sql/` |
|
||||
| `GET` | [Get the result of a SQL query execution](./api/get-the-result-of-a-sql-query-execution) | `/api/v1/sqllab/results/` |
|
||||
| `GET` | [Get the bootstrap data for SqlLab page](/developer-docs/api/get-the-bootstrap-data-for-sqllab-page) | `/api/v1/sqllab/` |
|
||||
| `POST` | [Estimate the SQL query execution cost](/developer-docs/api/estimate-the-sql-query-execution-cost) | `/api/v1/sqllab/estimate/` |
|
||||
| `POST` | [Execute a SQL query](/developer-docs/api/execute-a-sql-query) | `/api/v1/sqllab/execute/` |
|
||||
| `GET` | [Export the SQL query results to a CSV](/developer-docs/api/export-the-sql-query-results-to-a-csv) | `/api/v1/sqllab/export/{client_id}/` |
|
||||
| `POST` | [Format SQL code](/developer-docs/api/format-sql-code) | `/api/v1/sqllab/format_sql/` |
|
||||
| `GET` | [Get the result of a SQL query execution](/developer-docs/api/get-the-result-of-a-sql-query-execution) | `/api/v1/sqllab/results/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -215,23 +215,23 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a list of queries](./api/get-a-list-of-queries) | `/api/v1/query/` |
|
||||
| `GET` | [Get query detail information](./api/get-query-detail-information) | `/api/v1/query/{pk}` |
|
||||
| `GET` | [Get distinct values from field data (query-distinct-column-name)](./api/get-distinct-values-from-field-data-query-distinct-column-name) | `/api/v1/query/distinct/{column_name}` |
|
||||
| `GET` | [Get related fields data (query-related-column-name)](./api/get-related-fields-data-query-related-column-name) | `/api/v1/query/related/{column_name}` |
|
||||
| `POST` | [Manually stop a query with client_id](./api/manually-stop-a-query-with-client-id) | `/api/v1/query/stop` |
|
||||
| `GET` | [Get a list of queries that changed after last_updated_ms](./api/get-a-list-of-queries-that-changed-after-last-updated-ms) | `/api/v1/query/updated_since` |
|
||||
| `DELETE` | [Bulk delete saved queries](./api/bulk-delete-saved-queries) | `/api/v1/saved_query/` |
|
||||
| `GET` | [Get a list of saved queries](./api/get-a-list-of-saved-queries) | `/api/v1/saved_query/` |
|
||||
| `POST` | [Create a saved query](./api/create-a-saved-query) | `/api/v1/saved_query/` |
|
||||
| `GET` | [Get metadata information about this API resource (saved-query--info)](./api/get-metadata-information-about-this-api-resource-saved-query-info) | `/api/v1/saved_query/_info` |
|
||||
| `DELETE` | [Delete a saved query](./api/delete-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `GET` | [Get a saved query](./api/get-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `PUT` | [Update a saved query](./api/update-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `GET` | [Get distinct values from field data (saved-query-distinct-column-name)](./api/get-distinct-values-from-field-data-saved-query-distinct-column-name) | `/api/v1/saved_query/distinct/{column_name}` |
|
||||
| `GET` | [Download multiple saved queries as YAML files](./api/download-multiple-saved-queries-as-yaml-files) | `/api/v1/saved_query/export/` |
|
||||
| `POST` | [Import saved queries with associated databases](./api/import-saved-queries-with-associated-databases) | `/api/v1/saved_query/import/` |
|
||||
| `GET` | [Get related fields data (saved-query-related-column-name)](./api/get-related-fields-data-saved-query-related-column-name) | `/api/v1/saved_query/related/{column_name}` |
|
||||
| `GET` | [Get a list of queries](/developer-docs/api/get-a-list-of-queries) | `/api/v1/query/` |
|
||||
| `GET` | [Get query detail information](/developer-docs/api/get-query-detail-information) | `/api/v1/query/{pk}` |
|
||||
| `GET` | [Get distinct values from field data (query-distinct-column-name)](/developer-docs/api/get-distinct-values-from-field-data-query-distinct-column-name) | `/api/v1/query/distinct/{column_name}` |
|
||||
| `GET` | [Get related fields data (query-related-column-name)](/developer-docs/api/get-related-fields-data-query-related-column-name) | `/api/v1/query/related/{column_name}` |
|
||||
| `POST` | [Manually stop a query with client_id](/developer-docs/api/manually-stop-a-query-with-client-id) | `/api/v1/query/stop` |
|
||||
| `GET` | [Get a list of queries that changed after last_updated_ms](/developer-docs/api/get-a-list-of-queries-that-changed-after-last-updated-ms) | `/api/v1/query/updated_since` |
|
||||
| `DELETE` | [Bulk delete saved queries](/developer-docs/api/bulk-delete-saved-queries) | `/api/v1/saved_query/` |
|
||||
| `GET` | [Get a list of saved queries](/developer-docs/api/get-a-list-of-saved-queries) | `/api/v1/saved_query/` |
|
||||
| `POST` | [Create a saved query](/developer-docs/api/create-a-saved-query) | `/api/v1/saved_query/` |
|
||||
| `GET` | [Get metadata information about this API resource (saved-query--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-saved-query-info) | `/api/v1/saved_query/_info` |
|
||||
| `DELETE` | [Delete a saved query](/developer-docs/api/delete-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `GET` | [Get a saved query](/developer-docs/api/get-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `PUT` | [Update a saved query](/developer-docs/api/update-a-saved-query) | `/api/v1/saved_query/{pk}` |
|
||||
| `GET` | [Get distinct values from field data (saved-query-distinct-column-name)](/developer-docs/api/get-distinct-values-from-field-data-saved-query-distinct-column-name) | `/api/v1/saved_query/distinct/{column_name}` |
|
||||
| `GET` | [Download multiple saved queries as YAML files](/developer-docs/api/download-multiple-saved-queries-as-yaml-files) | `/api/v1/saved_query/export/` |
|
||||
| `POST` | [Import saved queries with associated databases](/developer-docs/api/import-saved-queries-with-associated-databases) | `/api/v1/saved_query/import/` |
|
||||
| `GET` | [Get related fields data (saved-query-related-column-name)](/developer-docs/api/get-related-fields-data-saved-query-related-column-name) | `/api/v1/saved_query/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -240,7 +240,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get possible values for a datasource column](./api/get-possible-values-for-a-datasource-column) | `/api/v1/datasource/{datasource_type}/{datasource_id}/column/{column_name}/values/` |
|
||||
| `GET` | [Get possible values for a datasource column](/developer-docs/api/get-possible-values-for-a-datasource-column) | `/api/v1/datasource/{datasource_type}/{datasource_id}/column/{column_name}/values/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -249,8 +249,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Return an AdvancedDataTypeResponse](./api/return-an-advanceddatatyperesponse) | `/api/v1/advanced_data_type/convert` |
|
||||
| `GET` | [Return a list of available advanced data types](./api/return-a-list-of-available-advanced-data-types) | `/api/v1/advanced_data_type/types` |
|
||||
| `GET` | [Return an AdvancedDataTypeResponse](/developer-docs/api/return-an-advanceddatatyperesponse) | `/api/v1/advanced_data_type/convert` |
|
||||
| `GET` | [Return a list of available advanced data types](/developer-docs/api/return-a-list-of-available-advanced-data-types) | `/api/v1/advanced_data_type/types` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -261,21 +261,21 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete tags](./api/bulk-delete-tags) | `/api/v1/tag/` |
|
||||
| `GET` | [Get a list of tags](./api/get-a-list-of-tags) | `/api/v1/tag/` |
|
||||
| `POST` | [Create a tag](./api/create-a-tag) | `/api/v1/tag/` |
|
||||
| `GET` | [Get metadata information about tag API endpoints](./api/get-metadata-information-about-tag-api-endpoints) | `/api/v1/tag/_info` |
|
||||
| `POST` | [Add tags to an object](./api/add-tags-to-an-object) | `/api/v1/tag/{object_type}/{object_id}/` |
|
||||
| `DELETE` | [Delete a tagged object](./api/delete-a-tagged-object) | `/api/v1/tag/{object_type}/{object_id}/{tag}/` |
|
||||
| `DELETE` | [Delete a tag](./api/delete-a-tag) | `/api/v1/tag/{pk}` |
|
||||
| `GET` | [Get a tag detail information](./api/get-a-tag-detail-information) | `/api/v1/tag/{pk}` |
|
||||
| `PUT` | [Update a tag](./api/update-a-tag) | `/api/v1/tag/{pk}` |
|
||||
| `DELETE` | [Delete tag by pk favorites](./api/delete-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
|
||||
| `POST` | [Create tag by pk favorites](./api/create-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
|
||||
| `POST` | [Bulk create tags and tagged objects](./api/bulk-create-tags-and-tagged-objects) | `/api/v1/tag/bulk_create` |
|
||||
| `GET` | [Get tag favorite status](./api/get-tag-favorite-status) | `/api/v1/tag/favorite_status/` |
|
||||
| `GET` | [Get all objects associated with a tag](./api/get-all-objects-associated-with-a-tag) | `/api/v1/tag/get_objects/` |
|
||||
| `GET` | [Get related fields data (tag-related-column-name)](./api/get-related-fields-data-tag-related-column-name) | `/api/v1/tag/related/{column_name}` |
|
||||
| `DELETE` | [Bulk delete tags](/developer-docs/api/bulk-delete-tags) | `/api/v1/tag/` |
|
||||
| `GET` | [Get a list of tags](/developer-docs/api/get-a-list-of-tags) | `/api/v1/tag/` |
|
||||
| `POST` | [Create a tag](/developer-docs/api/create-a-tag) | `/api/v1/tag/` |
|
||||
| `GET` | [Get metadata information about tag API endpoints](/developer-docs/api/get-metadata-information-about-tag-api-endpoints) | `/api/v1/tag/_info` |
|
||||
| `POST` | [Add tags to an object](/developer-docs/api/add-tags-to-an-object) | `/api/v1/tag/{object_type}/{object_id}/` |
|
||||
| `DELETE` | [Delete a tagged object](/developer-docs/api/delete-a-tagged-object) | `/api/v1/tag/{object_type}/{object_id}/{tag}/` |
|
||||
| `DELETE` | [Delete a tag](/developer-docs/api/delete-a-tag) | `/api/v1/tag/{pk}` |
|
||||
| `GET` | [Get a tag detail information](/developer-docs/api/get-a-tag-detail-information) | `/api/v1/tag/{pk}` |
|
||||
| `PUT` | [Update a tag](/developer-docs/api/update-a-tag) | `/api/v1/tag/{pk}` |
|
||||
| `DELETE` | [Delete tag by pk favorites](/developer-docs/api/delete-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
|
||||
| `POST` | [Create tag by pk favorites](/developer-docs/api/create-tag-by-pk-favorites) | `/api/v1/tag/{pk}/favorites/` |
|
||||
| `POST` | [Bulk create tags and tagged objects](/developer-docs/api/bulk-create-tags-and-tagged-objects) | `/api/v1/tag/bulk_create` |
|
||||
| `GET` | [Get tag favorite status](/developer-docs/api/get-tag-favorite-status) | `/api/v1/tag/favorite_status/` |
|
||||
| `GET` | [Get all objects associated with a tag](/developer-docs/api/get-all-objects-associated-with-a-tag) | `/api/v1/tag/get_objects/` |
|
||||
| `GET` | [Get related fields data (tag-related-column-name)](/developer-docs/api/get-related-fields-data-tag-related-column-name) | `/api/v1/tag/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -284,20 +284,20 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Delete multiple annotation layers in a bulk operation](./api/delete-multiple-annotation-layers-in-a-bulk-operation) | `/api/v1/annotation_layer/` |
|
||||
| `GET` | [Get a list of annotation layers (annotation-layer)](./api/get-a-list-of-annotation-layers-annotation-layer) | `/api/v1/annotation_layer/` |
|
||||
| `POST` | [Create an annotation layer (annotation-layer)](./api/create-an-annotation-layer-annotation-layer) | `/api/v1/annotation_layer/` |
|
||||
| `GET` | [Get metadata information about this API resource (annotation-layer--info)](./api/get-metadata-information-about-this-api-resource-annotation-layer-info) | `/api/v1/annotation_layer/_info` |
|
||||
| `DELETE` | [Delete annotation layer (annotation-layer-pk)](./api/delete-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `GET` | [Get an annotation layer (annotation-layer-pk)](./api/get-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `PUT` | [Update an annotation layer (annotation-layer-pk)](./api/update-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `DELETE` | [Bulk delete annotation layers](./api/bulk-delete-annotation-layers) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `GET` | [Get a list of annotation layers (annotation-layer-pk-annotation)](./api/get-a-list-of-annotation-layers-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `POST` | [Create an annotation layer (annotation-layer-pk-annotation)](./api/create-an-annotation-layer-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `DELETE` | [Delete annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/delete-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `GET` | [Get an annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/get-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `PUT` | [Update an annotation layer (annotation-layer-pk-annotation-annotation-id)](./api/update-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `GET` | [Get related fields data (annotation-layer-related-column-name)](./api/get-related-fields-data-annotation-layer-related-column-name) | `/api/v1/annotation_layer/related/{column_name}` |
|
||||
| `DELETE` | [Delete multiple annotation layers in a bulk operation](/developer-docs/api/delete-multiple-annotation-layers-in-a-bulk-operation) | `/api/v1/annotation_layer/` |
|
||||
| `GET` | [Get a list of annotation layers (annotation-layer)](/developer-docs/api/get-a-list-of-annotation-layers-annotation-layer) | `/api/v1/annotation_layer/` |
|
||||
| `POST` | [Create an annotation layer (annotation-layer)](/developer-docs/api/create-an-annotation-layer-annotation-layer) | `/api/v1/annotation_layer/` |
|
||||
| `GET` | [Get metadata information about this API resource (annotation-layer--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-annotation-layer-info) | `/api/v1/annotation_layer/_info` |
|
||||
| `DELETE` | [Delete annotation layer (annotation-layer-pk)](/developer-docs/api/delete-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `GET` | [Get an annotation layer (annotation-layer-pk)](/developer-docs/api/get-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `PUT` | [Update an annotation layer (annotation-layer-pk)](/developer-docs/api/update-an-annotation-layer-annotation-layer-pk) | `/api/v1/annotation_layer/{pk}` |
|
||||
| `DELETE` | [Bulk delete annotation layers](/developer-docs/api/bulk-delete-annotation-layers) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `GET` | [Get a list of annotation layers (annotation-layer-pk-annotation)](/developer-docs/api/get-a-list-of-annotation-layers-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `POST` | [Create an annotation layer (annotation-layer-pk-annotation)](/developer-docs/api/create-an-annotation-layer-annotation-layer-pk-annotation) | `/api/v1/annotation_layer/{pk}/annotation/` |
|
||||
| `DELETE` | [Delete annotation layer (annotation-layer-pk-annotation-annotation-id)](/developer-docs/api/delete-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `GET` | [Get an annotation layer (annotation-layer-pk-annotation-annotation-id)](/developer-docs/api/get-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `PUT` | [Update an annotation layer (annotation-layer-pk-annotation-annotation-id)](/developer-docs/api/update-an-annotation-layer-annotation-layer-pk-annotation-annotation-id) | `/api/v1/annotation_layer/{pk}/annotation/{annotation_id}` |
|
||||
| `GET` | [Get related fields data (annotation-layer-related-column-name)](/developer-docs/api/get-related-fields-data-annotation-layer-related-column-name) | `/api/v1/annotation_layer/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -306,14 +306,14 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete CSS templates](./api/bulk-delete-css-templates) | `/api/v1/css_template/` |
|
||||
| `GET` | [Get a list of CSS templates](./api/get-a-list-of-css-templates) | `/api/v1/css_template/` |
|
||||
| `POST` | [Create a CSS template](./api/create-a-css-template) | `/api/v1/css_template/` |
|
||||
| `GET` | [Get metadata information about this API resource (css-template--info)](./api/get-metadata-information-about-this-api-resource-css-template-info) | `/api/v1/css_template/_info` |
|
||||
| `DELETE` | [Delete a CSS template](./api/delete-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `GET` | [Get a CSS template](./api/get-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `PUT` | [Update a CSS template](./api/update-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `GET` | [Get related fields data (css-template-related-column-name)](./api/get-related-fields-data-css-template-related-column-name) | `/api/v1/css_template/related/{column_name}` |
|
||||
| `DELETE` | [Bulk delete CSS templates](/developer-docs/api/bulk-delete-css-templates) | `/api/v1/css_template/` |
|
||||
| `GET` | [Get a list of CSS templates](/developer-docs/api/get-a-list-of-css-templates) | `/api/v1/css_template/` |
|
||||
| `POST` | [Create a CSS template](/developer-docs/api/create-a-css-template) | `/api/v1/css_template/` |
|
||||
| `GET` | [Get metadata information about this API resource (css-template--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-css-template-info) | `/api/v1/css_template/_info` |
|
||||
| `DELETE` | [Delete a CSS template](/developer-docs/api/delete-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `GET` | [Get a CSS template](/developer-docs/api/get-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `PUT` | [Update a CSS template](/developer-docs/api/update-a-css-template) | `/api/v1/css_template/{pk}` |
|
||||
| `GET` | [Get related fields data (css-template-related-column-name)](/developer-docs/api/get-related-fields-data-css-template-related-column-name) | `/api/v1/css_template/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -324,8 +324,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new dashboard's permanent link](./api/create-a-new-dashboard-s-permanent-link) | `/api/v1/dashboard/{pk}/permalink` |
|
||||
| `GET` | [Get dashboard's permanent link state](./api/get-dashboard-s-permanent-link-state) | `/api/v1/dashboard/permalink/{key}` |
|
||||
| `POST` | [Create a new dashboard's permanent link](/developer-docs/api/create-a-new-dashboard-s-permanent-link) | `/api/v1/dashboard/{pk}/permalink` |
|
||||
| `GET` | [Get dashboard's permanent link state](/developer-docs/api/get-dashboard-s-permanent-link-state) | `/api/v1/dashboard/permalink/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -334,8 +334,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new permanent link (explore-permalink)](./api/create-a-new-permanent-link-explore-permalink) | `/api/v1/explore/permalink` |
|
||||
| `GET` | [Get chart's permanent link state](./api/get-chart-s-permanent-link-state) | `/api/v1/explore/permalink/{key}` |
|
||||
| `POST` | [Create a new permanent link (explore-permalink)](/developer-docs/api/create-a-new-permanent-link-explore-permalink) | `/api/v1/explore/permalink` |
|
||||
| `GET` | [Get chart's permanent link state](/developer-docs/api/get-chart-s-permanent-link-state) | `/api/v1/explore/permalink/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -344,8 +344,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new permanent link (sqllab-permalink)](./api/create-a-new-permanent-link-sqllab-permalink) | `/api/v1/sqllab/permalink` |
|
||||
| `GET` | [Get permanent link state for SQLLab editor.](./api/get-permanent-link-state-for-sqllab-editor) | `/api/v1/sqllab/permalink/{key}` |
|
||||
| `POST` | [Create a new permanent link (sqllab-permalink)](/developer-docs/api/create-a-new-permanent-link-sqllab-permalink) | `/api/v1/sqllab/permalink` |
|
||||
| `GET` | [Get permanent link state for SQLLab editor.](/developer-docs/api/get-permanent-link-state-for-sqllab-editor) | `/api/v1/sqllab/permalink/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -354,7 +354,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a report schedule log (embedded-dashboard-uuid)](./api/get-a-report-schedule-log-embedded-dashboard-uuid) | `/api/v1/embedded_dashboard/{uuid}` |
|
||||
| `GET` | [Get a report schedule log (embedded-dashboard-uuid)](/developer-docs/api/get-a-report-schedule-log-embedded-dashboard-uuid) | `/api/v1/embedded_dashboard/{uuid}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -363,10 +363,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a dashboard's filter state](./api/create-a-dashboard-s-filter-state) | `/api/v1/dashboard/{pk}/filter_state` |
|
||||
| `DELETE` | [Delete a dashboard's filter state value](./api/delete-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
| `GET` | [Get a dashboard's filter state value](./api/get-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
| `PUT` | [Update a dashboard's filter state value](./api/update-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
| `POST` | [Create a dashboard's filter state](/developer-docs/api/create-a-dashboard-s-filter-state) | `/api/v1/dashboard/{pk}/filter_state` |
|
||||
| `DELETE` | [Delete a dashboard's filter state value](/developer-docs/api/delete-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
| `GET` | [Get a dashboard's filter state value](/developer-docs/api/get-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
| `PUT` | [Update a dashboard's filter state value](/developer-docs/api/update-a-dashboard-s-filter-state-value) | `/api/v1/dashboard/{pk}/filter_state/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -375,10 +375,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Create a new form_data](./api/create-a-new-form-data) | `/api/v1/explore/form_data` |
|
||||
| `DELETE` | [Delete a form_data](./api/delete-a-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
| `GET` | [Get a form_data](./api/get-a-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
| `PUT` | [Update an existing form_data](./api/update-an-existing-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
| `POST` | [Create a new form_data](/developer-docs/api/create-a-new-form-data) | `/api/v1/explore/form_data` |
|
||||
| `DELETE` | [Delete a form_data](/developer-docs/api/delete-a-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
| `GET` | [Get a form_data](/developer-docs/api/get-a-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
| `PUT` | [Update an existing form_data](/developer-docs/api/update-an-existing-form-data) | `/api/v1/explore/form_data/{key}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -389,17 +389,17 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete report schedules](./api/bulk-delete-report-schedules) | `/api/v1/report/` |
|
||||
| `GET` | [Get a list of report schedules](./api/get-a-list-of-report-schedules) | `/api/v1/report/` |
|
||||
| `POST` | [Create a report schedule](./api/create-a-report-schedule) | `/api/v1/report/` |
|
||||
| `GET` | [Get metadata information about this API resource (report--info)](./api/get-metadata-information-about-this-api-resource-report-info) | `/api/v1/report/_info` |
|
||||
| `DELETE` | [Delete a report schedule](./api/delete-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `GET` | [Get a report schedule](./api/get-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `PUT` | [Update a report schedule](./api/update-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `GET` | [Get a list of report schedule logs](./api/get-a-list-of-report-schedule-logs) | `/api/v1/report/{pk}/log/` |
|
||||
| `GET` | [Get a report schedule log (report-pk-log-log-id)](./api/get-a-report-schedule-log-report-pk-log-log-id) | `/api/v1/report/{pk}/log/{log_id}` |
|
||||
| `GET` | [Get related fields data (report-related-column-name)](./api/get-related-fields-data-report-related-column-name) | `/api/v1/report/related/{column_name}` |
|
||||
| `GET` | [Get slack channels](./api/get-slack-channels) | `/api/v1/report/slack_channels/` |
|
||||
| `DELETE` | [Bulk delete report schedules](/developer-docs/api/bulk-delete-report-schedules) | `/api/v1/report/` |
|
||||
| `GET` | [Get a list of report schedules](/developer-docs/api/get-a-list-of-report-schedules) | `/api/v1/report/` |
|
||||
| `POST` | [Create a report schedule](/developer-docs/api/create-a-report-schedule) | `/api/v1/report/` |
|
||||
| `GET` | [Get metadata information about this API resource (report--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-report-info) | `/api/v1/report/_info` |
|
||||
| `DELETE` | [Delete a report schedule](/developer-docs/api/delete-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `GET` | [Get a report schedule](/developer-docs/api/get-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `PUT` | [Update a report schedule](/developer-docs/api/update-a-report-schedule) | `/api/v1/report/{pk}` |
|
||||
| `GET` | [Get a list of report schedule logs](/developer-docs/api/get-a-list-of-report-schedule-logs) | `/api/v1/report/{pk}/log/` |
|
||||
| `GET` | [Get a report schedule log (report-pk-log-log-id)](/developer-docs/api/get-a-report-schedule-log-report-pk-log-log-id) | `/api/v1/report/{pk}/log/{log_id}` |
|
||||
| `GET` | [Get related fields data (report-related-column-name)](/developer-docs/api/get-related-fields-data-report-related-column-name) | `/api/v1/report/related/{column_name}` |
|
||||
| `GET` | [Get slack channels](/developer-docs/api/get-slack-channels) | `/api/v1/report/slack_channels/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -410,16 +410,16 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security roles](./api/get-security-roles) | `/api/v1/security/roles/` |
|
||||
| `POST` | [Create security roles](./api/create-security-roles) | `/api/v1/security/roles/` |
|
||||
| `GET` | [Get security roles info](./api/get-security-roles-info) | `/api/v1/security/roles/_info` |
|
||||
| `DELETE` | [Delete security roles by pk](./api/delete-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `GET` | [Get security roles by pk](./api/get-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `PUT` | [Update security roles by pk](./api/update-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `POST` | [Create security roles by role_id permissions](./api/create-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions` |
|
||||
| `GET` | [Get security roles by role_id permissions](./api/get-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions/` |
|
||||
| `PUT` | [Update security roles by role_id users](./api/update-security-roles-by-role-id-users) | `/api/v1/security/roles/{role_id}/users` |
|
||||
| `GET` | [List roles](./api/list-roles) | `/api/v1/security/roles/search/` |
|
||||
| `GET` | [Get security roles](/developer-docs/api/get-security-roles) | `/api/v1/security/roles/` |
|
||||
| `POST` | [Create security roles](/developer-docs/api/create-security-roles) | `/api/v1/security/roles/` |
|
||||
| `GET` | [Get security roles info](/developer-docs/api/get-security-roles-info) | `/api/v1/security/roles/_info` |
|
||||
| `DELETE` | [Delete security roles by pk](/developer-docs/api/delete-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `GET` | [Get security roles by pk](/developer-docs/api/get-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `PUT` | [Update security roles by pk](/developer-docs/api/update-security-roles-by-pk) | `/api/v1/security/roles/{pk}` |
|
||||
| `POST` | [Create security roles by role_id permissions](/developer-docs/api/create-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions` |
|
||||
| `GET` | [Get security roles by role_id permissions](/developer-docs/api/get-security-roles-by-role-id-permissions) | `/api/v1/security/roles/{role_id}/permissions/` |
|
||||
| `PUT` | [Update security roles by role_id users](/developer-docs/api/update-security-roles-by-role-id-users) | `/api/v1/security/roles/{role_id}/users` |
|
||||
| `GET` | [List roles](/developer-docs/api/list-roles) | `/api/v1/security/roles/search/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -428,12 +428,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security users](./api/get-security-users) | `/api/v1/security/users/` |
|
||||
| `POST` | [Create security users](./api/create-security-users) | `/api/v1/security/users/` |
|
||||
| `GET` | [Get security users info](./api/get-security-users-info) | `/api/v1/security/users/_info` |
|
||||
| `DELETE` | [Delete security users by pk](./api/delete-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `GET` | [Get security users by pk](./api/get-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `PUT` | [Update security users by pk](./api/update-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `GET` | [Get security users](/developer-docs/api/get-security-users) | `/api/v1/security/users/` |
|
||||
| `POST` | [Create security users](/developer-docs/api/create-security-users) | `/api/v1/security/users/` |
|
||||
| `GET` | [Get security users info](/developer-docs/api/get-security-users-info) | `/api/v1/security/users/_info` |
|
||||
| `DELETE` | [Delete security users by pk](/developer-docs/api/delete-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `GET` | [Get security users by pk](/developer-docs/api/get-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
| `PUT` | [Update security users by pk](/developer-docs/api/update-security-users-by-pk) | `/api/v1/security/users/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -442,9 +442,9 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security permissions](./api/get-security-permissions) | `/api/v1/security/permissions/` |
|
||||
| `GET` | [Get security permissions info](./api/get-security-permissions-info) | `/api/v1/security/permissions/_info` |
|
||||
| `GET` | [Get security permissions by pk](./api/get-security-permissions-by-pk) | `/api/v1/security/permissions/{pk}` |
|
||||
| `GET` | [Get security permissions](/developer-docs/api/get-security-permissions) | `/api/v1/security/permissions/` |
|
||||
| `GET` | [Get security permissions info](/developer-docs/api/get-security-permissions-info) | `/api/v1/security/permissions/_info` |
|
||||
| `GET` | [Get security permissions by pk](/developer-docs/api/get-security-permissions-by-pk) | `/api/v1/security/permissions/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -453,12 +453,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security resources](./api/get-security-resources) | `/api/v1/security/resources/` |
|
||||
| `POST` | [Create security resources](./api/create-security-resources) | `/api/v1/security/resources/` |
|
||||
| `GET` | [Get security resources info](./api/get-security-resources-info) | `/api/v1/security/resources/_info` |
|
||||
| `DELETE` | [Delete security resources by pk](./api/delete-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `GET` | [Get security resources by pk](./api/get-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `PUT` | [Update security resources by pk](./api/update-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `GET` | [Get security resources](/developer-docs/api/get-security-resources) | `/api/v1/security/resources/` |
|
||||
| `POST` | [Create security resources](/developer-docs/api/create-security-resources) | `/api/v1/security/resources/` |
|
||||
| `GET` | [Get security resources info](/developer-docs/api/get-security-resources-info) | `/api/v1/security/resources/_info` |
|
||||
| `DELETE` | [Delete security resources by pk](/developer-docs/api/delete-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `GET` | [Get security resources by pk](/developer-docs/api/get-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
| `PUT` | [Update security resources by pk](/developer-docs/api/update-security-resources-by-pk) | `/api/v1/security/resources/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -467,12 +467,12 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get security permissions resources](./api/get-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `POST` | [Create security permissions resources](./api/create-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `GET` | [Get security permissions resources info](./api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` |
|
||||
| `DELETE` | [Delete security permissions resources by pk](./api/delete-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `GET` | [Get security permissions resources by pk](./api/get-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `PUT` | [Update security permissions resources by pk](./api/update-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `GET` | [Get security permissions resources](/developer-docs/api/get-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `POST` | [Create security permissions resources](/developer-docs/api/create-security-permissions-resources) | `/api/v1/security/permissions-resources/` |
|
||||
| `GET` | [Get security permissions resources info](/developer-docs/api/get-security-permissions-resources-info) | `/api/v1/security/permissions-resources/_info` |
|
||||
| `DELETE` | [Delete security permissions resources by pk](/developer-docs/api/delete-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `GET` | [Get security permissions resources by pk](/developer-docs/api/get-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
| `PUT` | [Update security permissions resources by pk](/developer-docs/api/update-security-permissions-resources-by-pk) | `/api/v1/security/permissions-resources/{pk}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -481,14 +481,14 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `DELETE` | [Bulk delete RLS rules](./api/bulk-delete-rls-rules) | `/api/v1/rowlevelsecurity/` |
|
||||
| `GET` | [Get a list of RLS](./api/get-a-list-of-rls) | `/api/v1/rowlevelsecurity/` |
|
||||
| `POST` | [Create a new RLS rule](./api/create-a-new-rls-rule) | `/api/v1/rowlevelsecurity/` |
|
||||
| `GET` | [Get metadata information about this API resource (rowlevelsecurity--info)](./api/get-metadata-information-about-this-api-resource-rowlevelsecurity-info) | `/api/v1/rowlevelsecurity/_info` |
|
||||
| `DELETE` | [Delete an RLS](./api/delete-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `GET` | [Get an RLS](./api/get-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `PUT` | [Update an RLS rule](./api/update-an-rls-rule) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `GET` | [Get related fields data (rowlevelsecurity-related-column-name)](./api/get-related-fields-data-rowlevelsecurity-related-column-name) | `/api/v1/rowlevelsecurity/related/{column_name}` |
|
||||
| `DELETE` | [Bulk delete RLS rules](/developer-docs/api/bulk-delete-rls-rules) | `/api/v1/rowlevelsecurity/` |
|
||||
| `GET` | [Get a list of RLS](/developer-docs/api/get-a-list-of-rls) | `/api/v1/rowlevelsecurity/` |
|
||||
| `POST` | [Create a new RLS rule](/developer-docs/api/create-a-new-rls-rule) | `/api/v1/rowlevelsecurity/` |
|
||||
| `GET` | [Get metadata information about this API resource (rowlevelsecurity--info)](/developer-docs/api/get-metadata-information-about-this-api-resource-rowlevelsecurity-info) | `/api/v1/rowlevelsecurity/_info` |
|
||||
| `DELETE` | [Delete an RLS](/developer-docs/api/delete-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `GET` | [Get an RLS](/developer-docs/api/get-an-rls) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `PUT` | [Update an RLS rule](/developer-docs/api/update-an-rls-rule) | `/api/v1/rowlevelsecurity/{pk}` |
|
||||
| `GET` | [Get related fields data (rowlevelsecurity-related-column-name)](/developer-docs/api/get-related-fields-data-rowlevelsecurity-related-column-name) | `/api/v1/rowlevelsecurity/related/{column_name}` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -499,8 +499,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Export all assets](./api/export-all-assets) | `/api/v1/assets/export/` |
|
||||
| `POST` | [Import multiple assets](./api/import-multiple-assets) | `/api/v1/assets/import/` |
|
||||
| `GET` | [Export all assets](/developer-docs/api/export-all-assets) | `/api/v1/assets/export/` |
|
||||
| `POST` | [Import multiple assets](/developer-docs/api/import-multiple-assets) | `/api/v1/assets/import/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -509,7 +509,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | [Invalidate cache records and remove the database records](./api/invalidate-cache-records-and-remove-the-database-records) | `/api/v1/cachekey/invalidate` |
|
||||
| `POST` | [Invalidate cache records and remove the database records](/developer-docs/api/invalidate-cache-records-and-remove-the-database-records) | `/api/v1/cachekey/invalidate` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -518,10 +518,10 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get a list of logs](./api/get-a-list-of-logs) | `/api/v1/log/` |
|
||||
| `POST` | [Create log](./api/create-log) | `/api/v1/log/` |
|
||||
| `GET` | [Get a log detail information](./api/get-a-log-detail-information) | `/api/v1/log/{pk}` |
|
||||
| `GET` | [Get recent activity data for a user](./api/get-recent-activity-data-for-a-user) | `/api/v1/log/recent_activity/` |
|
||||
| `GET` | [Get a list of logs](/developer-docs/api/get-a-list-of-logs) | `/api/v1/log/` |
|
||||
| `POST` | [Create log](/developer-docs/api/create-log) | `/api/v1/log/` |
|
||||
| `GET` | [Get a log detail information](/developer-docs/api/get-a-log-detail-information) | `/api/v1/log/{pk}` |
|
||||
| `GET` | [Get recent activity data for a user](/developer-docs/api/get-recent-activity-data-for-a-user) | `/api/v1/log/recent_activity/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -532,8 +532,8 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the user object](./api/get-the-user-object) | `/api/v1/me/` |
|
||||
| `GET` | [Get the user roles](./api/get-the-user-roles) | `/api/v1/me/roles/` |
|
||||
| `GET` | [Get the user object](/developer-docs/api/get-the-user-object) | `/api/v1/me/` |
|
||||
| `GET` | [Get the user roles](/developer-docs/api/get-the-user-roles) | `/api/v1/me/roles/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -542,7 +542,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get the user avatar](./api/get-the-user-avatar) | `/api/v1/user/{user_id}/avatar.png` |
|
||||
| `GET` | [Get the user avatar](/developer-docs/api/get-the-user-avatar) | `/api/v1/user/{user_id}/avatar.png` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -551,7 +551,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get menu](./api/get-menu) | `/api/v1/menu/` |
|
||||
| `GET` | [Get menu](/developer-docs/api/get-menu) | `/api/v1/menu/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -560,7 +560,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get all available domains](./api/get-all-available-domains) | `/api/v1/available_domains/` |
|
||||
| `GET` | [Get all available domains](/developer-docs/api/get-all-available-domains) | `/api/v1/available_domains/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -569,7 +569,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Read off of the Redis events stream](./api/read-off-of-the-redis-events-stream) | `/api/v1/async_event/` |
|
||||
| `GET` | [Read off of the Redis events stream](/developer-docs/api/read-off-of-the-redis-events-stream) | `/api/v1/async_event/` |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -578,7 +578,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `GET` | [Get api by version openapi](./api/get-api-by-version-openapi) | `/api/{version}/_openapi` |
|
||||
| `GET` | [Get api by version openapi](/developer-docs/api/get-api-by-version-openapi) | `/api/{version}/_openapi` |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ superset-extensions validate: Validates the extension structure and metadata.
|
||||
When creating a new extension with `superset-extensions init`, the CLI generates a standardized folder structure:
|
||||
|
||||
```
|
||||
my-org.dataset-references/
|
||||
dataset-references/
|
||||
├── extension.json
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
@@ -76,7 +76,7 @@ my-org.dataset-references/
|
||||
```
|
||||
|
||||
**Note**: With publisher `my-org` and name `dataset-references`, the technical names are:
|
||||
- Directory name: `my-org.dataset-references` (kebab-case)
|
||||
- Directory name: `dataset-references` (kebab-case)
|
||||
- Backend Python namespace: `my_org.dataset_references`
|
||||
- Backend distribution package: `my_org-dataset_references`
|
||||
- Frontend package name: `@my-org/dataset-references` (scoped)
|
||||
|
||||
@@ -75,7 +75,7 @@ This approach ensures that extensions from different organizations cannot confli
|
||||
This creates a complete project structure:
|
||||
|
||||
```
|
||||
my-org.hello-world/
|
||||
hello-world/
|
||||
├── extension.json # Extension metadata and configuration
|
||||
├── backend/ # Backend Python code
|
||||
│ ├── src/
|
||||
|
||||
@@ -52,7 +52,6 @@ module.exports = {
|
||||
'extensions/development',
|
||||
'extensions/deployment',
|
||||
'extensions/mcp',
|
||||
'extensions/mcp-server',
|
||||
'extensions/security',
|
||||
'extensions/tasks',
|
||||
'extensions/registry',
|
||||
|
||||
245
docs/docs/using-superset/using-ai-with-superset.mdx
Normal file
245
docs/docs/using-superset/using-ai-with-superset.mdx
Normal file
@@ -0,0 +1,245 @@
|
||||
---
|
||||
title: Using AI with Superset
|
||||
hide_title: true
|
||||
sidebar_position: 5
|
||||
version: 1
|
||||
---
|
||||
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Using AI with Superset
|
||||
|
||||
Superset supports AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). Connect Claude, ChatGPT, or other MCP-compatible clients to explore your data, build charts, create dashboards, and run SQL -- all through natural language.
|
||||
|
||||
:::info
|
||||
Requires Superset 5.0+. Your admin must enable and deploy the MCP server before you can connect.
|
||||
See the **[MCP Server admin guide](/admin-docs/configuration/mcp-server)** for setup instructions.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## What Can AI Do with Superset?
|
||||
|
||||
### Explore Your Data
|
||||
|
||||
Ask your AI assistant to browse what's available in your Superset instance:
|
||||
|
||||
- **List datasets** -- see all datasets you have access to, with filtering and search
|
||||
- **Get dataset details** -- column names, types, available metrics, and filters
|
||||
- **List charts and dashboards** -- find existing visualizations by name or keyword
|
||||
- **Get chart and dashboard details** -- understand what a chart shows, its query, and configuration
|
||||
|
||||
**Example prompts:**
|
||||
> "What datasets are available?"
|
||||
> "Show me the columns in the sales_orders dataset"
|
||||
> "Find dashboards related to revenue"
|
||||
|
||||
### Build Charts
|
||||
|
||||
Describe the visualization you want and AI creates it for you:
|
||||
|
||||
- **Create charts from natural language** -- describe what you want to see and AI picks the right chart type, metrics, and dimensions
|
||||
- **Preview before saving** -- AI generates a preview so you can review before committing
|
||||
- **Modify existing charts** -- update filters, change chart types, add metrics
|
||||
- **Get Explore links** -- open any chart in Superset's Explore view for further refinement
|
||||
|
||||
**Example prompts:**
|
||||
> "Create a bar chart showing monthly revenue by region from the sales dataset"
|
||||
> "Update chart 42 to use a line chart instead"
|
||||
> "Give me a link to explore this chart further"
|
||||
|
||||
### Create Dashboards
|
||||
|
||||
Build dashboards from a collection of charts:
|
||||
|
||||
- **Generate dashboards** -- create a new dashboard with a set of charts, automatically laid out
|
||||
- **Add charts to existing dashboards** -- place a chart on an existing dashboard with automatic positioning
|
||||
|
||||
**Example prompts:**
|
||||
> "Create a dashboard called 'Q4 Sales Overview' with charts 10, 15, and 22"
|
||||
> "Add the revenue trend chart to the executive dashboard"
|
||||
|
||||
### Run SQL Queries
|
||||
|
||||
Execute SQL directly through your AI assistant:
|
||||
|
||||
- **Run queries** -- execute SQL with full Superset RBAC enforcement (you can only query data your roles allow)
|
||||
- **Open SQL Lab** -- get a link to SQL Lab pre-populated with a query, ready to run and explore
|
||||
|
||||
**Example prompts:**
|
||||
> "Run this query: SELECT region, SUM(revenue) FROM sales GROUP BY region"
|
||||
> "Open SQL Lab with a query to show the top 10 customers by order count"
|
||||
|
||||
### Analyze Chart Data
|
||||
|
||||
Pull the raw data behind any chart:
|
||||
|
||||
- **Get chart data** -- retrieve the data a chart displays, with support for JSON, CSV, and Excel export formats
|
||||
- **Inspect results** -- useful for verifying what a visualization shows or feeding data into other tools
|
||||
|
||||
**Example prompts:**
|
||||
> "Get the data behind chart 42"
|
||||
> "Export chart 15 data as CSV"
|
||||
|
||||
### Check Instance Status
|
||||
|
||||
- **Health check** -- verify your Superset instance is up and the MCP connection is working
|
||||
- **Instance info** -- get high-level statistics about your Superset instance (number of datasets, charts, dashboards)
|
||||
|
||||
**Example prompts:**
|
||||
> "Is Superset healthy?"
|
||||
> "How many dashboards are in this instance?"
|
||||
|
||||
---
|
||||
|
||||
## Connecting Your AI Client
|
||||
|
||||
Once your admin has deployed the MCP server, connect your AI client using the instructions below.
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Edit your Claude Desktop config file:
|
||||
|
||||
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"url": "http://localhost:5008/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Restart Claude Desktop. The hammer icon in the chat bar confirms the connection.
|
||||
|
||||
If your admin has enabled JWT authentication, you may need to include a token:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"mcp-remote@latest",
|
||||
"http://your-superset-host:5008/mcp",
|
||||
"--header",
|
||||
"Authorization: Bearer YOUR_TOKEN"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Code (CLI)
|
||||
|
||||
Add to your project's `.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"superset": {
|
||||
"type": "url",
|
||||
"url": "http://localhost:5008/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ChatGPT
|
||||
|
||||
1. Click your profile icon > **Settings** > **Apps and Connectors**
|
||||
2. Enable **Developer Mode** in Advanced Settings
|
||||
3. In the chat composer, press **+** > **Add sources** > **App** > **Connect more** > **Create app**
|
||||
4. Enter a name and your MCP server URL
|
||||
5. Click **I understand and continue**
|
||||
|
||||
:::info
|
||||
ChatGPT MCP connectors require a Pro, Team, Enterprise, or Edu plan.
|
||||
:::
|
||||
|
||||
Ask your admin for the MCP server URL and any authentication tokens you need.
|
||||
|
||||
---
|
||||
|
||||
## Tips for Best Results
|
||||
|
||||
- **Be specific** -- "Create a bar chart of monthly revenue by region from the sales dataset" works better than "Make me a chart"
|
||||
- **Start with exploration** -- ask what datasets and charts exist before creating new ones
|
||||
- **Review AI-generated content** -- always check chart configurations and SQL before saving or sharing
|
||||
- **Use Explore for refinement** -- ask AI for an Explore link, then fine-tune interactively in the Superset UI
|
||||
- **Check permissions if you get errors** -- AI respects Superset's RBAC, so you can only access data your roles allow
|
||||
|
||||
---
|
||||
|
||||
## Available Tools Reference
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `health_check` | Verify the MCP server is running and connected |
|
||||
| `get_instance_info` | Get instance statistics (dataset, chart, dashboard counts) |
|
||||
| `get_schema` | Discover available charts, datasets, and dashboards with schema info |
|
||||
| `list_datasets` | List datasets with filtering and search |
|
||||
| `get_dataset_info` | Get dataset metadata (columns, metrics, filters) |
|
||||
| `list_charts` | List charts with filtering and search |
|
||||
| `get_chart_info` | Get chart metadata and configuration |
|
||||
| `get_chart_data` | Retrieve chart data (JSON, CSV, or Excel) |
|
||||
| `get_chart_preview` | Generate a chart preview (URL, ASCII, table, or Vega-Lite) |
|
||||
| `generate_chart` | Create a new chart from a specification |
|
||||
| `update_chart` | Modify an existing chart's configuration |
|
||||
| `update_chart_preview` | Update a cached chart preview without saving |
|
||||
| `list_dashboards` | List dashboards with filtering and search |
|
||||
| `get_dashboard_info` | Get dashboard metadata and layout |
|
||||
| `generate_dashboard` | Create a new dashboard with specified charts |
|
||||
| `add_chart_to_existing_dashboard` | Add a chart to an existing dashboard |
|
||||
| `execute_sql` | Run a SQL query with RBAC enforcement |
|
||||
| `open_sql_lab_with_context` | Open SQL Lab with a pre-populated query |
|
||||
| `generate_explore_link` | Generate an Explore URL for interactive visualization |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" or "Cannot connect"
|
||||
|
||||
- Confirm the MCP server URL with your admin
|
||||
- For Claude Desktop: fully quit the app (not just close the window) and restart after config changes
|
||||
- Check that the URL path ends with `/mcp` (e.g., `http://localhost:5008/mcp`)
|
||||
|
||||
### "Permission denied" or missing data
|
||||
|
||||
- Superset's RBAC controls what you can access through AI, just like in the Superset UI
|
||||
- Ask your admin to verify your roles and permissions
|
||||
- Try accessing the same data through the Superset web UI to confirm your access
|
||||
|
||||
### "Response too large"
|
||||
|
||||
- Ask for smaller result sets: use filters, reduce `page_size`, or request specific columns
|
||||
- Example: "Show me the top 10 rows from the sales dataset" instead of "Show me all sales data"
|
||||
|
||||
### AI doesn't see Superset tools
|
||||
|
||||
- Verify the connection in your AI client (e.g., the hammer icon in Claude Desktop)
|
||||
- Ask the AI "What Superset tools are available?" to confirm the connection
|
||||
- Restart your AI client if you recently changed the configuration
|
||||
@@ -202,7 +202,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\
|
||||
mdx += `| Method | Endpoint | Description |\n`;
|
||||
mdx += `|--------|----------|-------------|\n`;
|
||||
for (const ep of tagEndpoints['Security']) {
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](/developer-docs/api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
}
|
||||
mdx += '\n';
|
||||
renderedTags.add('Security');
|
||||
@@ -229,7 +229,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\
|
||||
mdx += `|--------|----------|-------------|\n`;
|
||||
|
||||
for (const ep of endpoints) {
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](/developer-docs/api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
}
|
||||
|
||||
mdx += `\n</details>\n\n`;
|
||||
@@ -252,7 +252,7 @@ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \\
|
||||
mdx += `|--------|----------|-------------|\n`;
|
||||
|
||||
for (const ep of endpoints) {
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](./api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
mdx += `| \`${ep.method}\` | [${ep.summary}](/developer-docs/api/${ep.slug}) | \`${ep.path}\` |\n`;
|
||||
}
|
||||
|
||||
mdx += `\n</details>\n\n`;
|
||||
|
||||
120
docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
Normal file
120
docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Swizzled from docusaurus-theme-openapi-docs to fix SSG crash.
|
||||
*
|
||||
* The original component calls useTypedSelector (Redux) at the top level,
|
||||
* which fails during static site generation because no Redux store is
|
||||
* available. This version moves the hook into a browser-only child component
|
||||
* so SSG can render the page without a store context.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
interface ServerVariable {
|
||||
default?: string;
|
||||
}
|
||||
|
||||
interface ServerValue {
|
||||
url: string;
|
||||
variables?: Record<string, ServerVariable>;
|
||||
}
|
||||
|
||||
interface StoreState {
|
||||
server: { value: ServerValue | null };
|
||||
}
|
||||
|
||||
function colorForMethod(method: string) {
|
||||
switch (method.toLowerCase()) {
|
||||
case "get":
|
||||
return "primary";
|
||||
case "post":
|
||||
return "success";
|
||||
case "delete":
|
||||
return "danger";
|
||||
case "put":
|
||||
return "info";
|
||||
case "patch":
|
||||
return "warning";
|
||||
case "head":
|
||||
return "secondary";
|
||||
case "event":
|
||||
return "secondary";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
method: string;
|
||||
path: string;
|
||||
context?: "endpoint" | "callback";
|
||||
}
|
||||
|
||||
// Inner component rendered only in the browser, where the Redux store exists.
|
||||
function ServerUrl() {
|
||||
const serverValue = useSelector((state: StoreState) => state.server.value);
|
||||
|
||||
if (serverValue && serverValue.variables) {
|
||||
let serverUrlWithVariables = serverValue.url.replace(/\/$/, "");
|
||||
Object.keys(serverValue.variables).forEach((variable) => {
|
||||
serverUrlWithVariables = serverUrlWithVariables.replace(
|
||||
`{${variable}}`,
|
||||
serverValue.variables?.[variable].default ?? ""
|
||||
);
|
||||
});
|
||||
return <>{serverUrlWithVariables}</>;
|
||||
}
|
||||
|
||||
if (serverValue && serverValue.url) {
|
||||
return <>{serverValue.url}</>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function MethodEndpoint({ method, path, context }: Props) {
|
||||
const renderServerUrl = () => {
|
||||
if (context === "callback") {
|
||||
return "";
|
||||
}
|
||||
return <BrowserOnly>{() => <ServerUrl />}</BrowserOnly>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<pre className="openapi__method-endpoint">
|
||||
<span className={"badge badge--" + colorForMethod(method)}>
|
||||
{method === "event" ? "Webhook" : method.toUpperCase()}
|
||||
</span>{" "}
|
||||
{method !== "event" && (
|
||||
<h2 className="openapi__method-endpoint-path">
|
||||
{renderServerUrl()}
|
||||
{`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`}
|
||||
</h2>
|
||||
)}
|
||||
</pre>
|
||||
<div className="openapi__divider" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MethodEndpoint;
|
||||
@@ -144,7 +144,7 @@ solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.12, <0.3.0"]
|
||||
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
|
||||
excel = ["xlrd>=1.2.0, <1.3"]
|
||||
fastmcp = ["fastmcp==2.14.3"]
|
||||
fastmcp = ["fastmcp>=3.1.0,<4.0"]
|
||||
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
|
||||
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
|
||||
gevent = ["gevent>=23.9.1"]
|
||||
@@ -372,6 +372,7 @@ unfixable = []
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"superset/mcp_service/app.py" = ["S608", "E501"] # LLM instruction text: SQL examples (S608) and long lines in multiline string (E501)
|
||||
"scripts/*" = ["TID251"]
|
||||
"setup.py" = ["TID251"]
|
||||
"superset/config.py" = ["TID251"]
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
# via
|
||||
# -r requirements/development.in
|
||||
# apache-superset
|
||||
aiofile==3.9.0
|
||||
# via py-key-value-aio
|
||||
alembic==1.15.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -26,8 +28,10 @@ anyio==4.11.0
|
||||
# via
|
||||
# httpx
|
||||
# mcp
|
||||
# py-key-value-aio
|
||||
# sse-starlette
|
||||
# starlette
|
||||
# watchfiles
|
||||
apispec==6.6.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -65,9 +69,7 @@ bcrypt==4.3.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# paramiko
|
||||
beartype==0.22.5
|
||||
# via
|
||||
# py-key-value-aio
|
||||
# py-key-value-shared
|
||||
# via py-key-value-aio
|
||||
billiard==4.2.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -100,6 +102,8 @@ cachetools==6.2.1
|
||||
# -c requirements/base-constraint.txt
|
||||
# google-auth
|
||||
# py-key-value-aio
|
||||
caio==0.9.25
|
||||
# via aiofile
|
||||
cattrs==25.1.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -138,7 +142,6 @@ click==8.2.1
|
||||
# click-repl
|
||||
# flask
|
||||
# flask-appbuilder
|
||||
# typer
|
||||
# uvicorn
|
||||
click-didyoumean==0.3.1
|
||||
# via
|
||||
@@ -156,8 +159,6 @@ click-repl==0.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# celery
|
||||
cloudpickle==3.1.2
|
||||
# via pydocket
|
||||
cmdstanpy==1.1.0
|
||||
# via prophet
|
||||
colorama==0.4.6
|
||||
@@ -206,8 +207,6 @@ deprecation==2.1.0
|
||||
# apache-superset
|
||||
dill==0.4.0
|
||||
# via pylint
|
||||
diskcache==5.6.3
|
||||
# via py-key-value-aio
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
dnspython==2.7.0
|
||||
@@ -237,9 +236,7 @@ et-xmlfile==2.0.0
|
||||
# openpyxl
|
||||
exceptiongroup==1.3.0
|
||||
# via fastmcp
|
||||
fakeredis==2.32.1
|
||||
# via pydocket
|
||||
fastmcp==2.14.3
|
||||
fastmcp==3.1.0
|
||||
# via apache-superset
|
||||
filelock==3.20.3
|
||||
# via
|
||||
@@ -474,6 +471,8 @@ jsonpath-ng==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
jsonref==1.1.0
|
||||
# via fastmcp
|
||||
jsonschema==4.23.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -504,8 +503,6 @@ limits==5.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-limiter
|
||||
lupa==2.6
|
||||
# via fakeredis
|
||||
mako==1.3.10
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -603,7 +600,7 @@ openpyxl==3.1.5
|
||||
# -c requirements/base-constraint.txt
|
||||
# pandas
|
||||
opentelemetry-api==1.39.1
|
||||
# via pydocket
|
||||
# via fastmcp
|
||||
ordered-set==4.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -622,6 +619,7 @@ packaging==25.0
|
||||
# deprecation
|
||||
# docker
|
||||
# duckdb-engine
|
||||
# fastmcp
|
||||
# google-cloud-bigquery
|
||||
# gunicorn
|
||||
# limits
|
||||
@@ -653,8 +651,6 @@ parsedatetime==2.6
|
||||
# apache-superset
|
||||
pathable==0.4.3
|
||||
# via jsonschema-path
|
||||
pathvalidate==3.3.1
|
||||
# via py-key-value-aio
|
||||
pgsanity==0.2.9
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -691,8 +687,6 @@ prison==0.2.1
|
||||
# flask-appbuilder
|
||||
progress==1.6
|
||||
# via apache-superset
|
||||
prometheus-client==0.23.1
|
||||
# via pydocket
|
||||
prompt-toolkit==3.0.51
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -714,12 +708,8 @@ psutil==6.1.0
|
||||
# via apache-superset
|
||||
psycopg2-binary==2.9.9
|
||||
# via apache-superset
|
||||
py-key-value-aio==0.3.0
|
||||
# via
|
||||
# fastmcp
|
||||
# pydocket
|
||||
py-key-value-shared==0.3.0
|
||||
# via py-key-value-aio
|
||||
py-key-value-aio==0.4.4
|
||||
# via fastmcp
|
||||
pyarrow==16.1.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -758,8 +748,6 @@ pydantic-settings==2.10.1
|
||||
# via mcp
|
||||
pydata-google-auth==1.9.0
|
||||
# via pandas-gbq
|
||||
pydocket==0.17.1
|
||||
# via fastmcp
|
||||
pydruid==0.6.9
|
||||
# via apache-superset
|
||||
pyfakefs==5.3.5
|
||||
@@ -844,8 +832,6 @@ python-dotenv==1.1.0
|
||||
# apache-superset
|
||||
# fastmcp
|
||||
# pydantic-settings
|
||||
python-json-logger==4.0.0
|
||||
# via pydocket
|
||||
python-ldap==3.4.4
|
||||
# via apache-superset
|
||||
python-multipart==0.0.20
|
||||
@@ -866,15 +852,13 @@ pyyaml==6.0.2
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# apispec
|
||||
# fastmcp
|
||||
# jsonschema-path
|
||||
# pre-commit
|
||||
redis==5.3.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# fakeredis
|
||||
# py-key-value-aio
|
||||
# pydocket
|
||||
referencing==0.36.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -910,9 +894,7 @@ rich==13.9.4
|
||||
# cyclopts
|
||||
# fastmcp
|
||||
# flask-limiter
|
||||
# pydocket
|
||||
# rich-rst
|
||||
# typer
|
||||
rich-rst==1.3.1
|
||||
# via cyclopts
|
||||
rpds-py==0.25.0
|
||||
@@ -944,8 +926,6 @@ setuptools==80.9.0
|
||||
# pydata-google-auth
|
||||
# zope-event
|
||||
# zope-interface
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
shillelagh==1.4.3
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -973,7 +953,6 @@ sniffio==1.3.1
|
||||
sortedcontainers==2.4.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# fakeredis
|
||||
# trio
|
||||
sqlalchemy==1.4.54
|
||||
# via
|
||||
@@ -1034,8 +1013,6 @@ trio-websocket==0.12.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# selenium
|
||||
typer==0.20.0
|
||||
# via pydocket
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -1048,16 +1025,14 @@ typing-extensions==4.15.0
|
||||
# limits
|
||||
# mcp
|
||||
# opentelemetry-api
|
||||
# py-key-value-shared
|
||||
# py-key-value-aio
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# pydocket
|
||||
# pyopenssl
|
||||
# referencing
|
||||
# selenium
|
||||
# shillelagh
|
||||
# starlette
|
||||
# typer
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
# via
|
||||
@@ -1072,6 +1047,8 @@ tzdata==2025.2
|
||||
# pandas
|
||||
tzlocal==5.2
|
||||
# via trino
|
||||
uncalled-for==0.2.0
|
||||
# via fastmcp
|
||||
url-normalize==2.2.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -1101,6 +1078,8 @@ watchdog==6.0.0
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
# apache-superset-extensions-cli
|
||||
watchfiles==1.1.1
|
||||
# via fastmcp
|
||||
wcwidth==0.2.13
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
|
||||
@@ -18,20 +18,20 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-core"
|
||||
version = "0.1.0rc1"
|
||||
version = "0.1.0rc2"
|
||||
description = "Core Python package for building Apache Superset backend extensions and integrations"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Apache Software Foundation", email = "dev@superset.apache.org" },
|
||||
]
|
||||
license = { file="LICENSE.txt" }
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE.txt"]
|
||||
requires-python = ">=3.10"
|
||||
keywords = ["superset", "apache", "analytics", "business-intelligence", "extensions", "visualization"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
|
||||
@@ -37,6 +37,13 @@ Usage:
|
||||
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
||||
try:
|
||||
from mcp.types import ToolAnnotations
|
||||
except (
|
||||
ImportError
|
||||
): # MCP extras may not be installed in superset-core-only environments
|
||||
ToolAnnotations = dict
|
||||
|
||||
# Type variable for decorated functions
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
@@ -48,11 +55,15 @@ def tool(
|
||||
description: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
protect: bool = True,
|
||||
class_permission_name: str | None = None,
|
||||
method_permission_name: str | None = None,
|
||||
annotations: ToolAnnotations | None = None,
|
||||
) -> Any: # Use Any to avoid mypy issues with dependency injection
|
||||
"""
|
||||
Decorator to register an MCP tool with optional authentication.
|
||||
|
||||
This decorator combines FastMCP tool registration with optional authentication.
|
||||
This decorator combines FastMCP tool registration with optional authentication
|
||||
and RBAC permission checking.
|
||||
|
||||
Can be used as:
|
||||
@tool
|
||||
@@ -69,6 +80,13 @@ def tool(
|
||||
description: Tool description (defaults to function docstring)
|
||||
tags: List of tags for categorizing the tool (defaults to empty list)
|
||||
protect: Whether to require Superset authentication (defaults to True)
|
||||
class_permission_name: FAB view/resource name for RBAC checking
|
||||
(e.g., "Chart", "Dashboard", "SQLLab"). When set, enables
|
||||
permission checking via security_manager.can_access().
|
||||
method_permission_name: FAB action name (e.g., "read", "write").
|
||||
Defaults to "write" if tags includes "mutate", else "read".
|
||||
annotations: MCP tool annotations (title, readOnlyHint, destructiveHint, etc.)
|
||||
These hints help MCP clients understand tool behavior and safety.
|
||||
|
||||
Returns:
|
||||
Decorator function that registers and wraps the tool, or the wrapped function
|
||||
@@ -90,6 +108,18 @@ def tool(
|
||||
def public_tool() -> str:
|
||||
'''Public tool accessible without auth'''
|
||||
return "Hello world"
|
||||
|
||||
@tool(class_permission_name="Chart") # RBAC: requires can_read on Chart
|
||||
def list_charts() -> list:
|
||||
'''List charts the user can access'''
|
||||
return []
|
||||
|
||||
@tool( # RBAC: can_write on Chart
|
||||
tags=["mutate"], class_permission_name="Chart",
|
||||
)
|
||||
def create_chart(name: str) -> dict:
|
||||
'''Create a new chart'''
|
||||
return {"name": name}
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"MCP tool decorator not initialized. "
|
||||
@@ -158,4 +188,5 @@ def prompt(
|
||||
__all__ = [
|
||||
"tool",
|
||||
"prompt",
|
||||
"ToolAnnotations",
|
||||
]
|
||||
|
||||
@@ -17,20 +17,20 @@
|
||||
|
||||
[project]
|
||||
name = "apache-superset-extensions-cli"
|
||||
version = "0.1.0rc1"
|
||||
version = "0.1.0rc2"
|
||||
description = "Official command-line interface for building, bundling, and managing Apache Superset extensions"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Apache Software Foundation", email = "dev@superset.apache.org" },
|
||||
]
|
||||
license = { file="LICENSE.txt" }
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE.txt"]
|
||||
requires-python = ">=3.10"
|
||||
keywords = ["superset", "apache", "cli", "extensions", "analytics", "business-intelligence", "development-tools"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
|
||||
@@ -426,9 +426,9 @@ def bundle(ctx: click.Context, output: Path | None) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
id_ = manifest["id"]
|
||||
name = manifest["name"]
|
||||
version = manifest["version"]
|
||||
default_filename = f"{id_}-{version}.supx"
|
||||
default_filename = f"{name}-{version}.supx"
|
||||
|
||||
if output is None:
|
||||
zip_path = Path(default_filename)
|
||||
@@ -663,7 +663,7 @@ def init(
|
||||
else click.confirm("Include backend?", default=True)
|
||||
)
|
||||
|
||||
target_dir = Path.cwd() / names["id"]
|
||||
target_dir = Path.cwd() / names["name"]
|
||||
if target_dir.exists():
|
||||
click.secho(f"❌ Directory {target_dir} already exists.", fg="red")
|
||||
sys.exit(1)
|
||||
@@ -686,7 +686,7 @@ def init(
|
||||
click.secho("✅ Created extension.json", fg="green")
|
||||
|
||||
# Create .gitignore
|
||||
gitignore = env.get_template(".gitignore.j2").render(ctx)
|
||||
gitignore = env.get_template("gitignore.j2").render(ctx)
|
||||
(target_dir / ".gitignore").write_text(gitignore)
|
||||
click.secho("✅ Created .gitignore", fg="green")
|
||||
|
||||
|
||||
@@ -43,10 +43,10 @@ def test_bundle_command_creates_zip_with_default_name(
|
||||
result = cli_runner.invoke(app, ["bundle"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "✅ Bundle created: test-org.test-extension-1.0.0.supx" in result.output
|
||||
assert "✅ Bundle created: test-extension-1.0.0.supx" in result.output
|
||||
|
||||
# Verify zip file was created
|
||||
zip_path = isolated_filesystem / "test-org.test-extension-1.0.0.supx"
|
||||
zip_path = isolated_filesystem / "test-extension-1.0.0.supx"
|
||||
assert_file_exists(zip_path)
|
||||
|
||||
# Verify zip contents
|
||||
@@ -100,7 +100,7 @@ def test_bundle_command_with_output_directory(
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify zip file was created in output directory
|
||||
expected_path = output_dir / "test-org.test-extension-1.0.0.supx"
|
||||
expected_path = output_dir / "test-extension-1.0.0.supx"
|
||||
assert_file_exists(expected_path)
|
||||
assert f"✅ Bundle created: {expected_path}" in result.output
|
||||
|
||||
@@ -193,7 +193,7 @@ def test_bundle_includes_all_files_recursively(
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify zip file and contents
|
||||
zip_path = isolated_filesystem / "complex-org.complex-extension-2.1.0.supx"
|
||||
zip_path = isolated_filesystem / "complex-extension-2.1.0.supx"
|
||||
assert_file_exists(zip_path)
|
||||
|
||||
with zipfile.ZipFile(zip_path, "r") as zipf:
|
||||
|
||||
@@ -48,12 +48,12 @@ def test_init_creates_extension_with_both_frontend_and_backend(
|
||||
)
|
||||
|
||||
# Verify directory structure
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
assert_directory_exists(extension_path, "main extension directory")
|
||||
|
||||
expected_structure = create_test_extension_structure(
|
||||
isolated_filesystem,
|
||||
"test-org.test-extension",
|
||||
"test-extension",
|
||||
include_frontend=True,
|
||||
include_backend=True,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ def test_init_creates_extension_with_frontend_only(
|
||||
|
||||
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
assert_directory_exists(extension_path)
|
||||
|
||||
# Should have frontend directory and package.json
|
||||
@@ -97,7 +97,7 @@ def test_init_creates_extension_with_backend_only(
|
||||
|
||||
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
assert_directory_exists(extension_path)
|
||||
|
||||
# Should have backend directory and pyproject.toml
|
||||
@@ -120,7 +120,7 @@ def test_init_creates_extension_with_neither_frontend_nor_backend(
|
||||
|
||||
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
assert_directory_exists(extension_path)
|
||||
|
||||
# Should only have extension.json
|
||||
@@ -138,8 +138,8 @@ def test_init_accepts_valid_display_name(cli_runner, isolated_filesystem):
|
||||
result = cli_runner.invoke(app, ["init"], input=cli_input)
|
||||
|
||||
assert result.exit_code == 0, f"Should accept display name: {result.output}"
|
||||
assert Path("test-org.my-awesome-extension").exists(), (
|
||||
"Directory for generated composite ID should be created"
|
||||
assert Path("my-awesome-extension").exists(), (
|
||||
"Directory with extension name should be created"
|
||||
)
|
||||
|
||||
|
||||
@@ -152,23 +152,21 @@ def test_init_accepts_mixed_alphanumeric_name(cli_runner, isolated_filesystem):
|
||||
assert result.exit_code == 0, (
|
||||
f"Mixed alphanumeric display name should be valid: {result.output}"
|
||||
)
|
||||
assert Path("test-org.tool-123").exists(), (
|
||||
"Directory for 'test-org.tool-123' should be created"
|
||||
)
|
||||
assert Path("tool-123").exists(), "Directory for 'tool-123' should be created"
|
||||
|
||||
|
||||
@pytest.mark.cli
|
||||
@pytest.mark.parametrize(
|
||||
"display_name,expected_id",
|
||||
"display_name,expected_dir",
|
||||
[
|
||||
("Test Extension", "test-org.test-extension"),
|
||||
("My Tool v2", "test-org.my-tool-v2"),
|
||||
("Dashboard Helper", "test-org.dashboard-helper"),
|
||||
("Chart Builder Pro", "test-org.chart-builder-pro"),
|
||||
("Test Extension", "test-extension"),
|
||||
("My Tool v2", "my-tool-v2"),
|
||||
("Dashboard Helper", "dashboard-helper"),
|
||||
("Chart Builder Pro", "chart-builder-pro"),
|
||||
],
|
||||
)
|
||||
def test_init_with_various_display_names(cli_runner, display_name, expected_id):
|
||||
"""Test that init accepts various display names and generates proper IDs."""
|
||||
def test_init_with_various_display_names(cli_runner, display_name, expected_dir):
|
||||
"""Test that init accepts various display names and creates directory named after extension."""
|
||||
with cli_runner.isolated_filesystem():
|
||||
cli_input = f"{display_name}\n\ntest-org\n0.1.0\nApache-2.0\ny\ny\n"
|
||||
result = cli_runner.invoke(app, ["init"], input=cli_input)
|
||||
@@ -176,8 +174,8 @@ def test_init_with_various_display_names(cli_runner, display_name, expected_id):
|
||||
assert result.exit_code == 0, (
|
||||
f"Valid display name '{display_name}' was rejected: {result.output}"
|
||||
)
|
||||
assert Path(expected_id).exists(), (
|
||||
f"Directory for '{expected_id}' was not created"
|
||||
assert Path(expected_dir).exists(), (
|
||||
f"Directory '{expected_dir}' was not created"
|
||||
)
|
||||
|
||||
|
||||
@@ -187,7 +185,7 @@ def test_init_fails_when_directory_already_exists(
|
||||
):
|
||||
"""Test that init fails gracefully when target directory already exists."""
|
||||
# Create the directory first
|
||||
existing_dir = isolated_filesystem / "test-org.test-extension"
|
||||
existing_dir = isolated_filesystem / "test-extension"
|
||||
existing_dir.mkdir()
|
||||
|
||||
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
||||
@@ -204,7 +202,7 @@ def test_extension_json_content_is_correct(
|
||||
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
||||
assert result.exit_code == 0
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
extension_json_path = extension_path / "extension.json"
|
||||
|
||||
# Verify the JSON structure and values
|
||||
@@ -238,7 +236,7 @@ def test_frontend_package_json_content_is_correct(
|
||||
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
||||
assert result.exit_code == 0
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
package_json_path = extension_path / "frontend" / "package.json"
|
||||
|
||||
# Verify the package.json structure and values
|
||||
@@ -267,7 +265,7 @@ def test_backend_pyproject_toml_is_created(
|
||||
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
||||
assert result.exit_code == 0
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
pyproject_path = extension_path / "backend" / "pyproject.toml"
|
||||
|
||||
assert_file_exists(pyproject_path, "backend pyproject.toml")
|
||||
@@ -305,7 +303,7 @@ def test_gitignore_content_is_correct(cli_runner, isolated_filesystem, cli_input
|
||||
result = cli_runner.invoke(app, ["init"], input=cli_input_both)
|
||||
assert result.exit_code == 0
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.test-extension"
|
||||
extension_path = isolated_filesystem / "test-extension"
|
||||
gitignore_path = extension_path / ".gitignore"
|
||||
|
||||
assert_file_exists(gitignore_path, ".gitignore")
|
||||
@@ -330,7 +328,7 @@ def test_init_with_custom_version_and_license(cli_runner, isolated_filesystem):
|
||||
|
||||
assert result.exit_code == 0
|
||||
|
||||
extension_path = isolated_filesystem / "test-org.my-extension"
|
||||
extension_path = isolated_filesystem / "my-extension"
|
||||
extension_json_path = extension_path / "extension.json"
|
||||
|
||||
assert_json_content(
|
||||
@@ -357,10 +355,10 @@ def test_full_init_workflow_integration(cli_runner, isolated_filesystem):
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify complete directory structure
|
||||
extension_path = isolated_filesystem / "awesome-org.awesome-charts"
|
||||
extension_path = isolated_filesystem / "awesome-charts"
|
||||
expected_structure = create_test_extension_structure(
|
||||
isolated_filesystem,
|
||||
"awesome-org.awesome-charts",
|
||||
"awesome-charts",
|
||||
include_frontend=True,
|
||||
include_backend=True,
|
||||
)
|
||||
@@ -412,7 +410,7 @@ def test_init_non_interactive_with_all_options(cli_runner, isolated_filesystem):
|
||||
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
||||
assert "🎉 Extension My Extension (ID: my-org.my-ext) initialized" in result.output
|
||||
|
||||
extension_path = isolated_filesystem / "my-org.my-ext"
|
||||
extension_path = isolated_filesystem / "my-ext"
|
||||
assert_directory_exists(extension_path)
|
||||
assert_directory_exists(extension_path / "frontend")
|
||||
assert_directory_exists(extension_path / "backend")
|
||||
@@ -449,7 +447,7 @@ def test_init_frontend_only_with_cli_options(cli_runner, isolated_filesystem):
|
||||
|
||||
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
||||
|
||||
extension_path = isolated_filesystem / "frontend-org.frontend-ext"
|
||||
extension_path = isolated_filesystem / "frontend-ext"
|
||||
assert_directory_exists(extension_path / "frontend")
|
||||
assert not (extension_path / "backend").exists()
|
||||
|
||||
@@ -478,7 +476,7 @@ def test_init_backend_only_with_cli_options(cli_runner, isolated_filesystem):
|
||||
|
||||
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
||||
|
||||
extension_path = isolated_filesystem / "backend-org.backend-ext"
|
||||
extension_path = isolated_filesystem / "backend-ext"
|
||||
assert not (extension_path / "frontend").exists()
|
||||
assert_directory_exists(extension_path / "backend")
|
||||
|
||||
@@ -505,7 +503,7 @@ def test_init_prompts_for_missing_options(cli_runner, isolated_filesystem):
|
||||
|
||||
assert result.exit_code == 0, f"Command failed with output: {result.output}"
|
||||
|
||||
extension_path = isolated_filesystem / "default-org.default-ext"
|
||||
extension_path = isolated_filesystem / "default-ext"
|
||||
extension_json = load_json_file(extension_path / "extension.json")
|
||||
assert extension_json["version"] == "0.1.0"
|
||||
assert extension_json["license"] == "Apache-2.0"
|
||||
|
||||
18
superset-frontend/package-lock.json
generated
18
superset-frontend/package-lock.json
generated
@@ -242,6 +242,7 @@
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest-dom": "^5.5.0",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.1",
|
||||
@@ -24351,6 +24352,16 @@
|
||||
"eslint": ">=2"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-no-only-tests": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-no-only-tests/-/eslint-plugin-no-only-tests-3.3.0.tgz",
|
||||
"integrity": "sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "5.5.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz",
|
||||
@@ -51390,8 +51401,8 @@
|
||||
},
|
||||
"packages/superset-core": {
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.1.0-rc1",
|
||||
"license": "ISC",
|
||||
"version": "0.1.0-rc2",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/core": "^7.29.0",
|
||||
@@ -51434,7 +51445,8 @@
|
||||
"dependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@types/react": "*",
|
||||
"lodash": "^4.17.23"
|
||||
"lodash": "^4.17.23",
|
||||
"tinycolor2": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "superset",
|
||||
"version": "0.0.0-dev",
|
||||
"version": "6.1.0",
|
||||
"description": "Superset is a data exploration platform designed to be visual, intuitive, and interactive.",
|
||||
"keywords": [
|
||||
"big",
|
||||
@@ -323,6 +323,7 @@
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jest-dom": "^5.5.0",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-no-only-tests": "^3.3.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@apache-superset/core",
|
||||
"version": "0.1.0-rc1",
|
||||
"version": "0.1.0-rc2",
|
||||
"description": "This package contains UI elements, APIs, and utility functions used by Superset.",
|
||||
"sideEffects": false,
|
||||
"main": "lib/index.js",
|
||||
@@ -70,8 +70,8 @@
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"author": "Apache Software Foundation",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.28.6",
|
||||
"@babel/core": "^7.29.0",
|
||||
|
||||
@@ -369,6 +369,28 @@ export interface EditorProps {
|
||||
theme?: SupersetTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single text change expressed as an offset-based replacement.
|
||||
*/
|
||||
export interface ContentChange {
|
||||
/** Character offset in the document where the replaced range starts */
|
||||
rangeOffset: number;
|
||||
/** Length in characters of the replaced range (0 for pure insertions) */
|
||||
rangeLength: number;
|
||||
/** Text inserted at rangeOffset (empty string for pure deletions) */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload delivered to `onDidChangeContent` listeners.
|
||||
*/
|
||||
export interface ContentChangeEvent {
|
||||
/** Returns the full current content of the editor */
|
||||
getValue(): string;
|
||||
/** The individual changes that occurred in this event */
|
||||
changes: ReadonlyArray<ContentChange>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imperative API for controlling the editor programmatically.
|
||||
*
|
||||
@@ -492,6 +514,27 @@ export interface EditorHandle {
|
||||
* - CodeMirror: editor.requestMeasure()
|
||||
*/
|
||||
resize(): void;
|
||||
|
||||
/**
|
||||
* Subscribe to content changes in the editor.
|
||||
*
|
||||
* The listener receives a {@link ContentChangeEvent} with:
|
||||
* - `getValue()` — lazy accessor for the full content (call only when needed
|
||||
* to avoid unnecessary O(n) string allocation on every keystroke)
|
||||
* - `changes` — the individual edits that occurred, as offset-based replacements
|
||||
*
|
||||
* @param listener Called with a ContentChangeEvent on every change
|
||||
* @param thisArgs Optional `this` context for the listener
|
||||
* @returns A Disposable that unsubscribes the listener when disposed
|
||||
*
|
||||
* @example
|
||||
* const disposable = editor.onDidChangeContent(e => {
|
||||
* setStatements(parseStatements(e.getValue()));
|
||||
* });
|
||||
* // Later, to unsubscribe:
|
||||
* disposable.dispose();
|
||||
*/
|
||||
onDidChangeContent: Event<ContentChangeEvent>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -252,6 +252,22 @@ export interface QueryResult {
|
||||
*/
|
||||
export declare const getActivePanel: () => Panel;
|
||||
|
||||
/**
|
||||
* Switches the active panel in the SQL Lab south pane.
|
||||
* Built-in panel IDs are 'Results' and 'History'.
|
||||
* Pinned table panels use the table's ID as their panel ID.
|
||||
*
|
||||
* @param panelId The ID of the panel to activate
|
||||
* @returns Promise that resolves when the panel is activated
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Focus the Results panel after running a query
|
||||
* await setActivePanel('Results');
|
||||
* ```
|
||||
*/
|
||||
export declare function setActivePanel(panelId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets the currently active tab in SQL Lab.
|
||||
*
|
||||
|
||||
@@ -839,3 +839,73 @@ test('Theme includes both echartsOptionsOverrides and echartsOptionsOverridesByC
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('colorLink derives from colorPrimary when merging with base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
colorLink: '#2893B3',
|
||||
colorInfo: '#66bcfe',
|
||||
},
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#f759ab',
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
|
||||
expect(theme.theme.colorPrimary).toBe('#f759ab');
|
||||
expect(theme.theme.colorLink).toBe('#f759ab');
|
||||
expect(theme.theme.colorInfo).toBe('#66bcfe');
|
||||
});
|
||||
|
||||
test('colorLink is not overridden when user explicitly sets it', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
colorLink: '#2893B3',
|
||||
},
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#f759ab',
|
||||
colorLink: '#ff0000',
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
|
||||
expect(theme.theme.colorPrimary).toBe('#f759ab');
|
||||
expect(theme.theme.colorLink).toBe('#ff0000');
|
||||
});
|
||||
|
||||
test('colorLink derives from colorPrimary in setConfig when not explicitly set', () => {
|
||||
const theme = Theme.fromConfig();
|
||||
|
||||
theme.setConfig({
|
||||
token: {
|
||||
colorPrimary: '#f759ab',
|
||||
},
|
||||
});
|
||||
|
||||
expect(theme.theme.colorPrimary).toBe('#f759ab');
|
||||
expect(theme.theme.colorLink).toBe('#f759ab');
|
||||
});
|
||||
|
||||
test('colorLink is preserved in setConfig when explicitly set', () => {
|
||||
const theme = Theme.fromConfig();
|
||||
|
||||
theme.setConfig({
|
||||
token: {
|
||||
colorPrimary: '#f759ab',
|
||||
colorLink: '#ff0000',
|
||||
},
|
||||
});
|
||||
|
||||
expect(theme.theme.colorPrimary).toBe('#f759ab');
|
||||
expect(theme.theme.colorLink).toBe('#ff0000');
|
||||
});
|
||||
|
||||
@@ -66,6 +66,17 @@ export class Theme {
|
||||
mergedConfig = mergeWith({}, baseTheme, config, (objValue, srcValue) =>
|
||||
Array.isArray(srcValue) ? srcValue : undefined,
|
||||
);
|
||||
|
||||
// In Ant Design v5, colorLink derives from colorInfo, not colorPrimary.
|
||||
// Currently we expectlinks to follow the brand/primary color. When the user
|
||||
// overrides colorPrimary without explicitly setting colorLink, update the
|
||||
// merged colorLink so links match the new primary palette.
|
||||
if (config.token?.colorPrimary && !config.token?.colorLink) {
|
||||
const mToken = mergedConfig?.token;
|
||||
if (mToken) {
|
||||
mToken.colorLink = mToken.colorPrimary;
|
||||
}
|
||||
}
|
||||
} else if (baseTheme && !config) {
|
||||
mergedConfig = baseTheme;
|
||||
}
|
||||
@@ -98,6 +109,10 @@ export class Theme {
|
||||
setConfig(config: AnyThemeConfig): void {
|
||||
const antdConfig = normalizeThemeConfig(config);
|
||||
|
||||
if (antdConfig.token?.colorPrimary && !antdConfig.token?.colorLink) {
|
||||
antdConfig.token.colorLink = antdConfig.token.colorPrimary;
|
||||
}
|
||||
|
||||
// First phase: Let Ant Design compute the tokens
|
||||
const tokens = Theme.getFilteredAntdTheme(antdConfig);
|
||||
|
||||
|
||||
@@ -168,6 +168,55 @@ export interface SupersetSpecificTokens {
|
||||
* Defaults to colorPrimaryBgHover if not specified.
|
||||
*/
|
||||
colorEditorSelection?: string;
|
||||
|
||||
// Secondary button tokens (Superset-specific)
|
||||
// Ant Design's filled variant has no component tokens, so we provide our own.
|
||||
// These fallback to colorPrimary* derived tokens when not set.
|
||||
/**
|
||||
* Text color for secondary buttons.
|
||||
* Fallback: colorPrimary
|
||||
*/
|
||||
buttonSecondaryColor?: string;
|
||||
/**
|
||||
* Background color for secondary buttons.
|
||||
* Fallback: colorPrimaryBg
|
||||
*/
|
||||
buttonSecondaryBg?: string;
|
||||
/**
|
||||
* Border color for secondary buttons.
|
||||
* Fallback: transparent
|
||||
*/
|
||||
buttonSecondaryBorderColor?: string;
|
||||
/**
|
||||
* Text color for secondary buttons on hover.
|
||||
* Fallback: colorPrimary
|
||||
*/
|
||||
buttonSecondaryHoverColor?: string;
|
||||
/**
|
||||
* Background color for secondary buttons on hover.
|
||||
* Fallback: colorPrimaryBgHover
|
||||
*/
|
||||
buttonSecondaryHoverBg?: string;
|
||||
/**
|
||||
* Border color for secondary buttons on hover.
|
||||
* Fallback: transparent
|
||||
*/
|
||||
buttonSecondaryHoverBorderColor?: string;
|
||||
/**
|
||||
* Text color for secondary buttons when active/pressed.
|
||||
* Fallback: colorPrimary
|
||||
*/
|
||||
buttonSecondaryActiveColor?: string;
|
||||
/**
|
||||
* Background color for secondary buttons when active/pressed.
|
||||
* Fallback: colorPrimaryBorder
|
||||
*/
|
||||
buttonSecondaryActiveBg?: string;
|
||||
/**
|
||||
* Border color for secondary buttons when active/pressed.
|
||||
* Fallback: transparent
|
||||
*/
|
||||
buttonSecondaryActiveBorderColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -426,6 +475,7 @@ export interface ThemeControllerOptions {
|
||||
canUpdateTheme?: () => boolean;
|
||||
canUpdateMode?: () => boolean;
|
||||
isGlobalContext?: boolean;
|
||||
initialMode?: ThemeMode;
|
||||
}
|
||||
|
||||
export interface ThemeContextType {
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"dependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@types/react": "*",
|
||||
"lodash": "^4.17.23"
|
||||
"lodash": "^4.17.23",
|
||||
"tinycolor2": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
|
||||
@@ -59,7 +59,7 @@ export function ColumnOption({
|
||||
const type = hasExpression ? 'expression' : type_generic;
|
||||
const [tooltipText, setTooltipText] = useState<ReactNode>(column.column_name);
|
||||
const [columnTypeTooltipText, setcolumnTypeTooltipText] = useState<ReactNode>(
|
||||
column.type,
|
||||
getColumnTypeTooltipNode(column),
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -21,7 +21,11 @@ import { styled, css } from '@apache-superset/core/theme';
|
||||
export const ControlSubSectionHeader = styled.div`
|
||||
${({ theme }) => css`
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
margin-top: ${theme.sizeUnit * 3}px;
|
||||
margin-bottom: ${theme.sizeUnit}px;
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${theme.colorTextSecondary};
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ReactNode, RefObject } from 'react';
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { css, styled } from '@apache-superset/core/theme';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
|
||||
|
||||
const TooltipSectionWrapper = styled.div`
|
||||
@@ -64,11 +65,29 @@ export const getColumnLabelText = (column: ColumnMeta): string =>
|
||||
column.verbose_name || column.column_name;
|
||||
|
||||
export const getColumnTypeTooltipNode = (column: ColumnMeta): ReactNode => {
|
||||
if (!column.type) {
|
||||
const rawType = typeof column.type === 'string' ? column.type.trim() : '';
|
||||
|
||||
let typeLabel: ReactNode | null = null;
|
||||
|
||||
if (rawType && rawType.toLowerCase() !== 'column') {
|
||||
typeLabel = rawType;
|
||||
} else if (typeof column.type_generic === 'number') {
|
||||
if (column.type_generic === GenericDataType.String) {
|
||||
typeLabel = t('string');
|
||||
} else if (column.type_generic === GenericDataType.Numeric) {
|
||||
typeLabel = t('numeric');
|
||||
} else if (column.type_generic === GenericDataType.Temporal) {
|
||||
typeLabel = t('timestamp');
|
||||
} else if (column.type_generic === GenericDataType.Boolean) {
|
||||
typeLabel = t('boolean');
|
||||
}
|
||||
}
|
||||
|
||||
if (!typeLabel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <TooltipSection label={t('Column type')} text={column.type} />;
|
||||
return <TooltipSection label={t('Column type')} text={typeLabel} />;
|
||||
};
|
||||
|
||||
export const getColumnTooltipNode = (
|
||||
|
||||
@@ -28,7 +28,9 @@ export const getTimeOffset = (
|
||||
// offset is represented as <offset>, group by list
|
||||
series.name.includes(`${timeOffset},`) ||
|
||||
// offset is represented as <metric>__<offset>
|
||||
series.name.includes(`__${timeOffset}`),
|
||||
series.name.includes(`__${timeOffset}`) ||
|
||||
// offset is represented as <metric>, <offset>
|
||||
series.name.includes(`, ${timeOffset}`),
|
||||
);
|
||||
|
||||
export const hasTimeOffset = (
|
||||
@@ -45,10 +47,14 @@ export const getOriginalSeries = (
|
||||
): string => {
|
||||
let result = seriesName;
|
||||
timeCompare.forEach(compare => {
|
||||
// offset is represented as <offset>, group by list
|
||||
// offset in the middle: <metric>, <offset>, <dimension>
|
||||
result = result.replace(`, ${compare},`, ',');
|
||||
// offset at start: <offset>, <dimension>
|
||||
result = result.replace(`${compare},`, '');
|
||||
// offset is represented as <metric>__<offset>
|
||||
// offset with double underscore: <metric>__<offset>
|
||||
result = result.replace(`__${compare}`, '');
|
||||
// offset at end: <metric>, <offset>
|
||||
result = result.replace(`, ${compare}`, '');
|
||||
});
|
||||
return result.trim();
|
||||
};
|
||||
|
||||
@@ -25,30 +25,17 @@ export const matrixifyEnableSection: ControlPanelSectionConfig = {
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'matrixify_enable_horizontal_layout',
|
||||
name: 'matrixify_enable',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Enable horizontal layout (columns)'),
|
||||
description: t(
|
||||
'Create matrix columns by placing charts side-by-side',
|
||||
),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'matrixify_enable_vertical_layout',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Enable vertical layout (rows)'),
|
||||
description: t('Create matrix rows by stacking charts vertically'),
|
||||
type: 'SwitchControl',
|
||||
label: t('Enable matrixify'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
['matrixify_mode_columns'],
|
||||
['matrixify_mode_rows'],
|
||||
],
|
||||
tabOverride: 'matrixify',
|
||||
};
|
||||
@@ -57,8 +44,11 @@ export const matrixifySection: ControlPanelSectionConfig = {
|
||||
label: t('Cell layout & styling'),
|
||||
expanded: false,
|
||||
visibility: ({ controls }) =>
|
||||
controls?.matrixify_enable_vertical_layout?.value === true ||
|
||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
||||
controls?.matrixify_enable?.value === true &&
|
||||
(controls?.matrixify_mode_rows?.value === 'metrics' ||
|
||||
controls?.matrixify_mode_rows?.value === 'dimensions' ||
|
||||
controls?.matrixify_mode_columns?.value === 'metrics' ||
|
||||
controls?.matrixify_mode_columns?.value === 'dimensions'),
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
@@ -119,13 +109,13 @@ export const matrixifySection: ControlPanelSectionConfig = {
|
||||
};
|
||||
|
||||
export const matrixifyRowSection: ControlPanelSectionConfig = {
|
||||
label: t('Vertical layout (rows)'),
|
||||
expanded: false,
|
||||
visibility: ({ controls }) =>
|
||||
controls?.matrixify_enable_vertical_layout?.value === true,
|
||||
controls?.matrixify_enable?.value === true &&
|
||||
(controls?.matrixify_mode_rows?.value === 'metrics' ||
|
||||
controls?.matrixify_mode_rows?.value === 'dimensions'),
|
||||
controlSetRows: [
|
||||
['matrixify_show_row_labels'],
|
||||
['matrixify_mode_rows'],
|
||||
['matrixify_rows'],
|
||||
['matrixify_dimension_rows'],
|
||||
['matrixify_dimension_selection_mode_rows'],
|
||||
@@ -137,13 +127,13 @@ export const matrixifyRowSection: ControlPanelSectionConfig = {
|
||||
};
|
||||
|
||||
export const matrixifyColumnSection: ControlPanelSectionConfig = {
|
||||
label: t('Horizontal layout (columns)'),
|
||||
expanded: false,
|
||||
visibility: ({ controls }) =>
|
||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
||||
controls?.matrixify_enable?.value === true &&
|
||||
(controls?.matrixify_mode_columns?.value === 'metrics' ||
|
||||
controls?.matrixify_mode_columns?.value === 'dimensions'),
|
||||
controlSetRows: [
|
||||
['matrixify_show_column_headers'],
|
||||
['matrixify_mode_columns'],
|
||||
['matrixify_columns'],
|
||||
['matrixify_dimension_columns'],
|
||||
['matrixify_dimension_selection_mode_columns'],
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { isMatrixifyVisible } from './matrixifyControls';
|
||||
|
||||
/**
|
||||
* Helper to build a controls object matching the shape used by
|
||||
* control panel visibility callbacks.
|
||||
*/
|
||||
function makeControls(
|
||||
overrides: Record<string, unknown> = {},
|
||||
): Record<string, { value: unknown }> {
|
||||
const defaults: Record<string, unknown> = {
|
||||
matrixify_enable: false,
|
||||
matrixify_mode_rows: 'disabled',
|
||||
matrixify_mode_columns: 'disabled',
|
||||
matrixify_dimension_selection_mode_rows: 'members',
|
||||
matrixify_dimension_selection_mode_columns: 'members',
|
||||
};
|
||||
const merged = { ...defaults, ...overrides };
|
||||
return Object.fromEntries(
|
||||
Object.entries(merged).map(([k, v]) => [k, { value: v }]),
|
||||
);
|
||||
}
|
||||
|
||||
// ── matrixify_enable guard ──────────────────────────────────────────
|
||||
|
||||
test('returns false when matrixify_enable is false, even with active axis modes', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: false,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'rows')).toBe(false);
|
||||
expect(isMatrixifyVisible(controls, 'columns')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when matrixify_enable is undefined (old form_data without the field)', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_mode_rows: 'metrics',
|
||||
});
|
||||
delete (controls as any).matrixify_enable;
|
||||
expect(isMatrixifyVisible(controls, 'rows')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false when controls object is undefined', () => {
|
||||
expect(isMatrixifyVisible(undefined, 'rows')).toBe(false);
|
||||
});
|
||||
|
||||
// ── axis mode checks ────────────────────────────────────────────────
|
||||
|
||||
test('returns false when axis mode is disabled', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'disabled',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'rows')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true when matrixify_enable is true and axis mode is metrics', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'rows')).toBe(true);
|
||||
});
|
||||
|
||||
test('returns true when matrixify_enable is true and axis mode is dimensions', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'columns')).toBe(true);
|
||||
});
|
||||
|
||||
// ── mode filter ─────────────────────────────────────────────────────
|
||||
|
||||
test('returns false when mode filter does not match axis value', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'rows', 'dimensions')).toBe(false);
|
||||
});
|
||||
|
||||
test('returns true when mode filter matches axis value', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'rows', 'dimensions')).toBe(true);
|
||||
});
|
||||
|
||||
// ── selectionMode filter ────────────────────────────────────────────
|
||||
|
||||
test('returns true when selectionMode matches', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_dimension_selection_mode_rows: 'topn',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'rows', 'dimensions', 'topn')).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when selectionMode does not match', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_dimension_selection_mode_rows: 'members',
|
||||
});
|
||||
expect(isMatrixifyVisible(controls, 'rows', 'dimensions', 'topn')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores selectionMode filter when mode is metrics', () => {
|
||||
const controls = makeControls({
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_columns: 'metrics',
|
||||
});
|
||||
// selectionMode only applies to dimensions mode, should be ignored
|
||||
expect(isMatrixifyVisible(controls, 'columns', 'metrics', 'topn')).toBe(true);
|
||||
});
|
||||
@@ -34,19 +34,20 @@ const isMatrixifyVisible = (
|
||||
controls: any,
|
||||
axis: 'rows' | 'columns',
|
||||
mode?: 'metrics' | 'dimensions',
|
||||
selectionMode?: 'members' | 'topn',
|
||||
selectionMode?: 'members' | 'topn' | 'all',
|
||||
) => {
|
||||
const layoutControl = `matrixify_enable_${axis === 'rows' ? 'vertical' : 'horizontal'}_layout`;
|
||||
if (controls?.matrixify_enable?.value !== true) return false;
|
||||
|
||||
const modeControl = `matrixify_mode_${axis}`;
|
||||
const selectionModeControl = `matrixify_dimension_selection_mode_${axis}`;
|
||||
|
||||
const isLayoutEnabled = controls?.[layoutControl]?.value === true;
|
||||
const modeValue = controls?.[modeControl]?.value;
|
||||
const isLayoutEnabled = modeValue === 'metrics' || modeValue === 'dimensions';
|
||||
|
||||
if (!isLayoutEnabled) return false;
|
||||
|
||||
if (mode) {
|
||||
const isModeMatch = controls?.[modeControl]?.value === mode;
|
||||
if (!isModeMatch) return false;
|
||||
if (modeValue !== mode) return false;
|
||||
|
||||
if (selectionMode && mode === 'dimensions') {
|
||||
return controls?.[selectionModeControl]?.value === selectionMode;
|
||||
@@ -66,22 +67,20 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
|
||||
matrixifyControls[`matrixify_mode_${axis}`] = {
|
||||
type: 'RadioButtonControl',
|
||||
label: t(`Metrics / Dimensions`),
|
||||
default: axis === 'columns' ? 'metrics' : 'dimensions',
|
||||
default: 'disabled',
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) => isMatrixifyVisible(controls, axis),
|
||||
mapStateToProps: ({ controls }) => {
|
||||
const otherAxisControlName = `matrixify_mode_${otherAxis}`;
|
||||
|
||||
const otherAxisValue =
|
||||
controls?.[otherAxisControlName]?.value ??
|
||||
(otherAxis === 'columns' ? 'metrics' : 'dimensions');
|
||||
controls?.[otherAxisControlName]?.value ?? 'disabled';
|
||||
|
||||
const isMetricsDisabled = otherAxisValue === 'metrics';
|
||||
|
||||
return {
|
||||
options: [
|
||||
{ value: 'disabled', label: t('Disabled') },
|
||||
{
|
||||
value: 'metrics',
|
||||
label: t('Metrics'),
|
||||
@@ -92,7 +91,7 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
{ value: 'dimensions', label: t('Dimension members') },
|
||||
{ value: 'dimensions', label: t('Dimensions') },
|
||||
],
|
||||
};
|
||||
},
|
||||
@@ -125,6 +124,7 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
`matrixify_topn_metric_${axis}`,
|
||||
`matrixify_topn_order_${axis}`,
|
||||
`matrixify_dimension_selection_mode_${axis}`,
|
||||
`matrixify_all_sort_by_${axis}`,
|
||||
];
|
||||
|
||||
return fieldsToCheck.some(
|
||||
@@ -161,7 +161,10 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
selectionMode,
|
||||
topNMetric: getValue(`matrixify_topn_metric_${axis}`),
|
||||
topNValue: getValue(`matrixify_topn_value_${axis}`),
|
||||
topNOrder: getValue(`matrixify_topn_order_${axis}`),
|
||||
topNOrder: getValue(`matrixify_topn_order_${axis}`, true)
|
||||
? 'DESC'
|
||||
: 'ASC',
|
||||
allSortBy: getValue(`matrixify_all_sort_by_${axis}`, 'a_to_z'),
|
||||
formData: form_data,
|
||||
validators,
|
||||
};
|
||||
@@ -187,19 +190,24 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
visibility: () => false,
|
||||
};
|
||||
|
||||
// Add selection mode control (Dimension Members vs TopN)
|
||||
// Add selection mode control (Dimension Members / Top N / All)
|
||||
matrixifyControls[`matrixify_dimension_selection_mode_${axis}`] = {
|
||||
type: 'RadioButtonControl',
|
||||
type: 'VerticalRadioControl',
|
||||
label: t(`Selection method`),
|
||||
default: 'members',
|
||||
options: [
|
||||
['members', t('Dimension members')],
|
||||
['topn', t('Top n')],
|
||||
],
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) =>
|
||||
isMatrixifyVisible(controls, axis, 'dimensions'),
|
||||
options: [
|
||||
{ value: 'members', label: t('Dimension members') },
|
||||
{ value: 'topn', label: t('Top n') },
|
||||
{
|
||||
value: 'all',
|
||||
label: t('All dimensions'),
|
||||
tooltip: t('Uses the first 25 values if the dimension has more.'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// TopN controls
|
||||
@@ -236,15 +244,15 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
description: t(`Metric to use for ordering Top N values`),
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) =>
|
||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
|
||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn') ||
|
||||
(isMatrixifyVisible(controls, axis, 'dimensions', 'all') &&
|
||||
controls?.[`matrixify_all_sort_by_${axis}`]?.value === 'metric'),
|
||||
mapStateToProps: (state, controlState) => {
|
||||
const { controls, datasource } = state;
|
||||
const isVisible = isMatrixifyVisible(
|
||||
controls,
|
||||
axis,
|
||||
'dimensions',
|
||||
'topn',
|
||||
);
|
||||
const isVisible =
|
||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn') ||
|
||||
(isMatrixifyVisible(controls, axis, 'dimensions', 'all') &&
|
||||
controls?.[`matrixify_all_sort_by_${axis}`]?.value === 'metric');
|
||||
|
||||
const originalProps =
|
||||
dndAdhocMetricControl.mapStateToProps?.(state, controlState) || {};
|
||||
@@ -261,17 +269,31 @@ const matrixifyControls: Record<string, SharedControlConfig<any>> = {};
|
||||
};
|
||||
|
||||
matrixifyControls[`matrixify_topn_order_${axis}`] = {
|
||||
type: 'RadioButtonControl',
|
||||
label: t(`Sort order`),
|
||||
default: 'desc',
|
||||
options: [
|
||||
['asc', t('Ascending')],
|
||||
['desc', t('Descending')],
|
||||
],
|
||||
type: 'CheckboxControl',
|
||||
label: t('Sort descending'),
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) =>
|
||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn'),
|
||||
isMatrixifyVisible(controls, axis, 'dimensions', 'topn') ||
|
||||
(isMatrixifyVisible(controls, axis, 'dimensions', 'all') &&
|
||||
controls?.[`matrixify_all_sort_by_${axis}`]?.value === 'metric'),
|
||||
};
|
||||
|
||||
matrixifyControls[`matrixify_all_sort_by_${axis}`] = {
|
||||
type: 'SelectControl',
|
||||
label: t('Sort by'),
|
||||
default: 'a_to_z',
|
||||
clearable: false,
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) =>
|
||||
isMatrixifyVisible(controls, axis, 'dimensions', 'all'),
|
||||
choices: [
|
||||
['a_to_z', t('A-Z')],
|
||||
['z_to_a', t('Z-A')],
|
||||
['metric', t('Metric')],
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -317,24 +339,6 @@ matrixifyControls.matrixify_charts_per_row = {
|
||||
!controls?.matrixify_fit_columns_dynamically?.value,
|
||||
};
|
||||
|
||||
matrixifyControls.matrixify_enable_vertical_layout = {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Enable vertical layout (rows)'),
|
||||
description: t('Create matrix rows by stacking charts vertically'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
};
|
||||
|
||||
matrixifyControls.matrixify_enable_horizontal_layout = {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Enable horizontal layout (columns)'),
|
||||
description: t('Create matrix columns by placing charts side-by-side'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
};
|
||||
|
||||
// Cell title control for Matrixify
|
||||
matrixifyControls.matrixify_cell_title_template = {
|
||||
type: 'TextControl',
|
||||
@@ -345,8 +349,8 @@ matrixifyControls.matrixify_cell_title_template = {
|
||||
default: '',
|
||||
renderTrigger: true,
|
||||
visibility: ({ controls }) =>
|
||||
controls?.matrixify_enable_vertical_layout?.value === true ||
|
||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
||||
isMatrixifyVisible(controls, 'rows') ||
|
||||
isMatrixifyVisible(controls, 'columns'),
|
||||
};
|
||||
|
||||
// Matrix display controls
|
||||
@@ -357,8 +361,7 @@ matrixifyControls.matrixify_show_row_labels = {
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) =>
|
||||
controls?.matrixify_enable_vertical_layout?.value === true,
|
||||
visibility: ({ controls }) => isMatrixifyVisible(controls, 'rows'),
|
||||
};
|
||||
|
||||
matrixifyControls.matrixify_show_column_headers = {
|
||||
@@ -368,8 +371,7 @@ matrixifyControls.matrixify_show_column_headers = {
|
||||
default: true,
|
||||
renderTrigger: true,
|
||||
tabOverride: 'matrixify',
|
||||
visibility: ({ controls }) =>
|
||||
controls?.matrixify_enable_horizontal_layout?.value === true,
|
||||
visibility: ({ controls }) => isMatrixifyVisible(controls, 'columns'),
|
||||
};
|
||||
|
||||
export { matrixifyControls };
|
||||
export { matrixifyControls, isMatrixifyVisible };
|
||||
|
||||
@@ -507,6 +507,11 @@ export type ColorFormatters = {
|
||||
) => string | undefined;
|
||||
}[];
|
||||
|
||||
export type ResolvedColorFormatterResult = {
|
||||
backgroundColor?: string;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export default {};
|
||||
|
||||
export function isColumnMeta(column: AnyDict): column is ColumnMeta {
|
||||
|
||||
@@ -20,11 +20,13 @@ import memoizeOne from 'memoize-one';
|
||||
import { isString, isBoolean } from 'lodash';
|
||||
import { isBlank } from '@apache-superset/core/utils';
|
||||
import { addAlpha, DataRecord } from '@superset-ui/core';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {
|
||||
ColorFormatters,
|
||||
Comparator,
|
||||
ConditionalFormattingConfig,
|
||||
MultipleValueComparators,
|
||||
ResolvedColorFormatterResult,
|
||||
} from '../types';
|
||||
|
||||
export const round = (num: number, precision = 0) =>
|
||||
@@ -33,6 +35,11 @@ export const round = (num: number, precision = 0) =>
|
||||
const MIN_OPACITY_BOUNDED = 0.05;
|
||||
const MIN_OPACITY_UNBOUNDED = 0;
|
||||
const MAX_OPACITY = 1;
|
||||
const READABLE_TEXT_COLORS = [
|
||||
{ r: 0, g: 0, b: 0 },
|
||||
{ r: 255, g: 255, b: 255 },
|
||||
];
|
||||
|
||||
export const getOpacity = (
|
||||
value: number | string | boolean | null,
|
||||
cutoffPoint: number | string,
|
||||
@@ -325,3 +332,59 @@ export const getColorFormatters = memoizeOne(
|
||||
[],
|
||||
) ?? [],
|
||||
);
|
||||
|
||||
export const getReadableTextColor = (
|
||||
backgroundColor: string | undefined,
|
||||
surfaceColor: string,
|
||||
): string | undefined => {
|
||||
if (!backgroundColor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const background = tinycolor(backgroundColor);
|
||||
const surface = tinycolor(surfaceColor);
|
||||
|
||||
if (!background.isValid() || !surface.isValid()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { r: bgR, g: bgG, b: bgB, a: bgAlpha } = background.toRgb();
|
||||
const { r: surfaceR, g: surfaceG, b: surfaceB } = surface.toRgb();
|
||||
const alpha = bgAlpha;
|
||||
|
||||
const compositeColor = tinycolor({
|
||||
r: bgR * alpha + surfaceR * (1 - alpha),
|
||||
g: bgG * alpha + surfaceG * (1 - alpha),
|
||||
b: bgB * alpha + surfaceB * (1 - alpha),
|
||||
});
|
||||
|
||||
return tinycolor
|
||||
.mostReadable(compositeColor, READABLE_TEXT_COLORS, {
|
||||
includeFallbackColors: true,
|
||||
level: 'AA',
|
||||
size: 'small',
|
||||
})
|
||||
.toRgbString();
|
||||
};
|
||||
|
||||
export const getNormalizedTextColor = (
|
||||
color: string | undefined,
|
||||
): string | undefined => {
|
||||
if (!color) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsedColor = tinycolor(color);
|
||||
if (!parsedColor.isValid()) {
|
||||
return color;
|
||||
}
|
||||
|
||||
return parsedColor.setAlpha(1).toRgbString();
|
||||
};
|
||||
|
||||
export const getTextColorForBackground = (
|
||||
result: ResolvedColorFormatterResult,
|
||||
surfaceColor: string,
|
||||
): string | undefined =>
|
||||
getNormalizedTextColor(result.color) ??
|
||||
getReadableTextColor(result.backgroundColor, surfaceColor);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getMetricTooltipNode,
|
||||
getColumnTypeTooltipNode,
|
||||
} from '../../src/components/labelUtils';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
|
||||
test("should get column name when column doesn't have verbose_name", () => {
|
||||
expect(
|
||||
@@ -89,6 +90,24 @@ test('should get column datatype rendered as tooltip when column has a type', ()
|
||||
expect(screen.getByText('text')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should fall back to generic data type label when type is "column"', () => {
|
||||
render(
|
||||
<>
|
||||
{getColumnTypeTooltipNode({
|
||||
id: 123,
|
||||
column_name: 'column name',
|
||||
verbose_name: '',
|
||||
description: '',
|
||||
type: 'column',
|
||||
type_generic: GenericDataType.String,
|
||||
})}
|
||||
</>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Column type')).toBeVisible();
|
||||
expect(screen.getByText('string')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should get column name, verbose name and description when it has a verbose name', () => {
|
||||
const ref = { current: { scrollWidth: 100, clientWidth: 100 } };
|
||||
render(
|
||||
|
||||
@@ -16,15 +16,101 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { getOriginalSeries } from '@superset-ui/chart-controls';
|
||||
import {
|
||||
getOriginalSeries,
|
||||
getTimeOffset,
|
||||
hasTimeOffset,
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
test('returns the series name when time compare is empty', () => {
|
||||
test('getOriginalSeries returns the series name when time compare is empty', () => {
|
||||
const seriesName = 'sum';
|
||||
expect(getOriginalSeries(seriesName, [])).toEqual(seriesName);
|
||||
});
|
||||
|
||||
test('returns the original series name', () => {
|
||||
test('getOriginalSeries returns the original series name with __ pattern', () => {
|
||||
const seriesName = 'sum__1_month_ago';
|
||||
const timeCompare = ['1_month_ago'];
|
||||
expect(getOriginalSeries(seriesName, timeCompare)).toEqual('sum');
|
||||
});
|
||||
|
||||
test('getOriginalSeries returns the original series name with <offset>, pattern', () => {
|
||||
const seriesName = '1 year ago, groupby_value';
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(getOriginalSeries(seriesName, timeCompare)).toEqual('groupby_value');
|
||||
});
|
||||
|
||||
test('getOriginalSeries returns the original series name with , <offset> pattern', () => {
|
||||
const seriesName = 'AVG(price_each), 1 year ago';
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(getOriginalSeries(seriesName, timeCompare)).toEqual('AVG(price_each)');
|
||||
});
|
||||
|
||||
test('getOriginalSeries handles multiple time compares', () => {
|
||||
const seriesName = 'count, 1 year ago';
|
||||
const timeCompare = ['1 month ago', '1 year ago'];
|
||||
expect(getOriginalSeries(seriesName, timeCompare)).toEqual('count');
|
||||
});
|
||||
|
||||
test('getOriginalSeries strips offset in the middle with dimension', () => {
|
||||
const seriesName = 'SUM(sales), 28 days ago, Medium';
|
||||
const timeCompare = ['28 days ago'];
|
||||
expect(getOriginalSeries(seriesName, timeCompare)).toEqual(
|
||||
'SUM(sales), Medium',
|
||||
);
|
||||
});
|
||||
|
||||
test('getOriginalSeries strips offset in the middle with multiple dimensions', () => {
|
||||
const seriesName = 'SUM(sales), 1 year ago, Medium, 11';
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(getOriginalSeries(seriesName, timeCompare)).toEqual(
|
||||
'SUM(sales), Medium, 11',
|
||||
);
|
||||
});
|
||||
|
||||
test('getTimeOffset returns undefined when no time offset pattern matches', () => {
|
||||
const series = { name: 'count' };
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(getTimeOffset(series, timeCompare)).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getTimeOffset detects __ pattern', () => {
|
||||
const series = { name: 'count__1 year ago' };
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(getTimeOffset(series, timeCompare)).toEqual('1 year ago');
|
||||
});
|
||||
|
||||
test('getTimeOffset detects <offset>, pattern', () => {
|
||||
const series = { name: '1 year ago, groupby_value' };
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(getTimeOffset(series, timeCompare)).toEqual('1 year ago');
|
||||
});
|
||||
|
||||
test('getTimeOffset detects , <offset> pattern', () => {
|
||||
const series = { name: 'AVG(price_each), 1 year ago' };
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(getTimeOffset(series, timeCompare)).toEqual('1 year ago');
|
||||
});
|
||||
|
||||
test('getTimeOffset detects , <offset>, pattern (offset in middle)', () => {
|
||||
const series = { name: 'SUM(sales), 28 days ago, Medium' };
|
||||
const timeCompare = ['28 days ago'];
|
||||
expect(getTimeOffset(series, timeCompare)).toEqual('28 days ago');
|
||||
});
|
||||
|
||||
test('hasTimeOffset returns false for original series', () => {
|
||||
const series = { name: 'count' };
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(hasTimeOffset(series, timeCompare)).toBe(false);
|
||||
});
|
||||
|
||||
test('hasTimeOffset returns true for derived series with , <offset> pattern', () => {
|
||||
const series = { name: 'AVG(price_each), 1 year ago' };
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(hasTimeOffset(series, timeCompare)).toBe(true);
|
||||
});
|
||||
|
||||
test('hasTimeOffset returns false when series name is not a string', () => {
|
||||
const series = { name: 123 };
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(hasTimeOffset(series, timeCompare)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -24,6 +24,11 @@ import {
|
||||
getColorFormatters,
|
||||
getColorFunction,
|
||||
} from '../../src';
|
||||
import {
|
||||
getReadableTextColor,
|
||||
getNormalizedTextColor,
|
||||
getTextColorForBackground,
|
||||
} from '../../src/utils/getColorFormatters';
|
||||
|
||||
configure();
|
||||
const mockData = [
|
||||
@@ -107,6 +112,64 @@ test('getColorFunction LESS_THAN', () => {
|
||||
expect(colorFunction(50)).toEqual('#FF0000FF');
|
||||
});
|
||||
|
||||
test('getReadableTextColor returns white for dark backgrounds', () => {
|
||||
expect(getReadableTextColor('#111111', '#ffffff')).toBe('rgb(255, 255, 255)');
|
||||
});
|
||||
|
||||
test('getReadableTextColor returns black for light backgrounds', () => {
|
||||
expect(getReadableTextColor('#f5f5f5', '#ffffff')).toBe('rgb(0, 0, 0)');
|
||||
});
|
||||
|
||||
test('getReadableTextColor blends alpha over the provided surface', () => {
|
||||
expect(getReadableTextColor('rgba(0, 0, 0, 0.6)', '#ffffff')).toBe(
|
||||
'rgb(255, 255, 255)',
|
||||
);
|
||||
expect(getReadableTextColor('rgba(255, 255, 255, 0.6)', '#000000')).toBe(
|
||||
'rgb(0, 0, 0)',
|
||||
);
|
||||
});
|
||||
|
||||
test('getReadableTextColor returns undefined for invalid colors', () => {
|
||||
expect(getReadableTextColor('not-a-color', '#ffffff')).toBeUndefined();
|
||||
expect(getReadableTextColor('#111111', 'not-a-color')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getTextColorForBackground prefers explicit text color', () => {
|
||||
expect(
|
||||
getTextColorForBackground(
|
||||
{ backgroundColor: '#111111', color: '#ace1c4ff' },
|
||||
'#ffffff',
|
||||
),
|
||||
).toBe('rgb(172, 225, 196)');
|
||||
});
|
||||
|
||||
test('getNormalizedTextColor removes alpha from explicit text colors', () => {
|
||||
expect(getNormalizedTextColor('#ace1c40d')).toBe('rgb(172, 225, 196)');
|
||||
expect(getNormalizedTextColor('rgba(172, 225, 196, 0.2)')).toBe(
|
||||
'rgb(172, 225, 196)',
|
||||
);
|
||||
});
|
||||
|
||||
test('getNormalizedTextColor preserves invalid explicit text colors', () => {
|
||||
expect(getNormalizedTextColor('not-a-color')).toBe('not-a-color');
|
||||
});
|
||||
|
||||
test('getTextColorForBackground normalizes explicit text color alpha', () => {
|
||||
expect(
|
||||
getTextColorForBackground(
|
||||
{ backgroundColor: '#111111', color: '#ace1c40d' },
|
||||
'#ffffff',
|
||||
),
|
||||
).toBe('rgb(172, 225, 196)');
|
||||
});
|
||||
|
||||
test('getTextColorForBackground falls back to adaptive contrast', () => {
|
||||
expect(
|
||||
getTextColorForBackground({ backgroundColor: '#111111' }, '#ffffff'),
|
||||
).toBe('rgb(255, 255, 255)');
|
||||
expect(getTextColorForBackground({}, '#ffffff')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getColorFunction GREATER_OR_EQUAL', () => {
|
||||
const colorFunction = getColorFunction(
|
||||
{
|
||||
|
||||
@@ -39,6 +39,7 @@ const createSqlMetric = (label: string, sql: string): AdhocMetric => ({
|
||||
const baseFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createAdhocMetric('Revenue'), createAdhocMetric('Profit')],
|
||||
@@ -77,6 +78,7 @@ test('should generate grid for dimensions mode', () => {
|
||||
const dimensionFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: {
|
||||
@@ -117,6 +119,7 @@ test('should generate grid for mixed mode (metrics rows, dimensions columns)', (
|
||||
const mixedFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_rows: [createAdhocMetric('Total Sales')],
|
||||
@@ -139,6 +142,7 @@ test('should handle empty configuration', () => {
|
||||
const emptyFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [],
|
||||
@@ -157,6 +161,7 @@ test('should handle single row and column', () => {
|
||||
const singleCellFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createAdhocMetric('Count')],
|
||||
@@ -177,6 +182,7 @@ test('should handle string metrics', () => {
|
||||
const stringMetricFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: ['count', 'sum'],
|
||||
@@ -190,10 +196,30 @@ test('should handle string metrics', () => {
|
||||
expect(grid!.colHeaders).toEqual(['avg', 'max']);
|
||||
});
|
||||
|
||||
test('should skip missing column metrics when generating cell form data', () => {
|
||||
const missingColumnMetricFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createAdhocMetric('Revenue')],
|
||||
matrixify_columns: [null],
|
||||
};
|
||||
|
||||
const grid = generateMatrixifyGrid(missingColumnMetricFormData);
|
||||
|
||||
expect(grid).not.toBeNull();
|
||||
expect(grid!.cells[0][0]!.formData.metrics).toEqual([
|
||||
createAdhocMetric('Revenue'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should not escape HTML entities in cell titles', () => {
|
||||
const formDataWithSpecialChars: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createAdhocMetric('Sales & Revenue')],
|
||||
@@ -309,6 +335,7 @@ test('should generate single-column grid when only rows are configured', () => {
|
||||
const rowsOnlyFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [createAdhocMetric('Revenue'), createAdhocMetric('Profit')],
|
||||
// No column config
|
||||
@@ -326,6 +353,7 @@ test('should generate single-row grid when only columns are configured', () => {
|
||||
const colsOnlyFormData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_columns: [
|
||||
createSqlMetric('Q1', 'SUM(q1)'),
|
||||
@@ -359,6 +387,7 @@ test('should return empty string header for null metric in array (line 76)', ()
|
||||
const formData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [null],
|
||||
@@ -373,6 +402,7 @@ test('should return empty string header for empty-string dimension value (line 8
|
||||
const formData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: { dimension: 'country', values: [''] },
|
||||
@@ -387,6 +417,7 @@ test('should skip dimension filter when value is undefined (lines 151, 165)', ()
|
||||
const formData: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: {
|
||||
@@ -418,6 +449,7 @@ test('should handle metrics without labels', () => {
|
||||
const metricsWithoutLabels: TestFormData = {
|
||||
viz_type: 'table',
|
||||
datasource: '1__table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [
|
||||
@@ -438,3 +470,23 @@ test('should handle metrics without labels', () => {
|
||||
expect(grid!.rowHeaders).toEqual(['']);
|
||||
expect(grid!.colHeaders).toEqual(['count']);
|
||||
});
|
||||
|
||||
test('should preserve slice_id and dashboardId for embedded dashboard permissions', () => {
|
||||
const formDataWithDashboardContext: TestFormData = {
|
||||
...baseFormData,
|
||||
slice_id: 42,
|
||||
dashboardId: 123,
|
||||
};
|
||||
|
||||
const grid = generateMatrixifyGrid(formDataWithDashboardContext);
|
||||
|
||||
expect(grid).not.toBeNull();
|
||||
const cell = grid!.cells[0][0];
|
||||
|
||||
// slice_id must be preserved for embedded dashboard permission checks
|
||||
// The backend uses slice_id to verify the chart belongs to the dashboard
|
||||
expect(cell!.formData.slice_id).toBe(42);
|
||||
|
||||
// dashboardId must be preserved for embedded dashboard context
|
||||
expect(cell!.formData.dashboardId).toBe(123);
|
||||
});
|
||||
|
||||
@@ -125,9 +125,9 @@ function generateCellFormData(
|
||||
});
|
||||
|
||||
// Override fields that could cause issues in grid cells
|
||||
// Note: slice_id is intentionally preserved for embedded dashboard permission checks
|
||||
const overrides: Partial<QueryFormData> = {
|
||||
slice_name: undefined,
|
||||
slice_id: undefined,
|
||||
header_font_size: undefined,
|
||||
subheader: undefined,
|
||||
show_title: undefined,
|
||||
|
||||
@@ -22,6 +22,7 @@ import '@testing-library/jest-dom';
|
||||
import { ThemeProvider } from '@apache-superset/core/theme';
|
||||
import { supersetTheme } from '@apache-superset/core/theme';
|
||||
import MatrixifyGridRenderer from './MatrixifyGridRenderer';
|
||||
import type { MatrixifyMode } from '../../types/matrixify';
|
||||
import { generateMatrixifyGrid } from './MatrixifyGridGenerator';
|
||||
|
||||
// Mock the MatrixifyGridGenerator
|
||||
@@ -74,8 +75,9 @@ test('should create single group when fitting columns dynamically', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_fit_columns_dynamically: true,
|
||||
matrixify_charts_per_row: 3,
|
||||
matrixify_show_row_labels: true,
|
||||
@@ -124,8 +126,9 @@ test('should create multiple groups when not fitting columns dynamically', () =>
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_fit_columns_dynamically: false,
|
||||
matrixify_charts_per_row: 3,
|
||||
matrixify_show_row_labels: true,
|
||||
@@ -160,8 +163,9 @@ test('should handle exact division of columns', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_fit_columns_dynamically: false,
|
||||
matrixify_charts_per_row: 2,
|
||||
matrixify_show_row_labels: true,
|
||||
@@ -189,8 +193,9 @@ test('should handle case where charts_per_row exceeds total columns', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_fit_columns_dynamically: false,
|
||||
matrixify_charts_per_row: 5,
|
||||
matrixify_show_row_labels: true,
|
||||
@@ -220,8 +225,9 @@ test('should show headers for each group when wrapping occurs', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_fit_columns_dynamically: false,
|
||||
matrixify_charts_per_row: 2,
|
||||
matrixify_show_row_labels: true,
|
||||
@@ -255,8 +261,9 @@ test('should show headers only on first row when not wrapping', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_fit_columns_dynamically: true, // No wrapping
|
||||
matrixify_show_row_labels: true,
|
||||
matrixify_show_column_headers: true,
|
||||
@@ -285,8 +292,9 @@ test('should hide headers when disabled', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_show_row_labels: false,
|
||||
matrixify_show_column_headers: false,
|
||||
};
|
||||
@@ -313,8 +321,9 @@ test('should place cells correctly in wrapped layout', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
matrixify_fit_columns_dynamically: false,
|
||||
matrixify_charts_per_row: 2,
|
||||
matrixify_show_row_labels: true,
|
||||
@@ -344,8 +353,9 @@ test('should handle null grid gracefully', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
};
|
||||
|
||||
const { container } = renderWithTheme(
|
||||
@@ -366,8 +376,9 @@ test('should handle empty grid gracefully', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
};
|
||||
|
||||
const { container } = renderWithTheme(
|
||||
@@ -391,8 +402,9 @@ test('should use default values for missing configuration', () => {
|
||||
|
||||
const formData = {
|
||||
viz_type: 'test_chart',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable_horizontal_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics' as MatrixifyMode,
|
||||
matrixify_mode_columns: 'metrics' as MatrixifyMode,
|
||||
// Missing optional configurations
|
||||
};
|
||||
|
||||
|
||||
@@ -130,10 +130,12 @@ function MatrixifyGridRenderer({
|
||||
|
||||
// Determine layout parameters - only show headers/labels if layout is enabled
|
||||
const showRowLabels =
|
||||
formData.matrixify_enable_vertical_layout === true &&
|
||||
formData.matrixify_mode_rows !== undefined &&
|
||||
formData.matrixify_mode_rows !== 'disabled' &&
|
||||
(formData.matrixify_show_row_labels ?? true);
|
||||
const showColumnHeaders =
|
||||
formData.matrixify_enable_horizontal_layout === true &&
|
||||
formData.matrixify_mode_columns !== undefined &&
|
||||
formData.matrixify_mode_columns !== 'disabled' &&
|
||||
(formData.matrixify_show_column_headers ?? true);
|
||||
const rowHeight = formData.matrixify_row_height || DEFAULT_ROW_HEIGHT;
|
||||
const fitColumnsDynamically =
|
||||
|
||||
@@ -37,12 +37,11 @@ test('isMatrixifyEnabled should return false when no matrixify configuration exi
|
||||
expect(isMatrixifyEnabled(formData)).toBe(false);
|
||||
});
|
||||
|
||||
test('isMatrixifyEnabled should return false when layout controls are false', () => {
|
||||
test('isMatrixifyEnabled should return false when layout controls are disabled', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: false,
|
||||
matrixify_enable_horizontal_layout: false,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_rows: 'disabled',
|
||||
matrixify_mode_columns: 'disabled',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
} as MatrixifyFormData;
|
||||
|
||||
@@ -52,7 +51,7 @@ test('isMatrixifyEnabled should return false when layout controls are false', ()
|
||||
test('isMatrixifyEnabled should return true for valid metrics mode configuration', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
@@ -65,7 +64,7 @@ test('isMatrixifyEnabled should return true for valid metrics mode configuration
|
||||
test('isMatrixifyEnabled should return true for valid dimensions mode configuration', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
|
||||
@@ -78,7 +77,7 @@ test('isMatrixifyEnabled should return true for valid dimensions mode configurat
|
||||
test('isMatrixifyEnabled should return true for mixed mode configuration', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
@@ -91,7 +90,7 @@ test('isMatrixifyEnabled should return true for mixed mode configuration', () =>
|
||||
test('isMatrixifyEnabled should return true for topn dimension selection mode', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: {
|
||||
@@ -110,7 +109,7 @@ test('isMatrixifyEnabled should return true for topn dimension selection mode',
|
||||
test('isMatrixifyEnabled should return false when both axes have empty metrics arrays', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [],
|
||||
@@ -123,7 +122,7 @@ test('isMatrixifyEnabled should return false when both axes have empty metrics a
|
||||
test('isMatrixifyEnabled should return false when both dimensions have empty values and no topn mode', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: { dimension: 'country', values: [] },
|
||||
@@ -138,10 +137,31 @@ test('getMatrixifyConfig should return null when no matrixify configuration exis
|
||||
expect(getMatrixifyConfig(formData)).toBeNull();
|
||||
});
|
||||
|
||||
test('getMatrixifyConfig should return null when matrixify_enable is false', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable: false,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
matrixify_columns: [createMetric('Q1')],
|
||||
} as MatrixifyFormData;
|
||||
expect(getMatrixifyConfig(formData)).toBeNull();
|
||||
});
|
||||
|
||||
test('getMatrixifyConfig should return null when no axes are enabled', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable: true,
|
||||
} as MatrixifyFormData;
|
||||
|
||||
expect(getMatrixifyConfig(formData)).toBeNull();
|
||||
});
|
||||
|
||||
test('getMatrixifyConfig should return valid config for metrics mode', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
@@ -159,7 +179,7 @@ test('getMatrixifyConfig should return valid config for metrics mode', () => {
|
||||
test('getMatrixifyConfig should return valid config for dimensions mode', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
|
||||
@@ -183,7 +203,7 @@ test('getMatrixifyConfig should return valid config for dimensions mode', () =>
|
||||
test('getMatrixifyConfig should handle topn selection mode', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: {
|
||||
@@ -201,11 +221,50 @@ test('getMatrixifyConfig should handle topn selection mode', () => {
|
||||
expect(config!.rows.dimension).toEqual(formData.matrixify_dimension_rows);
|
||||
});
|
||||
|
||||
test('getMatrixifyConfig should preserve ascending topn order when explicitly disabled', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
matrixify_dimension_rows: { dimension: 'country', values: ['USA'] },
|
||||
matrixify_dimension_columns: { dimension: 'product', values: ['Widget'] },
|
||||
matrixify_topn_order_rows: false,
|
||||
matrixify_topn_order_columns: false,
|
||||
} as MatrixifyFormData;
|
||||
|
||||
const config = getMatrixifyConfig(formData);
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.rows.topnOrder).toBe('asc');
|
||||
expect(config!.columns.topnOrder).toBe('asc');
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors should return empty array when matrixify is not enabled', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: false,
|
||||
matrixify_enable_horizontal_layout: false,
|
||||
matrixify_mode_rows: 'disabled',
|
||||
matrixify_mode_columns: 'disabled',
|
||||
} as MatrixifyFormData;
|
||||
|
||||
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors should return empty array when matrixify_enable is false even with stale mode values', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable: false,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
} as MatrixifyFormData;
|
||||
|
||||
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors should return empty array when matrixify_enable is undefined with stale defaults', () => {
|
||||
const formData = {
|
||||
viz_type: 'bar',
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [],
|
||||
} as MatrixifyFormData;
|
||||
|
||||
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
|
||||
@@ -214,7 +273,7 @@ test('getMatrixifyValidationErrors should return empty array when matrixify is n
|
||||
test('getMatrixifyValidationErrors should return empty array when properly configured', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
@@ -224,20 +283,30 @@ test('getMatrixifyValidationErrors should return empty array when properly confi
|
||||
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors should return empty array when enabled with no active axes', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable: true,
|
||||
} as MatrixifyFormData;
|
||||
|
||||
expect(getMatrixifyValidationErrors(formData)).toEqual([]);
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors should return error when enabled but no configuration exists', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
} as MatrixifyFormData;
|
||||
|
||||
const errors = getMatrixifyValidationErrors(formData);
|
||||
expect(errors).toContain('Please configure at least one row or column axis');
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors should return error when metrics mode has no metrics', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [],
|
||||
matrixify_columns: [],
|
||||
@@ -260,19 +329,29 @@ test('should handle empty form data object', () => {
|
||||
expect(isMatrixifyEnabled(formData)).toBe(false);
|
||||
});
|
||||
|
||||
test('isMatrixifyEnabled should return false when layout enabled but no axis modes configured', () => {
|
||||
test('isMatrixifyEnabled should return false when no axis modes configured', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
// No matrixify_mode_rows or matrixify_mode_columns set
|
||||
} as MatrixifyFormData;
|
||||
expect(isMatrixifyEnabled(formData)).toBe(false);
|
||||
});
|
||||
|
||||
test('isMatrixifyEnabled should return false when switch is off even with valid axis config', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable: false,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
} as MatrixifyFormData;
|
||||
expect(isMatrixifyEnabled(formData)).toBe(false);
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors should return dimension error for rows when dimension has no data', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
// No matrixify_dimension_rows set
|
||||
matrixify_mode_columns: 'metrics',
|
||||
@@ -286,7 +365,7 @@ test('getMatrixifyValidationErrors should return dimension error for rows when d
|
||||
test('getMatrixifyValidationErrors should return metric error for columns when metrics array is empty', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
matrixify_mode_columns: 'metrics',
|
||||
@@ -300,7 +379,7 @@ test('getMatrixifyValidationErrors should return metric error for columns when m
|
||||
test('getMatrixifyValidationErrors should return dimension error for columns when no dimension data', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
@@ -311,10 +390,10 @@ test('getMatrixifyValidationErrors should return dimension error for columns whe
|
||||
expect(errors).toContain('Please select a dimension and values for columns');
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors skips row check when matrixify_mode_rows is not set (line 240 false, line 279 || false)', () => {
|
||||
test('getMatrixifyValidationErrors skips row check when matrixify_mode_rows is not set', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
// No matrixify_mode_rows — hasRowMode = false
|
||||
matrixify_mode_columns: 'metrics',
|
||||
matrixify_columns: [createMetric('Q1')],
|
||||
@@ -324,10 +403,10 @@ test('getMatrixifyValidationErrors skips row check when matrixify_mode_rows is n
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('getMatrixifyValidationErrors evaluates full && expression when dimension is set but values are empty (lines 244, 264, 283, 291 true branches)', () => {
|
||||
test('getMatrixifyValidationErrors evaluates full && expression when dimension is set but values are empty', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'dimensions',
|
||||
matrixify_dimension_rows: { dimension: 'country', values: [] },
|
||||
matrixify_mode_columns: 'dimensions',
|
||||
@@ -345,7 +424,7 @@ test('getMatrixifyValidationErrors evaluates full && expression when dimension i
|
||||
test('should handle partial configuration with one axis only', () => {
|
||||
const formData = {
|
||||
viz_type: 'table',
|
||||
matrixify_enable_vertical_layout: true,
|
||||
matrixify_enable: true,
|
||||
matrixify_mode_rows: 'metrics',
|
||||
matrixify_rows: [createMetric('Revenue')],
|
||||
// No columns configuration
|
||||
|
||||
@@ -23,6 +23,7 @@ import { AdhocMetric } from '../../query';
|
||||
* Constants for Matrixify filter generation
|
||||
* These match the literal types used in Filter.ts
|
||||
*/
|
||||
|
||||
export const MatrixifyFilterConstants = {
|
||||
// Filter expression types
|
||||
ExpressionType: {
|
||||
@@ -46,12 +47,12 @@ export const MatrixifyFilterConstants = {
|
||||
/**
|
||||
* Mode for selecting matrix axis values
|
||||
*/
|
||||
export type MatrixifyMode = 'metrics' | 'dimensions';
|
||||
export type MatrixifyMode = 'disabled' | 'metrics' | 'dimensions';
|
||||
|
||||
/**
|
||||
* Selection method for dimension values
|
||||
*/
|
||||
export type MatrixifySelectionMode = 'members' | 'topn';
|
||||
export type MatrixifySelectionMode = 'members' | 'topn' | 'all';
|
||||
|
||||
/**
|
||||
* Sort order for top N selection
|
||||
@@ -96,18 +97,18 @@ export interface MatrixifyAxisConfig {
|
||||
* Complete Matrixify configuration in form data
|
||||
*/
|
||||
export interface MatrixifyFormData {
|
||||
// Layout enable controls
|
||||
matrixify_enable_vertical_layout?: boolean;
|
||||
matrixify_enable_horizontal_layout?: boolean;
|
||||
// Global enable switch
|
||||
matrixify_enable?: boolean;
|
||||
|
||||
// Row axis configuration
|
||||
// Row axis configuration (mode 'disabled' means axis is off)
|
||||
matrixify_mode_rows?: MatrixifyMode;
|
||||
matrixify_rows?: AdhocMetric[];
|
||||
matrixify_dimension_selection_mode_rows?: MatrixifySelectionMode;
|
||||
matrixify_dimension_rows?: MatrixifyDimensionValue;
|
||||
matrixify_topn_value_rows?: number;
|
||||
matrixify_topn_metric_rows?: AdhocMetric;
|
||||
matrixify_topn_order_rows?: MatrixifySortOrder;
|
||||
matrixify_topn_order_rows?: boolean;
|
||||
matrixify_all_sort_by_rows?: 'a_to_z' | 'z_to_a' | 'metric';
|
||||
|
||||
// Column axis configuration
|
||||
matrixify_mode_columns?: MatrixifyMode;
|
||||
@@ -116,7 +117,8 @@ export interface MatrixifyFormData {
|
||||
matrixify_dimension_columns?: MatrixifyDimensionValue;
|
||||
matrixify_topn_value_columns?: number;
|
||||
matrixify_topn_metric_columns?: AdhocMetric;
|
||||
matrixify_topn_order_columns?: MatrixifySortOrder;
|
||||
matrixify_topn_order_columns?: boolean;
|
||||
matrixify_all_sort_by_columns?: 'a_to_z' | 'z_to_a' | 'metric';
|
||||
|
||||
// Grid layout configuration
|
||||
matrixify_row_height?: number;
|
||||
@@ -139,75 +141,86 @@ export interface MatrixifyConfig {
|
||||
columns: MatrixifyAxisConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given axis mode is active (not disabled)
|
||||
*/
|
||||
function isAxisEnabled(mode?: MatrixifyMode): boolean {
|
||||
return mode === 'metrics' || mode === 'dimensions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to extract Matrixify configuration from form data
|
||||
*/
|
||||
export function getMatrixifyConfig(
|
||||
formData: MatrixifyFormData & any,
|
||||
): MatrixifyConfig | null {
|
||||
const hasRowConfig = formData.matrixify_mode_rows;
|
||||
const hasColumnConfig = formData.matrixify_mode_columns;
|
||||
if (formData.matrixify_enable !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasRowConfig && !hasColumnConfig) {
|
||||
const rowEnabled = isAxisEnabled(formData.matrixify_mode_rows);
|
||||
const colEnabled = isAxisEnabled(formData.matrixify_mode_columns);
|
||||
|
||||
if (!rowEnabled && !colEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
rows: {
|
||||
mode: formData.matrixify_mode_rows || 'metrics',
|
||||
mode: formData.matrixify_mode_rows || 'disabled',
|
||||
metrics: formData.matrixify_rows,
|
||||
selectionMode: formData.matrixify_dimension_selection_mode_rows,
|
||||
dimension: formData.matrixify_dimension_rows,
|
||||
topnValue: formData.matrixify_topn_value_rows,
|
||||
topnMetric: formData.matrixify_topn_metric_rows,
|
||||
topnOrder: formData.matrixify_topn_order_rows,
|
||||
topnOrder: formData.matrixify_topn_order_rows === false ? 'asc' : 'desc',
|
||||
},
|
||||
columns: {
|
||||
mode: formData.matrixify_mode_columns || 'metrics',
|
||||
mode: formData.matrixify_mode_columns || 'disabled',
|
||||
metrics: formData.matrixify_columns,
|
||||
selectionMode: formData.matrixify_dimension_selection_mode_columns,
|
||||
dimension: formData.matrixify_dimension_columns,
|
||||
topnValue: formData.matrixify_topn_value_columns,
|
||||
topnMetric: formData.matrixify_topn_metric_columns,
|
||||
topnOrder: formData.matrixify_topn_order_columns,
|
||||
topnOrder:
|
||||
formData.matrixify_topn_order_columns === false ? 'asc' : 'desc',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Matrixify is enabled and properly configured
|
||||
*/
|
||||
export function isMatrixifyEnabled(formData: MatrixifyFormData): boolean {
|
||||
// Check if either vertical or horizontal layout is enabled
|
||||
const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true;
|
||||
const hasHorizontalLayout =
|
||||
formData.matrixify_enable_horizontal_layout === true;
|
||||
|
||||
if (!hasVerticalLayout && !hasHorizontalLayout) {
|
||||
if (formData.matrixify_enable !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then validate that we have proper configuration
|
||||
const config = getMatrixifyConfig(formData);
|
||||
if (!config) {
|
||||
const rowEnabled = isAxisEnabled(formData.matrixify_mode_rows);
|
||||
const colEnabled = isAxisEnabled(formData.matrixify_mode_columns);
|
||||
|
||||
if (!rowEnabled && !colEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const config = getMatrixifyConfig(formData)!;
|
||||
|
||||
const hasRowData =
|
||||
config.rows.mode === 'metrics'
|
||||
rowEnabled &&
|
||||
(config.rows.mode === 'metrics'
|
||||
? config.rows.metrics && config.rows.metrics.length > 0
|
||||
: config.rows.dimension?.dimension &&
|
||||
(config.rows.selectionMode === 'topn' ||
|
||||
config.rows.selectionMode === 'all' ||
|
||||
(config.rows.dimension.values &&
|
||||
config.rows.dimension.values.length > 0));
|
||||
config.rows.dimension.values.length > 0)));
|
||||
|
||||
const hasColumnData =
|
||||
config.columns.mode === 'metrics'
|
||||
colEnabled &&
|
||||
(config.columns.mode === 'metrics'
|
||||
? config.columns.metrics && config.columns.metrics.length > 0
|
||||
: config.columns.dimension?.dimension &&
|
||||
(config.columns.selectionMode === 'topn' ||
|
||||
config.columns.selectionMode === 'all' ||
|
||||
(config.columns.dimension.values &&
|
||||
config.columns.dimension.values.length > 0));
|
||||
config.columns.dimension.values.length > 0)));
|
||||
|
||||
return Boolean(hasRowData || hasColumnData);
|
||||
}
|
||||
@@ -218,22 +231,20 @@ export function isMatrixifyEnabled(formData: MatrixifyFormData): boolean {
|
||||
export function getMatrixifyValidationErrors(
|
||||
formData: MatrixifyFormData,
|
||||
): string[] {
|
||||
if (formData.matrixify_enable !== true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
// Only validate if matrixify is enabled
|
||||
const hasVerticalLayout = formData.matrixify_enable_vertical_layout === true;
|
||||
const hasHorizontalLayout =
|
||||
formData.matrixify_enable_horizontal_layout === true;
|
||||
const rowEnabled = isAxisEnabled(formData.matrixify_mode_rows);
|
||||
const colEnabled = isAxisEnabled(formData.matrixify_mode_columns);
|
||||
|
||||
if (!hasVerticalLayout && !hasHorizontalLayout) {
|
||||
if (!rowEnabled && !colEnabled) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
const config = getMatrixifyConfig(formData);
|
||||
if (!config) {
|
||||
errors.push('Please configure at least one row or column axis');
|
||||
return errors;
|
||||
}
|
||||
const config = getMatrixifyConfig(formData)!;
|
||||
|
||||
// Check row configuration (only if explicitly set in form data)
|
||||
const hasRowMode = Boolean(formData.matrixify_mode_rows);
|
||||
@@ -243,6 +254,7 @@ export function getMatrixifyValidationErrors(
|
||||
? config.rows.metrics && config.rows.metrics.length > 0
|
||||
: config.rows.dimension?.dimension &&
|
||||
(config.rows.selectionMode === 'topn' ||
|
||||
config.rows.selectionMode === 'all' ||
|
||||
(config.rows.dimension.values &&
|
||||
config.rows.dimension.values.length > 0));
|
||||
|
||||
@@ -263,6 +275,7 @@ export function getMatrixifyValidationErrors(
|
||||
? config.columns.metrics && config.columns.metrics.length > 0
|
||||
: config.columns.dimension?.dimension &&
|
||||
(config.columns.selectionMode === 'topn' ||
|
||||
config.columns.selectionMode === 'all' ||
|
||||
(config.columns.dimension.values &&
|
||||
config.columns.dimension.values.length > 0));
|
||||
|
||||
@@ -281,6 +294,7 @@ export function getMatrixifyValidationErrors(
|
||||
? config.rows.metrics && config.rows.metrics.length > 0
|
||||
: config.rows.dimension?.dimension &&
|
||||
(config.rows.selectionMode === 'topn' ||
|
||||
config.rows.selectionMode === 'all' ||
|
||||
(config.rows.dimension.values &&
|
||||
config.rows.dimension.values.length > 0));
|
||||
|
||||
@@ -289,6 +303,7 @@ export function getMatrixifyValidationErrors(
|
||||
? config.columns.metrics && config.columns.metrics.length > 0
|
||||
: config.columns.dimension?.dimension &&
|
||||
(config.columns.selectionMode === 'topn' ||
|
||||
config.columns.selectionMode === 'all' ||
|
||||
(config.columns.dimension.values &&
|
||||
config.columns.dimension.values.length > 0));
|
||||
|
||||
|
||||
@@ -283,6 +283,16 @@ export function AsyncAceEditor(
|
||||
color: ${token.colorText} !important;
|
||||
}
|
||||
|
||||
/* Fix cursor misalignment by ensuring consistent font-family */
|
||||
.ace_editor .ace_content {
|
||||
font-family: ${editorFontFamily} !important;
|
||||
}
|
||||
|
||||
/* Ensure the text layer uses the same font-family */
|
||||
.ace_editor .ace_text-layer {
|
||||
font-family: ${editorFontFamily} !important;
|
||||
}
|
||||
|
||||
/* Adjust gutter colors */
|
||||
.ace_editor .ace_gutter {
|
||||
background-color: ${token.colorBgElevated} !important;
|
||||
@@ -309,6 +319,11 @@ export function AsyncAceEditor(
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Style bracket matching to blend with theme */
|
||||
.ace_editor .ace_bracket {
|
||||
border-color: ${token.colorPrimaryBorderHover} !important;
|
||||
}
|
||||
|
||||
/* Adjust cursor color */
|
||||
.ace_editor .ace_cursor {
|
||||
color: ${token.colorPrimaryText} !important;
|
||||
|
||||
@@ -33,20 +33,22 @@ import type { PlaceholderProps } from './types';
|
||||
function DefaultPlaceholder({
|
||||
width,
|
||||
height,
|
||||
showLoadingForImport = false,
|
||||
showLoadingForImport = true,
|
||||
placeholderStyle: style,
|
||||
}: PlaceholderProps) {
|
||||
return (
|
||||
// since `width` defaults to 100%, we can display the placeholder once
|
||||
// height is specified.
|
||||
(height && (
|
||||
if (showLoadingForImport) {
|
||||
return (
|
||||
<div key="async-asm-placeholder" style={{ width, height, ...style }}>
|
||||
{showLoadingForImport && <Loading position="floating" />}
|
||||
<Loading position="floating" size="s" />
|
||||
</div>
|
||||
)) ||
|
||||
// `|| null` is for in case of height=0.
|
||||
null
|
||||
);
|
||||
);
|
||||
}
|
||||
if (height) {
|
||||
return (
|
||||
<div key="async-asm-placeholder" style={{ width, height, ...style }} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,11 @@ import {
|
||||
SIZES as buttonSizes,
|
||||
STYLES as buttonStyles,
|
||||
} from './Button.stories';
|
||||
import {
|
||||
getSecondaryButtonStyle,
|
||||
getSecondaryButtonHoverStyles,
|
||||
} from './index';
|
||||
import type { SupersetTheme } from '@apache-superset/core/theme';
|
||||
|
||||
test('works with an onClick handler', () => {
|
||||
const mockAction = jest.fn();
|
||||
@@ -47,3 +52,165 @@ test('All the sorybook gallery variants mount', () => {
|
||||
|
||||
expect(getAllByRole('button')).toHaveLength(permutationCount);
|
||||
});
|
||||
|
||||
test('secondary button renders without errors', () => {
|
||||
const { getByRole } = render(
|
||||
<Button buttonStyle="secondary">Secondary</Button>,
|
||||
);
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('getSecondaryButtonStyle uses fallback tokens when custom tokens not set', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
colorPrimaryBg: '#e6f4f7',
|
||||
colorPrimaryBgHover: '#cce9ef',
|
||||
colorPrimaryBorder: '#99d3df',
|
||||
} as SupersetTheme;
|
||||
|
||||
const styles = getSecondaryButtonStyle(mockTheme);
|
||||
|
||||
// Default state uses inline styles (no !important needed)
|
||||
expect(styles.color).toBe('#2893B3');
|
||||
expect(styles.backgroundColor).toBe('#e6f4f7');
|
||||
expect(styles.borderColor).toBe('transparent');
|
||||
});
|
||||
|
||||
test('getSecondaryButtonHoverStyles uses fallback tokens when custom tokens not set', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
colorPrimaryBg: '#e6f4f7',
|
||||
colorPrimaryBgHover: '#cce9ef',
|
||||
colorPrimaryBorder: '#99d3df',
|
||||
} as SupersetTheme;
|
||||
|
||||
const hoverStyles = getSecondaryButtonHoverStyles(mockTheme);
|
||||
|
||||
// Hover/active states use CSS with !important for specificity
|
||||
expect(hoverStyles['&:hover'].backgroundColor).toBe('#cce9ef !important');
|
||||
expect(hoverStyles['&:active'].backgroundColor).toBe('#99d3df !important');
|
||||
});
|
||||
|
||||
test('getSecondaryButtonStyle uses custom tokens when provided', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
colorPrimaryBg: '#e6f4f7',
|
||||
colorPrimaryBgHover: '#cce9ef',
|
||||
colorPrimaryBorder: '#99d3df',
|
||||
// Custom secondary button tokens
|
||||
buttonSecondaryColor: '#custom-color',
|
||||
buttonSecondaryBg: '#custom-bg',
|
||||
buttonSecondaryBorderColor: '#custom-border',
|
||||
} as SupersetTheme;
|
||||
|
||||
const styles = getSecondaryButtonStyle(mockTheme);
|
||||
|
||||
// Default state uses inline styles (no !important needed)
|
||||
expect(styles.color).toBe('#custom-color');
|
||||
expect(styles.backgroundColor).toBe('#custom-bg');
|
||||
expect(styles.borderColor).toBe('#custom-border');
|
||||
});
|
||||
|
||||
test('getSecondaryButtonHoverStyles uses custom tokens when provided', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
colorPrimaryBg: '#e6f4f7',
|
||||
colorPrimaryBgHover: '#cce9ef',
|
||||
colorPrimaryBorder: '#99d3df',
|
||||
// Custom secondary button tokens
|
||||
buttonSecondaryHoverColor: '#custom-hover-color',
|
||||
buttonSecondaryHoverBg: '#custom-hover-bg',
|
||||
buttonSecondaryHoverBorderColor: '#custom-hover-border',
|
||||
buttonSecondaryActiveColor: '#custom-active-color',
|
||||
buttonSecondaryActiveBg: '#custom-active-bg',
|
||||
buttonSecondaryActiveBorderColor: '#custom-active-border',
|
||||
} as SupersetTheme;
|
||||
|
||||
const hoverStyles = getSecondaryButtonHoverStyles(mockTheme);
|
||||
|
||||
// Hover/active states use CSS with !important for specificity
|
||||
expect(hoverStyles['&:hover'].color).toBe('#custom-hover-color !important');
|
||||
expect(hoverStyles['&:hover'].backgroundColor).toBe(
|
||||
'#custom-hover-bg !important',
|
||||
);
|
||||
expect(hoverStyles['&:hover'].borderColor).toBe(
|
||||
'#custom-hover-border !important',
|
||||
);
|
||||
expect(hoverStyles['&:active'].color).toBe('#custom-active-color !important');
|
||||
expect(hoverStyles['&:active'].backgroundColor).toBe(
|
||||
'#custom-active-bg !important',
|
||||
);
|
||||
expect(hoverStyles['&:active'].borderColor).toBe(
|
||||
'#custom-active-border !important',
|
||||
);
|
||||
});
|
||||
|
||||
test('getSecondaryButtonStyle supports partial token overrides', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
colorPrimaryBg: '#e6f4f7',
|
||||
colorPrimaryBgHover: '#cce9ef',
|
||||
colorPrimaryBorder: '#99d3df',
|
||||
// Only override some tokens
|
||||
buttonSecondaryBg: '#custom-bg',
|
||||
buttonSecondaryBorderColor: '#custom-border',
|
||||
} as SupersetTheme;
|
||||
|
||||
const styles = getSecondaryButtonStyle(mockTheme);
|
||||
|
||||
// Should use custom values where provided (no !important for inline styles)
|
||||
expect(styles.backgroundColor).toBe('#custom-bg');
|
||||
expect(styles.borderColor).toBe('#custom-border');
|
||||
// Should fallback to Ant Design tokens where not provided
|
||||
expect(styles.color).toBe('#2893B3');
|
||||
});
|
||||
|
||||
test('getSecondaryButtonHoverStyles supports partial token overrides', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
colorPrimaryBg: '#e6f4f7',
|
||||
colorPrimaryBgHover: '#cce9ef',
|
||||
colorPrimaryBorder: '#99d3df',
|
||||
// Only override hover bg
|
||||
buttonSecondaryHoverBg: '#custom-hover-bg',
|
||||
} as SupersetTheme;
|
||||
|
||||
const hoverStyles = getSecondaryButtonHoverStyles(mockTheme);
|
||||
|
||||
// Should use custom value where provided
|
||||
expect(hoverStyles['&:hover'].backgroundColor).toBe(
|
||||
'#custom-hover-bg !important',
|
||||
);
|
||||
// Should fallback to Ant Design tokens where not provided
|
||||
expect(hoverStyles['&:hover'].color).toBe('#2893B3 !important');
|
||||
expect(hoverStyles['&:active'].backgroundColor).toBe('#99d3df !important');
|
||||
});
|
||||
|
||||
test('getSecondaryButtonStyle falls back when tokens are empty strings', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
colorPrimaryBg: '#e6f4f7',
|
||||
buttonSecondaryColor: '',
|
||||
buttonSecondaryBg: '',
|
||||
buttonSecondaryBorderColor: '',
|
||||
} as SupersetTheme;
|
||||
|
||||
const styles = getSecondaryButtonStyle(mockTheme);
|
||||
|
||||
// Empty strings should trigger fallback to primary tokens
|
||||
expect(styles.color).toBe('#2893B3');
|
||||
expect(styles.backgroundColor).toBe('#e6f4f7');
|
||||
expect(styles.borderColor).toBe('transparent');
|
||||
});
|
||||
|
||||
test('secondary button merges consumer style with theme styles', () => {
|
||||
const { getByRole } = render(
|
||||
<Button buttonStyle="secondary" style={{ marginTop: 10, padding: 20 }}>
|
||||
Styled Secondary
|
||||
</Button>,
|
||||
);
|
||||
const button = getByRole('button');
|
||||
|
||||
// Consumer styles should be applied
|
||||
expect(button).toHaveStyle({ marginTop: '10px', padding: '20px' });
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import cx from 'classnames';
|
||||
import { Button as AntdButton } from 'antd';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import type { SupersetTheme } from '@apache-superset/core/theme';
|
||||
import type {
|
||||
ButtonColorType,
|
||||
ButtonProps,
|
||||
@@ -30,6 +31,59 @@ import type {
|
||||
OnClickHandler,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Secondary Button Theming
|
||||
*
|
||||
* Ant Design's "filled" variant (used for secondary buttons) has no component-level
|
||||
* tokens for customization. To enable full theming of secondary buttons, we use
|
||||
* Superset-specific tokens (buttonSecondary*) with fallbacks to Ant Design's
|
||||
* colorPrimary* derived tokens.
|
||||
*
|
||||
* Implementation approach (follows PR #38679 pattern for label tokens):
|
||||
* - Default state: Applied via inline `style` prop (higher specificity than CSS classes)
|
||||
* - Hover/Active states: Applied via `css` prop with !important (pseudo-selectors
|
||||
* cannot be applied via inline styles)
|
||||
*
|
||||
* Available tokens (all optional, with sensible fallbacks):
|
||||
* - buttonSecondaryColor: Text color (fallback: colorPrimary)
|
||||
* - buttonSecondaryBg: Background color (fallback: colorPrimaryBg)
|
||||
* - buttonSecondaryBorderColor: Border color (fallback: transparent)
|
||||
* - buttonSecondaryHoverColor: Hover text color (fallback: colorPrimary)
|
||||
* - buttonSecondaryHoverBg: Hover background (fallback: colorPrimaryBgHover)
|
||||
* - buttonSecondaryHoverBorderColor: Hover border (fallback: transparent)
|
||||
* - buttonSecondaryActiveColor: Active/pressed text color (fallback: colorPrimary)
|
||||
* - buttonSecondaryActiveBg: Active/pressed background (fallback: colorPrimaryBorder)
|
||||
* - buttonSecondaryActiveBorderColor: Active/pressed border (fallback: transparent)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates inline styles for secondary buttons (default state).
|
||||
* Inline styles have higher specificity than CSS classes, so no !important needed.
|
||||
*/
|
||||
export const getSecondaryButtonStyle = (theme: SupersetTheme) => ({
|
||||
color: theme.buttonSecondaryColor || theme.colorPrimary,
|
||||
backgroundColor: theme.buttonSecondaryBg || theme.colorPrimaryBg,
|
||||
borderColor: theme.buttonSecondaryBorderColor || 'transparent',
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates CSS styles for secondary button hover/active states.
|
||||
* Must use CSS (not inline styles) since pseudo-selectors cannot be applied via style prop.
|
||||
* Uses !important to override Ant Design's default styles.
|
||||
*/
|
||||
export const getSecondaryButtonHoverStyles = (theme: SupersetTheme) => ({
|
||||
'&:hover': {
|
||||
color: `${theme.buttonSecondaryHoverColor || theme.colorPrimary} !important`,
|
||||
backgroundColor: `${theme.buttonSecondaryHoverBg || theme.colorPrimaryBgHover} !important`,
|
||||
borderColor: `${theme.buttonSecondaryHoverBorderColor || 'transparent'} !important`,
|
||||
},
|
||||
'&:active': {
|
||||
color: `${theme.buttonSecondaryActiveColor || theme.colorPrimary} !important`,
|
||||
backgroundColor: `${theme.buttonSecondaryActiveBg || theme.colorPrimaryBorder} !important`,
|
||||
borderColor: `${theme.buttonSecondaryActiveBorderColor || 'transparent'} !important`,
|
||||
},
|
||||
});
|
||||
|
||||
const BUTTON_STYLE_MAP: Record<
|
||||
ButtonStyle,
|
||||
{
|
||||
@@ -98,6 +152,12 @@ export function Button(props: ButtonProps) {
|
||||
|
||||
const effectiveButtonStyle: ButtonStyle = buttonStyle ?? 'primary';
|
||||
|
||||
// Secondary button inline styles (default state) - inline styles override CSS classes
|
||||
const secondaryStyle =
|
||||
effectiveButtonStyle === 'secondary' && !disabled
|
||||
? getSecondaryButtonStyle(theme)
|
||||
: undefined;
|
||||
|
||||
const button = (
|
||||
<AntdButton
|
||||
href={disabled ? undefined : href}
|
||||
@@ -132,15 +192,18 @@ export function Button(props: ButtonProps) {
|
||||
'& > span > :first-of-type': {
|
||||
marginRight: firstChildMargin,
|
||||
},
|
||||
':not(:hover)': effectiveButtonStyle === 'secondary' &&
|
||||
!disabled && {
|
||||
// NOTE: This is the best we can do contrast wise for the secondary button using antd tokens
|
||||
// and abusing the semantics. Should be revisited when possible. https://github.com/apache/superset/pull/34253#issuecomment-3104834692
|
||||
color: `${theme.colorPrimaryTextHover} !important`,
|
||||
},
|
||||
// Secondary button hover/active states via CSS
|
||||
...(effectiveButtonStyle === 'secondary' &&
|
||||
!disabled &&
|
||||
getSecondaryButtonHoverStyles(theme)),
|
||||
}}
|
||||
icon={icon}
|
||||
{...restProps}
|
||||
style={
|
||||
secondaryStyle
|
||||
? { ...secondaryStyle, ...restProps.style }
|
||||
: restProps.style
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</AntdButton>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen } from '../../spec';
|
||||
import { render, screen, fireEvent } from '../../spec';
|
||||
import CodeSyntaxHighlighter from './index';
|
||||
|
||||
// Simple mock that just returns the content
|
||||
@@ -153,4 +153,44 @@ describe('CodeSyntaxHighlighter', () => {
|
||||
|
||||
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows copy button by default', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql">SELECT 1;</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTitle('Copy to clipboard')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('hides copy button when showCopyButton is false', () => {
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql" showCopyButton={false}>
|
||||
SELECT 1;
|
||||
</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTitle('Copy to clipboard')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('copy button does not throw when clipboard API is unavailable', () => {
|
||||
const originalClipboard = navigator.clipboard;
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: undefined,
|
||||
configurable: true,
|
||||
});
|
||||
document.execCommand = jest.fn().mockReturnValue(true);
|
||||
|
||||
render(
|
||||
<CodeSyntaxHighlighter language="sql">SELECT 1;</CodeSyntaxHighlighter>,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
fireEvent.click(screen.getByTitle('Copy to clipboard')),
|
||||
).not.toThrow();
|
||||
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: originalClipboard,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,11 +16,14 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import SyntaxHighlighterBase from 'react-syntax-highlighter/dist/cjs/light';
|
||||
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
|
||||
import tomorrow from 'react-syntax-highlighter/dist/cjs/styles/hljs/tomorrow-night';
|
||||
import { isThemeDark, useTheme } from '@apache-superset/core/theme';
|
||||
import { css, isThemeDark, useTheme } from '@apache-superset/core/theme';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import copyTextToClipboard from '../../utils/copy';
|
||||
import { Icons } from '../Icons';
|
||||
|
||||
export type SupportedLanguage = 'sql' | 'htmlbars' | 'markdown' | 'json';
|
||||
|
||||
@@ -31,6 +34,7 @@ export interface CodeSyntaxHighlighterProps {
|
||||
showLineNumbers?: boolean;
|
||||
wrapLines?: boolean;
|
||||
style?: any; // Override theme style if needed
|
||||
showCopyButton?: boolean;
|
||||
}
|
||||
|
||||
// Track which languages have been registered to avoid duplicate registrations
|
||||
@@ -76,11 +80,14 @@ export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
|
||||
showLineNumbers = false,
|
||||
wrapLines = true,
|
||||
style: overrideStyle,
|
||||
showCopyButton = true,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [isLanguageReady, setIsLanguageReady] = useState(
|
||||
registeredLanguages.has(language),
|
||||
);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadLanguage = async () => {
|
||||
@@ -93,6 +100,21 @@ export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
|
||||
loadLanguage();
|
||||
}, [language]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
copyTextToClipboard(() => Promise.resolve(children)).then(() => {
|
||||
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
|
||||
setCopied(true);
|
||||
copyTimeoutRef.current = setTimeout(() => setCopied(false), 1500);
|
||||
});
|
||||
}, [children]);
|
||||
|
||||
const isDark = isThemeDark(theme);
|
||||
const themeStyle = overrideStyle || (isDark ? tomorrow : github);
|
||||
|
||||
@@ -104,32 +126,79 @@ export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
|
||||
...customStyle,
|
||||
};
|
||||
|
||||
const copyButton = showCopyButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
handleCopy();
|
||||
}}
|
||||
title={copied ? t('Copied!') : t('Copy to clipboard')}
|
||||
css={css`
|
||||
position: absolute;
|
||||
top: ${theme.sizeUnit}px;
|
||||
right: ${theme.sizeUnit}px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: ${theme.sizeUnit}px;
|
||||
color: ${copied ? theme.colorSuccess : theme.colorTextSecondary};
|
||||
line-height: 1;
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
&:hover {
|
||||
color: ${copied ? theme.colorSuccess : theme.colorText};
|
||||
background: ${theme.colorBgTextHover};
|
||||
}
|
||||
`}
|
||||
>
|
||||
{copied ? (
|
||||
<Icons.CheckOutlined style={{ fontSize: theme.fontSizeSM }} />
|
||||
) : (
|
||||
<Icons.CopyOutlined style={{ fontSize: theme.fontSizeSM }} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
// Show a simple pre-formatted text while language is loading
|
||||
if (!isLanguageReady) {
|
||||
return (
|
||||
<pre
|
||||
style={{
|
||||
...defaultCustomStyle,
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
margin: 0,
|
||||
}}
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
{copyButton}
|
||||
<pre
|
||||
style={{
|
||||
...defaultCustomStyle,
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SyntaxHighlighterBase
|
||||
language={language}
|
||||
style={themeStyle}
|
||||
customStyle={defaultCustomStyle}
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLines={wrapLines}
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</SyntaxHighlighterBase>
|
||||
{copyButton}
|
||||
<SyntaxHighlighterBase
|
||||
language={language}
|
||||
style={themeStyle}
|
||||
customStyle={defaultCustomStyle}
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLines={wrapLines}
|
||||
>
|
||||
{children}
|
||||
</SyntaxHighlighterBase>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -352,6 +352,7 @@ export const DropdownContainer = forwardRef(
|
||||
onOpenChange={visible => setPopoverVisible(visible)}
|
||||
placement="bottom"
|
||||
forceRender={forceRender}
|
||||
fresh // This prop prevents caching and stale data for filter scoping.
|
||||
>
|
||||
<Tooltip title={dropdownTriggerTooltip}>
|
||||
<Button
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { css, styled } from '@apache-superset/core/theme';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useLayoutEffect, useState, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import { CertifiedBadge } from '../CertifiedBadge';
|
||||
@@ -105,7 +105,7 @@ export function EditableTitle({
|
||||
return 0;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const { font } = window.getComputedStyle(
|
||||
contentRef.current?.resizableTextArea?.textArea || document.body,
|
||||
);
|
||||
@@ -113,7 +113,7 @@ export function EditableTitle({
|
||||
const padding = 20;
|
||||
const maxAllowedWidth = typeof maxWidth === 'number' ? maxWidth : Infinity;
|
||||
setInputWidth(Math.min(textWidth + padding, maxAllowedWidth));
|
||||
}, [currentTitle]);
|
||||
}, [currentTitle, maxWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (title !== currentTitle) {
|
||||
@@ -132,8 +132,7 @@ export function EditableTitle({
|
||||
textArea.scrollTop = textArea.scrollHeight;
|
||||
}
|
||||
}
|
||||
onEditingChange?.(isEditing);
|
||||
}, [isEditing, onEditingChange]);
|
||||
}, [isEditing]);
|
||||
|
||||
function handleClick() {
|
||||
if (!canEdit || isEditing) return;
|
||||
@@ -144,6 +143,7 @@ export function EditableTitle({
|
||||
textArea.setSelectionRange(length, length);
|
||||
}
|
||||
setIsEditing(true);
|
||||
onEditingChange?.(true);
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
@@ -151,6 +151,7 @@ export function EditableTitle({
|
||||
|
||||
if (!canEdit) return;
|
||||
setIsEditing(false);
|
||||
onEditingChange?.(false);
|
||||
|
||||
if (!formattedTitle.length) {
|
||||
setCurrentTitle(lastTitle);
|
||||
|
||||
@@ -17,10 +17,10 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<svg viewBox="0 0 119 78" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="17" y="1" width="86" height="62" rx="3" stroke="currentColor" stroke-width="2"/>
|
||||
<rect x="21" y="5" width="78" height="14" rx="3" stroke="currentColor" stroke-width="2"/>
|
||||
<rect x="21" y="23" width="38" height="36" rx="3" stroke="currentColor" stroke-width="2"/>
|
||||
<rect x="63" y="37" width="36" height="22" rx="3" stroke="currentColor" stroke-width="2"/>
|
||||
<rect x="63" y="23" width="36" height="10" rx="3" stroke="currentColor" stroke-width="2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 55.2892C6.07439 57.7013 0 60.9397 0 64.5C0 71.9559 26.6391 78 59.5 78C92.3609 78 119 71.9559 119 64.5C119 61.0609 113.332 57.9221 104 55.5383V60C104 62.2092 102.209 64 100 64H20C17.7909 64 16 62.2092 16 60V55.2892Z" fill="currentColor" fill-opacity="0.25" stroke="currentColor"/>
|
||||
<path d="M99.3193 1H20.6639C19.0349 1 17.7143 2.32057 17.7143 3.94958V59.0084C17.7143 60.6374 19.0349 61.958 20.6639 61.958H99.3193C100.948 61.958 102.269 60.6374 102.269 59.0084V3.94958C102.269 2.32057 100.948 1 99.3193 1Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M95.3866 4.93277H24.5966C22.9676 4.93277 21.647 6.25335 21.647 7.88235V15.7479C21.647 17.3769 22.9676 18.6975 24.5966 18.6975H95.3866C97.0156 18.6975 98.3361 17.3769 98.3361 15.7479V7.88235C98.3361 6.25335 97.0156 4.93277 95.3866 4.93277Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M56.0588 22.6303H24.5966C22.9676 22.6303 21.647 23.9508 21.647 25.5798V55.0756C21.647 56.7046 22.9676 58.0252 24.5966 58.0252H56.0588C57.6878 58.0252 59.0084 56.7046 59.0084 55.0756V25.5798C59.0084 23.9508 57.6878 22.6303 56.0588 22.6303Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M95.3865 36.395H65.8907C64.2617 36.395 62.9412 37.7155 62.9412 39.3445V55.0756C62.9412 56.7046 64.2617 58.0252 65.8907 58.0252H95.3865C97.0155 58.0252 98.3361 56.7046 98.3361 55.0756V39.3445C98.3361 37.7155 97.0155 36.395 95.3865 36.395Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M95.3865 22.6303H65.8907C64.2617 22.6303 62.9412 23.9508 62.9412 25.5798V29.5126C62.9412 31.1416 64.2617 32.4622 65.8907 32.4622H95.3865C97.0155 32.4622 98.3361 31.1416 98.3361 29.5126V25.5798C98.3361 23.9508 97.0155 22.6303 95.3865 22.6303Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M17.2227 59.0086C17.2228 60.9091 18.7637 62.45 20.6641 62.45H99.3193C101.22 62.45 102.761 60.9091 102.761 59.0086V53.9891L103.374 54.1453C107.981 55.3221 111.718 56.6931 114.315 58.2137C116.877 59.7134 118.491 61.4572 118.491 63.4324C118.491 64.5056 118.01 65.513 117.177 66.4373C116.346 67.3587 115.143 68.2232 113.635 69.032C110.618 70.6501 106.286 72.0925 100.975 73.2977C90.343 75.7099 75.6797 77.1971 59.5 77.1971C43.3203 77.1971 28.657 75.7099 18.0254 73.2977C12.7137 72.0925 8.38162 70.6501 5.36523 69.032C3.85742 68.2232 2.65424 67.3587 1.82324 66.4373C0.989641 65.513 0.508789 64.5056 0.508789 63.4324C0.508977 61.389 2.23587 59.5926 4.96484 58.0535C7.73449 56.4916 11.717 55.0896 16.6152 53.8992L17.2227 53.7518V59.0086Z" fill="currentColor" fill-opacity="0.25" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.0 KiB |
@@ -17,6 +17,6 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<svg viewBox="0 0 119 76" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M83.1952 1.36598L103 24V62C103 64.2091 101.209 66 99 66H20C17.7909 66 16 64.2091 16 62V24L35.8048 1.36598C36.5643 0.497921 37.6616 0 38.8151 0H80.1849C81.3384 0 82.4357 0.497922 83.1952 1.36598ZM101 26V62C101 63.1046 100.105 64 99 64H20C18.8954 64 18 63.1046 18 62V26H35.25C37.8734 26 40 28.1266 40 30.75C40 34.4779 43.0221 37.5 46.75 37.5H72.25C75.9779 37.5 79 34.4779 79 30.75C79 28.1266 81.1266 26 83.75 26H101ZM100.342 24L81.6901 2.68299C81.3103 2.24896 80.7617 2 80.1849 2H38.8151C38.2383 2 37.6897 2.24896 37.3099 2.68299L18.6575 24H35.25C38.9779 24 42 27.0221 42 30.75C42 33.3734 44.1266 35.5 46.75 35.5H72.25C74.8734 35.5 77 33.3734 77 30.75C77 27.0221 80.0221 24 83.75 24H100.342Z" fill="currentColor"/>
|
||||
<path d="M16 53.2891C6.07439 55.7012 0 58.9396 0 62.4999C0 69.9557 26.6391 75.9999 59.5 75.9999C92.3609 75.9999 119 69.9557 119 62.4999C119 58.9396 112.926 55.7012 103 53.2891V61.9999C103 64.209 101.209 65.9999 99 65.9999H20C17.7909 65.9999 16 64.209 16 61.9999V53.2891Z" fill="currentColor" stroke="currentColor" fill-opacity="0.25" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.7969 1.34302L102.269 23.5966V60.958C102.269 63.13 100.508 64.8908 98.3361 64.8908H20.6639C18.4919 64.8908 16.7311 63.13 16.7311 60.958V23.5966L36.203 1.34302C36.9498 0.489553 38.0286 0 39.1627 0H79.8372C80.9713 0 82.0502 0.489554 82.7969 1.34302ZM100.303 25.563V60.958C100.303 62.044 99.4225 62.9244 98.3361 62.9244H20.6639C19.5778 62.9244 18.6975 62.044 18.6975 60.958V25.563H35.6575C38.2369 25.563 40.3277 27.6539 40.3277 30.2332C40.3277 33.8984 43.299 36.8698 46.9643 36.8698H72.0357C75.7009 36.8698 78.6723 33.8984 78.6723 30.2332C78.6723 27.6539 80.7631 25.563 83.3424 25.563H100.303ZM99.6556 23.5966L81.3171 2.6379C80.9437 2.21116 80.4043 1.96639 79.8372 1.96639H39.1627C38.5956 1.96639 38.0562 2.21116 37.6828 2.6379L19.3439 23.5966H35.6575C39.3228 23.5966 42.2941 26.568 42.2941 30.2332C42.2941 32.8125 44.385 34.9034 46.9643 34.9034H72.0357C74.615 34.9034 76.7059 32.8125 76.7059 30.2332C76.7059 26.568 79.6772 23.5966 83.3424 23.5966H99.6556Z" fill="currentColor"/>
|
||||
<path d="M102.385 51.9159C107.283 53.1063 111.266 54.5083 114.035 56.0702C116.764 57.6093 118.491 59.4058 118.491 61.4492C118.491 62.5223 118.01 63.5298 117.177 64.454C116.346 65.3754 115.143 66.2399 113.635 67.0488C110.618 68.6668 106.286 70.1092 100.975 71.3144C90.343 73.7266 75.6797 75.2138 59.5 75.2138C43.3203 75.2138 28.657 73.7266 18.0254 71.3144C12.7137 70.1092 8.38162 68.6668 5.36523 67.0488C3.85743 66.2399 2.65424 65.3754 1.82324 64.454C0.989642 63.5298 0.508789 62.5223 0.508789 61.4492C0.508976 59.4057 2.23587 57.6093 4.96484 56.0702C7.73449 54.5083 11.717 53.1063 16.6152 51.9159L17.2227 51.7685V60.9579C17.2227 62.8584 18.7636 64.3993 20.6641 64.3993H98.3359C100.236 64.3993 101.777 62.8584 101.777 60.9579V51.7685L102.385 51.9159Z" fill="currentColor" stroke="currentColor" fill-opacity="0.25" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -17,6 +17,6 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
<svg viewBox="0 0 119 78" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M59.2123 1.55857C59.4575 0.81382 60.5425 0.813801 60.7878 1.55857L67.5616 22.1322C67.9434 23.292 69.0328 24.0653 70.249 24.0653L92.1695 24.0653C92.9982 24.0653 93.2777 25.0719 92.6607 25.5143L74.9266 38.2295C73.9348 38.9406 73.5131 40.2085 73.8958 41.3707L80.6696 61.9444C80.8987 62.6402 80.076 63.3262 79.3906 62.8348L61.6566 50.1196C60.6679 49.4107 59.3321 49.4107 58.3434 50.1196L40.6094 62.8348C39.924 63.3262 39.1013 62.6402 39.3304 61.9444L46.1042 41.3707C46.4869 40.2085 46.0652 38.9406 45.0734 38.2295L27.3393 25.5143C26.7223 25.0719 27.0018 24.0653 27.8305 24.0653L49.751 24.0653C50.9672 24.0653 52.0566 23.292 52.4384 22.1322L59.2123 1.55857Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.6782 51.616C17.5239 53.3343 0 58.4528 0 64.5C0 71.9558 26.6391 78 59.5 78C92.3609 78 119 71.9558 119 64.5C119 58.5387 101.97 53.4798 78.3466 51.6913L81.6194 61.6317C82.1664 63.2928 80.2398 64.6741 78.808 63.6475L61.1757 51.0053C60.619 51.0018 60.0604 51 59.5 51C59.2765 51 59.0534 51.0003 58.8305 51.0008L41.192 63.6475C39.7602 64.6741 37.8337 63.2928 38.3806 61.6317L41.6782 51.616Z" fill="currentColor" fill-opacity="0.25" stroke="currentColor"/>
|
||||
<path d="M59.2172 1.54918C59.4582 0.816949 60.525 0.81693 60.7662 1.54918L67.4261 21.777C67.8015 22.9173 68.8726 23.6776 70.0684 23.6776H91.6205C92.4352 23.6776 92.71 24.6673 92.1034 25.1023L74.6673 37.6038C73.6922 38.3029 73.2776 39.5495 73.6539 40.6922L80.3138 60.9201C80.5391 61.6042 79.7302 62.2787 79.0563 61.7956L61.6204 49.2941C60.6483 48.5971 59.3349 48.5971 58.3629 49.2941L40.9269 61.7956C40.253 62.2787 39.4441 61.6042 39.6694 60.9201L46.3294 40.6922C46.7056 39.5495 46.291 38.3029 45.3159 37.6038L27.8798 25.1023C27.2732 24.6673 27.548 23.6776 28.3628 23.6776H49.9149C51.1106 23.6776 52.1817 22.9173 52.5571 21.777L59.2172 1.54918Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M59.5 49.6685C60.052 49.6685 60.6021 49.6699 61.1504 49.6733L61.3066 49.6743L61.4336 49.7651L78.7695 62.1948C79.8103 62.9411 81.1618 61.9247 80.7803 60.7661L77.5625 50.9927L77.333 50.2935L78.0674 50.3491C89.7005 51.2299 99.7327 52.9176 106.87 55.1284C110.435 56.2328 113.307 57.4759 115.298 58.8335C117.27 60.1787 118.491 61.7173 118.491 63.4331C118.491 64.5061 118.01 65.5129 117.177 66.437C116.346 67.3584 115.143 68.2239 113.635 69.0327C110.618 70.6507 106.286 72.0922 100.975 73.2974C90.343 75.7096 75.6797 77.1978 59.5 77.1978C43.3203 77.1978 28.657 75.7096 18.0254 73.2974C12.7139 72.0922 8.3816 70.6507 5.36523 69.0327C3.85739 68.2239 2.65424 67.3584 1.82324 66.437C0.989783 65.5129 0.508887 64.5061 0.508789 63.4331C0.508789 61.6928 1.7644 60.135 3.78906 58.7749C5.83338 57.4017 8.78191 56.1452 12.4404 55.0327C19.7642 52.8057 30.0482 51.1211 41.9424 50.2749L42.6738 50.2231L42.4443 50.9194L39.2021 60.7661C38.8207 61.9247 40.1722 62.9409 41.2129 62.1948L58.5557 49.7612L58.6836 49.6694L58.8408 49.6685C59.0602 49.668 59.2799 49.6685 59.5 49.6685Z" fill="currentColor" fill-opacity="0.25" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -115,6 +115,7 @@ import {
|
||||
PlusSquareOutlined,
|
||||
PlusOutlined,
|
||||
ProfileOutlined,
|
||||
PushpinFilled,
|
||||
PushpinOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
@@ -270,6 +271,7 @@ const AntdIcons = {
|
||||
PlusSquareOutlined,
|
||||
PlusOutlined,
|
||||
ProfileOutlined,
|
||||
PushpinFilled,
|
||||
PushpinOutlined,
|
||||
ReloadOutlined,
|
||||
QuestionCircleOutlined,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user