mirror of
https://github.com/apache/superset.git
synced 2026-06-14 03:59:22 +00:00
Compare commits
2 Commits
fix-oom-ts
...
docs/testi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d2c332165 | ||
|
|
572f3392d7 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -20,7 +20,7 @@
|
||||
|
||||
# Notify PMC members of changes to GitHub Actions
|
||||
|
||||
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe
|
||||
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar
|
||||
|
||||
# Notify PMC members of changes to required GitHub Actions
|
||||
|
||||
|
||||
70
.github/dependabot.yml
vendored
70
.github/dependabot.yml
vendored
@@ -5,7 +5,7 @@ updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
ignore:
|
||||
@@ -18,7 +18,7 @@ updates:
|
||||
- dependency-name: "jest-environment-jsdom"
|
||||
directory: "/superset-frontend/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -40,21 +40,21 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: ".github/actions"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/docs/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
open-pull-requests-limit: 10
|
||||
versioning-strategy: increase
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-websocket/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -63,7 +63,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-websocket/utils/client-ws-app/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -75,7 +75,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-calendar/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -85,7 +85,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-histogram/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -95,7 +95,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -105,7 +105,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-world-map/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -115,7 +115,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-pivot-table/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -125,7 +125,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-chord/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -135,7 +135,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-horizon/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -145,7 +145,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-rose/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -155,7 +155,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-preset-chart-deckgl/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -165,7 +165,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-table/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -175,7 +175,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-country-map/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -185,7 +185,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-map-box/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -195,7 +195,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -205,7 +205,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -215,7 +215,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-word-cloud/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -225,7 +225,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-event-flow/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -235,7 +235,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -245,7 +245,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-sankey-loop/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -255,7 +255,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -265,7 +265,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/preset-chart-xy/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -275,7 +275,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-heatmap/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -285,7 +285,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -295,7 +295,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-sunburst/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -305,7 +305,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -315,7 +315,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/generator-superset/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -325,7 +325,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-chart-controls/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -339,7 +339,7 @@ updates:
|
||||
- dependency-name: "react-markdown"
|
||||
- dependency-name: "remark-gfm"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -349,7 +349,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-demo/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
@@ -359,7 +359,7 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-switchboard/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
|
||||
2
.github/workflows/bump-python-package.yml
vendored
2
.github/workflows/bump-python-package.yml
vendored
@@ -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@v5
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@v5
|
||||
- name: Check and notify
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/claude.yml
vendored
2
.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@v7
|
||||
with:
|
||||
script: |
|
||||
const message = `👋 Hi @${{ github.event.comment.user.login || github.event.review.user.login || github.event.issue.user.login }}!
|
||||
|
||||
2
.github/workflows/embedded-sdk-release.yml
vendored
2
.github/workflows/embedded-sdk-release.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
2
.github/workflows/embedded-sdk-test.yml
vendored
2
.github/workflows/embedded-sdk-test.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
working-directory: superset-embedded-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-embedded-sdk/.nvmrc'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
4
.github/workflows/ephemeral-env-pr-close.yml
vendored
4
.github/workflows/ephemeral-env-pr-close.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
- name: Comment (success)
|
||||
if: steps.describe-services.outputs.active == 'true'
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
|
||||
16
.github/workflows/ephemeral-env.yml
vendored
16
.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@v7
|
||||
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@v7
|
||||
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@v7
|
||||
if: steps.eval-label.outputs.result == 'up'
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -189,7 +189,7 @@ jobs:
|
||||
--extra-flags "--build-arg INCLUDE_CHROMIUM=false"
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
@@ -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@v7
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
script: |
|
||||
@@ -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@v7
|
||||
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@v7
|
||||
with:
|
||||
github-token: ${{github.token}}
|
||||
script: |
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
|
||||
2
.github/workflows/no-hold-label.yml
vendored
2
.github/workflows/no-hold-label.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check for 'hold' label
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
echo "HOMEBREW_REPOSITORY=$HOMEBREW_REPOSITORY" >>"${GITHUB_ENV}"
|
||||
brew install norwoodj/tap/helm-docs
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Install Node.js
|
||||
if: env.HAS_TAGS
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
|
||||
2
.github/workflows/showtime-trigger.yml
vendored
2
.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@v7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
with:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
submodules: recursive
|
||||
ref: master
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install eyes-storybook dependencies
|
||||
|
||||
2
.github/workflows/superset-docs-deploy.yml
vendored
2
.github/workflows/superset-docs-deploy.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
- name: Setup Python
|
||||
|
||||
22
.github/workflows/superset-docs-verify.yml
vendored
22
.github/workflows/superset-docs-verify.yml
vendored
@@ -21,14 +21,12 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# an ASF Infra ticket to allow the new version first!
|
||||
- uses: JustinBeckwith/linkinator-action@3d5ba091319fa7b0ac14703761eebb7d100e6f6d # v1.11.0
|
||||
- uses: JustinBeckwith/linkinator-action@v1.11.0
|
||||
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
|
||||
with:
|
||||
paths: "**/*.md, **/*.mdx"
|
||||
paths: "**/*.md, **/*.mdx, !superset-frontend/CHANGELOG.md"
|
||||
linksToSkip: >-
|
||||
^https://github.com/apache/(superset|incubator-superset)/(pull|issues)/\d+,
|
||||
^https://github.com/apache/(superset|incubator-superset)/commit/[a-f0-9]+,
|
||||
superset-frontend/.*CHANGELOG\.md,
|
||||
^https://github.com/apache/(superset|incubator-superset)/(pull|issue)/\d+,
|
||||
http://localhost:8088/,
|
||||
http://127.0.0.1:3000/,
|
||||
http://localhost:9001/,
|
||||
@@ -43,12 +41,12 @@ jobs:
|
||||
http://theiconic.com.au/,
|
||||
https://dev.mysql.com/doc/refman/5.7/en/innodb-limits.html,
|
||||
^https://img\.shields\.io/.*,
|
||||
https://vkusvill.ru/,
|
||||
https://www.linkedin.com/in/mark-thomas-b16751158/,
|
||||
https://theiconic.com.au/,
|
||||
https://wattbewerb.de/,
|
||||
https://timbr.ai/,
|
||||
https://opensource.org/license/apache-2-0,
|
||||
https://vkusvill.ru/
|
||||
https://www.linkedin.com/in/mark-thomas-b16751158/
|
||||
https://theiconic.com.au/
|
||||
https://wattbewerb.de/
|
||||
https://timbr.ai/
|
||||
https://opensource.org/license/apache-2-0
|
||||
https://www.plaidcloud.com/
|
||||
build-deploy:
|
||||
name: Build & Deploy
|
||||
@@ -63,7 +61,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './docs/.nvmrc'
|
||||
- name: yarn install
|
||||
|
||||
2
.github/workflows/superset-e2e.yml
vendored
2
.github/workflows/superset-e2e.yml
vendored
@@ -109,7 +109,7 @@ jobs:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
|
||||
2
.github/workflows/superset-helm-release.yml
vendored
2
.github/workflows/superset-helm-release.yml
vendored
@@ -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@v7
|
||||
with:
|
||||
script: |
|
||||
const branchName = '${{ env.branch_name }}';
|
||||
|
||||
2
.github/workflows/superset-playwright.yml
vendored
2
.github/workflows/superset-playwright.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
||||
run: testdata
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.python || steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install npm dependencies
|
||||
|
||||
2
.github/workflows/superset-translations.yml
vendored
2
.github/workflows/superset-translations.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check.outputs.frontend
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
- name: Install dependencies
|
||||
|
||||
2
.github/workflows/supersetbot.yml
vendored
2
.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@v7
|
||||
with:
|
||||
script: |
|
||||
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
|
||||
|
||||
4
.github/workflows/tag-release.yml
vendored
4
.github/workflows/tag-release.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
build: "true"
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
2
.github/workflows/tech-debt.yml
vendored
2
.github/workflows/tech-debt.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './superset-frontend/.nvmrc'
|
||||
|
||||
|
||||
11
AGENTS.md
11
AGENTS.md
@@ -102,6 +102,17 @@ superset/
|
||||
- **`selectOption()`** - Select component helper
|
||||
- **React Testing Library** - NO Enzyme (removed)
|
||||
|
||||
### Test Structure Guidelines
|
||||
- **Use `test()` instead of `describe()` and `it()`** - Follow the [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principle
|
||||
- **Why**: Reduces unnecessary nesting, improves test isolation, and makes tests more readable
|
||||
- **Pattern**: Write flat test files with descriptive test names that fully describe what's being tested
|
||||
- **Example**: Instead of nested `describe('Component', () => { it('should render', ...) })`, use `test('Component renders correctly', ...)`
|
||||
- **Benefits**:
|
||||
- Each test stands alone with a clear, searchable name
|
||||
- Easier to run individual tests
|
||||
- Forces you to write more descriptive test names
|
||||
- Reduces cognitive overhead from nested context switching
|
||||
|
||||
### Test Database Patterns
|
||||
- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
|
||||
- **API tests**: Update expected columns when adding new model fields
|
||||
|
||||
@@ -26,9 +26,6 @@ ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
|
||||
# Include translations in the final build
|
||||
ARG BUILD_TRANSLATIONS="false"
|
||||
|
||||
# Build arg to pre-populate examples DuckDB file
|
||||
ARG LOAD_EXAMPLES_DUCKDB="false"
|
||||
|
||||
######################################################################
|
||||
# superset-node-ci used as a base for building frontend assets and CI
|
||||
######################################################################
|
||||
@@ -146,8 +143,8 @@ RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
|
||||
######################################################################
|
||||
FROM python-base AS python-common
|
||||
|
||||
# Re-declare build arg to receive it in this stage
|
||||
ARG LOAD_EXAMPLES_DUCKDB
|
||||
# Build arg to pre-populate examples DuckDB file
|
||||
ARG LOAD_EXAMPLES_DUCKDB="false"
|
||||
|
||||
ENV SUPERSET_HOME="/app/superset_home" \
|
||||
HOME="/app/superset_home" \
|
||||
|
||||
@@ -1769,48 +1769,6 @@ You can use the `Extra` field in the **Edit Databases** form to configure SSL:
|
||||
}
|
||||
}
|
||||
```
|
||||
##### Custom Error Messages
|
||||
You can use the `CUSTOM_DATABASE_ERRORS` in the `superset/custom_database_errors.py` file or overwrite it in your config file to configure custom error messages for database exceptions.
|
||||
|
||||
This feature lets you transform raw database errors into user-friendly messages, optionally including documentation links and hiding default error codes.
|
||||
|
||||
Provide an empty string as the first value to keep the original error message. This way, you can add just a link to the documentation
|
||||
**Example usage:**
|
||||
```Python
|
||||
CUSTOM_DATABASE_ERRORS = {
|
||||
"database_name": {
|
||||
re.compile('permission denied for view'): (
|
||||
__(
|
||||
'Permission denied'
|
||||
),
|
||||
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
|
||||
{
|
||||
"custom_doc_links": [
|
||||
{
|
||||
"url": "https://example.com/docs/1",
|
||||
"label": "Check documentation"
|
||||
},
|
||||
],
|
||||
"show_issue_info": False,
|
||||
}
|
||||
)
|
||||
},
|
||||
"examples": {
|
||||
re.compile(r'message="(?P<message>[^"]*)"'): (
|
||||
__(
|
||||
'Unexpected error: "%(message)s"'
|
||||
),
|
||||
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
- ``custom_doc_links``: List of documentation links to display with the error.
|
||||
- ``show_issue_info``: Set to ``False`` to hide default error codes.
|
||||
|
||||
## Misc
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"version:remove:components": "node scripts/manage-versions.mjs remove components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@docusaurus/core": "3.8.1",
|
||||
"@docusaurus/plugin-client-redirects": "3.8.1",
|
||||
"@docusaurus/preset-classic": "3.8.1",
|
||||
@@ -49,7 +49,7 @@
|
||||
"@storybook/preview-api": "^8.6.11",
|
||||
"@storybook/theming": "^8.6.11",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"antd": "^5.27.4",
|
||||
"antd": "^5.26.7",
|
||||
"caniuse-lite": "^1.0.30001739",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
@@ -63,7 +63,7 @@
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.1.1",
|
||||
"storybook": "^8.6.11",
|
||||
"swagger-ui-react": "^5.29.1",
|
||||
"swagger-ui-react": "^5.27.1",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"ts-loader": "^9.5.4"
|
||||
},
|
||||
@@ -74,15 +74,15 @@
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.3",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"webpack": "^5.102.0"
|
||||
"typescript-eslint": "^8.39.0",
|
||||
"webpack": "^5.101.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -23,8 +23,8 @@ import type { Plugin } from '@docusaurus/types';
|
||||
export default function webpackExtendPlugin(): Plugin<void> {
|
||||
return {
|
||||
name: 'custom-webpack-plugin',
|
||||
configureWebpack(config) {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
configureWebpack(config, isServer, utils) {
|
||||
const { isDev } = utils;
|
||||
return {
|
||||
devtool: isDev ? 'eval-source-map' : config.devtool,
|
||||
...(isDev && {
|
||||
|
||||
@@ -2,27 +2,12 @@
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"strict": false,
|
||||
"types": ["@docusaurus/module-type-aliases"]
|
||||
"baseUrl": "."
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@superset-ui/core": ["../superset-frontend/packages/superset-ui-core/src"],
|
||||
"@superset-ui/core/*": ["../superset-frontend/packages/superset-ui-core/src/*"],
|
||||
"*": ["src/*", "node_modules/*"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"../superset-frontend/**/*",
|
||||
"src/webpack.extend.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
578
docs/yarn.lock
578
docs/yarn.lock
@@ -232,15 +232,15 @@
|
||||
classnames "^2.2.6"
|
||||
rc-util "^5.31.1"
|
||||
|
||||
"@ant-design/icons@^6.1.0":
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.1.0.tgz#97cc14a3c0528b8e2b37f41f232b019f2ca38c2c"
|
||||
integrity sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==
|
||||
"@ant-design/icons@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.0.0.tgz#302c935b8b0b429e4444cbc45809247276186d94"
|
||||
integrity sha512-o0aCCAlHc1o4CQcapAwWzHeaW2x9F49g7P3IDtvtNXgHowtRWYb7kiubt8sQPFvfVIVU/jLw2hzeSlNt0FU+Uw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^8.0.0"
|
||||
"@ant-design/icons-svg" "^4.4.0"
|
||||
"@rc-component/util" "^1.3.0"
|
||||
clsx "^2.1.1"
|
||||
"@rc-component/util" "^1.2.1"
|
||||
classnames "^2.2.6"
|
||||
|
||||
"@ant-design/react-slick@~1.1.2":
|
||||
version "1.1.2"
|
||||
@@ -2385,10 +2385,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz#585624dc829cfb6e7c0aa6c3ca7d7e6daa87e34f"
|
||||
integrity sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz#7308df158e064f0dd8b8fdb58aa14fa2a7f913b3"
|
||||
integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a"
|
||||
integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==
|
||||
dependencies:
|
||||
eslint-visitor-keys "^3.4.3"
|
||||
|
||||
@@ -2433,10 +2433,10 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.36.0", "@eslint/js@^9.32.0":
|
||||
version "9.36.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.36.0.tgz#b1a3893dd6ce2defed5fd49de805ba40368e8fef"
|
||||
integrity sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==
|
||||
"@eslint/js@9.34.0", "@eslint/js@^9.32.0":
|
||||
version "9.34.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.34.0.tgz#fc423168b9d10e08dea9088d083788ec6442996b"
|
||||
integrity sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==
|
||||
|
||||
"@eslint/object-schema@^2.1.6":
|
||||
version "2.1.6"
|
||||
@@ -2746,10 +2746,10 @@
|
||||
rc-resize-observer "^1.3.1"
|
||||
rc-util "^5.44.0"
|
||||
|
||||
"@rc-component/util@^1.3.0":
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.3.0.tgz#fc8e1ce1e5292592ef7f45839a3c07366288275c"
|
||||
integrity sha512-hfXE04CVsxI/slmWKeSh6du7sSKpbvVdVEZCa8A+2QWDlL97EsCYme2c3ZWLn1uC9FR21JoewlrhUPWO4QgO8w==
|
||||
"@rc-component/util@^1.2.1":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.2.2.tgz#f8363b0e1cc78af3ec56e2235cc3438eb8832040"
|
||||
integrity sha512-p3zQr9Wu8BKncqmuW23olzBoAFsN8PYMS9FaI4JwJLwknH7DvfHAr1fwbfl9aAWw4Jva64ucpenbgG4fznLUSw==
|
||||
dependencies:
|
||||
is-mobile "^5.0.0"
|
||||
react-is "^18.2.0"
|
||||
@@ -3050,34 +3050,7 @@
|
||||
ramda-adjunct "^5.0.0"
|
||||
unraw "^3.0.0"
|
||||
|
||||
"@swagger-api/apidom-ast@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.50.tgz#9e0f4132ea54dd4fca0f31f919a0c5aad12d93c1"
|
||||
integrity sha512-uUBUm6J6KlyKppyfS7DIW37De6oyMVIpHYmaNV3YAaDMuRMov5KHHWXKbqWlI+l493OljOcXEqDIPeLzm6B5PQ==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
unraw "^3.0.0"
|
||||
|
||||
"@swagger-api/apidom-core@>=1.0.0-beta.50 <1.0.0-rc.0", "@swagger-api/apidom-core@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.50.tgz#d9ed6821d1b38b66ed8cd0b8d8bf5de997354178"
|
||||
integrity sha512-9N7ySdyzx/3kUnprAi63GQNt+Kq8VUvErwDgPcMRAsZX8jUhk9KLJ9N0fup4mWm6+xGs0JH35wxBxnanS6aiqw==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-ast" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
minim "~0.23.8"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
short-unique-id "^5.3.2"
|
||||
ts-mixer "^6.0.3"
|
||||
|
||||
"@swagger-api/apidom-core@^1.0.0-beta.46":
|
||||
"@swagger-api/apidom-core@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-core@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.46.tgz#6c879f6cf9e67fa0cc36955d3a7f0dd185e36b4f"
|
||||
integrity sha512-CC8SdAkjSA/l31xfg5l4860cixbYABIXamJeNcXJr+O1z9YNKHIIENeI0zIwORk/TYUaSOg1KUjDnV2Pdctufg==
|
||||
@@ -3092,31 +3065,14 @@
|
||||
short-unique-id "^5.3.2"
|
||||
ts-mixer "^6.0.3"
|
||||
|
||||
"@swagger-api/apidom-error@>=1.0.0-beta.50 <1.0.0-rc.0", "@swagger-api/apidom-error@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.50.tgz#4354c4ade0482824ec7b17667390057f87838ddd"
|
||||
integrity sha512-vdpi2nRVcxXLGc68JPNwTcKrCKl8PnOEPuykZSxeNbDKnZY80APbsoLDX+1gdRgafK/7k5XdsBkpDQscsTkDng==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.20.7"
|
||||
|
||||
"@swagger-api/apidom-error@^1.0.0-beta.46":
|
||||
"@swagger-api/apidom-error@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-error@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.46.tgz#fe9039c23ac655930e1f06d99b8ea28bc40d905d"
|
||||
integrity sha512-AN2ZOHp7gtVsCa6hWxH3nk/1PS7bRtqDVCih3C5qA8k5JfHjP2wfks7b14Ns971SUvDD/SubaCmXmu0CRBHLag==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.20.7"
|
||||
|
||||
"@swagger-api/apidom-json-pointer@>=1.0.0-beta.50 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.50.tgz#8d2c4129c0d59cf059df3f0e40703114a5c3b49a"
|
||||
integrity sha512-2TgFKHlZ/SlnTZzY7EwE8xx5Pr2BYePX52xZJFqWnueSAIcCcsrqZeazWIAaDe/gXd47CDqU95nDChMECERspA==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@swaggerexpert/json-pointer" "^2.10.1"
|
||||
|
||||
"@swagger-api/apidom-json-pointer@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.46":
|
||||
"@swagger-api/apidom-json-pointer@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-json-pointer@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.46.tgz#40edcaf5fddc5c748ed4084c9042a2669cbf507c"
|
||||
integrity sha512-JsbCkYDG7rbf1QmEhtXgbuCGJ4oW+uWDOEnW+Gar49sjk/OKX/2GSGUpywRstnZcopjNarLcEx/H1mHRLoL45A==
|
||||
@@ -3180,20 +3136,6 @@
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-2019-09@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-2019-09/-/apidom-ns-json-schema-2019-09-1.0.0-beta.50.tgz#83c0eae65b2c4563c5233acdfbccb66c07065e93"
|
||||
integrity sha512-QP6DuthV8ZWQnthYbPEVikK5rTN4T5lhnAnmO1v6zOCS9B1heKCFcIYgBhcqCnuZ0Tt8kGOfLyqGMb57lPkCdw==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-ns-json-schema-draft-7" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-2020-12@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.46.tgz#779dbfb80ad10c3d6b2fd182a35231803e9ee387"
|
||||
@@ -3208,20 +3150,6 @@
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-2020-12@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.50.tgz#0310fc351efd42695227c012b1407728e1400e92"
|
||||
integrity sha512-ZaqrtZEXUx35x66ND8sc5vf1sIuWPERA15EdRHeca56E09RnjZMUHkiDvdx78165h31QmM67YLi04zEBYhQS0g==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-ns-json-schema-2019-09" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-draft-4@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.46.tgz#f17bb2010dd933fb6875c82d83826eda62f87bb6"
|
||||
@@ -3235,19 +3163,6 @@
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-draft-4@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.50.tgz#43b0e37d135b805ab3ea7efce0c5fc5678e95abc"
|
||||
integrity sha512-aqCwW+iuN7RokH10vDp/eEwlrT4LAlHGy1pLzAS9aFVJyUutfm0I4fxLfddOKD2yd04z858zhLwOVSo4BjrLHg==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-ast" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-draft-6@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.46.tgz#6629cb35e9e10e670e34a4fd50b6d159f5666766"
|
||||
@@ -3262,20 +3177,6 @@
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-draft-6@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.50.tgz#a311488627a12953a4a331d3b4231c20311aed9c"
|
||||
integrity sha512-trF1TZZ79WJOjQw3C1Y7wcqNMxxgHMZtJW2/tP5MwII1hqsExGzmGyUuNlVuSC9k9v/9sCj85hQlJ4TW6HFciQ==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-ns-json-schema-draft-4" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-draft-7@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.46.tgz#04fa39cfe0f869e0c7be0f70ceb34ffb6cc830ad"
|
||||
@@ -3290,20 +3191,6 @@
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-json-schema-draft-7@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.50.tgz#eb99fdecf9746905331f281578e924082659aef0"
|
||||
integrity sha512-g9VscnMwjPUYCfR6UxUwsLiIKnyXy2W28J+zN0rbijoSEtUdakcrxwdPhqwgJZHPci8NHNE8574zaocqKBiqSg==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-ns-json-schema-draft-6" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.4"
|
||||
|
||||
"@swagger-api/apidom-ns-openapi-2@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-2@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.0.0-beta.46.tgz#448f5b7bacd29a3dc37124a7a62f154c94cb818d"
|
||||
@@ -3332,37 +3219,7 @@
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.3"
|
||||
|
||||
"@swagger-api/apidom-ns-openapi-3-0@^1.0.0-beta.50":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.0.0-beta.50.tgz#1d9631c0c7504a8985671f2f8e5ca435235b1a54"
|
||||
integrity sha512-I4GHyNILNxDsYKYeG1+ZA3rnfU1RAYtNp3dA+G8LCX5AB/2N7dT2VPK8HS4cj9m3ZVz7dl1o+X6tpaJIN5kDsA==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-ns-json-schema-draft-4" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.3"
|
||||
|
||||
"@swagger-api/apidom-ns-openapi-3-1@>=1.0.0-beta.50 <1.0.0-rc.0":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.50.tgz#8b9a0c97849cc107228e66dd746bed858b78c50d"
|
||||
integrity sha512-kxwuaFl1kQddk/RBS5Mz3rE/6v5mXggqhzVwDBObGjgkRmDRVF5nUalziBRNg6A3NcpYbsjNMU/OCA1JihFkrg==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-ast" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-json-pointer" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-ns-json-schema-2020-12" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-ns-openapi-3-0" "^1.0.0-beta.50"
|
||||
"@types/ramda" "~0.30.0"
|
||||
ramda "~0.30.0"
|
||||
ramda-adjunct "^5.0.0"
|
||||
ts-mixer "^6.0.3"
|
||||
|
||||
"@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.46":
|
||||
"@swagger-api/apidom-ns-openapi-3-1@>=1.0.0-beta.41 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.40 <1.0.0-rc.0", "@swagger-api/apidom-ns-openapi-3-1@^1.0.0-beta.46":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.46.tgz#87056343a5e2ff48992a27af99611fcbdfbc0f22"
|
||||
integrity sha512-dSs9h4YEty7h8HJDtWSeC0cg0O2XUTwTVbnOylHX4Z7tXYub8TJ2LEbngRic/R4T58xt76Ro5rZqGUcrt5qXDQ==
|
||||
@@ -3566,16 +3423,16 @@
|
||||
tree-sitter "=0.22.4"
|
||||
web-tree-sitter "=0.24.5"
|
||||
|
||||
"@swagger-api/apidom-reference@>=1.0.0-beta.50 <1.0.0-rc.0":
|
||||
version "1.0.0-beta.50"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.50.tgz#d03f5506327e92dff81a4fac6460edcd2bf3989e"
|
||||
integrity sha512-aD7gTWPgkJb9oYaC4jZPvxb7YbQKG9pWDYZigAkVGqOAbeYxUXeI00XyCLj/cH8l7KwyhTZNX70F7VnfxOkq7w==
|
||||
"@swagger-api/apidom-reference@>=1.0.0-beta.41 <1.0.0-rc.0":
|
||||
version "1.0.0-beta.46"
|
||||
resolved "https://registry.yarnpkg.com/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.46.tgz#c9cb8edfc1099f05e19075c880c4e7c4a87d5015"
|
||||
integrity sha512-OpJ72k17hyrUJYvj5HgNg93uUsk/28cBWkDAna/cZtCiTdz9zji31JiDUr5GoUd81rpVGuxo4AuUvxXVsH35tA==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.26.10"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.50"
|
||||
"@swagger-api/apidom-core" "^1.0.0-beta.46"
|
||||
"@swagger-api/apidom-error" "^1.0.0-beta.46"
|
||||
"@types/ramda" "~0.30.0"
|
||||
axios "^1.12.2"
|
||||
axios "^1.9.0"
|
||||
minimatch "^7.4.3"
|
||||
process "^0.11.10"
|
||||
ramda "~0.30.0"
|
||||
@@ -4230,79 +4087,117 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.45.0", "@typescript-eslint/eslint-plugin@^8.37.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz#9f251d4e85ec5089e7cccb09257ce93dbf0d7744"
|
||||
integrity sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==
|
||||
"@typescript-eslint/eslint-plugin@8.40.0", "@typescript-eslint/eslint-plugin@^8.37.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz#19f959f273b32f5082c891903645e6a85328db4e"
|
||||
integrity sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.45.0"
|
||||
"@typescript-eslint/type-utils" "8.45.0"
|
||||
"@typescript-eslint/utils" "8.45.0"
|
||||
"@typescript-eslint/visitor-keys" "8.45.0"
|
||||
"@typescript-eslint/scope-manager" "8.40.0"
|
||||
"@typescript-eslint/type-utils" "8.40.0"
|
||||
"@typescript-eslint/utils" "8.40.0"
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^7.0.0"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/parser@8.45.0", "@typescript-eslint/parser@^8.42.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.45.0.tgz#571660c98824aefb4a6ec3b3766655d1348520a4"
|
||||
integrity sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==
|
||||
"@typescript-eslint/parser@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.40.0.tgz#1bc9f3701ced29540eb76ff2d95ce0d52ddc7e69"
|
||||
integrity sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.45.0"
|
||||
"@typescript-eslint/types" "8.45.0"
|
||||
"@typescript-eslint/typescript-estree" "8.45.0"
|
||||
"@typescript-eslint/visitor-keys" "8.45.0"
|
||||
"@typescript-eslint/scope-manager" "8.40.0"
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/typescript-estree" "8.40.0"
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/project-service@8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.45.0.tgz#f83dda1bca31dae2fd6821f9131daf1edebfd46c"
|
||||
integrity sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==
|
||||
"@typescript-eslint/parser@^8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.42.0.tgz#20ea66f4867981fb5bb62cbe1454250fc4a440ab"
|
||||
integrity sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.45.0"
|
||||
"@typescript-eslint/types" "^8.45.0"
|
||||
"@typescript-eslint/scope-manager" "8.42.0"
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
"@typescript-eslint/typescript-estree" "8.42.0"
|
||||
"@typescript-eslint/visitor-keys" "8.42.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz#59615ba506a9e3479d1efb0d09d6ab52f2a19142"
|
||||
integrity sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==
|
||||
"@typescript-eslint/project-service@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.40.0.tgz#1b7ba6079ff580c3215882fe75a43e5d3ed166b9"
|
||||
integrity sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.45.0"
|
||||
"@typescript-eslint/visitor-keys" "8.45.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.40.0"
|
||||
"@typescript-eslint/types" "^8.40.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.45.0", "@typescript-eslint/tsconfig-utils@^8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz#63d38282790a2566c571bad423e7c1cad1f3d64c"
|
||||
integrity sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==
|
||||
|
||||
"@typescript-eslint/type-utils@8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz#04004bdf2598844faa29fb936fb6b0ee10d6d3f3"
|
||||
integrity sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==
|
||||
"@typescript-eslint/project-service@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.42.0.tgz#636eb3418b6c42c98554dce884943708bf41a583"
|
||||
integrity sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.45.0"
|
||||
"@typescript-eslint/typescript-estree" "8.45.0"
|
||||
"@typescript-eslint/utils" "8.45.0"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.42.0"
|
||||
"@typescript-eslint/types" "^8.42.0"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz#2fbfcc8643340d8cd692267e61548b946190be8a"
|
||||
integrity sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz#36016757bc85b46ea42bae47b61f9421eddedde3"
|
||||
integrity sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
"@typescript-eslint/visitor-keys" "8.42.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.40.0", "@typescript-eslint/tsconfig-utils@^8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz#8e8fdb9b988854aedd04abdde3239c4bdd2d26e4"
|
||||
integrity sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.42.0", "@typescript-eslint/tsconfig-utils@^8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz#21a3e74396fd7443ff930bc41b27789ba7e9236e"
|
||||
integrity sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==
|
||||
|
||||
"@typescript-eslint/type-utils@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz#a7e4a1f0815dd0ba3e4eef945cc87193ca32c422"
|
||||
integrity sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/typescript-estree" "8.40.0"
|
||||
"@typescript-eslint/utils" "8.40.0"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/types@8.45.0", "@typescript-eslint/types@^8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.45.0.tgz#fc01cd2a4690b9713b02f895e82fb43f7d960684"
|
||||
integrity sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==
|
||||
"@typescript-eslint/types@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.40.0.tgz#0b580fdf643737aa5c01285314b5c6e9543846a9"
|
||||
integrity sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz#3498500f109a89b104d2770497c707e56dfe062d"
|
||||
integrity sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==
|
||||
"@typescript-eslint/types@8.42.0", "@typescript-eslint/types@^8.40.0", "@typescript-eslint/types@^8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.42.0.tgz#ae15c09cebda20473772902033328e87372db008"
|
||||
integrity sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz#295149440ce7da81c790a4e14e327599a3a1e5c9"
|
||||
integrity sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.45.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.45.0"
|
||||
"@typescript-eslint/types" "8.45.0"
|
||||
"@typescript-eslint/visitor-keys" "8.45.0"
|
||||
"@typescript-eslint/project-service" "8.40.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.40.0"
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/visitor-keys" "8.40.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -4310,22 +4205,46 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.45.0.tgz#6e68e92d99019fdf56018d0e6664c76a70470c95"
|
||||
integrity sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==
|
||||
"@typescript-eslint/typescript-estree@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz#593c3af87d4462252c0d7239d1720b84a1b56864"
|
||||
integrity sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.42.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.42.0"
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
"@typescript-eslint/visitor-keys" "8.42.0"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
minimatch "^9.0.4"
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.1.0"
|
||||
|
||||
"@typescript-eslint/utils@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.40.0.tgz#8d0c6430ed2f5dc350784bb0d8be514da1e54054"
|
||||
integrity sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.7.0"
|
||||
"@typescript-eslint/scope-manager" "8.45.0"
|
||||
"@typescript-eslint/types" "8.45.0"
|
||||
"@typescript-eslint/typescript-estree" "8.45.0"
|
||||
"@typescript-eslint/scope-manager" "8.40.0"
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
"@typescript-eslint/typescript-estree" "8.40.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.45.0":
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz#4e3bcc55da64ac61069ebfe62ca240567ac7d784"
|
||||
integrity sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==
|
||||
"@typescript-eslint/visitor-keys@8.40.0":
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz#c1b45196981311fed7256863be4bfb2d3eda332a"
|
||||
integrity sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.45.0"
|
||||
"@typescript-eslint/types" "8.40.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.42.0":
|
||||
version "8.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz#87c6caaa1ac307bc73a87c1fc469f88f0162f27e"
|
||||
integrity sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.42.0"
|
||||
eslint-visitor-keys "^4.2.1"
|
||||
|
||||
"@ungap/structured-clone@^1.0.0":
|
||||
@@ -4625,10 +4544,10 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^5.27.4:
|
||||
version "5.27.4"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.27.4.tgz#13c97deb12e6aeb43adecd23f3dbe3139a62e579"
|
||||
integrity sha512-rhArohoAUCxhkPjGI/BXthOrrjaElL4Fb7d4vEHnIR3DpxFXfegd4rN21IgGdiF+Iz4EFuUZu8MdS8NuJHLSVQ==
|
||||
antd@^5.26.7:
|
||||
version "5.27.1"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.27.1.tgz#5378fc017cb4057ffefe2a670f20e54b924d897d"
|
||||
integrity sha512-jGMSdBN7hAMvPV27B4RhzZfL6n6yu8yDbo7oXrlJasaOqB7bSDPcjdEy1kXy3JPsny/Qazb1ykzRI4EfcByAPQ==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.1"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4666,10 +4585,10 @@ antd@^5.27.4:
|
||||
rc-resize-observer "^1.4.3"
|
||||
rc-segmented "~2.7.0"
|
||||
rc-select "~14.16.8"
|
||||
rc-slider "~11.1.9"
|
||||
rc-slider "~11.1.8"
|
||||
rc-steps "~6.0.1"
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.53.0"
|
||||
rc-table "~7.51.1"
|
||||
rc-tabs "~15.7.0"
|
||||
rc-textarea "~1.10.2"
|
||||
rc-tooltip "~6.4.0"
|
||||
@@ -4846,10 +4765,10 @@ available-typed-arrays@^1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
axios@^1.12.2:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
|
||||
integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
|
||||
axios@^1.9.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.0.tgz#11248459be05a5ee493485628fa0e4323d0abfc3"
|
||||
integrity sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==
|
||||
dependencies:
|
||||
follow-redirects "^1.15.6"
|
||||
form-data "^4.0.4"
|
||||
@@ -4943,16 +4862,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
base64-js@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.8.9:
|
||||
version "2.8.10"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz#32eb5e253d633fa3fa3ffb1685fabf41680d9e8a"
|
||||
integrity sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
||||
@@ -5066,15 +4980,14 @@ browser-assert@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200"
|
||||
integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==
|
||||
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.24.5, browserslist@^4.25.0, browserslist@^4.25.3:
|
||||
version "4.26.3"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.26.3.tgz#40fbfe2d1cd420281ce5b1caa8840049c79afb56"
|
||||
integrity sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==
|
||||
browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.0, browserslist@^4.25.3:
|
||||
version "4.25.3"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.3.tgz#9167c9cbb40473f15f75f85189290678b99b16c5"
|
||||
integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
|
||||
dependencies:
|
||||
baseline-browser-mapping "^2.8.9"
|
||||
caniuse-lite "^1.0.30001746"
|
||||
electron-to-chromium "^1.5.227"
|
||||
node-releases "^2.0.21"
|
||||
caniuse-lite "^1.0.30001735"
|
||||
electron-to-chromium "^1.5.204"
|
||||
node-releases "^2.0.19"
|
||||
update-browserslist-db "^1.1.3"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
@@ -5082,14 +4995,6 @@ buffer-from@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
|
||||
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
|
||||
dependencies:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.2.1"
|
||||
|
||||
bytes@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
|
||||
@@ -5177,16 +5082,11 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001739:
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001735, caniuse-lite@^1.0.30001739:
|
||||
version "1.0.30001739"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4"
|
||||
integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==
|
||||
|
||||
caniuse-lite@^1.0.30001746:
|
||||
version "1.0.30001746"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001746.tgz#199d20f04f5369825e00ff7067d45d5dfa03aee7"
|
||||
integrity sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
|
||||
@@ -5354,7 +5254,7 @@ clone-deep@^4.0.1:
|
||||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
clsx@^2.0.0, clsx@^2.1.1:
|
||||
clsx@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
@@ -6440,7 +6340,14 @@ domhandler@^5.0.2, domhandler@^5.0.3:
|
||||
dependencies:
|
||||
domelementtype "^2.3.0"
|
||||
|
||||
dompurify@=3.2.6, dompurify@^3.2.5:
|
||||
dompurify@=3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.4.tgz#af5a5a11407524431456cf18836c55d13441cd8e"
|
||||
integrity sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==
|
||||
optionalDependencies:
|
||||
"@types/trusted-types" "^2.0.7"
|
||||
|
||||
dompurify@^3.2.5:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.6.tgz#ca040a6ad2b88e2a92dc45f38c79f84a714a1cad"
|
||||
integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==
|
||||
@@ -6509,10 +6416,10 @@ ee-first@1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||
|
||||
electron-to-chromium@^1.5.227:
|
||||
version "1.5.228"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.228.tgz#38b849bc8714bd21fb64f5ad56bf8cfd8638e1e9"
|
||||
integrity sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==
|
||||
electron-to-chromium@^1.5.204:
|
||||
version "1.5.207"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz#0fedde3eec615065ee95531c09a10578644c5552"
|
||||
integrity sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
@@ -6868,18 +6775,18 @@ eslint-visitor-keys@^4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
|
||||
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
|
||||
|
||||
eslint@^9.36.0:
|
||||
version "9.36.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.36.0.tgz#9cc5cbbfb9c01070425d9bfed81b4e79a1c09088"
|
||||
integrity sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==
|
||||
eslint@^9.34.0:
|
||||
version "9.34.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.34.0.tgz#0ea1f2c1b5d1671db8f01aa6b8ce722302016f7b"
|
||||
integrity sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.8.0"
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@eslint-community/regexpp" "^4.12.1"
|
||||
"@eslint/config-array" "^0.21.0"
|
||||
"@eslint/config-helpers" "^0.3.1"
|
||||
"@eslint/core" "^0.15.2"
|
||||
"@eslint/eslintrc" "^3.3.1"
|
||||
"@eslint/js" "9.36.0"
|
||||
"@eslint/js" "9.34.0"
|
||||
"@eslint/plugin-kit" "^0.3.5"
|
||||
"@humanfs/node" "^0.16.6"
|
||||
"@humanwhocodes/module-importer" "^1.0.1"
|
||||
@@ -10076,10 +9983,10 @@ node-gyp-build@^4.8.0, node-gyp-build@^4.8.2, node-gyp-build@^4.8.4:
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8"
|
||||
integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==
|
||||
|
||||
node-releases@^2.0.21:
|
||||
version "2.0.21"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.21.tgz#f59b018bc0048044be2d4c4c04e4c8b18160894c"
|
||||
integrity sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==
|
||||
node-releases@^2.0.19:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
||||
integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -11548,10 +11455,10 @@ rc-select@~14.16.2, rc-select@~14.16.8:
|
||||
rc-util "^5.16.1"
|
||||
rc-virtual-list "^3.5.2"
|
||||
|
||||
rc-slider@~11.1.9:
|
||||
version "11.1.9"
|
||||
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-11.1.9.tgz#d872130fbf4ec51f28543d62e90451091d6f5208"
|
||||
integrity sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==
|
||||
rc-slider@~11.1.8:
|
||||
version "11.1.8"
|
||||
resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-11.1.8.tgz#cf3b30dacac8f98d44f7685f733f6f7da146fc06"
|
||||
integrity sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.5"
|
||||
@@ -11575,10 +11482,10 @@ rc-switch@~4.1.0:
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.30.0"
|
||||
|
||||
rc-table@~7.53.0:
|
||||
version "7.53.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.53.1.tgz#b891aa39e9d1d944711f018692d2c52013afc90f"
|
||||
integrity sha512-firAd7Z+liqIDS5TubJ1qqcoBd6YcANLKWQDZhFf3rfoOTt/UNPj4n3O+2vhl+z4QMqwPEUVAil661WHA8H8Aw==
|
||||
rc-table@~7.51.1:
|
||||
version "7.51.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.51.1.tgz#cd69ae3262d3b61e4c93c979c12786906e944691"
|
||||
integrity sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/context" "^1.4.0"
|
||||
@@ -12608,7 +12515,7 @@ setprototypeof@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
sha.js@^2.4.12:
|
||||
sha.js@^2.4.11:
|
||||
version "2.4.12"
|
||||
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f"
|
||||
integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==
|
||||
@@ -13083,18 +12990,18 @@ svgo@^3.0.2, svgo@^3.2.0:
|
||||
csso "^5.0.5"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
swagger-client@^3.35.7:
|
||||
version "3.35.7"
|
||||
resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.35.7.tgz#2f649c1875cb88a747254963ea85dad09605f238"
|
||||
integrity sha512-AAVk7lBFIw41wI0tsqyh/l4dwJ0/eslHL2Ex4hmsGtuKcD6/wXunetO8AsmE5MptK4YgRvpmUDvKnF1TaGzdiQ==
|
||||
swagger-client@^3.35.5:
|
||||
version "3.35.6"
|
||||
resolved "https://registry.yarnpkg.com/swagger-client/-/swagger-client-3.35.6.tgz#03892a1ec9995db44d7b49feb7aafafc06d9ed51"
|
||||
integrity sha512-OgwNneIdC45KXwOfwrlkwgWPeAKiV4K75mOnZioTddo1mpp9dTboCDVJas7185Ww1ziBwzShBqXpNGmyha9ZQg==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.22.15"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
"@swagger-api/apidom-core" ">=1.0.0-beta.50 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-error" ">=1.0.0-beta.50 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-json-pointer" ">=1.0.0-beta.50 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-ns-openapi-3-1" ">=1.0.0-beta.50 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-reference" ">=1.0.0-beta.50 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-core" ">=1.0.0-beta.41 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-error" ">=1.0.0-beta.41 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-json-pointer" ">=1.0.0-beta.41 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-ns-openapi-3-1" ">=1.0.0-beta.41 <1.0.0-rc.0"
|
||||
"@swagger-api/apidom-reference" ">=1.0.0-beta.41 <1.0.0-rc.0"
|
||||
"@swaggerexpert/cookie" "^2.0.2"
|
||||
deepmerge "~4.3.0"
|
||||
fast-json-patch "^3.0.0-1"
|
||||
@@ -13107,19 +13014,18 @@ swagger-client@^3.35.7:
|
||||
ramda "^0.30.1"
|
||||
ramda-adjunct "^5.1.0"
|
||||
|
||||
swagger-ui-react@^5.29.1:
|
||||
version "5.29.1"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.29.1.tgz#90875ef551f0fc271e369db60e4a8cd369ce70f9"
|
||||
integrity sha512-simkO3VoHrWIQWLH2vCpOJSPVbocV+RLGvHzzh+jhqs5heWsA1cnadx31BbKJVajfxIO4dC3lclxgMYbLUk3fQ==
|
||||
swagger-ui-react@^5.27.1:
|
||||
version "5.27.1"
|
||||
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.27.1.tgz#315b59970c33933a5f62ca0f702789741dcedc7c"
|
||||
integrity sha512-wwDoavIeJI/Pwiavn32FMJ5dfptz0BAOKjSrj7EdU22QdP3gdk9+MZHdzzjxWURmVj0kc0XoQfsFgjln0toJaw==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.27.1"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
base64-js "^1.5.1"
|
||||
buffer "^6.0.3"
|
||||
classnames "^2.5.1"
|
||||
css.escape "1.5.1"
|
||||
deep-extend "0.6.0"
|
||||
dompurify "=3.2.6"
|
||||
dompurify "=3.2.4"
|
||||
ieee754 "^1.2.1"
|
||||
immutable "^3.x.x"
|
||||
js-file-download "^0.4.12"
|
||||
@@ -13140,8 +13046,8 @@ swagger-ui-react@^5.29.1:
|
||||
remarkable "^2.0.1"
|
||||
reselect "^5.1.1"
|
||||
serialize-error "^8.1.0"
|
||||
sha.js "^2.4.12"
|
||||
swagger-client "^3.35.7"
|
||||
sha.js "^2.4.11"
|
||||
swagger-client "^3.35.5"
|
||||
url-parse "^1.5.10"
|
||||
xml "=1.0.1"
|
||||
xml-but-prettier "^1.0.1"
|
||||
@@ -13154,10 +13060,10 @@ synckit@^0.11.7:
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.2.9"
|
||||
|
||||
tapable@^2.0.0, tapable@^2.2.0, tapable@^2.2.1, tapable@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.3.tgz#4b67b635b2d97578a06a2713d2f04800c237e99b"
|
||||
integrity sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==
|
||||
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.2.tgz#ab4984340d30cb9989a490032f086dbb8b56d872"
|
||||
integrity sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==
|
||||
|
||||
terser-webpack-plugin@^5.3.11, terser-webpack-plugin@^5.3.9:
|
||||
version "5.3.14"
|
||||
@@ -13415,15 +13321,15 @@ types-ramda@^0.30.1:
|
||||
dependencies:
|
||||
ts-toolbelt "^9.6.0"
|
||||
|
||||
typescript-eslint@^8.45.0:
|
||||
version "8.45.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.45.0.tgz#98ab164234dc04c112747ec0a4ae29a94efe123b"
|
||||
integrity sha512-qzDmZw/Z5beNLUrXfd0HIW6MzIaAV5WNDxmMs9/3ojGOpYavofgNAAD/nC6tGV2PczIi0iw8vot2eAe/sBn7zg==
|
||||
typescript-eslint@^8.39.0:
|
||||
version "8.40.0"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.40.0.tgz#27541748f3ca889c9698327bdacf815f7dc61804"
|
||||
integrity sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.45.0"
|
||||
"@typescript-eslint/parser" "8.45.0"
|
||||
"@typescript-eslint/typescript-estree" "8.45.0"
|
||||
"@typescript-eslint/utils" "8.45.0"
|
||||
"@typescript-eslint/eslint-plugin" "8.40.0"
|
||||
"@typescript-eslint/parser" "8.40.0"
|
||||
"@typescript-eslint/typescript-estree" "8.40.0"
|
||||
"@typescript-eslint/utils" "8.40.0"
|
||||
|
||||
typescript@~5.9.2:
|
||||
version "5.9.2"
|
||||
@@ -13839,7 +13745,7 @@ vscode-uri@~3.0.8:
|
||||
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f"
|
||||
integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
|
||||
|
||||
watchpack@^2.4.4:
|
||||
watchpack@^2.4.1:
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.4.tgz#473bda72f0850453da6425081ea46fc0d7602947"
|
||||
integrity sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==
|
||||
@@ -13962,10 +13868,10 @@ webpack-virtual-modules@^0.6.2:
|
||||
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
|
||||
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
|
||||
|
||||
webpack@^5.102.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.102.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.102.0.tgz#7a2416e6da356c35f1fb35333d2f5cee0133e953"
|
||||
integrity sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==
|
||||
webpack@^5.101.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
version "5.101.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.101.3.tgz#3633b2375bb29ea4b06ffb1902734d977bc44346"
|
||||
integrity sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.8"
|
||||
@@ -13975,7 +13881,7 @@ webpack@^5.102.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
"@webassemblyjs/wasm-parser" "^1.14.1"
|
||||
acorn "^8.15.0"
|
||||
acorn-import-phases "^1.0.3"
|
||||
browserslist "^4.24.5"
|
||||
browserslist "^4.24.0"
|
||||
chrome-trace-event "^1.0.2"
|
||||
enhanced-resolve "^5.17.3"
|
||||
es-module-lexer "^1.2.1"
|
||||
@@ -13988,9 +13894,9 @@ webpack@^5.102.0, webpack@^5.88.1, webpack@^5.95.0:
|
||||
mime-types "^2.1.27"
|
||||
neo-async "^2.6.2"
|
||||
schema-utils "^4.3.2"
|
||||
tapable "^2.2.3"
|
||||
tapable "^2.1.1"
|
||||
terser-webpack-plugin "^5.3.11"
|
||||
watchpack "^2.4.4"
|
||||
watchpack "^2.4.1"
|
||||
webpack-sources "^3.3.3"
|
||||
|
||||
webpackbar@^6.0.1:
|
||||
|
||||
@@ -133,8 +133,7 @@ denodo = ["denodo-sqlalchemy~=1.0.6"]
|
||||
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
|
||||
drill = ["sqlalchemy-drill>=1.1.4, <2"]
|
||||
druid = ["pydruid>=0.6.5,<0.7"]
|
||||
# DuckDB 1.x has type system incompatibilities with duckdb-engine.
|
||||
duckdb = ["duckdb>=0.10.2,<0.11", "duckdb-engine>=0.17.0"]
|
||||
duckdb = ["duckdb-engine>=0.17.0"]
|
||||
dynamodb = ["pydynamodb>=0.4.2"]
|
||||
solr = ["sqlalchemy-solr >= 0.2.0"]
|
||||
elasticsearch = ["elasticsearch-dbapi>=0.2.9, <0.3.0"]
|
||||
|
||||
@@ -181,10 +181,8 @@ dnspython==2.7.0
|
||||
# email-validator
|
||||
docker==7.0.0
|
||||
# via apache-superset
|
||||
duckdb==0.10.3
|
||||
# via
|
||||
# apache-superset
|
||||
# duckdb-engine
|
||||
duckdb==1.3.2
|
||||
# via duckdb-engine
|
||||
duckdb-engine==0.17.0
|
||||
# via apache-superset
|
||||
email-validator==2.2.0
|
||||
|
||||
@@ -83,6 +83,7 @@ module.exports = {
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:react-prefer-function-component/recommended',
|
||||
'plugin:storybook/recommended',
|
||||
'plugin:react-you-might-not-need-an-effect/legacy-recommended',
|
||||
],
|
||||
parser: '@babel/eslint-parser',
|
||||
parserOptions: {
|
||||
@@ -272,10 +273,6 @@ module.exports = {
|
||||
{
|
||||
files: ['packages/**'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: true },
|
||||
],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
@@ -327,12 +324,7 @@ module.exports = {
|
||||
'*.stories.tsx',
|
||||
'*.stories.jsx',
|
||||
'fixtures.*',
|
||||
'**/test/**/*',
|
||||
'**/tests/**/*',
|
||||
'spec/**/*',
|
||||
'**/fixtures/**/*',
|
||||
'**/__mocks__/**/*',
|
||||
'**/spec/**/*',
|
||||
'playwright/**/*',
|
||||
],
|
||||
excludedFiles: 'cypress-base/cypress/**/*',
|
||||
plugins: ['jest', 'jest-dom', 'no-only-tests', 'testing-library'],
|
||||
@@ -356,9 +348,7 @@ module.exports = {
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
'jest/consistent-test-it': 'error',
|
||||
'no-only-tests/no-only-tests': 'error',
|
||||
'prefer-promise-reject-errors': 0,
|
||||
'max-classes-per-file': 0,
|
||||
// temporary rules to help with migration - please re-enable!
|
||||
'testing-library/await-async-queries': 0,
|
||||
@@ -397,12 +387,6 @@ module.exports = {
|
||||
'*.stories.tsx',
|
||||
'*.stories.jsx',
|
||||
'fixtures.*',
|
||||
'**/test/**/*',
|
||||
'**/tests/**/*',
|
||||
'spec/**/*',
|
||||
'**/fixtures/**/*',
|
||||
'**/__mocks__/**/*',
|
||||
'**/spec/**/*',
|
||||
'cypress-base/cypress/**/*',
|
||||
'Stories.tsx',
|
||||
'packages/superset-ui-core/src/theme/index.tsx',
|
||||
@@ -416,27 +400,10 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
{
|
||||
// Override specifically for packages stories and overview files
|
||||
// This must come LAST to override other rules
|
||||
files: [
|
||||
'packages/**/*.stories.*',
|
||||
'packages/**/*.overview.*',
|
||||
'packages/**/fixtures.*',
|
||||
],
|
||||
files: ['playwright/**/*'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Allow @playwright/test imports in Playwright test files
|
||||
files: ['playwright/**/*.ts', 'playwright/**/*.js'],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
devDependencies: true,
|
||||
},
|
||||
],
|
||||
'import/no-unresolved': 0, // Playwright is not installed in main build
|
||||
'import/no-extraneous-dependencies': 0, // Playwright is not installed in main build
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -445,13 +412,7 @@ module.exports = {
|
||||
'theme-colors/no-literal-colors': 'error',
|
||||
'icons/no-fa-icons-usage': 'error',
|
||||
'i18n-strings/no-template-vars': ['error', true],
|
||||
camelcase: [
|
||||
'error',
|
||||
{
|
||||
allow: ['^UNSAFE_'],
|
||||
properties: 'never',
|
||||
},
|
||||
],
|
||||
'i18n-strings/sentence-case-buttons': 'error',
|
||||
'class-methods-use-this': 0,
|
||||
curly: 2,
|
||||
'func-names': 0,
|
||||
|
||||
1310
superset-frontend/package-lock.json
generated
1310
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -129,7 +129,7 @@
|
||||
"@visx/xychart": "^3.5.1",
|
||||
"ag-grid-community": "34.2.0",
|
||||
"ag-grid-react": "34.2.0",
|
||||
"antd": "^5.24.9",
|
||||
"antd": "^5.24.6",
|
||||
"chrono-node": "^2.7.8",
|
||||
"classnames": "^2.2.5",
|
||||
"content-disposition": "^0.5.4",
|
||||
@@ -211,7 +211,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.60.0",
|
||||
"@applitools/eyes-storybook": "^3.55.6",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@babel/compat-data": "^7.28.0",
|
||||
"@babel/core": "^7.28.3",
|
||||
@@ -225,7 +225,7 @@
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@babel/register": "^7.23.7",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@babel/runtime-corejs3": "^7.28.2",
|
||||
"@babel/types": "^7.26.9",
|
||||
"@cypress/react": "^8.0.2",
|
||||
@@ -260,7 +260,7 @@
|
||||
"@types/node": "^22.12.0",
|
||||
"@types/react": "^17.0.83",
|
||||
"@types/react-dom": "^17.0.26",
|
||||
"@types/react-json-tree": "^0.13.0",
|
||||
"@types/react-json-tree": "^0.6.11",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-resizable": "^3.0.8",
|
||||
@@ -318,7 +318,7 @@
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-html-reporter": "^4.3.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lerna": "^8.2.3",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"open-cli": "^8.0.0",
|
||||
@@ -341,7 +341,7 @@
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.102.0",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"webpack-cli": "^6.0.1",
|
||||
"webpack-dev-server": "^5.2.2",
|
||||
|
||||
@@ -16,17 +16,10 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
declare module '@theme/Layout' {
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly children?: ReactNode;
|
||||
readonly noFooter?: boolean;
|
||||
readonly wrapperClassName?: string;
|
||||
readonly title?: string;
|
||||
readonly description?: string;
|
||||
{
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
}
|
||||
}
|
||||
|
||||
export default function Layout(props: Props): ReactNode;
|
||||
}
|
||||
@@ -22,7 +22,7 @@
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^5.24.9",
|
||||
"antd": "^5.24.6",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('aggregationOperator', () => {
|
||||
granularity: 'month',
|
||||
};
|
||||
|
||||
it('should return undefined for LAST_VALUE aggregation', () => {
|
||||
test('should return undefined for LAST_VALUE aggregation', () => {
|
||||
const formDataWithLastValue = {
|
||||
...formData,
|
||||
aggregation: 'LAST_VALUE',
|
||||
@@ -51,7 +51,7 @@ describe('aggregationOperator', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when metrics is empty', () => {
|
||||
test('should return undefined when metrics is empty', () => {
|
||||
const queryObjectWithoutMetrics = {
|
||||
...queryObject,
|
||||
metrics: [],
|
||||
@@ -67,7 +67,7 @@ describe('aggregationOperator', () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should apply sum aggregation to all metrics', () => {
|
||||
test('should apply sum aggregation to all metrics', () => {
|
||||
const formDataWithSum = {
|
||||
...formData,
|
||||
aggregation: 'sum',
|
||||
@@ -91,7 +91,7 @@ describe('aggregationOperator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply mean aggregation to all metrics', () => {
|
||||
test('should apply mean aggregation to all metrics', () => {
|
||||
const formDataWithMean = {
|
||||
...formData,
|
||||
aggregation: 'mean',
|
||||
@@ -115,7 +115,7 @@ describe('aggregationOperator', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default aggregation when not specified', () => {
|
||||
test('should use default aggregation when not specified', () => {
|
||||
expect(aggregationOperator(formData, queryObject)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
68
superset-frontend/packages/superset-ui-core/.eslintrc
Normal file
68
superset-frontend/packages/superset-ui-core/.eslintrc
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
{
|
||||
"plugins": ["jest", "jest-dom", "no-only-tests", "testing-library"],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"settings": {
|
||||
"jest": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest-dom/recommended",
|
||||
"plugin:testing-library/react"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/*.stories.*",
|
||||
"**/*.overview.*",
|
||||
"**/fixtures.*"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"jest/consistent-test-it": "error",
|
||||
"no-only-tests/no-only-tests": "error",
|
||||
"prefer-promise-reject-errors": 0,
|
||||
|
||||
"testing-library/no-node-access": "off",
|
||||
"testing-library/prefer-screen-queries": "off",
|
||||
"testing-library/no-container": "off",
|
||||
"testing-library/await-async-queries": "off",
|
||||
"testing-library/await-async-utils": "off",
|
||||
"testing-library/no-await-sync-events": "off",
|
||||
"testing-library/no-render-in-lifecycle": "off",
|
||||
"testing-library/no-unnecessary-act": "off",
|
||||
"testing-library/no-wait-for-multiple-assertions": "off",
|
||||
"testing-library/await-async-events": "off",
|
||||
"testing-library/no-wait-for-side-effects": "off",
|
||||
"testing-library/prefer-presence-queries": "off",
|
||||
"testing-library/render-result-naming-convention": "off",
|
||||
"testing-library/prefer-find-by": "off",
|
||||
"testing-library/no-manual-cleanup": "off"
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
"dependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"@babel/runtime": "^7.28.2",
|
||||
"@fontsource/fira-code": "^5.2.6",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
@@ -78,7 +78,7 @@
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/jquery": "^3.5.8",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^22.10.3",
|
||||
@@ -91,7 +91,7 @@
|
||||
"timezone-mock": "1.3.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"antd": "^5.24.9",
|
||||
"antd": "^5.24.6",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen, fireEvent } from '@superset-ui/core/spec';
|
||||
import { render, screen } from '@superset-ui/core/spec';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { TableInstance, useTable } from 'react-table';
|
||||
import TableCollection from '.';
|
||||
@@ -36,28 +36,19 @@ beforeEach(() => {
|
||||
accessor: 'col2',
|
||||
id: 'col2',
|
||||
},
|
||||
{
|
||||
Header: 'Nested Field',
|
||||
accessor: 'parent.child',
|
||||
id: 'parent.child',
|
||||
dataIndex: ['parent', 'child'],
|
||||
},
|
||||
];
|
||||
const data = [
|
||||
{
|
||||
col1: 'Line 01 - Col 01',
|
||||
col2: 'Line 01 - Col 02',
|
||||
parent: { child: 'Nested Value 1' },
|
||||
},
|
||||
{
|
||||
col1: 'Line 02 - Col 01',
|
||||
col2: 'Line 02 - Col 02',
|
||||
parent: { child: 'Nested Value 2' },
|
||||
},
|
||||
{
|
||||
col1: 'Line 03 - Col 01',
|
||||
col2: 'Line 03 - Col 02',
|
||||
parent: { child: 'Nested Value 3' },
|
||||
},
|
||||
];
|
||||
// @ts-ignore
|
||||
@@ -215,28 +206,3 @@ test('Bulk selection should work with pagination', () => {
|
||||
const checkboxes = screen.getAllByRole('checkbox');
|
||||
expect(checkboxes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should call setSortBy when clicking sortable column header', () => {
|
||||
const setSortBy = jest.fn();
|
||||
const sortingProps = {
|
||||
...defaultProps,
|
||||
setSortBy,
|
||||
};
|
||||
|
||||
render(<TableCollection {...sortingProps} />);
|
||||
|
||||
// Target the nested field column (the column that needs the array-to-dot conversion)
|
||||
const nestedFieldHeader = screen.getByText('Nested Field');
|
||||
expect(nestedFieldHeader).toBeInTheDocument();
|
||||
|
||||
// Click on the nested field column header to trigger sorting
|
||||
fireEvent.click(nestedFieldHeader);
|
||||
|
||||
// Verify setSortBy was called with the correct arguments and dot notation conversion
|
||||
expect(setSortBy).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 'parent.child',
|
||||
desc: expect.any(Boolean),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -215,14 +215,9 @@ function TableCollection<T extends object>({
|
||||
const handleTableChange = useCallback(
|
||||
(_pagination: any, _filters: any, sorter: SorterResult) => {
|
||||
if (sorter && sorter.field) {
|
||||
// Convert array field back to dot notation for nested fields
|
||||
const fieldId = Array.isArray(sorter.field)
|
||||
? sorter.field.join('.')
|
||||
: sorter.field;
|
||||
|
||||
setSortBy?.([
|
||||
{
|
||||
id: fieldId,
|
||||
id: sorter.field,
|
||||
desc: sorter.order === 'descend',
|
||||
},
|
||||
] as SortingRule<T>[]);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('Typography Component', () => {
|
||||
|
||||
it('renders strong text', () => {
|
||||
render(<Typography.Text strong>Strong Text</Typography.Text>);
|
||||
expect(screen.getByText('Strong Text')).toHaveStyle('font-weight: 600');
|
||||
expect(screen.getByText('Strong Text')).toHaveStyle('font-weight: 500');
|
||||
});
|
||||
|
||||
it('renders underlined text', () => {
|
||||
|
||||
@@ -16,18 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/200.css';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
import '@fontsource/fira-code/400.css';
|
||||
import '@fontsource/fira-code/500.css';
|
||||
import '@fontsource/fira-code/600.css';
|
||||
/* eslint-enable import/extensions */
|
||||
|
||||
import { css, useTheme, Global } from '@emotion/react';
|
||||
|
||||
export const GlobalStyles = () => {
|
||||
|
||||
@@ -49,12 +49,14 @@ describe('Theme', () => {
|
||||
});
|
||||
|
||||
describe('fromConfig', () => {
|
||||
it('creates a theme with Ant Design defaults when no config is provided', () => {
|
||||
it('creates a theme with default tokens when no config is provided', () => {
|
||||
const theme = Theme.fromConfig();
|
||||
|
||||
// Verify Ant Design default tokens are set
|
||||
expect(theme.theme.colorPrimary).toBeDefined();
|
||||
expect(theme.theme.fontFamily).toBeDefined();
|
||||
// Verify default primary color is set
|
||||
expect(theme.theme.colorPrimary).toBe('#2893b3');
|
||||
|
||||
// Verify default font family is set
|
||||
expect(theme.theme.fontFamily).toContain('Inter');
|
||||
|
||||
// Verify the theme is initialized with semantic color tokens
|
||||
expect(theme.theme.colorText).toBeDefined();
|
||||
@@ -77,8 +79,8 @@ describe('Theme', () => {
|
||||
// Verify custom font family is set
|
||||
expect(theme.theme.fontFamily).toBe('CustomFont, sans-serif');
|
||||
|
||||
// Unspecified values will use Ant Design defaults
|
||||
expect(theme.theme.colorError).toBeDefined();
|
||||
// But default tokens should still be preserved for unspecified values
|
||||
expect(theme.theme.colorError).toBe('#e04355');
|
||||
});
|
||||
|
||||
it('creates a theme with dark mode when dark algorithm is specified', () => {
|
||||
@@ -203,586 +205,4 @@ describe('Theme', () => {
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromConfig with baseTheme', () => {
|
||||
it('applies base theme tokens under the main config', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#ff0000',
|
||||
colorError: '#00ff00',
|
||||
fontFamily: 'BaseFont',
|
||||
},
|
||||
};
|
||||
|
||||
const userConfig: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#0000ff',
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userConfig, baseTheme);
|
||||
|
||||
// User config overrides base theme
|
||||
expect(theme.theme.colorPrimary).toBe('#0000ff');
|
||||
|
||||
// Base theme tokens are preserved when not overridden
|
||||
expect(theme.theme.colorError).toBe('#00ff00');
|
||||
expect(theme.theme.fontFamily).toBe('BaseFont');
|
||||
});
|
||||
|
||||
it('applies base theme when no user config is provided', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#ff0000',
|
||||
fontFamily: 'TestFont',
|
||||
},
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(undefined, baseTheme);
|
||||
|
||||
// Color may be transformed by dark algorithm, check fontFamily instead
|
||||
expect(theme.theme.fontFamily).toBe('TestFont');
|
||||
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('handles empty config with base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#ff0000',
|
||||
},
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
const emptyConfig: AnyThemeConfig = {};
|
||||
|
||||
const theme = Theme.fromConfig(emptyConfig, baseTheme);
|
||||
|
||||
// Base theme tokens should be applied
|
||||
expect(theme.theme.colorPrimary).toBe('#ff0000');
|
||||
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
|
||||
});
|
||||
|
||||
it('merges algorithms correctly with base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.compactAlgorithm,
|
||||
};
|
||||
|
||||
const userConfig: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userConfig, baseTheme);
|
||||
|
||||
// User algorithm should override base algorithm
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('merges component overrides with base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#basebutton',
|
||||
},
|
||||
Input: {
|
||||
colorBorder: '#baseinput',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const userConfig: AnyThemeConfig = {
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#userbutton',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userConfig, baseTheme);
|
||||
const serialized = theme.toSerializedConfig();
|
||||
|
||||
// User component config overrides base
|
||||
expect(serialized.components?.Button?.colorPrimary).toBe('#userbutton');
|
||||
|
||||
// Base component config preserved when not overridden
|
||||
expect(serialized.components?.Input?.colorBorder).toBe('#baseinput');
|
||||
});
|
||||
|
||||
it('handles undefined config and undefined base theme', () => {
|
||||
const theme = Theme.fromConfig(undefined, undefined);
|
||||
|
||||
// Should get Ant Design defaults
|
||||
expect(theme.theme.colorPrimary).toBeDefined();
|
||||
expect(theme.theme.fontFamily).toBeDefined();
|
||||
});
|
||||
|
||||
it('preserves custom tokens in base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#ff0000',
|
||||
// Custom superset-specific tokens
|
||||
brandLogoAlt: 'CustomLogo',
|
||||
menuHoverBackgroundColor: '#00ff00',
|
||||
} as Record<string, any>,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig({}, baseTheme);
|
||||
|
||||
// Standard token
|
||||
expect(theme.theme.colorPrimary).toBe('#ff0000');
|
||||
|
||||
// Custom tokens should be preserved
|
||||
expect((theme.theme as any).brandLogoAlt).toBe('CustomLogo');
|
||||
expect((theme.theme as any).menuHoverBackgroundColor).toBe('#00ff00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases with base theme and dark mode', () => {
|
||||
it('correctly applies base theme tokens in dark mode', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#1890ff',
|
||||
fontFamily: 'TestFont',
|
||||
},
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
const baseThemeDark: AnyThemeConfig = {
|
||||
...baseTheme,
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
// Simulate light mode with base theme
|
||||
const lightTheme = Theme.fromConfig({}, baseTheme);
|
||||
expect(lightTheme.theme.colorPrimary).toBe('#1890ff');
|
||||
expect(lightTheme.theme.fontFamily).toBe('TestFont');
|
||||
|
||||
// Simulate dark mode with base theme dark
|
||||
const darkTheme = Theme.fromConfig({}, baseThemeDark);
|
||||
// Dark algorithm transforms colors, but fontFamily should be preserved
|
||||
expect(darkTheme.theme.fontFamily).toBe('TestFont');
|
||||
|
||||
// Verify the algorithm is different
|
||||
const lightSerialized = lightTheme.toSerializedConfig();
|
||||
const darkSerialized = darkTheme.toSerializedConfig();
|
||||
expect(lightSerialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
|
||||
expect(darkSerialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('handles switching from custom theme back to base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#1890ff',
|
||||
},
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
// First apply custom theme
|
||||
const customConfig: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#52c41a',
|
||||
},
|
||||
};
|
||||
const themeWithCustom = Theme.fromConfig(customConfig, baseTheme);
|
||||
expect(themeWithCustom.theme.colorPrimary).toBe('#52c41a');
|
||||
|
||||
// Then switch back to empty config (simulating removal of custom theme)
|
||||
const themeWithEmpty = Theme.fromConfig({}, baseTheme);
|
||||
expect(themeWithEmpty.theme.colorPrimary).toBe('#1890ff');
|
||||
|
||||
// Verify they produce different outputs
|
||||
expect(themeWithCustom.theme.colorPrimary).not.toBe(
|
||||
themeWithEmpty.theme.colorPrimary,
|
||||
);
|
||||
});
|
||||
|
||||
it('handles algorithm-only config with base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
fontFamily: 'TestFont',
|
||||
borderRadius: 8,
|
||||
},
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
// Config that only specifies algorithm (common for THEME_DARK)
|
||||
const algorithmOnlyConfig: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(algorithmOnlyConfig, baseTheme);
|
||||
|
||||
// Should have base theme tokens
|
||||
expect(theme.theme.fontFamily).toBe('TestFont');
|
||||
expect(theme.theme.borderRadius).toBe(8);
|
||||
|
||||
// Should have user's algorithm
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('base theme integration tests', () => {
|
||||
it('merges base theme tokens with empty user theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
colorError: '#e04355',
|
||||
fontFamily: 'Inter, Helvetica',
|
||||
},
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
|
||||
expect(theme.theme.colorPrimary).toBe('#2893B3');
|
||||
expect(theme.theme.colorError).toBe('#e04355');
|
||||
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
|
||||
|
||||
// Should have user's algorithm
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
|
||||
});
|
||||
|
||||
it('allows user theme to override specific base theme tokens', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
colorError: '#e04355',
|
||||
fontFamily: 'Inter, Helvetica',
|
||||
borderRadius: 4,
|
||||
},
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: '#123456', // Override primary color
|
||||
// Leave other tokens from base
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
|
||||
// User override should win
|
||||
expect(theme.theme.colorPrimary).toBe('#123456');
|
||||
|
||||
// Base theme tokens should be preserved
|
||||
expect(theme.theme.colorError).toBe('#e04355');
|
||||
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
|
||||
expect(theme.theme.borderRadius).toBe(4);
|
||||
});
|
||||
|
||||
it('handles base theme with dark algorithm correctly', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
fontFamily: 'Inter, Helvetica',
|
||||
},
|
||||
};
|
||||
|
||||
const baseThemeDark: AnyThemeConfig = {
|
||||
...baseTheme,
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
const userDarkTheme: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userDarkTheme, baseThemeDark);
|
||||
|
||||
// Should have base tokens
|
||||
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
|
||||
|
||||
// Should be in dark mode
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('works with real-world Superset base theme configuration', () => {
|
||||
// Simulate actual Superset base theme (THEME_DEFAULT/THEME_DARK from config)
|
||||
const supersetBaseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
colorError: '#e04355',
|
||||
colorWarning: '#fcc700',
|
||||
colorSuccess: '#5ac189',
|
||||
colorInfo: '#66bcfe',
|
||||
fontFamily: "'Inter', Helvetica, Arial",
|
||||
fontFamilyCode: "'Fira Code', 'Courier New', monospace",
|
||||
},
|
||||
};
|
||||
|
||||
// Simulate THEME_DEFAULT from config
|
||||
const themeDefault: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
// Simulate THEME_DARK from config
|
||||
const themeDark: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
};
|
||||
|
||||
// Test light mode
|
||||
const lightTheme = Theme.fromConfig(themeDefault, supersetBaseTheme);
|
||||
expect(lightTheme.theme.colorPrimary).toBe('#2893B3');
|
||||
expect(lightTheme.theme.fontFamily).toBe("'Inter', Helvetica, Arial");
|
||||
|
||||
// Test dark mode
|
||||
const darkTheme = Theme.fromConfig(themeDark, {
|
||||
...supersetBaseTheme,
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
});
|
||||
expect(darkTheme.theme.fontFamily).toBe("'Inter', Helvetica, Arial");
|
||||
|
||||
const darkSerialized = darkTheme.toSerializedConfig();
|
||||
expect(darkSerialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('handles component overrides in base theme', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
primaryColor: '#custom-button',
|
||||
borderRadius: 8,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
|
||||
// Should preserve component overrides
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.components?.Button?.primaryColor).toBe(
|
||||
'#custom-button',
|
||||
);
|
||||
expect(serialized.components?.Button?.borderRadius).toBe(8);
|
||||
});
|
||||
|
||||
it('properly handles algorithm property override', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
},
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
token: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
const serialized = theme.toSerializedConfig();
|
||||
|
||||
// User algorithm should override base algorithm
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
|
||||
// Both base and user tokens should be merged
|
||||
expect(serialized.token?.colorPrimary).toBeTruthy();
|
||||
expect(serialized.token?.borderRadius).toBe(8);
|
||||
});
|
||||
|
||||
it('handles cssVar, hashed and inherit properties correctly', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
},
|
||||
cssVar: true,
|
||||
hashed: false,
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
inherit: true,
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
const serialized = theme.toSerializedConfig();
|
||||
|
||||
// User properties override/add to base
|
||||
expect(serialized.inherit).toBe(true);
|
||||
expect(serialized.cssVar).toBe(true);
|
||||
expect(serialized.hashed).toBe(false);
|
||||
|
||||
// Tokens are still merged
|
||||
expect(serialized.token?.colorPrimary).toBeTruthy();
|
||||
expect(serialized.token?.borderRadius).toBe(8);
|
||||
});
|
||||
|
||||
it('merges nested component styles correctly', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
fontFamily: 'BaseFont',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
colorPrimary: '#basebutton',
|
||||
fontSize: 14,
|
||||
},
|
||||
Input: {
|
||||
colorBorder: '#baseinput',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
fontSize: 16, // Override Button fontSize
|
||||
},
|
||||
Select: {
|
||||
colorBorder: '#userselect', // Add new component
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
const serialized = theme.toSerializedConfig();
|
||||
|
||||
// Tokens should be merged
|
||||
// Note: components present may affect color transformation
|
||||
expect(serialized.token?.colorPrimary).toBeTruthy();
|
||||
expect(serialized.token?.borderRadius).toBe(8);
|
||||
expect(serialized.token?.fontFamily).toBe('BaseFont');
|
||||
|
||||
// Components should be merged (shallow merge per component)
|
||||
expect(serialized.components?.Button?.colorPrimary).toBe('#basebutton');
|
||||
expect(serialized.components?.Button?.fontSize).toBe(16); // User override
|
||||
expect(serialized.components?.Input?.colorBorder).toBe('#baseinput');
|
||||
expect(serialized.components?.Select?.colorBorder).toBe('#userselect');
|
||||
});
|
||||
|
||||
it('setConfig replaces theme config entirely (does not preserve base theme)', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
fontFamily: 'Inter',
|
||||
},
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig({}, baseTheme);
|
||||
|
||||
expect(theme.theme.colorPrimary).toBe('#2893B3');
|
||||
expect(theme.theme.fontFamily).toBe('Inter');
|
||||
|
||||
// Update config (simulating theme change)
|
||||
theme.setConfig({
|
||||
token: {
|
||||
colorPrimary: '#654321',
|
||||
},
|
||||
algorithm: antdThemeImport.darkAlgorithm,
|
||||
});
|
||||
|
||||
// setConfig replaces the entire config, so base theme is NOT preserved
|
||||
// This is expected behavior - setConfig is for complete replacement
|
||||
expect(theme.theme.colorPrimary).toBeTruthy();
|
||||
// fontFamily reverts to Ant Design default since base theme is not reapplied
|
||||
expect(theme.theme.fontFamily).not.toBe('Inter');
|
||||
});
|
||||
|
||||
it('minimal theme preserves ALL base theme tokens except overridden ones', () => {
|
||||
// Simulate a comprehensive base theme with many tokens
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
colorError: '#e04355',
|
||||
colorWarning: '#fcc700',
|
||||
colorSuccess: '#5ac189',
|
||||
colorInfo: '#66bcfe',
|
||||
fontFamily: 'Inter, Helvetica',
|
||||
fontSize: 14,
|
||||
borderRadius: 4,
|
||||
lineWidth: 1,
|
||||
controlHeight: 32,
|
||||
// Custom Superset tokens
|
||||
brandLogoAlt: 'CustomLogo',
|
||||
menuHoverBackgroundColor: '#eeeeee',
|
||||
} as Record<string, any>,
|
||||
algorithm: antdThemeImport.defaultAlgorithm,
|
||||
};
|
||||
|
||||
// Minimal theme that only overrides primary color and algorithm
|
||||
const minimalTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#ff05dd', // Only override this
|
||||
},
|
||||
algorithm: antdThemeImport.darkAlgorithm, // Change to dark
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(minimalTheme, baseTheme);
|
||||
|
||||
// User's override should apply
|
||||
expect(theme.theme.colorPrimary).toBe('#ff05dd');
|
||||
|
||||
// ALL base theme tokens should be preserved
|
||||
expect(theme.theme.colorError).toBe('#e04355');
|
||||
expect(theme.theme.colorWarning).toBe('#fcc700');
|
||||
expect(theme.theme.colorSuccess).toBe('#5ac189');
|
||||
expect(theme.theme.colorInfo).toBe('#66bcfe');
|
||||
expect(theme.theme.fontFamily).toBe('Inter, Helvetica');
|
||||
expect(theme.theme.fontSize).toBe(14);
|
||||
expect(theme.theme.borderRadius).toBe(4);
|
||||
expect(theme.theme.lineWidth).toBe(1);
|
||||
expect(theme.theme.controlHeight).toBe(32);
|
||||
|
||||
// Custom tokens should also be preserved
|
||||
expect((theme.theme as any).brandLogoAlt).toBe('CustomLogo');
|
||||
expect((theme.theme as any).menuHoverBackgroundColor).toBe('#eeeeee');
|
||||
|
||||
// Algorithm should be updated
|
||||
const serialized = theme.toSerializedConfig();
|
||||
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
|
||||
});
|
||||
|
||||
it('arrays in themes are replaced entirely, not merged by index', () => {
|
||||
const baseTheme: AnyThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#2893B3',
|
||||
},
|
||||
algorithm: [
|
||||
antdThemeImport.compactAlgorithm,
|
||||
antdThemeImport.defaultAlgorithm,
|
||||
],
|
||||
};
|
||||
|
||||
const userTheme: AnyThemeConfig = {
|
||||
algorithm: [antdThemeImport.darkAlgorithm], // Replace with single item array
|
||||
};
|
||||
|
||||
const theme = Theme.fromConfig(userTheme, baseTheme);
|
||||
const serialized = theme.toSerializedConfig();
|
||||
|
||||
// User's array should completely replace base array
|
||||
expect(Array.isArray(serialized.algorithm)).toBe(true);
|
||||
expect(serialized.algorithm).toHaveLength(1);
|
||||
expect(serialized.algorithm).toContain(ThemeAlgorithm.DARK);
|
||||
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.COMPACT);
|
||||
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DEFAULT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,13 +20,31 @@
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
import React from 'react';
|
||||
import { theme as antdThemeImport, ConfigProvider } from 'antd';
|
||||
|
||||
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/200.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/400.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/500.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/inter/600.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/fira-code/400.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/fira-code/500.css';
|
||||
/* eslint-disable import/extensions */
|
||||
import '@fontsource/fira-code/600.css';
|
||||
|
||||
import {
|
||||
ThemeProvider,
|
||||
CacheProvider as EmotionCacheProvider,
|
||||
} from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
import { noop, mergeWith } from 'lodash';
|
||||
import { noop } from 'lodash';
|
||||
import { GlobalStyles } from './GlobalStyles';
|
||||
|
||||
import {
|
||||
AntdThemeConfig,
|
||||
AnyThemeConfig,
|
||||
@@ -35,16 +53,63 @@ import {
|
||||
allowedAntdTokens,
|
||||
SharedAntdTokens,
|
||||
} from './types';
|
||||
|
||||
import { normalizeThemeConfig, serializeThemeConfig } from './utils';
|
||||
|
||||
/* eslint-disable theme-colors/no-literal-colors */
|
||||
|
||||
export class Theme {
|
||||
theme: SupersetTheme;
|
||||
|
||||
private static readonly defaultTokens = {
|
||||
// Brand
|
||||
brandLogoAlt: 'Apache Superset',
|
||||
brandLogoUrl: '/static/assets/images/superset-logo-horiz.png',
|
||||
brandLogoMargin: '18px',
|
||||
brandLogoHref: '/',
|
||||
brandLogoHeight: '24px',
|
||||
|
||||
// Spinner
|
||||
brandSpinnerUrl: undefined,
|
||||
brandSpinnerSvg: undefined,
|
||||
|
||||
// Default colors
|
||||
colorPrimary: '#2893B3', // NOTE: previous lighter primary color was #20a7c9
|
||||
colorLink: '#2893B3',
|
||||
colorError: '#e04355',
|
||||
colorWarning: '#fcc700',
|
||||
colorSuccess: '#5ac189',
|
||||
colorInfo: '#66bcfe',
|
||||
|
||||
// Forcing some default tokens
|
||||
fontFamily: `'Inter', Helvetica, Arial`,
|
||||
fontFamilyCode: `'Fira Code', 'Courier New', monospace`,
|
||||
|
||||
// Extra tokens
|
||||
transitionTiming: 0.3,
|
||||
brandIconMaxWidth: 37,
|
||||
fontSizeXS: '8',
|
||||
fontSizeXXL: '28',
|
||||
fontWeightNormal: '400',
|
||||
fontWeightLight: '300',
|
||||
fontWeightStrong: 500,
|
||||
};
|
||||
|
||||
private antdConfig: AntdThemeConfig;
|
||||
|
||||
private constructor({ config }: { config?: AnyThemeConfig }) {
|
||||
this.SupersetThemeProvider = this.SupersetThemeProvider.bind(this);
|
||||
this.setConfig(config || {});
|
||||
|
||||
// Create a new config object with default tokens
|
||||
const newConfig: AnyThemeConfig = config ? { ...config } : {};
|
||||
|
||||
// Ensure token property exists with defaults
|
||||
newConfig.token = {
|
||||
...Theme.defaultTokens,
|
||||
...(config?.token || {}),
|
||||
};
|
||||
|
||||
this.setConfig(newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,24 +118,9 @@ export class Theme {
|
||||
* If simple tokens are provided as { token: {...} }, they will be applied with defaults
|
||||
* If no config is provided, uses default tokens
|
||||
* Dark mode can be set via the algorithm property in the config
|
||||
* @param config - The theme configuration
|
||||
* @param baseTheme - Optional base theme to apply under the config
|
||||
*/
|
||||
static fromConfig(
|
||||
config?: AnyThemeConfig,
|
||||
baseTheme?: AnyThemeConfig,
|
||||
): Theme {
|
||||
let mergedConfig: AnyThemeConfig | undefined = config;
|
||||
|
||||
if (baseTheme && config) {
|
||||
mergedConfig = mergeWith({}, baseTheme, config, (objValue, srcValue) =>
|
||||
Array.isArray(srcValue) ? srcValue : undefined,
|
||||
);
|
||||
} else if (baseTheme && !config) {
|
||||
mergedConfig = baseTheme;
|
||||
}
|
||||
|
||||
return new Theme({ config: mergedConfig });
|
||||
static fromConfig(config?: AnyThemeConfig): Theme {
|
||||
return new Theme({ config });
|
||||
}
|
||||
|
||||
private static getFilteredAntdTheme(
|
||||
@@ -98,15 +148,22 @@ export class Theme {
|
||||
setConfig(config: AnyThemeConfig): void {
|
||||
const antdConfig = normalizeThemeConfig(config);
|
||||
|
||||
// Apply default tokens to token property
|
||||
antdConfig.token = {
|
||||
...Theme.defaultTokens,
|
||||
...(antdConfig.token || {}),
|
||||
};
|
||||
|
||||
// First phase: Let Ant Design compute the tokens
|
||||
const tokens = Theme.getFilteredAntdTheme(antdConfig);
|
||||
|
||||
// Set the base theme properties
|
||||
this.antdConfig = antdConfig;
|
||||
this.theme = {
|
||||
...tokens, // First apply Ant Design computed tokens
|
||||
...(antdConfig.token || {}), // Then override with our custom tokens
|
||||
} as SupersetTheme;
|
||||
...Theme.defaultTokens,
|
||||
...antdConfig.token, // Passing through the extra, superset-specific tokens
|
||||
...tokens,
|
||||
};
|
||||
|
||||
// Update the providers with the fully formed theme
|
||||
this.updateProviders(
|
||||
@@ -193,3 +250,5 @@ export class Theme {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-enable theme-colors/no-literal-colors */
|
||||
|
||||
@@ -78,7 +78,6 @@ export type SerializableThemeConfig = {
|
||||
algorithm?: ThemeAlgorithmOption;
|
||||
hashed?: boolean;
|
||||
inherit?: boolean;
|
||||
cssVar?: boolean | { key?: string; prefix?: string };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,13 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { theme as antdTheme } from 'antd';
|
||||
import {
|
||||
getFontSize,
|
||||
getColorVariants,
|
||||
isThemeDark,
|
||||
isThemeConfigDark,
|
||||
} from './themeUtils';
|
||||
import { getFontSize, getColorVariants, isThemeDark } from './themeUtils';
|
||||
import { Theme } from '../Theme';
|
||||
import { ThemeAlgorithm } from '../types';
|
||||
|
||||
@@ -77,7 +71,8 @@ describe('themeUtils', () => {
|
||||
token: { fontSize: '14' },
|
||||
});
|
||||
|
||||
expect(getFontSize(minimalTheme.theme, 'xs')).toBe('14');
|
||||
// Ant Design provides fontSizeXS: '8' by default
|
||||
expect(getFontSize(minimalTheme.theme, 'xs')).toBe('8');
|
||||
expect(getFontSize(minimalTheme.theme, 'm')).toBe('14');
|
||||
});
|
||||
});
|
||||
@@ -136,111 +131,4 @@ describe('themeUtils', () => {
|
||||
expect(variants.bg).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isThemeConfigDark', () => {
|
||||
it('returns true for config with dark algorithm', () => {
|
||||
const config = {
|
||||
algorithm: antdTheme.darkAlgorithm,
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for config with dark algorithm in array', () => {
|
||||
const config = {
|
||||
algorithm: [antdTheme.darkAlgorithm, antdTheme.compactAlgorithm],
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for config without dark algorithm', () => {
|
||||
const config = {
|
||||
algorithm: antdTheme.defaultAlgorithm,
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for config with no algorithm', () => {
|
||||
const config = {
|
||||
token: {
|
||||
colorPrimary: '#1890ff',
|
||||
},
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(false);
|
||||
});
|
||||
|
||||
it('detects manually-created dark theme without dark algorithm', () => {
|
||||
// This is the edge case: dark colors without dark algorithm
|
||||
const config = {
|
||||
token: {
|
||||
colorBgContainer: '#1a1a1a', // Dark background
|
||||
colorBgBase: '#0a0a0a', // Dark base
|
||||
colorText: '#ffffff', // Light text
|
||||
},
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not false-positive on light theme with custom colors', () => {
|
||||
const config = {
|
||||
token: {
|
||||
colorBgContainer: '#ffffff', // Light background
|
||||
colorBgBase: '#f5f5f5', // Light base
|
||||
colorText: '#000000', // Dark text
|
||||
},
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(false);
|
||||
});
|
||||
|
||||
it('handles partial color tokens gracefully', () => {
|
||||
// With actual theme computation, a dark colorBgContainer results in a dark theme
|
||||
const config = {
|
||||
token: {
|
||||
colorBgContainer: '#1a1a1a', // Dark background
|
||||
// Missing other color tokens
|
||||
},
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('respects colorBgContainer as the primary indicator', () => {
|
||||
// The computed theme uses colorBgContainer as the main background
|
||||
const darkConfig = {
|
||||
token: {
|
||||
colorBgContainer: '#1a1a1a', // Dark background
|
||||
colorText: '#000000', // Dark text (unusual but doesn't override)
|
||||
},
|
||||
};
|
||||
expect(isThemeConfigDark(darkConfig)).toBe(true);
|
||||
|
||||
const lightConfig = {
|
||||
token: {
|
||||
colorBgContainer: '#ffffff', // Light background
|
||||
colorText: '#ffffff', // Light text (unusual but doesn't override)
|
||||
},
|
||||
};
|
||||
expect(isThemeConfigDark(lightConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it('handles non-string color tokens gracefully', () => {
|
||||
const config = {
|
||||
token: {
|
||||
colorBgContainer: undefined,
|
||||
colorText: null,
|
||||
colorBgBase: 123, // Invalid type
|
||||
},
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for empty config', () => {
|
||||
expect(isThemeConfigDark({})).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for config with empty token object', () => {
|
||||
const config = {
|
||||
token: {},
|
||||
};
|
||||
expect(isThemeConfigDark(config)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,14 +18,7 @@
|
||||
*/
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { useTheme as useEmotionTheme } from '@emotion/react';
|
||||
import { theme as antdTheme } from 'antd';
|
||||
import type {
|
||||
SupersetTheme,
|
||||
FontSizeKey,
|
||||
ColorVariants,
|
||||
AnyThemeConfig,
|
||||
} from '../types';
|
||||
import { normalizeThemeConfig } from '../utils';
|
||||
import type { SupersetTheme, FontSizeKey, ColorVariants } from '../types';
|
||||
|
||||
const fontSizeMap: Record<FontSizeKey, keyof SupersetTheme> = {
|
||||
xs: 'fontSizeXS',
|
||||
@@ -120,22 +113,6 @@ export function isThemeDark(theme: SupersetTheme): boolean {
|
||||
return tinycolor(theme.colorBgContainer).isDark();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a theme configuration results in a dark theme
|
||||
* @param config - The theme configuration to check
|
||||
* @returns true if the config results in a dark theme, false otherwise
|
||||
*/
|
||||
export function isThemeConfigDark(config: AnyThemeConfig): boolean {
|
||||
try {
|
||||
const normalizedConfig = normalizeThemeConfig(config);
|
||||
const themeConfig = antdTheme.getDesignToken(normalizedConfig);
|
||||
|
||||
return tinycolor(themeConfig.colorBgContainer).isDark();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to determine if the current theme is dark mode
|
||||
* @returns true if theme is dark, false if light
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
it('should use getBuckets for linear_palette color scheme', () => {
|
||||
test('should use getBuckets for linear_palette color scheme', () => {
|
||||
const propsWithLinearPalette = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -138,7 +138,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use getBuckets for fixed_color color scheme', () => {
|
||||
test('should use getBuckets for fixed_color color scheme', () => {
|
||||
const propsWithFixedColor = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -154,7 +154,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => {
|
||||
test('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => {
|
||||
const propsWithBreakpoints = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -187,7 +187,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
|
||||
expect(mockGetBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => {
|
||||
test('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => {
|
||||
const propsWithUndefinedScheme = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -203,7 +203,7 @@ describe('DeckGLPolygon bucket generation logic', () => {
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use getBuckets for unsupported color schemes (categorical_palette)', () => {
|
||||
test('should use getBuckets for unsupported color schemes (categorical_palette)', () => {
|
||||
const propsWithUnsupportedScheme = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -230,7 +230,7 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
it('handles empty features data gracefully', () => {
|
||||
test('handles empty features data gracefully', () => {
|
||||
const propsWithEmptyData = {
|
||||
...mockProps,
|
||||
payload: {
|
||||
@@ -249,7 +249,7 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
|
||||
expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles missing color_breakpoints for color_breakpoints scheme', () => {
|
||||
test('handles missing color_breakpoints for color_breakpoints scheme', () => {
|
||||
const propsWithMissingBreakpoints = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -266,7 +266,7 @@ describe('DeckGLPolygon Error Handling and Edge Cases', () => {
|
||||
expect(mockGetBuckets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles null legend_position correctly', () => {
|
||||
test('handles null legend_position correctly', () => {
|
||||
const propsWithNullLegendPosition = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -294,7 +294,7 @@ describe('DeckGLPolygon Legend Integration', () => {
|
||||
const renderWithTheme = (component: React.ReactElement) =>
|
||||
render(<ThemeProvider theme={supersetTheme}>{component}</ThemeProvider>);
|
||||
|
||||
it('renders legend with non-empty categories when metric and linear_palette are defined', () => {
|
||||
test('renders legend with non-empty categories when metric and linear_palette are defined', () => {
|
||||
const { container } = renderWithTheme(<DeckGLPolygon {...mockProps} />);
|
||||
|
||||
// Verify the component renders and calls the correct bucket function
|
||||
@@ -309,7 +309,7 @@ describe('DeckGLPolygon Legend Integration', () => {
|
||||
expect(Object.keys(categoriesData)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('does not render legend when metric is null', () => {
|
||||
test('does not render legend when metric is null', () => {
|
||||
const propsWithoutMetric = {
|
||||
...mockProps,
|
||||
formData: {
|
||||
@@ -326,7 +326,7 @@ describe('DeckGLPolygon Legend Integration', () => {
|
||||
});
|
||||
|
||||
describe('getPoints utility', () => {
|
||||
it('extracts points from polygon data', () => {
|
||||
test('extracts points from polygon data', () => {
|
||||
const data = [
|
||||
{
|
||||
polygon: [
|
||||
|
||||
@@ -64,7 +64,7 @@ Object.defineProperty(document, 'getElementById', {
|
||||
const mockDecode = decode as jest.MockedFunction<typeof decode>;
|
||||
|
||||
describe('spatialUtils', () => {
|
||||
it('getSpatialColumns returns correct columns for latlong type', () => {
|
||||
test('getSpatialColumns returns correct columns for latlong type', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'latlong',
|
||||
lonCol: 'longitude',
|
||||
@@ -75,7 +75,7 @@ describe('spatialUtils', () => {
|
||||
expect(result).toEqual(['longitude', 'latitude']);
|
||||
});
|
||||
|
||||
it('getSpatialColumns returns correct columns for delimited type', () => {
|
||||
test('getSpatialColumns returns correct columns for delimited type', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'delimited',
|
||||
lonlatCol: 'coordinates',
|
||||
@@ -85,7 +85,7 @@ describe('spatialUtils', () => {
|
||||
expect(result).toEqual(['coordinates']);
|
||||
});
|
||||
|
||||
it('getSpatialColumns returns correct columns for geohash type', () => {
|
||||
test('getSpatialColumns returns correct columns for geohash type', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'geohash',
|
||||
geohashCol: 'geohash_code',
|
||||
@@ -95,16 +95,16 @@ describe('spatialUtils', () => {
|
||||
expect(result).toEqual(['geohash_code']);
|
||||
});
|
||||
|
||||
it('getSpatialColumns throws error when spatial is null', () => {
|
||||
test('getSpatialColumns throws error when spatial is null', () => {
|
||||
expect(() => getSpatialColumns(null as any)).toThrow('Bad spatial key');
|
||||
});
|
||||
|
||||
it('getSpatialColumns throws error when spatial type is missing', () => {
|
||||
test('getSpatialColumns throws error when spatial type is missing', () => {
|
||||
const spatial = {} as SpatialFormData['spatial'];
|
||||
expect(() => getSpatialColumns(spatial)).toThrow('Bad spatial key');
|
||||
});
|
||||
|
||||
it('getSpatialColumns throws error when latlong columns are missing', () => {
|
||||
test('getSpatialColumns throws error when latlong columns are missing', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'latlong',
|
||||
};
|
||||
@@ -113,7 +113,7 @@ describe('spatialUtils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getSpatialColumns throws error when delimited column is missing', () => {
|
||||
test('getSpatialColumns throws error when delimited column is missing', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'delimited',
|
||||
};
|
||||
@@ -122,7 +122,7 @@ describe('spatialUtils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getSpatialColumns throws error when geohash column is missing', () => {
|
||||
test('getSpatialColumns throws error when geohash column is missing', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'geohash',
|
||||
};
|
||||
@@ -131,7 +131,7 @@ describe('spatialUtils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getSpatialColumns throws error for unknown spatial type', () => {
|
||||
test('getSpatialColumns throws error for unknown spatial type', () => {
|
||||
const spatial = {
|
||||
type: 'unknown',
|
||||
} as any;
|
||||
@@ -140,7 +140,7 @@ describe('spatialUtils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('addSpatialNullFilters adds null filters for spatial columns', () => {
|
||||
test('addSpatialNullFilters adds null filters for spatial columns', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'latlong',
|
||||
lonCol: 'longitude',
|
||||
@@ -159,7 +159,7 @@ describe('spatialUtils', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('addSpatialNullFilters returns original filters when spatial is null', () => {
|
||||
test('addSpatialNullFilters returns original filters when spatial is null', () => {
|
||||
const existingFilters: QueryObjectFilterClause[] = [
|
||||
{ col: 'test_col', op: '==', val: 'test' },
|
||||
];
|
||||
@@ -168,7 +168,7 @@ describe('spatialUtils', () => {
|
||||
expect(result).toBe(existingFilters);
|
||||
});
|
||||
|
||||
it('addSpatialNullFilters works with empty filters array', () => {
|
||||
test('addSpatialNullFilters works with empty filters array', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'delimited',
|
||||
lonlatCol: 'coordinates',
|
||||
@@ -181,7 +181,7 @@ describe('spatialUtils', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('buildSpatialQuery throws error when spatial is missing', () => {
|
||||
test('buildSpatialQuery throws error when spatial is missing', () => {
|
||||
const formData = {} as SpatialFormData;
|
||||
|
||||
expect(() => buildSpatialQuery(formData)).toThrow(
|
||||
@@ -189,7 +189,7 @@ describe('spatialUtils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('buildSpatialQuery calls buildQueryContext with correct parameters', () => {
|
||||
test('buildSpatialQuery calls buildQueryContext with correct parameters', () => {
|
||||
const mockBuildQueryContext =
|
||||
jest.requireMock('@superset-ui/core').buildQueryContext;
|
||||
const formData: SpatialFormData = {
|
||||
@@ -209,7 +209,7 @@ describe('spatialUtils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('processSpatialData processes latlong data correctly', () => {
|
||||
test('processSpatialData processes latlong data correctly', () => {
|
||||
const records = [
|
||||
{ longitude: -122.4, latitude: 37.8, count: 10, extra: 'test1' },
|
||||
{ longitude: -122.5, latitude: 37.9, count: 20, extra: 'test2' },
|
||||
@@ -237,7 +237,7 @@ describe('spatialUtils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('processSpatialData processes delimited data correctly', () => {
|
||||
test('processSpatialData processes delimited data correctly', () => {
|
||||
const records = [
|
||||
{ coordinates: '-122.4,37.8', count: 15 },
|
||||
{ coordinates: '-122.5,37.9', count: 25 },
|
||||
@@ -257,7 +257,7 @@ describe('spatialUtils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('processSpatialData processes geohash data correctly', () => {
|
||||
test('processSpatialData processes geohash data correctly', () => {
|
||||
mockDecode.mockReturnValue({
|
||||
latitude: 37.8,
|
||||
longitude: -122.4,
|
||||
@@ -284,7 +284,7 @@ describe('spatialUtils', () => {
|
||||
expect(mockDecode).toHaveBeenCalledWith('dr5regw3p');
|
||||
});
|
||||
|
||||
it('processSpatialData reverses coordinates when reverseCheckbox is true', () => {
|
||||
test('processSpatialData reverses coordinates when reverseCheckbox is true', () => {
|
||||
const records = [{ longitude: -122.4, latitude: 37.8, count: 10 }];
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'latlong',
|
||||
@@ -298,7 +298,7 @@ describe('spatialUtils', () => {
|
||||
expect(result[0].position).toEqual([37.8, -122.4]);
|
||||
});
|
||||
|
||||
it('processSpatialData handles invalid coordinates', () => {
|
||||
test('processSpatialData handles invalid coordinates', () => {
|
||||
const records = [
|
||||
{ longitude: 'invalid', latitude: 37.8, count: 10 },
|
||||
{ longitude: -122.4, latitude: NaN, count: 20 },
|
||||
@@ -318,7 +318,7 @@ describe('spatialUtils', () => {
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('processSpatialData handles missing metric values', () => {
|
||||
test('processSpatialData handles missing metric values', () => {
|
||||
const records = [
|
||||
{ longitude: -122.4, latitude: 37.8, count: null },
|
||||
{ longitude: -122.5, latitude: 37.9 },
|
||||
@@ -338,7 +338,7 @@ describe('spatialUtils', () => {
|
||||
expect(result[2].weight).toBe(1);
|
||||
});
|
||||
|
||||
it('processSpatialData returns empty array for empty records', () => {
|
||||
test('processSpatialData returns empty array for empty records', () => {
|
||||
const spatial: SpatialFormData['spatial'] = {
|
||||
type: 'latlong',
|
||||
lonCol: 'longitude',
|
||||
@@ -350,7 +350,7 @@ describe('spatialUtils', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('processSpatialData returns empty array when spatial is null', () => {
|
||||
test('processSpatialData returns empty array when spatial is null', () => {
|
||||
const records = [{ longitude: -122.4, latitude: 37.8 }];
|
||||
|
||||
const result = processSpatialData(records, null as any);
|
||||
@@ -358,7 +358,7 @@ describe('spatialUtils', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('processSpatialData handles delimited coordinate edge cases', () => {
|
||||
test('processSpatialData handles delimited coordinate edge cases', () => {
|
||||
const records = [
|
||||
{ coordinates: '', count: 10 },
|
||||
{ coordinates: null, count: 20 },
|
||||
@@ -382,7 +382,7 @@ describe('spatialUtils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('processSpatialData copies additional properties correctly', () => {
|
||||
test('processSpatialData copies additional properties correctly', () => {
|
||||
const records = [
|
||||
{
|
||||
longitude: -122.4,
|
||||
@@ -416,7 +416,7 @@ describe('spatialUtils', () => {
|
||||
expect(result[0]).not.toHaveProperty('extra_col');
|
||||
});
|
||||
|
||||
it('transformSpatialProps transforms chart props correctly', () => {
|
||||
test('transformSpatialProps transforms chart props correctly', () => {
|
||||
const mockGetMetricLabel =
|
||||
jest.requireMock('@superset-ui/core').getMetricLabel;
|
||||
mockGetMetricLabel.mockReturnValue('count_label');
|
||||
@@ -510,7 +510,7 @@ describe('spatialUtils', () => {
|
||||
expect(result.payload.data.metricLabels).toEqual(['count_label']);
|
||||
});
|
||||
|
||||
it('transformSpatialProps handles missing hooks gracefully', () => {
|
||||
test('transformSpatialProps handles missing hooks gracefully', () => {
|
||||
const chartProps: ChartProps = {
|
||||
datasource: {
|
||||
id: 1,
|
||||
@@ -556,7 +556,7 @@ describe('spatialUtils', () => {
|
||||
expect(typeof result.setTooltip).toBe('function');
|
||||
});
|
||||
|
||||
it('transformSpatialProps handles missing metric', () => {
|
||||
test('transformSpatialProps handles missing metric', () => {
|
||||
const mockGetMetricLabel =
|
||||
jest.requireMock('@superset-ui/core').getMetricLabel;
|
||||
mockGetMetricLabel.mockReturnValue(undefined);
|
||||
|
||||
@@ -505,7 +505,6 @@ export const tooltipTemplate = {
|
||||
config: {
|
||||
type: TooltipTemplateControl,
|
||||
label: t('Customize tooltips template'),
|
||||
renderTrigger: true,
|
||||
debounceDelay: 30,
|
||||
default: '',
|
||||
description: '',
|
||||
|
||||
@@ -131,7 +131,10 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
|
||||
const defaultColDef = useMemo<ColDef>(
|
||||
() => ({
|
||||
flex: 1,
|
||||
filter: true,
|
||||
enableRowGroup: true,
|
||||
enableValue: true,
|
||||
sortable: true,
|
||||
resizable: true,
|
||||
minWidth: 100,
|
||||
@@ -248,12 +251,6 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
}
|
||||
}, [hasServerPageLengthChanged]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gridRef.current?.api) {
|
||||
gridRef.current.api.sizeColumnsToFit();
|
||||
}
|
||||
}, [width]);
|
||||
|
||||
const onGridReady = (params: GridReadyEvent) => {
|
||||
// This will make columns fill the grid width
|
||||
params.api.sizeColumnsToFit();
|
||||
@@ -315,6 +312,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
onCellClicked={handleCrossFilter}
|
||||
initialState={gridInitialState}
|
||||
suppressAggFuncInHeader
|
||||
rowGroupPanelShow="always"
|
||||
enableCellTextSelection
|
||||
quickFilterText={serverPagination ? '' : quickFilterText}
|
||||
suppressMovableColumns={!allowRearrangeColumns}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { mergeReplaceArrays } from '@superset-ui/core';
|
||||
|
||||
describe('Theme Override Deep Merge Behavior', () => {
|
||||
it('should merge nested objects correctly', () => {
|
||||
test('should merge nested objects correctly', () => {
|
||||
const baseOptions = {
|
||||
grid: {
|
||||
left: '5%',
|
||||
@@ -67,7 +67,7 @@ describe('Theme Override Deep Merge Behavior', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace arrays instead of merging them', () => {
|
||||
test('should replace arrays instead of merging them', () => {
|
||||
const baseOptions = {
|
||||
series: [
|
||||
{ name: 'Series 1', type: 'line' },
|
||||
@@ -86,7 +86,7 @@ describe('Theme Override Deep Merge Behavior', () => {
|
||||
expect(result.series).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle null overrides correctly', () => {
|
||||
test('should handle null overrides correctly', () => {
|
||||
const baseOptions = {
|
||||
grid: {
|
||||
left: '5%',
|
||||
@@ -127,7 +127,7 @@ describe('Theme Override Deep Merge Behavior', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle override precedence correctly', () => {
|
||||
test('should handle override precedence correctly', () => {
|
||||
const baseTheme = {
|
||||
textStyle: { color: '#000', fontSize: 12 },
|
||||
};
|
||||
@@ -167,7 +167,7 @@ describe('Theme Override Deep Merge Behavior', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve deep nested structures', () => {
|
||||
test('should preserve deep nested structures', () => {
|
||||
const baseOptions = {
|
||||
xAxis: {
|
||||
axisLabel: {
|
||||
@@ -215,7 +215,7 @@ describe('Theme Override Deep Merge Behavior', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle function values correctly', () => {
|
||||
test('should handle function values correctly', () => {
|
||||
const formatFunction = (value: any) => `${value}%`;
|
||||
const overrideFunction = (value: any) => `$${value}`;
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('Theme Override Deep Merge Behavior', () => {
|
||||
expect(result.yAxis.axisLabel.formatter('100')).toBe('$100');
|
||||
});
|
||||
|
||||
it('should handle empty objects and arrays', () => {
|
||||
test('should handle empty objects and arrays', () => {
|
||||
const baseOptions = {
|
||||
series: [{ name: 'Test', data: [1, 2, 3] }],
|
||||
grid: { left: '5%' },
|
||||
|
||||
@@ -117,7 +117,7 @@ const chartPropsConfig = {
|
||||
theme: supersetTheme,
|
||||
};
|
||||
|
||||
test('should transform chart props for viz with showQueryIdentifiers=false', () => {
|
||||
it('should transform chart props for viz with showQueryIdentifiers=false', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
@@ -158,7 +158,7 @@ test('should transform chart props for viz with showQueryIdentifiers=false', ()
|
||||
]);
|
||||
});
|
||||
|
||||
test('should transform chart props for viz with showQueryIdentifiers=true', () => {
|
||||
it('should transform chart props for viz with showQueryIdentifiers=true', () => {
|
||||
const chartPropsConfigWithIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
@@ -259,7 +259,7 @@ describe('legend sorting', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('legend margin: top orientation sets grid.top correctly', () => {
|
||||
it('legend margin: top orientation sets grid.top correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
@@ -274,7 +274,7 @@ test('legend margin: top orientation sets grid.top correctly', () => {
|
||||
expect((transformed.echartOptions.grid as any).top).toEqual(270);
|
||||
});
|
||||
|
||||
test('legend margin: bottom orientation sets grid.bottom correctly', () => {
|
||||
it('legend margin: bottom orientation sets grid.bottom correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
@@ -290,7 +290,7 @@ test('legend margin: bottom orientation sets grid.bottom correctly', () => {
|
||||
expect((transformed.echartOptions.grid as any).bottom).toEqual(270);
|
||||
});
|
||||
|
||||
test('legend margin: left orientation sets grid.left correctly', () => {
|
||||
it('legend margin: left orientation sets grid.left correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
@@ -306,7 +306,7 @@ test('legend margin: left orientation sets grid.left correctly', () => {
|
||||
expect((transformed.echartOptions.grid as any).left).toEqual(270);
|
||||
});
|
||||
|
||||
test('legend margin: right orientation sets grid.right correctly', () => {
|
||||
it('legend margin: right orientation sets grid.right correctly', () => {
|
||||
const chartPropsConfigWithoutIdentifiers = {
|
||||
...chartPropsConfig,
|
||||
formData: {
|
||||
|
||||
@@ -32,7 +32,7 @@ const mockColorScale = jest.fn(
|
||||
describe('transformSeries', () => {
|
||||
const series = { name: 'test-series' };
|
||||
|
||||
it('should use the colorScaleKey if timeShiftColor is enabled', () => {
|
||||
test('should use the colorScaleKey if timeShiftColor is enabled', () => {
|
||||
const opts = {
|
||||
timeShiftColor: true,
|
||||
colorScaleKey: 'test-key',
|
||||
@@ -44,7 +44,7 @@ describe('transformSeries', () => {
|
||||
expect((result as any)?.itemStyle.color).toBe('color-for-test-key-1');
|
||||
});
|
||||
|
||||
it('should use seriesKey if timeShiftColor is not enabled', () => {
|
||||
test('should use seriesKey if timeShiftColor is not enabled', () => {
|
||||
const opts = {
|
||||
timeShiftColor: false,
|
||||
seriesKey: 'series-key',
|
||||
@@ -56,7 +56,7 @@ describe('transformSeries', () => {
|
||||
expect((result as any)?.itemStyle.color).toBe('color-for-series-key-2');
|
||||
});
|
||||
|
||||
it('should apply border styles for bar series with connectNulls', () => {
|
||||
test('should apply border styles for bar series with connectNulls', () => {
|
||||
const opts = {
|
||||
seriesType: EchartsTimeseriesSeriesType.Bar,
|
||||
connectNulls: true,
|
||||
@@ -72,7 +72,7 @@ describe('transformSeries', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not apply border styles for non-bar series', () => {
|
||||
test('should not apply border styles for non-bar series', () => {
|
||||
const opts = {
|
||||
seriesType: EchartsTimeseriesSeriesType.Line,
|
||||
connectNulls: true,
|
||||
@@ -88,7 +88,7 @@ describe('transformSeries', () => {
|
||||
});
|
||||
|
||||
describe('transformNegativeLabelsPosition', () => {
|
||||
it('label position bottom of negative value no Horizontal', () => {
|
||||
test('label position bottom of negative value no Horizontal', () => {
|
||||
const isHorizontal = false;
|
||||
const series: SeriesOption = {
|
||||
data: [
|
||||
@@ -112,7 +112,7 @@ describe('transformNegativeLabelsPosition', () => {
|
||||
expect((result as any)[4].label).toBe(undefined);
|
||||
});
|
||||
|
||||
it('label position left of negative value is Horizontal', () => {
|
||||
test('label position left of negative value is Horizontal', () => {
|
||||
const isHorizontal = true;
|
||||
const series: SeriesOption = {
|
||||
data: [
|
||||
@@ -137,7 +137,7 @@ describe('transformNegativeLabelsPosition', () => {
|
||||
expect((result as any)[4].label.position).toBe('outside');
|
||||
});
|
||||
|
||||
it('label position to line type', () => {
|
||||
test('label position to line type', () => {
|
||||
const isHorizontal = false;
|
||||
const series: SeriesOption = {
|
||||
data: [
|
||||
@@ -165,7 +165,7 @@ describe('transformNegativeLabelsPosition', () => {
|
||||
expect((result as any)[4].label).toBe(undefined);
|
||||
});
|
||||
|
||||
it('label position to bar type and stack', () => {
|
||||
test('label position to bar type and stack', () => {
|
||||
const isHorizontal = false;
|
||||
const series: SeriesOption = {
|
||||
data: [
|
||||
|
||||
@@ -16,18 +16,25 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Flex, Icons } from '@superset-ui/core/components';
|
||||
|
||||
export type CustomDocLinkProps = {
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const CustomDocLink = ({ url, label }: CustomDocLinkProps) => (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<Flex align="center" gap={4}>
|
||||
{label} <Icons.Full iconSize="m" />
|
||||
</Flex>
|
||||
</a>
|
||||
);
|
||||
{
|
||||
"plugins": ["jest", "jest-dom", "no-only-tests", "testing-library"],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"settings": {
|
||||
"jest": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest-dom/recommended",
|
||||
"plugin:testing-library/react"
|
||||
],
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
|
||||
"jest/consistent-test-it": "error",
|
||||
"no-only-tests/no-only-tests": "error",
|
||||
"prefer-promise-reject-errors": 0
|
||||
}
|
||||
}
|
||||
30
superset-frontend/spec/javascripts/dashboard/.eslintrc
Normal file
30
superset-frontend/spec/javascripts/dashboard/.eslintrc
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "prettier",
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"prefer-template": 2,
|
||||
"new-cap": 2,
|
||||
"no-restricted-syntax": 2,
|
||||
"guard-for-in": 2,
|
||||
"prefer-arrow-callback": 2,
|
||||
"func-names": 2,
|
||||
"react/jsx-no-bind": 2,
|
||||
"no-confusing-arrow": 2,
|
||||
"jsx-a11y/no-static-element-interactions": 2,
|
||||
"jsx-a11y/anchor-has-content": 2,
|
||||
"react/require-default-props": 2,
|
||||
"no-plusplus": 2,
|
||||
"no-mixed-operators": 0,
|
||||
"no-continue": 2,
|
||||
"no-bitwise": 2,
|
||||
"no-multi-assign": 2,
|
||||
"no-restricted-properties": 2,
|
||||
"no-prototype-builtins": 2,
|
||||
"class-methods-use-this": 2,
|
||||
"import/no-named-as-default": 2,
|
||||
"react/no-unescaped-entities": 2,
|
||||
"react/no-string-refs": 2,
|
||||
"react/jsx-indent": 0,
|
||||
"prettier/prettier": "error"
|
||||
}
|
||||
}
|
||||
@@ -1017,13 +1017,12 @@ export function runTablePreviewQuery(newTable, runPreviewOnly) {
|
||||
};
|
||||
}
|
||||
|
||||
export function syncTable(table, tableMetadata, finalQueryEditorId) {
|
||||
export function syncTable(table, tableMetadata) {
|
||||
return function (dispatch) {
|
||||
const finalTable = { ...table, queryEditorId: finalQueryEditorId };
|
||||
const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
|
||||
? SupersetClient.post({
|
||||
endpoint: encodeURI('/tableschemaview/'),
|
||||
postPayload: { table: { ...tableMetadata, ...finalTable } },
|
||||
postPayload: { table: { ...tableMetadata, ...table } },
|
||||
})
|
||||
: Promise.resolve({ json: { id: table.id } });
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
|
||||
import TableElement, { Column } from 'src/SqlLab/components/TableElement';
|
||||
import { table, initialState } from 'src/SqlLab/fixtures';
|
||||
import { render, waitFor, fireEvent } from 'spec/helpers/testing-library';
|
||||
import * as sqlLabActions from 'src/SqlLab/actions/sqlLab';
|
||||
import { QueryEditor } from 'src/SqlLab/types';
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
@@ -81,27 +79,6 @@ const mockedProps = {
|
||||
activeKey: [table.id],
|
||||
};
|
||||
|
||||
const createStateWithQueryEditor = (queryEditor: Partial<QueryEditor>) => ({
|
||||
...initialState,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queryEditors: [queryEditor],
|
||||
},
|
||||
});
|
||||
|
||||
const setupSyncTableTest = () => {
|
||||
const spy = jest.spyOn(sqlLabActions, 'syncTable');
|
||||
mockedIsFeatureEnabled.mockImplementation(
|
||||
featureFlag => featureFlag === FeatureFlag.SqllabBackendPersistence,
|
||||
);
|
||||
fetchMock.post(
|
||||
updateTableSchemaEndpoint,
|
||||
{ id: 100 },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
return spy;
|
||||
};
|
||||
|
||||
test('renders', () => {
|
||||
expect(isValidElement(<TableElement table={table} />)).toBe(true);
|
||||
});
|
||||
@@ -230,212 +207,3 @@ test('refreshes table metadata when triggered', async () => {
|
||||
expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1),
|
||||
);
|
||||
});
|
||||
|
||||
test('calls syncTable with valid backend ID when query editor has tabViewId', async () => {
|
||||
const syncTableSpy = setupSyncTableTest();
|
||||
const testTable = {
|
||||
...table,
|
||||
initialized: false,
|
||||
queryEditorId: 'temp-id-123',
|
||||
};
|
||||
|
||||
const state = createStateWithQueryEditor({
|
||||
id: 'temp-id-123',
|
||||
tabViewId: '42',
|
||||
inLocalStorage: false,
|
||||
name: 'Test Editor',
|
||||
});
|
||||
|
||||
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
|
||||
useRedux: true,
|
||||
initialState: state,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(syncTableSpy).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
'42', // finalQueryEditorId
|
||||
);
|
||||
});
|
||||
|
||||
syncTableSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('does not call syncTable when query editor is in localStorage', async () => {
|
||||
const syncTableSpy = setupSyncTableTest();
|
||||
const testTable = {
|
||||
...table,
|
||||
initialized: false,
|
||||
queryEditorId: 'local-id',
|
||||
};
|
||||
|
||||
const state = createStateWithQueryEditor({
|
||||
id: 'local-id',
|
||||
tabViewId: undefined,
|
||||
inLocalStorage: true,
|
||||
name: 'Local Editor',
|
||||
});
|
||||
|
||||
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
|
||||
useRedux: true,
|
||||
initialState: state,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(syncTableSpy).not.toHaveBeenCalled();
|
||||
|
||||
syncTableSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('does not call syncTable with non-numeric queryEditorId', async () => {
|
||||
const syncTableSpy = setupSyncTableTest();
|
||||
const testTable = {
|
||||
...table,
|
||||
initialized: false,
|
||||
queryEditorId: 'not-a-number',
|
||||
};
|
||||
|
||||
const state = createStateWithQueryEditor({
|
||||
id: 'not-a-number',
|
||||
tabViewId: 'also-not-a-number',
|
||||
inLocalStorage: false,
|
||||
name: 'Invalid Editor',
|
||||
});
|
||||
|
||||
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
|
||||
useRedux: true,
|
||||
initialState: state,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(syncTableSpy).not.toHaveBeenCalled();
|
||||
|
||||
syncTableSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('does not call syncTable for already initialized tables', async () => {
|
||||
const syncTableSpy = setupSyncTableTest();
|
||||
const testTable = {
|
||||
...table,
|
||||
initialized: true, // Already initialized
|
||||
queryEditorId: '789',
|
||||
};
|
||||
|
||||
const state = createStateWithQueryEditor({
|
||||
id: '789',
|
||||
tabViewId: '789',
|
||||
inLocalStorage: false,
|
||||
name: 'Initialized Editor',
|
||||
});
|
||||
|
||||
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
|
||||
useRedux: true,
|
||||
initialState: state,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(syncTableSpy).not.toHaveBeenCalled();
|
||||
|
||||
syncTableSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('calls syncTable after query editor is migrated from localStorage', async () => {
|
||||
const syncTableSpy = setupSyncTableTest();
|
||||
const testTable = {
|
||||
...table,
|
||||
initialized: false,
|
||||
queryEditorId: 'temp-editor-id',
|
||||
};
|
||||
|
||||
// Start with editor in localStorage
|
||||
const localState = createStateWithQueryEditor({
|
||||
id: 'temp-editor-id',
|
||||
tabViewId: undefined,
|
||||
inLocalStorage: true,
|
||||
name: 'Temp Editor',
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<TableElement table={testTable} activeKey={[testTable.id]} />,
|
||||
{
|
||||
useRedux: true,
|
||||
initialState: localState,
|
||||
},
|
||||
);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(syncTableSpy).not.toHaveBeenCalled();
|
||||
|
||||
const migratedState = createStateWithQueryEditor({
|
||||
id: 'temp-editor-id',
|
||||
tabViewId: '999',
|
||||
inLocalStorage: false,
|
||||
name: 'Temp Editor',
|
||||
});
|
||||
|
||||
rerender(<TableElement table={testTable} activeKey={[testTable.id]} />);
|
||||
|
||||
const { unmount } = render(
|
||||
<TableElement table={testTable} activeKey={[testTable.id]} />,
|
||||
{
|
||||
useRedux: true,
|
||||
initialState: migratedState,
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(syncTableSpy).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
'999',
|
||||
);
|
||||
});
|
||||
|
||||
unmount();
|
||||
syncTableSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('passes numeric queryEditorId validation', async () => {
|
||||
const syncTableSpy = setupSyncTableTest();
|
||||
const testTable = {
|
||||
...table,
|
||||
initialized: false,
|
||||
queryEditorId: 'editor-123',
|
||||
};
|
||||
|
||||
const state = createStateWithQueryEditor({
|
||||
id: 'editor-123',
|
||||
tabViewId: '456',
|
||||
inLocalStorage: false,
|
||||
name: 'Valid Editor',
|
||||
});
|
||||
|
||||
render(<TableElement table={testTable} activeKey={[testTable.id]} />, {
|
||||
useRedux: true,
|
||||
initialState: state,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(syncTableSpy).toHaveBeenCalled();
|
||||
const [, , finalQueryEditorId] = syncTableSpy.mock.calls[0];
|
||||
// Verify it's a valid numeric string
|
||||
expect(Number.isNaN(Number(finalQueryEditorId))).toBe(false);
|
||||
expect(typeof finalQueryEditorId).toBe('string');
|
||||
expect(finalQueryEditorId).toMatch(/^\d+$/);
|
||||
});
|
||||
|
||||
syncTableSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import type { Table } from 'src/SqlLab/types';
|
||||
import {
|
||||
ButtonGroup,
|
||||
Card,
|
||||
@@ -103,19 +103,6 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
},
|
||||
{ skip: !expanded },
|
||||
);
|
||||
const tableData = {
|
||||
...tableMetadata,
|
||||
...tableExtendedMetadata,
|
||||
};
|
||||
const queryEditors = useSelector<SqlLabRootState, QueryEditor[]>(
|
||||
state => state.sqlLab.queryEditors,
|
||||
);
|
||||
const currentTable = { ...tableData, ...table };
|
||||
const { queryEditorId } = currentTable;
|
||||
const queryEditor = queryEditors.find(
|
||||
qe => qe.id === queryEditorId || qe.tabViewId === queryEditorId,
|
||||
);
|
||||
const currentQueryEditorId = queryEditor?.tabViewId || queryEditorId;
|
||||
|
||||
useEffect(() => {
|
||||
if (hasMetadataError || hasExtendedMetadataError) {
|
||||
@@ -125,16 +112,16 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
}
|
||||
}, [hasMetadataError, hasExtendedMetadataError, dispatch]);
|
||||
|
||||
const tableData = {
|
||||
...tableMetadata,
|
||||
...tableExtendedMetadata,
|
||||
};
|
||||
|
||||
// TODO: migrate syncTable logic by SIP-93
|
||||
const syncTableMetadata = useEffectEvent(() => {
|
||||
const { initialized } = table;
|
||||
// if not a valid number, wait for backend to assign one
|
||||
const hasFinalQueryEditorId =
|
||||
currentQueryEditorId &&
|
||||
!Number.isNaN(Number(currentQueryEditorId)) &&
|
||||
currentTable.queryEditorId !== currentQueryEditorId;
|
||||
if (!initialized && hasFinalQueryEditorId) {
|
||||
dispatch(syncTable(currentTable, tableData, currentQueryEditorId));
|
||||
if (!initialized) {
|
||||
dispatch(syncTable(table, tableData));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,12 +129,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
|
||||
if (isMetadataSuccess && isExtraMetadataSuccess) {
|
||||
syncTableMetadata();
|
||||
}
|
||||
}, [
|
||||
isMetadataSuccess,
|
||||
isExtraMetadataSuccess,
|
||||
currentQueryEditorId,
|
||||
syncTableMetadata,
|
||||
]);
|
||||
}, [isMetadataSuccess, isExtraMetadataSuccess, syncTableMetadata]);
|
||||
|
||||
const [sortColumns, setSortColumns] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
@@ -265,9 +265,7 @@ const ChangeDatasourceModal: FunctionComponent<ChangeDatasourceModalProps> = ({
|
||||
{confirmChange && (
|
||||
<ConfirmModalStyled>
|
||||
<div className="btn-container">
|
||||
<Button buttonStyle="secondary" onClick={handlerCancelConfirm}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button onClick={handlerCancelConfirm}>{t('Cancel')}</Button>
|
||||
<Button
|
||||
className="proceed-btn"
|
||||
buttonStyle="primary"
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import { CustomDocLink } from './CustomDocLink';
|
||||
|
||||
const mockedProps = {
|
||||
url: 'https://superset.apache.org/docs/',
|
||||
label: 'Superset Docs',
|
||||
};
|
||||
|
||||
test('should render the label', () => {
|
||||
render(<CustomDocLink {...mockedProps} />);
|
||||
expect(screen.getByText('Superset Docs')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the link with correct attributes', () => {
|
||||
render(<CustomDocLink {...mockedProps} />);
|
||||
const link = screen.getByRole('link');
|
||||
expect(link).toHaveAttribute('href', mockedProps.url);
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
@@ -53,38 +53,6 @@ const mockedProps = {
|
||||
subtitle: 'Error message',
|
||||
};
|
||||
|
||||
const mockedPropsWithCustomError = {
|
||||
...mockedProps,
|
||||
error: {
|
||||
...mockedProps.error,
|
||||
extra: {
|
||||
...mockedProps.error.extra,
|
||||
custom_doc_links: [
|
||||
{
|
||||
label: 'Custom Doc Link 1',
|
||||
url: 'https://example.com/custom-doc-1',
|
||||
},
|
||||
{
|
||||
label: 'Custom Doc Link 2',
|
||||
url: 'https://example.com/custom-doc-2',
|
||||
},
|
||||
],
|
||||
show_issue_info: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockedPropsWithCustomErrorAndBadLinks = {
|
||||
...mockedProps,
|
||||
error: {
|
||||
...mockedProps.error,
|
||||
extra: {
|
||||
...mockedProps.error.extra,
|
||||
custom_doc_links: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should render', () => {
|
||||
const nullExtraProps = {
|
||||
...mockedProps,
|
||||
@@ -144,28 +112,3 @@ test('should NOT render the owners', () => {
|
||||
screen.queryByText('Chart Owners: Owner A, Owner B'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render custom documentation links when provided', () => {
|
||||
render(<DatabaseErrorMessage {...mockedPropsWithCustomError} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
expect(screen.getByText('Custom Doc Link 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Doc Link 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should NOT render see more button when show_issue_info is false', () => {
|
||||
render(<DatabaseErrorMessage {...mockedPropsWithCustomError} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
const button = screen.queryByText('See more');
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render message when wrong value provided for custom_doc_urls', () => {
|
||||
// @ts-ignore
|
||||
render(<DatabaseErrorMessage {...mockedPropsWithCustomErrorAndBadLinks} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
const button = screen.queryByText('Error message');
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import { t, tn } from '@superset-ui/core';
|
||||
import type { ErrorMessageComponentProps } from './types';
|
||||
import { IssueCode } from './IssueCode';
|
||||
import { ErrorAlert } from './ErrorAlert';
|
||||
import { CustomDocLink, CustomDocLinkProps } from './CustomDocLink';
|
||||
|
||||
interface DatabaseErrorExtra {
|
||||
owners?: string[];
|
||||
@@ -31,8 +30,6 @@ interface DatabaseErrorExtra {
|
||||
message: string;
|
||||
}[];
|
||||
engine_name: string | null;
|
||||
custom_doc_links?: CustomDocLinkProps[];
|
||||
show_issue_info?: boolean;
|
||||
}
|
||||
|
||||
export function DatabaseErrorMessage({
|
||||
@@ -43,32 +40,20 @@ export function DatabaseErrorMessage({
|
||||
|
||||
const isVisualization = ['dashboard', 'explore'].includes(source || '');
|
||||
const [firstLine, ...remainingLines] = message.split('\n');
|
||||
const alertMessage = firstLine;
|
||||
const alertDescription =
|
||||
remainingLines.length > 0 ? remainingLines.join('\n') : null;
|
||||
let alertMessage: ReactNode = firstLine;
|
||||
|
||||
if (Array.isArray(extra?.custom_doc_links)) {
|
||||
alertMessage = (
|
||||
<>
|
||||
{firstLine}
|
||||
{extra.custom_doc_links.map(link => (
|
||||
<div key={link.url}>
|
||||
<CustomDocLink {...link} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const body = extra && extra.show_issue_info !== false && (
|
||||
const body = extra && (
|
||||
<>
|
||||
<p>
|
||||
{t('This may be triggered by:')}
|
||||
<br />
|
||||
{extra.issue_codes?.flatMap((issueCode, idx, arr) => [
|
||||
<IssueCode {...issueCode} key={issueCode.code} />,
|
||||
idx < arr.length - 1 ? <br key={`br-${issueCode.code}`} /> : null,
|
||||
])}
|
||||
{extra.issue_codes
|
||||
?.map<ReactNode>(issueCode => (
|
||||
<IssueCode {...issueCode} key={issueCode.code} />
|
||||
))
|
||||
.reduce((prev, curr) => [prev, <br />, curr])}
|
||||
</p>
|
||||
{isVisualization && extra.owners && (
|
||||
<>
|
||||
|
||||
@@ -100,7 +100,6 @@ export const ErrorAlert: React.FC<ErrorAlertProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const renderAlert = (closable: boolean) => (
|
||||
@@ -130,6 +129,7 @@ export const ErrorAlert: React.FC<ErrorAlertProps> = ({
|
||||
footer={null}
|
||||
>
|
||||
{renderAlert(false)}
|
||||
{children}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -132,7 +132,6 @@ class Markdown extends PureComponent {
|
||||
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
|
||||
this.handleResizeStart = this.handleResizeStart.bind(this);
|
||||
this.setEditor = this.setEditor.bind(this);
|
||||
this.shouldFocusMarkdown = this.shouldFocusMarkdown.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -269,13 +268,6 @@ class Markdown extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
shouldFocusMarkdown(event, container, menuRef) {
|
||||
if (container?.contains(event.target)) return true;
|
||||
if (menuRef?.contains(event.target)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
renderEditMode() {
|
||||
return (
|
||||
<MarkdownEditor
|
||||
@@ -351,7 +343,6 @@ class Markdown extends PureComponent {
|
||||
{({ dragSourceRef }) => (
|
||||
<WithPopoverMenu
|
||||
onChangeFocus={this.handleChangeFocus}
|
||||
shouldFocus={this.shouldFocusMarkdown}
|
||||
menuItems={[
|
||||
<MarkdownModeDropdown
|
||||
id={`${component.id}-mode`}
|
||||
|
||||
@@ -17,13 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { Provider } from 'react-redux';
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
userEvent,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { act, render, screen, fireEvent } from 'spec/helpers/testing-library';
|
||||
import { mockStore } from 'spec/fixtures/mockStore';
|
||||
import { dashboardLayout as mockLayout } from 'spec/fixtures/mockDashboardLayout';
|
||||
import MarkdownConnected from './Markdown';
|
||||
@@ -51,7 +45,6 @@ describe('Markdown', () => {
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
const originalError = console.error;
|
||||
jest.spyOn(console, 'error').mockImplementation(msg => {
|
||||
if (
|
||||
typeof msg === 'string' &&
|
||||
@@ -60,7 +53,7 @@ describe('Markdown', () => {
|
||||
!msg.includes('Warning: React does not recognize') &&
|
||||
!msg.includes("Warning: Can't perform a React state update")
|
||||
) {
|
||||
originalError.call(console, msg);
|
||||
console.error(msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -342,75 +335,4 @@ describe('Markdown', () => {
|
||||
// Check that width is no longer 248px
|
||||
expect(updatedParent).not.toHaveStyle('width: 248px');
|
||||
});
|
||||
|
||||
test('shouldFocusMarkdown returns true when clicking inside markdown container', async () => {
|
||||
await setup({ editMode: true });
|
||||
|
||||
const markdownContainer = screen.getByTestId(
|
||||
'dashboard-component-chart-holder',
|
||||
);
|
||||
|
||||
userEvent.click(markdownContainer);
|
||||
|
||||
expect(await screen.findByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shouldFocusMarkdown returns false when clicking outside markdown container', async () => {
|
||||
await setup({ editMode: true });
|
||||
|
||||
const markdownContainer = screen.getByTestId(
|
||||
'dashboard-component-chart-holder',
|
||||
);
|
||||
|
||||
userEvent.click(markdownContainer);
|
||||
|
||||
expect(await screen.findByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
userEvent.click(document.body);
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shouldFocusMarkdown keeps focus when clicking on menu items', async () => {
|
||||
await setup({ editMode: true });
|
||||
|
||||
const markdownContainer = screen.getByTestId(
|
||||
'dashboard-component-chart-holder',
|
||||
);
|
||||
|
||||
userEvent.click(markdownContainer);
|
||||
|
||||
expect(await screen.findByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
|
||||
userEvent.click(editButton);
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(screen.queryByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should exit edit mode when clicking outside in same row', async () => {
|
||||
await setup({ editMode: true });
|
||||
|
||||
const markdownContainer = screen.getByTestId(
|
||||
'dashboard-component-chart-holder',
|
||||
);
|
||||
|
||||
userEvent.click(markdownContainer);
|
||||
|
||||
expect(await screen.findByRole('textbox')).toBeInTheDocument();
|
||||
|
||||
const outsideElement = document.createElement('div');
|
||||
outsideElement.className = 'grid-row';
|
||||
document.body.appendChild(outsideElement);
|
||||
|
||||
userEvent.click(outsideElement);
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
|
||||
document.body.removeChild(outsideElement);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, userEvent } from 'spec/helpers/testing-library';
|
||||
import { fireEvent, render } from 'spec/helpers/testing-library';
|
||||
|
||||
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
|
||||
|
||||
@@ -44,102 +44,40 @@ test('should render the passed children', () => {
|
||||
expect(container.querySelector('#child')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should focus on click in editMode', async () => {
|
||||
test('should focus on click in editMode', () => {
|
||||
const { container } = setup({ editMode: true });
|
||||
await userEvent.click(container.querySelector('.with-popover-menu'));
|
||||
fireEvent.click(container.querySelector('.with-popover-menu'));
|
||||
expect(
|
||||
container.querySelector('.with-popover-menu--focused'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render menuItems when focused', async () => {
|
||||
test('should render menuItems when focused', () => {
|
||||
const { container } = setup({ editMode: true });
|
||||
expect(container.querySelector('#menu1')).not.toBeInTheDocument();
|
||||
expect(container.querySelector('#menu2')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(container.querySelector('.with-popover-menu'));
|
||||
fireEvent.click(container.querySelector('.with-popover-menu'));
|
||||
expect(container.querySelector('#menu1')).toBeInTheDocument();
|
||||
expect(container.querySelector('#menu2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should not focus when disableClick=true', async () => {
|
||||
test('should not focus when disableClick=true', () => {
|
||||
const { container } = setup({ disableClick: true, editMode: true });
|
||||
|
||||
await userEvent.click(container.querySelector('.with-popover-menu'));
|
||||
fireEvent.click(container.querySelector('.with-popover-menu'));
|
||||
expect(
|
||||
container.querySelector('.with-popover-menu--focused'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should use the passed shouldFocus func to determine if it should focus', async () => {
|
||||
test('should use the passed shouldFocus func to determine if it should focus', () => {
|
||||
const { container } = setup({ editMode: true, shouldFocus: () => false });
|
||||
expect(
|
||||
container.querySelector('.with-popover-menu--focused'),
|
||||
).not.toBeInTheDocument();
|
||||
await userEvent.click(container.querySelector('.with-popover-menu'));
|
||||
fireEvent.click(container.querySelector('.with-popover-menu'));
|
||||
expect(
|
||||
container.querySelector('.with-popover-menu--focused'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow event propagation to enable multiple components to work independently', async () => {
|
||||
const onChangeFocus = jest.fn();
|
||||
|
||||
const { container } = setup({
|
||||
editMode: true,
|
||||
disableClick: false,
|
||||
shouldFocus: () => true,
|
||||
onChangeFocus,
|
||||
});
|
||||
|
||||
const menuComponent = container.querySelector('.with-popover-menu');
|
||||
await userEvent.click(menuComponent);
|
||||
|
||||
expect(onChangeFocus).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('should unfocus when another component is clicked', async () => {
|
||||
const onChangeFocusA = jest.fn();
|
||||
const onChangeFocusB = jest.fn();
|
||||
|
||||
const componentA = render(
|
||||
<WithPopoverMenu
|
||||
{...props}
|
||||
editMode
|
||||
shouldFocus={(event, container) => container?.contains(event.target)}
|
||||
onChangeFocus={onChangeFocusA}
|
||||
>
|
||||
<div id="child-a" />
|
||||
</WithPopoverMenu>,
|
||||
);
|
||||
|
||||
const componentB = render(
|
||||
<WithPopoverMenu
|
||||
{...props}
|
||||
editMode
|
||||
shouldFocus={(event, container) => container?.contains(event.target)}
|
||||
onChangeFocus={onChangeFocusB}
|
||||
>
|
||||
<div id="child-b" />
|
||||
</WithPopoverMenu>,
|
||||
);
|
||||
|
||||
const menuA = componentA.container.querySelector('.with-popover-menu');
|
||||
const menuB = componentB.container.querySelector('.with-popover-menu');
|
||||
|
||||
await userEvent.click(menuA);
|
||||
expect(onChangeFocusA).toHaveBeenCalledWith(true);
|
||||
expect(
|
||||
componentA.container.querySelector('.with-popover-menu--focused'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(menuB);
|
||||
expect(onChangeFocusB).toHaveBeenCalledWith(true);
|
||||
expect(onChangeFocusA).toHaveBeenCalledWith(false);
|
||||
expect(
|
||||
componentA.container.querySelector('.with-popover-menu--focused'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
componentB.container.querySelector('.with-popover-menu--focused'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -33,11 +33,7 @@ interface WithPopoverMenuProps {
|
||||
// Event argument is left as "any" because of the clash. In defaultProps it seems
|
||||
// like it should be React.FocusEvent<>, however from handleClick() we can also
|
||||
// derive that type is EventListenerOrEventListenerObject.
|
||||
shouldFocus: (
|
||||
event: any,
|
||||
container: ShouldFocusContainer,
|
||||
menuRef: HTMLDivElement | null,
|
||||
) => Boolean;
|
||||
shouldFocus: (event: any, container: ShouldFocusContainer) => Boolean;
|
||||
editMode: Boolean;
|
||||
style: CSSProperties;
|
||||
}
|
||||
@@ -109,21 +105,16 @@ export default class WithPopoverMenu extends PureComponent<
|
||||
> {
|
||||
container: ShouldFocusContainer;
|
||||
|
||||
menuRef: HTMLDivElement | null;
|
||||
|
||||
static defaultProps = {
|
||||
children: null,
|
||||
disableClick: false,
|
||||
onChangeFocus: null,
|
||||
menuItems: [],
|
||||
isFocused: false,
|
||||
shouldFocus: (
|
||||
event: any,
|
||||
container: ShouldFocusContainer,
|
||||
menuRef: HTMLDivElement | null,
|
||||
) => {
|
||||
shouldFocus: (event: any, container: ShouldFocusContainer) => {
|
||||
if (container?.contains(event.target)) return true;
|
||||
if (menuRef?.contains(event.target)) return true;
|
||||
if (event.target.id === 'menu-item') return true;
|
||||
if (event.target.parentNode?.id === 'menu-item') return true;
|
||||
return false;
|
||||
},
|
||||
style: null,
|
||||
@@ -134,9 +125,7 @@ export default class WithPopoverMenu extends PureComponent<
|
||||
this.state = {
|
||||
isFocused: props.isFocused!,
|
||||
};
|
||||
this.menuRef = null;
|
||||
this.setRef = this.setRef.bind(this);
|
||||
this.setMenuRef = this.setMenuRef.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
@@ -161,49 +150,37 @@ export default class WithPopoverMenu extends PureComponent<
|
||||
this.container = ref;
|
||||
}
|
||||
|
||||
setMenuRef(ref: HTMLDivElement | null) {
|
||||
this.menuRef = ref;
|
||||
}
|
||||
|
||||
shouldHandleFocusChange(shouldFocus: Boolean): boolean {
|
||||
const { disableClick } = this.props;
|
||||
const { isFocused } = this.state;
|
||||
|
||||
return (
|
||||
(!disableClick && shouldFocus && !isFocused) ||
|
||||
(!shouldFocus && isFocused)
|
||||
);
|
||||
}
|
||||
|
||||
handleClick(event: any) {
|
||||
if (!this.props.editMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const {
|
||||
onChangeFocus,
|
||||
shouldFocus: shouldFocusFunc,
|
||||
disableClick,
|
||||
} = this.props;
|
||||
|
||||
const shouldFocus = shouldFocusFunc(event, this.container, this.menuRef);
|
||||
|
||||
if (shouldFocus === this.state.isFocused) return;
|
||||
const shouldFocus = shouldFocusFunc(event, this.container);
|
||||
|
||||
if (!disableClick && shouldFocus && !this.state.isFocused) {
|
||||
// if not focused, set focus and add a window event listener to capture outside clicks
|
||||
// this enables us to not set a click listener for ever item on a dashboard
|
||||
document.addEventListener('click', this.handleClick);
|
||||
document.addEventListener('drag', this.handleClick);
|
||||
|
||||
this.setState(() => ({ isFocused: true }));
|
||||
|
||||
if (onChangeFocus) onChangeFocus(true);
|
||||
if (onChangeFocus) {
|
||||
onChangeFocus(true);
|
||||
}
|
||||
} else if (!shouldFocus && this.state.isFocused) {
|
||||
document.removeEventListener('click', this.handleClick);
|
||||
document.removeEventListener('drag', this.handleClick);
|
||||
|
||||
this.setState(() => ({ isFocused: false }));
|
||||
|
||||
if (onChangeFocus) onChangeFocus(false);
|
||||
if (onChangeFocus) {
|
||||
onChangeFocus(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +201,7 @@ export default class WithPopoverMenu extends PureComponent<
|
||||
>
|
||||
{children}
|
||||
{editMode && isFocused && (menuItems?.length ?? 0) > 0 && (
|
||||
<PopoverMenuStyles ref={this.setMenuRef}>
|
||||
<PopoverMenuStyles>
|
||||
{menuItems.map((node: ReactNode, i: Number) => (
|
||||
<div className="menu-item" key={`menu-item-${i}`}>
|
||||
{node}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ExploreAlert } from './ExploreAlert';
|
||||
|
||||
test('renders with title and body text', () => {
|
||||
render(
|
||||
<ExploreAlert title="Test Title" bodyText="Test body text" type="info" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test body text')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders info type alert', () => {
|
||||
const { container } = render(
|
||||
<ExploreAlert title="Info Alert" bodyText="Info message" type="info" />,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.ant-alert-info')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders warning type alert', () => {
|
||||
const { container } = render(
|
||||
<ExploreAlert
|
||||
title="Warning Alert"
|
||||
bodyText="Warning message"
|
||||
type="warning"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.ant-alert-warning')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders error type alert', () => {
|
||||
const { container } = render(
|
||||
<ExploreAlert title="Error Alert" bodyText="Error message" type="error" />,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.ant-alert-error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders primary button when text and action provided', () => {
|
||||
const primaryAction = jest.fn();
|
||||
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert with button"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonText="Primary Action"
|
||||
primaryButtonAction={primaryAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Primary Action')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls primary button action when clicked', async () => {
|
||||
const primaryAction = jest.fn();
|
||||
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert with button"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonText="Click Me"
|
||||
primaryButtonAction={primaryAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Click Me'));
|
||||
expect(primaryAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('renders both primary and secondary buttons when provided', () => {
|
||||
const primaryAction = jest.fn();
|
||||
const secondaryAction = jest.fn();
|
||||
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert with buttons"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonText="Primary"
|
||||
primaryButtonAction={primaryAction}
|
||||
secondaryButtonText="Secondary"
|
||||
secondaryButtonAction={secondaryAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Primary')).toBeInTheDocument();
|
||||
expect(screen.getByText('Secondary')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls secondary button action when clicked', async () => {
|
||||
const primaryAction = jest.fn();
|
||||
const secondaryAction = jest.fn();
|
||||
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert with buttons"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonText="Primary"
|
||||
primaryButtonAction={primaryAction}
|
||||
secondaryButtonText="Secondary"
|
||||
secondaryButtonAction={secondaryAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Secondary'));
|
||||
expect(secondaryAction).toHaveBeenCalledTimes(1);
|
||||
expect(primaryAction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not render buttons when only text is provided without action', () => {
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert without action"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonText="Primary"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Primary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render buttons when only action is provided without text', () => {
|
||||
const primaryAction = jest.fn();
|
||||
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert without text"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonAction={primaryAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render secondary button when secondary action is missing', () => {
|
||||
const primaryAction = jest.fn();
|
||||
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonText="Primary"
|
||||
primaryButtonAction={primaryAction}
|
||||
secondaryButtonText="Secondary"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Primary')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Secondary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render secondary button when secondary text is missing', () => {
|
||||
const primaryAction = jest.fn();
|
||||
const secondaryAction = jest.fn();
|
||||
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
primaryButtonText="Primary"
|
||||
primaryButtonAction={primaryAction}
|
||||
secondaryButtonAction={secondaryAction}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Primary')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /secondary/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('applies custom className', () => {
|
||||
const { container } = render(
|
||||
<ExploreAlert
|
||||
title="Alert"
|
||||
bodyText="Body text"
|
||||
type="info"
|
||||
className="custom-class"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.custom-class')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders with ReactNode as bodyText', () => {
|
||||
render(
|
||||
<ExploreAlert
|
||||
title="Alert"
|
||||
bodyText={
|
||||
<div>
|
||||
<span>Line 1</span>
|
||||
<span>Line 2</span>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Line 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Line 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is not closable by default', () => {
|
||||
const { container } = render(
|
||||
<ExploreAlert title="Alert" bodyText="Body text" type="info" />,
|
||||
);
|
||||
|
||||
expect(
|
||||
container.querySelector('.ant-alert-close-icon'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows icon by default', () => {
|
||||
const { container } = render(
|
||||
<ExploreAlert title="Alert" bodyText="Body text" type="info" />,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.anticon')).toBeInTheDocument();
|
||||
});
|
||||
@@ -20,7 +20,6 @@
|
||||
import { forwardRef, RefObject, MouseEvent } from 'react';
|
||||
import { Button } from '@superset-ui/core/components';
|
||||
import { ErrorAlert } from 'src/components';
|
||||
import { styled } from '@superset-ui/core';
|
||||
|
||||
interface ControlPanelAlertProps {
|
||||
title: string;
|
||||
@@ -33,12 +32,6 @@ interface ControlPanelAlertProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
`;
|
||||
|
||||
export const ExploreAlert = forwardRef(
|
||||
(
|
||||
{
|
||||
@@ -62,16 +55,14 @@ export const ExploreAlert = forwardRef(
|
||||
showIcon
|
||||
>
|
||||
{primaryButtonText && primaryButtonAction && (
|
||||
<ButtonContainer>
|
||||
<div>
|
||||
{secondaryButtonAction && secondaryButtonText && (
|
||||
<Button buttonStyle="secondary" onClick={secondaryButtonAction}>
|
||||
<Button onClick={secondaryButtonAction}>
|
||||
{secondaryButtonText}
|
||||
</Button>
|
||||
)}
|
||||
<Button buttonStyle="secondary" onClick={primaryButtonAction}>
|
||||
{primaryButtonText}
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
<Button onClick={primaryButtonAction}>{primaryButtonText}</Button>
|
||||
</div>
|
||||
)}
|
||||
</ErrorAlert>
|
||||
),
|
||||
|
||||
@@ -371,6 +371,15 @@ function ExploreViewContainer(props) {
|
||||
props.form_data,
|
||||
]);
|
||||
|
||||
// Simple debounced auto-query for non-renderTrigger controls
|
||||
const debouncedAutoQuery = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
onQuery();
|
||||
}, 1000), // 1 second delay
|
||||
[onQuery],
|
||||
);
|
||||
|
||||
const handleKeydown = useCallback(
|
||||
event => {
|
||||
const controlOrCommand = event.ctrlKey || event.metaKey;
|
||||
@@ -564,8 +573,25 @@ function ExploreViewContainer(props) {
|
||||
if (displayControlsChanged.length > 0) {
|
||||
reRenderChart(displayControlsChanged);
|
||||
}
|
||||
|
||||
// Auto-update for non-renderTrigger controls
|
||||
const queryControlsChanged = changedControlKeys.filter(
|
||||
key =>
|
||||
!props.controls[key].renderTrigger &&
|
||||
!props.controls[key].dontRefreshOnChange,
|
||||
);
|
||||
if (queryControlsChanged.length > 0) {
|
||||
// Check if there are no validation errors before auto-updating
|
||||
const hasErrors = Object.values(props.controls).some(
|
||||
control =>
|
||||
control.validationErrors && control.validationErrors.length > 0,
|
||||
);
|
||||
if (!hasErrors) {
|
||||
debouncedAutoQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.controls, props.ownState]);
|
||||
}, [props.controls, props.ownState, debouncedAutoQuery]);
|
||||
|
||||
const chartIsStale = useMemo(() => {
|
||||
if (lastQueriedControls) {
|
||||
|
||||
@@ -41,7 +41,6 @@ export const useUnsavedChangesPrompt = ({
|
||||
const manualSaveRef = useRef(false); // Track if save was user-initiated (not via navigation)
|
||||
|
||||
const handleConfirmNavigation = useCallback(() => {
|
||||
setShowModal(false);
|
||||
confirmNavigationRef.current?.();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -22,116 +22,86 @@ import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'spec/helpers/testing-library';
|
||||
|
||||
let history = createMemoryHistory({
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/dashboard'],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory({ initialEntries: ['/dashboard'] });
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<Router history={history}>{children}</Router>
|
||||
);
|
||||
|
||||
test('should not show modal initially', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave: jest.fn(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('useUnsavedChangesPrompt', () => {
|
||||
test('should not show modal initially', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave: jest.fn(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
test('should block navigation and show modal if there are unsaved changes', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave: jest.fn(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Simulate blocked navigation
|
||||
act(() => {
|
||||
const unblock = history.block((tx: any) => tx);
|
||||
unblock();
|
||||
history.push('/another-page');
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.showModal).toBe(true);
|
||||
});
|
||||
test('should block navigation and show modal if there are unsaved changes', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave: jest.fn(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
test('should trigger onSave and hide modal on handleSaveAndCloseModal', async () => {
|
||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||
// Simulate blocked navigation
|
||||
act(() => {
|
||||
const unblock = history.block((tx: any) => tx);
|
||||
unblock();
|
||||
history.push('/another-page');
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSaveAndCloseModal();
|
||||
expect(result.current.showModal).toBe(true);
|
||||
});
|
||||
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
test('should trigger onSave and hide modal on handleSaveAndCloseModal', async () => {
|
||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
test('should trigger manual save and not show modal again', async () => {
|
||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
await act(async () => {
|
||||
await result.current.handleSaveAndCloseModal();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.triggerManualSave();
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
test('should trigger manual save and not show modal again', async () => {
|
||||
const onSave = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
test('should close modal when handleConfirmNavigation is called', () => {
|
||||
const onSave = jest.fn();
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useUnsavedChangesPrompt({
|
||||
hasUnsavedChanges: true,
|
||||
onSave,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
act(() => {
|
||||
result.current.triggerManualSave();
|
||||
});
|
||||
|
||||
// First, trigger navigation to show the modal
|
||||
act(() => {
|
||||
const unblock = history.block((tx: any) => tx);
|
||||
unblock();
|
||||
history.push('/another-page');
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.showModal).toBe(true);
|
||||
|
||||
// Then call handleConfirmNavigation to discard changes
|
||||
act(() => {
|
||||
result.current.handleConfirmNavigation();
|
||||
});
|
||||
|
||||
expect(result.current.showModal).toBe(false);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { t, SupersetClient, styled } from '@superset-ui/core';
|
||||
import {
|
||||
Tag,
|
||||
@@ -72,26 +72,12 @@ const FlexRowContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const IconTag = styled(Tag)`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const CONFIRM_OVERWRITE_MESSAGE = t(
|
||||
'You are importing one or more themes that already exist. ' +
|
||||
'Overwriting might cause you to lose some of your work. Are you ' +
|
||||
'sure you want to overwrite?',
|
||||
);
|
||||
|
||||
interface ConfirmModalConfig {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onConfirm: () => Promise<any>;
|
||||
successMessage: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
interface ThemesListProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
addSuccessToast: (msg: string) => void;
|
||||
@@ -126,10 +112,6 @@ function ThemesList({
|
||||
const [importingTheme, showImportModal] = useState<boolean>(false);
|
||||
const [appliedThemeId, setAppliedThemeId] = useState<number | null>(null);
|
||||
|
||||
// State for confirmation modal
|
||||
const [confirmModalConfig, setConfirmModalConfig] =
|
||||
useState<ConfirmModalConfig | null>(null);
|
||||
|
||||
const canCreate = hasPerm('can_write');
|
||||
const canEdit = hasPerm('can_write');
|
||||
const canDelete = hasPerm('can_write');
|
||||
@@ -207,23 +189,20 @@ function ThemesList({
|
||||
setThemeModalOpen(true);
|
||||
}
|
||||
|
||||
const handleThemeApply = useCallback(
|
||||
(themeObj: ThemeObject) => {
|
||||
if (themeObj.json_data) {
|
||||
try {
|
||||
const themeConfig = JSON.parse(themeObj.json_data);
|
||||
setTemporaryTheme(themeConfig);
|
||||
setAppliedThemeId(themeObj.id || null);
|
||||
addSuccessToast(t('Local theme set to "%s"', themeObj.theme_name));
|
||||
} catch (error) {
|
||||
addDangerToast(
|
||||
t('Failed to set local theme: Invalid JSON configuration'),
|
||||
);
|
||||
}
|
||||
function handleThemeApply(themeObj: ThemeObject) {
|
||||
if (themeObj.json_data) {
|
||||
try {
|
||||
const themeConfig = JSON.parse(themeObj.json_data);
|
||||
setTemporaryTheme(themeConfig);
|
||||
setAppliedThemeId(themeObj.id || null);
|
||||
addSuccessToast(t('Local theme set to "%s"', themeObj.theme_name));
|
||||
} catch (error) {
|
||||
addDangerToast(
|
||||
t('Failed to set local theme: Invalid JSON configuration'),
|
||||
);
|
||||
}
|
||||
},
|
||||
[setTemporaryTheme, addSuccessToast, addDangerToast],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleThemeModalApply() {
|
||||
// Clear any previously applied theme ID when applying from modal
|
||||
@@ -256,83 +235,60 @@ function ThemesList({
|
||||
};
|
||||
|
||||
// Generic confirmation modal utility to reduce code duplication
|
||||
const showThemeConfirmation = useCallback(
|
||||
(config: {
|
||||
title: string;
|
||||
content: string;
|
||||
onConfirm: () => Promise<any>;
|
||||
successMessage: string;
|
||||
errorMessage: string;
|
||||
}) => {
|
||||
setConfirmModalConfig({
|
||||
visible: true,
|
||||
title: config.title,
|
||||
message: config.content,
|
||||
onConfirm: config.onConfirm,
|
||||
successMessage: config.successMessage,
|
||||
errorMessage: config.errorMessage,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleConfirmModalOk = async () => {
|
||||
if (!confirmModalConfig) return;
|
||||
|
||||
try {
|
||||
await confirmModalConfig.onConfirm();
|
||||
refreshData();
|
||||
addSuccessToast(confirmModalConfig.successMessage);
|
||||
setConfirmModalConfig(null);
|
||||
} catch (err: any) {
|
||||
addDangerToast(t(confirmModalConfig.errorMessage, err.message));
|
||||
}
|
||||
const showThemeConfirmation = (config: {
|
||||
title: string;
|
||||
content: string;
|
||||
onConfirm: () => Promise<any>;
|
||||
successMessage: string;
|
||||
errorMessage: string;
|
||||
}) => {
|
||||
Modal.confirm({
|
||||
title: config.title,
|
||||
content: config.content,
|
||||
onOk: () => {
|
||||
config
|
||||
.onConfirm()
|
||||
.then(() => {
|
||||
refreshData();
|
||||
addSuccessToast(config.successMessage);
|
||||
})
|
||||
.catch(err => {
|
||||
addDangerToast(t(config.errorMessage, err.message));
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirmModalCancel = () => {
|
||||
setConfirmModalConfig(null);
|
||||
const handleSetSystemDefault = (theme: ThemeObject) => {
|
||||
showThemeConfirmation({
|
||||
title: t('Set System Default Theme'),
|
||||
content: t(
|
||||
'Are you sure you want to set "%s" as the system default theme? This will apply to all users who haven\'t set a personal preference.',
|
||||
theme.theme_name,
|
||||
),
|
||||
onConfirm: () => setSystemDefaultTheme(theme.id!),
|
||||
successMessage: t(
|
||||
'"%s" is now the system default theme',
|
||||
theme.theme_name,
|
||||
),
|
||||
errorMessage: 'Failed to set system default theme: %s',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetSystemDefault = useCallback(
|
||||
(theme: ThemeObject) => {
|
||||
showThemeConfirmation({
|
||||
title: t('Set System Default Theme'),
|
||||
content: t(
|
||||
'Are you sure you want to set "%s" as the system default theme? This will apply to all users who haven\'t set a personal preference.',
|
||||
theme.theme_name,
|
||||
),
|
||||
onConfirm: () => setSystemDefaultTheme(theme.id!),
|
||||
successMessage: t(
|
||||
'"%s" is now the system default theme',
|
||||
theme.theme_name,
|
||||
),
|
||||
errorMessage: 'Failed to set system default theme: %s',
|
||||
});
|
||||
},
|
||||
[showThemeConfirmation],
|
||||
);
|
||||
const handleSetSystemDark = (theme: ThemeObject) => {
|
||||
showThemeConfirmation({
|
||||
title: t('Set System Dark Theme'),
|
||||
content: t(
|
||||
'Are you sure you want to set "%s" as the system dark theme? This will apply to all users who haven\'t set a personal preference.',
|
||||
theme.theme_name,
|
||||
),
|
||||
onConfirm: () => setSystemDarkTheme(theme.id!),
|
||||
successMessage: t('"%s" is now the system dark theme', theme.theme_name),
|
||||
errorMessage: 'Failed to set system dark theme: %s',
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetSystemDark = useCallback(
|
||||
(theme: ThemeObject) => {
|
||||
showThemeConfirmation({
|
||||
title: t('Set System Dark Theme'),
|
||||
content: t(
|
||||
'Are you sure you want to set "%s" as the system dark theme? This will apply to all users who haven\'t set a personal preference.',
|
||||
theme.theme_name,
|
||||
),
|
||||
onConfirm: () => setSystemDarkTheme(theme.id!),
|
||||
successMessage: t(
|
||||
'"%s" is now the system dark theme',
|
||||
theme.theme_name,
|
||||
theme.theme_name,
|
||||
),
|
||||
errorMessage: 'Failed to set system dark theme: %s',
|
||||
});
|
||||
},
|
||||
[showThemeConfirmation],
|
||||
);
|
||||
|
||||
const handleUnsetSystemDefault = useCallback(() => {
|
||||
const handleUnsetSystemDefault = () => {
|
||||
showThemeConfirmation({
|
||||
title: t('Remove System Default Theme'),
|
||||
content: t(
|
||||
@@ -342,9 +298,9 @@ function ThemesList({
|
||||
successMessage: t('System default theme removed'),
|
||||
errorMessage: 'Failed to remove system default theme: %s',
|
||||
});
|
||||
}, [showThemeConfirmation]);
|
||||
};
|
||||
|
||||
const handleUnsetSystemDark = useCallback(() => {
|
||||
const handleUnsetSystemDark = () => {
|
||||
showThemeConfirmation({
|
||||
title: t('Remove System Dark Theme'),
|
||||
content: t(
|
||||
@@ -354,7 +310,7 @@ function ThemesList({
|
||||
successMessage: t('System dark theme removed'),
|
||||
errorMessage: 'Failed to remove system dark theme: %s',
|
||||
});
|
||||
}, [showThemeConfirmation]);
|
||||
};
|
||||
|
||||
const initialSort = [{ id: 'theme_name', desc: true }];
|
||||
const columns = useMemo(
|
||||
@@ -384,16 +340,16 @@ function ThemesList({
|
||||
)}
|
||||
{original.is_system_default && (
|
||||
<Tooltip title={t('This is the system default theme')}>
|
||||
<IconTag color="warning" icon={<Icons.SunOutlined />}>
|
||||
{t('Default')}
|
||||
</IconTag>
|
||||
<Tag color="warning">
|
||||
<Icons.SunOutlined /> {t('Default')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
{original.is_system_dark && (
|
||||
<Tooltip title={t('This is the system dark theme')}>
|
||||
<IconTag color="default" icon={<Icons.MoonOutlined />}>
|
||||
{t('Dark')}
|
||||
</IconTag>
|
||||
<Tag color="default">
|
||||
<Icons.MoonOutlined /> {t('Dark')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
</FlexRowContainer>
|
||||
@@ -531,19 +487,12 @@ function ThemesList({
|
||||
},
|
||||
],
|
||||
[
|
||||
canEdit,
|
||||
canDelete,
|
||||
canCreate,
|
||||
canApply,
|
||||
canExport,
|
||||
getCurrentCrudThemeId,
|
||||
appliedThemeId,
|
||||
canSetSystemThemes,
|
||||
addDangerToast,
|
||||
handleThemeApply,
|
||||
handleSetSystemDefault,
|
||||
handleUnsetSystemDefault,
|
||||
handleSetSystemDark,
|
||||
handleUnsetSystemDark,
|
||||
appliedThemeId,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -621,7 +570,7 @@ function ThemesList({
|
||||
paginate: true,
|
||||
},
|
||||
],
|
||||
[user],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -732,17 +681,6 @@ function ThemesList({
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
{preparingExport && <Loading />}
|
||||
{confirmModalConfig?.visible && (
|
||||
<Modal
|
||||
title={confirmModalConfig.title}
|
||||
show={confirmModalConfig.visible}
|
||||
onHide={handleConfirmModalCancel}
|
||||
onHandledPrimaryAction={handleConfirmModalOk}
|
||||
primaryButtonName={t('Yes')}
|
||||
>
|
||||
{confirmModalConfig.message}
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,15 +18,18 @@
|
||||
*/
|
||||
import {
|
||||
type AnyThemeConfig,
|
||||
type SupersetTheme,
|
||||
type SupersetThemeConfig,
|
||||
type ThemeControllerOptions,
|
||||
type ThemeStorage,
|
||||
isThemeConfigDark,
|
||||
Theme,
|
||||
ThemeMode,
|
||||
themeObject as supersetThemeObject,
|
||||
} from '@superset-ui/core';
|
||||
import { normalizeThemeConfig } from '@superset-ui/core/theme/utils';
|
||||
import {
|
||||
getAntdConfig,
|
||||
normalizeThemeConfig,
|
||||
} from '@superset-ui/core/theme/utils';
|
||||
import type {
|
||||
BootstrapThemeData,
|
||||
BootstrapThemeDataConfig,
|
||||
@@ -76,7 +79,7 @@ export class ThemeController {
|
||||
|
||||
private modeStorageKey: string;
|
||||
|
||||
private defaultTheme: AnyThemeConfig | null;
|
||||
private defaultTheme: AnyThemeConfig;
|
||||
|
||||
private darkTheme: AnyThemeConfig | null;
|
||||
|
||||
@@ -84,6 +87,8 @@ export class ThemeController {
|
||||
|
||||
private currentMode: ThemeMode;
|
||||
|
||||
private hasCustomThemes: boolean;
|
||||
|
||||
private onChangeCallbacks: Set<(theme: Theme) => void> = new Set();
|
||||
|
||||
private mediaQuery: MediaQueryList;
|
||||
@@ -111,13 +116,22 @@ export class ThemeController {
|
||||
this.globalTheme = themeObject;
|
||||
|
||||
// Initialize bootstrap data and themes
|
||||
const { bootstrapDefaultTheme, bootstrapDarkTheme }: BootstrapThemeData =
|
||||
this.loadBootstrapData();
|
||||
const {
|
||||
bootstrapDefaultTheme,
|
||||
bootstrapDarkTheme,
|
||||
hasCustomThemes,
|
||||
}: BootstrapThemeData = this.loadBootstrapData();
|
||||
|
||||
// Set themes from bootstrap data
|
||||
// These will be the THEME_DEFAULT and THEME_DARK from config
|
||||
this.defaultTheme = bootstrapDefaultTheme || defaultTheme || null;
|
||||
this.darkTheme = bootstrapDarkTheme;
|
||||
this.hasCustomThemes = hasCustomThemes;
|
||||
|
||||
// Set themes based on bootstrap data availability
|
||||
if (this.hasCustomThemes) {
|
||||
this.darkTheme = bootstrapDarkTheme;
|
||||
this.defaultTheme = bootstrapDefaultTheme || defaultTheme;
|
||||
} else {
|
||||
this.darkTheme = null;
|
||||
this.defaultTheme = defaultTheme;
|
||||
}
|
||||
|
||||
// Initialize system theme detection
|
||||
this.systemMode = ThemeController.getSystemPreferredMode();
|
||||
@@ -133,7 +147,7 @@ export class ThemeController {
|
||||
// Initialize theme and mode
|
||||
this.currentMode = this.determineInitialMode();
|
||||
const initialTheme =
|
||||
this.getThemeForMode(this.currentMode) || this.defaultTheme || {};
|
||||
this.getThemeForMode(this.currentMode) || this.defaultTheme;
|
||||
|
||||
// Setup change callback
|
||||
if (onChange) this.onChangeCallbacks.add(onChange);
|
||||
@@ -183,7 +197,6 @@ export class ThemeController {
|
||||
|
||||
/**
|
||||
* Gets the theme configuration for a specific context (global vs dashboard).
|
||||
* Dashboard themes are always merged with base theme.
|
||||
* @param forDashboard - Whether to get the dashboard theme or global theme
|
||||
* @returns The theme configuration for the specified context
|
||||
*/
|
||||
@@ -192,16 +205,7 @@ export class ThemeController {
|
||||
): AnyThemeConfig | null {
|
||||
// For dashboard context, prioritize dashboard CRUD theme
|
||||
if (forDashboard && this.dashboardCrudTheme) {
|
||||
// Dashboard CRUD themes should be merged with base theme
|
||||
const normalizedTheme = this.normalizeTheme(this.dashboardCrudTheme);
|
||||
const isDarkMode = isThemeConfigDark(normalizedTheme);
|
||||
const baseTheme = isDarkMode ? this.darkTheme : this.defaultTheme;
|
||||
|
||||
if (baseTheme) {
|
||||
const mergedTheme = Theme.fromConfig(normalizedTheme, baseTheme);
|
||||
return mergedTheme.toSerializedConfig();
|
||||
}
|
||||
return normalizedTheme;
|
||||
return this.dashboardCrudTheme;
|
||||
}
|
||||
|
||||
// For global context or when no dashboard theme, use mode-based theme
|
||||
@@ -237,15 +241,7 @@ export class ThemeController {
|
||||
// Controller creates and owns the dashboard theme
|
||||
const { Theme } = await import('@superset-ui/core');
|
||||
const normalizedConfig = this.normalizeTheme(themeConfig);
|
||||
|
||||
// Determine if this is a dark theme and get appropriate base
|
||||
const isDarkMode = isThemeConfigDark(normalizedConfig);
|
||||
const baseTheme = isDarkMode ? this.darkTheme : this.defaultTheme;
|
||||
|
||||
const dashboardTheme = Theme.fromConfig(
|
||||
normalizedConfig,
|
||||
baseTheme || undefined,
|
||||
);
|
||||
const dashboardTheme = Theme.fromConfig(normalizedConfig);
|
||||
|
||||
// Cache the theme for reuse
|
||||
this.dashboardThemes.set(themeId, dashboardTheme);
|
||||
@@ -329,7 +325,7 @@ export class ThemeController {
|
||||
public resetTheme(): void {
|
||||
this.currentMode = ThemeMode.DEFAULT;
|
||||
const defaultTheme: AnyThemeConfig =
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme || {};
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme;
|
||||
|
||||
this.updateTheme(defaultTheme);
|
||||
}
|
||||
@@ -377,8 +373,8 @@ export class ThemeController {
|
||||
JSON.stringify(theme),
|
||||
);
|
||||
|
||||
const mergedTheme = this.getThemeForMode(this.currentMode);
|
||||
if (mergedTheme) this.updateTheme(mergedTheme);
|
||||
const normalizedTheme = this.normalizeTheme(theme);
|
||||
this.updateTheme(normalizedTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,14 +384,10 @@ export class ThemeController {
|
||||
public clearLocalOverrides(): void {
|
||||
this.devThemeOverride = null;
|
||||
this.crudThemeId = null;
|
||||
this.dashboardCrudTheme = null;
|
||||
|
||||
this.storage.removeItem(STORAGE_KEYS.DEV_THEME_OVERRIDE);
|
||||
this.storage.removeItem(STORAGE_KEYS.CRUD_THEME_ID);
|
||||
|
||||
// Clear dashboard themes cache
|
||||
this.dashboardThemes.clear();
|
||||
|
||||
this.resetTheme();
|
||||
}
|
||||
|
||||
@@ -415,7 +407,7 @@ export class ThemeController {
|
||||
|
||||
/**
|
||||
* Checks if OS preference detection is allowed.
|
||||
* Allowed when dark theme is available (including base dark theme)
|
||||
* Allowed when both themes are available
|
||||
*/
|
||||
public canDetectOSPreference(): boolean {
|
||||
return this.darkTheme !== null;
|
||||
@@ -430,6 +422,7 @@ export class ThemeController {
|
||||
public setThemeConfig(config: SupersetThemeConfig): void {
|
||||
this.defaultTheme = config.theme_default;
|
||||
this.darkTheme = config.theme_dark || null;
|
||||
this.hasCustomThemes = true;
|
||||
|
||||
let newMode: ThemeMode;
|
||||
try {
|
||||
@@ -485,16 +478,13 @@ export class ThemeController {
|
||||
private updateTheme(theme?: AnyThemeConfig): void {
|
||||
try {
|
||||
// If no config provided, use current mode to get theme
|
||||
if (!theme) {
|
||||
// No theme provided, use the current mode's theme
|
||||
const modeTheme =
|
||||
this.getThemeForMode(this.currentMode) || this.defaultTheme || {};
|
||||
this.applyTheme(modeTheme);
|
||||
} else {
|
||||
// Theme provided, apply it directly
|
||||
this.applyTheme(theme);
|
||||
}
|
||||
const config: AnyThemeConfig =
|
||||
theme || this.getThemeForMode(this.currentMode) || this.defaultTheme;
|
||||
|
||||
// Normalize the theme
|
||||
const normalizedTheme = this.normalizeTheme(config);
|
||||
|
||||
this.applyTheme(normalizedTheme);
|
||||
this.persistMode();
|
||||
this.notifyListeners();
|
||||
} catch (error) {
|
||||
@@ -511,7 +501,7 @@ export class ThemeController {
|
||||
|
||||
// Get the default theme which will have the correct algorithm
|
||||
const defaultTheme: AnyThemeConfig =
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme || {};
|
||||
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme;
|
||||
|
||||
this.applyTheme(defaultTheme);
|
||||
this.persistMode();
|
||||
@@ -564,15 +554,10 @@ export class ThemeController {
|
||||
const hasValidDefault: boolean = this.isNonEmptyObject(defaultTheme);
|
||||
const hasValidDark: boolean = this.isNonEmptyObject(darkTheme);
|
||||
|
||||
// Check if themes have actual custom tokens (not just empty or algorithm-only)
|
||||
const hasCustomDefault =
|
||||
hasValidDefault && !this.isEmptyTheme(defaultTheme);
|
||||
const hasCustomDark = hasValidDark && !this.isEmptyTheme(darkTheme);
|
||||
|
||||
return {
|
||||
bootstrapDefaultTheme: hasCustomDefault ? defaultTheme : null,
|
||||
bootstrapDarkTheme: hasCustomDark ? darkTheme : null,
|
||||
hasCustomThemes: hasCustomDefault || hasCustomDark,
|
||||
bootstrapDefaultTheme: hasValidDefault ? defaultTheme : null,
|
||||
bootstrapDarkTheme: hasValidDark ? darkTheme : null,
|
||||
hasCustomThemes: hasValidDefault || hasValidDark,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -587,20 +572,6 @@ export class ThemeController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a theme is truly empty (not even an algorithm).
|
||||
* A theme with just an algorithm is still valid and should be used.
|
||||
*/
|
||||
private isEmptyTheme(theme: AnyThemeConfig | undefined): boolean {
|
||||
if (!theme) return true;
|
||||
|
||||
return !(
|
||||
theme.algorithm ||
|
||||
(theme.token && Object.keys(theme.token).length > 0) ||
|
||||
(theme.components && Object.keys(theme.components).length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the theme configuration to ensure it has a valid algorithm.
|
||||
* @param theme - The theme configuration to normalize
|
||||
@@ -617,45 +588,49 @@ export class ThemeController {
|
||||
* @returns The theme configuration for the specified mode or null if not available
|
||||
*/
|
||||
private getThemeForMode(mode: ThemeMode): AnyThemeConfig | null {
|
||||
// Priority 1: Dev theme override (highest priority for development)
|
||||
// Dev overrides affect all contexts
|
||||
if (this.devThemeOverride) {
|
||||
const normalizedOverride = this.normalizeTheme(this.devThemeOverride);
|
||||
const isDarkMode = isThemeConfigDark(normalizedOverride);
|
||||
const baseTheme = isDarkMode ? this.darkTheme : this.defaultTheme;
|
||||
|
||||
if (baseTheme) {
|
||||
const mergedTheme = Theme.fromConfig(normalizedOverride, baseTheme);
|
||||
return mergedTheme.toSerializedConfig();
|
||||
}
|
||||
|
||||
return normalizedOverride;
|
||||
return this.devThemeOverride;
|
||||
}
|
||||
|
||||
// Priority 2: System theme based on mode (applies to all contexts)
|
||||
let resolvedMode: ThemeMode = mode;
|
||||
|
||||
if (mode === ThemeMode.SYSTEM) {
|
||||
// OS preference is allowed when dark theme exists
|
||||
if (this.darkTheme === null) return null;
|
||||
resolvedMode = ThemeController.getSystemPreferredMode();
|
||||
}
|
||||
|
||||
if (resolvedMode === ThemeMode.DARK) return this.darkTheme;
|
||||
if (!this.hasCustomThemes) {
|
||||
const baseTheme = this.defaultTheme.token as Partial<SupersetTheme>;
|
||||
return getAntdConfig(baseTheme, resolvedMode === ThemeMode.DARK);
|
||||
}
|
||||
|
||||
return this.defaultTheme;
|
||||
// Handle bootstrap themes using existing normalization
|
||||
const selectedTheme: AnyThemeConfig =
|
||||
resolvedMode === ThemeMode.DARK
|
||||
? this.darkTheme || this.defaultTheme
|
||||
: this.defaultTheme;
|
||||
|
||||
return selectedTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the initial theme mode with error recovery.
|
||||
*/
|
||||
private determineInitialMode(): ThemeMode {
|
||||
// Try to restore saved mode first
|
||||
const savedMode: ThemeMode | null = this.loadSavedMode();
|
||||
if (savedMode && this.isValidThemeMode(savedMode)) return savedMode;
|
||||
|
||||
// If no dark theme is available, force default mode
|
||||
if (this.darkTheme === null) {
|
||||
this.storage.removeItem(this.modeStorageKey);
|
||||
return ThemeMode.DEFAULT;
|
||||
}
|
||||
|
||||
// Try to restore saved mode
|
||||
const savedMode: ThemeMode | null = this.loadSavedMode();
|
||||
if (savedMode && this.isValidThemeMode(savedMode)) return savedMode;
|
||||
|
||||
// Default to system preference when both themes are available
|
||||
return ThemeMode.SYSTEM;
|
||||
}
|
||||
@@ -688,14 +663,11 @@ export class ThemeController {
|
||||
// Validate that we have the required theme data for the mode
|
||||
switch (mode) {
|
||||
case ThemeMode.DARK:
|
||||
// Dark mode is valid if we have a dark theme
|
||||
return !!this.darkTheme;
|
||||
return !!(this.darkTheme || this.defaultTheme);
|
||||
case ThemeMode.DEFAULT:
|
||||
// Default mode is valid if we have a default theme
|
||||
return !!this.defaultTheme;
|
||||
case ThemeMode.SYSTEM:
|
||||
// System mode is valid if dark mode is available
|
||||
return !!this.darkTheme;
|
||||
return this.darkTheme !== null;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -726,15 +698,11 @@ export class ThemeController {
|
||||
* Applies the current theme configuration to the global theme.
|
||||
* This method sets the theme on the globalTheme and applies it to the Theme.
|
||||
* It also handles any errors that may occur during the application of the theme.
|
||||
* @param theme - The theme configuration to apply (may already include base theme tokens)
|
||||
* @param theme - The theme configuration to apply
|
||||
*/
|
||||
private applyTheme(theme: AnyThemeConfig): void {
|
||||
try {
|
||||
const normalizedConfig = normalizeThemeConfig(theme);
|
||||
|
||||
// Simply apply the theme - it should already be properly merged if needed
|
||||
// The merging with base theme happens in getThemeForMode() and other methods
|
||||
// that prepare themes before passing them to applyTheme()
|
||||
this.globalTheme.setConfig(normalizedConfig);
|
||||
} catch (error) {
|
||||
console.error('Failed to apply theme:', error);
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
Tooltip,
|
||||
XYChart,
|
||||
buildChartTheme,
|
||||
type SeriesProps,
|
||||
AxisScale,
|
||||
} from '@visx/xychart';
|
||||
import { extendedDayjs } from '@superset-ui/core/utils/dates';
|
||||
import {
|
||||
@@ -132,16 +134,14 @@ const SparklineCell = ({
|
||||
const xAccessor = (d: { x: number; y: number }) => d.x;
|
||||
const yAccessor = (d: { x: number; y: number }) => d.y;
|
||||
|
||||
type SparklineSeriesComponent =
|
||||
| typeof LineSeries
|
||||
| typeof BarSeries
|
||||
| typeof AreaSeries;
|
||||
|
||||
const chartSeriesMap = {
|
||||
const chartSeriesMap: Record<
|
||||
SparkType,
|
||||
(props: SeriesProps<AxisScale, AxisScale, object>) => JSX.Element
|
||||
> = {
|
||||
line: LineSeries,
|
||||
bar: BarSeries,
|
||||
area: AreaSeries,
|
||||
} as const satisfies Record<SparkType, SparklineSeriesComponent>;
|
||||
};
|
||||
|
||||
const SeriesComponent = chartSeriesMap[sparkType] || LineSeries;
|
||||
|
||||
|
||||
1446
superset-websocket/package-lock.json
generated
1446
superset-websocket/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,8 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.2",
|
||||
"hot-shots": "^11.2.0",
|
||||
"ioredis": "^5.8.0",
|
||||
"hot-shots": "^11.1.0",
|
||||
"ioredis": "^5.6.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"uuid": "^11.1.0",
|
||||
@@ -29,7 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
"@types/ioredis": "^4.27.8",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/lodash": "^4.17.20",
|
||||
@@ -38,17 +38,17 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.0",
|
||||
"@typescript-eslint/parser": "^8.42.0",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tscw-config": "^1.1.2",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.45.0"
|
||||
"typescript-eslint": "^8.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.4",
|
||||
|
||||
@@ -189,9 +189,7 @@ class TestConnectionDatabaseCommand(BaseCommand):
|
||||
engine=database.db_engine_spec.__name__,
|
||||
)
|
||||
# check for custom errors (wrong username, wrong password, etc)
|
||||
errors = database.db_engine_spec.extract_errors(
|
||||
ex, self._context, database_name=database.unique_name
|
||||
)
|
||||
errors = database.db_engine_spec.extract_errors(ex, self._context)
|
||||
raise SupersetErrorsException(errors, status=400) from ex
|
||||
except OAuth2RedirectError:
|
||||
raise
|
||||
|
||||
@@ -124,9 +124,7 @@ class ValidateDatabaseParametersCommand(BaseCommand):
|
||||
"username": url.username,
|
||||
"database": url.database,
|
||||
}
|
||||
errors = database.db_engine_spec.extract_errors(
|
||||
ex, context, database_name=database.unique_name
|
||||
)
|
||||
errors = database.db_engine_spec.extract_errors(ex, context)
|
||||
raise DatabaseTestConnectionFailedError(errors, status=400) from ex
|
||||
|
||||
if not alive:
|
||||
|
||||
@@ -129,11 +129,6 @@ class QueryContextProcessor:
|
||||
self, query_obj: QueryObject, force_cached: bool | None = False
|
||||
) -> dict[str, Any]:
|
||||
"""Handles caching around the df payload retrieval"""
|
||||
if query_obj:
|
||||
# Always validate the query object before generating cache key
|
||||
# This ensures sanitize_clause() is called and extras are normalized
|
||||
query_obj.validate()
|
||||
|
||||
cache_key = self.query_cache_key(query_obj)
|
||||
timeout = self.get_cache_timeout()
|
||||
force_query = self._query_context.force or timeout == CACHE_DISABLED_TIMEOUT
|
||||
@@ -144,6 +139,10 @@ class QueryContextProcessor:
|
||||
force_cached=force_cached,
|
||||
)
|
||||
|
||||
if query_obj:
|
||||
# Always validate the query object before processing
|
||||
query_obj.validate()
|
||||
|
||||
if query_obj and cache_key and not cache.is_loaded:
|
||||
try:
|
||||
if invalid_columns := [
|
||||
|
||||
@@ -36,7 +36,7 @@ from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from importlib.resources import files
|
||||
from typing import Any, Callable, Iterator, Literal, Optional, TYPE_CHECKING, TypedDict
|
||||
from typing import Any, Callable, Iterator, Literal, TYPE_CHECKING, TypedDict
|
||||
|
||||
import click
|
||||
from celery.schedules import crontab
|
||||
@@ -724,69 +724,46 @@ COMMON_BOOTSTRAP_OVERRIDES_FUNC: Callable[ # noqa: E731
|
||||
# This is merely a default
|
||||
EXTRA_CATEGORICAL_COLOR_SCHEMES: list[dict[str, Any]] = []
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Theme System Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
# ---------------------------------------------------
|
||||
# Theme Configuration for Superset
|
||||
# ---------------------------------------------------
|
||||
# Superset supports custom theming through Ant Design's theme structure.
|
||||
# This allows users to customize colors, fonts, and other UI elements.
|
||||
#
|
||||
# Theme Hierarchy:
|
||||
# 1. THEME_DEFAULT/THEME_DARK - Base themes defined in config (foundation)
|
||||
# 2. System themes - Set by admins via UI (when ENABLE_UI_THEME_ADMINISTRATION=True)
|
||||
# 3. Dashboard themes - Applied per dashboard using the theme bolt button
|
||||
#
|
||||
# How it works:
|
||||
# - Custom themes override base themes for any properties they define
|
||||
# - Properties not defined in custom themes use the base theme values
|
||||
# - Admins can set system-wide themes that apply to all users
|
||||
# - Users can apply specific themes to individual dashboards
|
||||
#
|
||||
# Theme Creation:
|
||||
# Theme Generation:
|
||||
# - Use the Ant Design theme editor: https://ant.design/theme-editor
|
||||
# - Export the generated JSON and use it in your theme configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
# - Export or copy the generated theme JSON and assign to the variables below
|
||||
# - For detailed instructions: https://superset.apache.org/docs/configuration/theming/
|
||||
#
|
||||
# To expose a JSON theme editor modal that can be triggered from the navbar
|
||||
# set the `ENABLE_THEME_EDITOR` feature flag to True.
|
||||
#
|
||||
# Theme Structure:
|
||||
# Each theme should follow Ant Design's theme format.
|
||||
# To create custom themes, use the Ant Design Theme Editor at https://ant.design/theme-editor
|
||||
# and copy the generated JSON configuration.
|
||||
#
|
||||
# Example theme definition:
|
||||
# THEME_DEFAULT = {
|
||||
# "token": {
|
||||
# "colorPrimary": "#2893B3",
|
||||
# "colorSuccess": "#5ac189",
|
||||
# "colorWarning": "#fcc700",
|
||||
# "colorError": "#e04355",
|
||||
# "fontFamily": "'Inter', Helvetica, Arial",
|
||||
# ... # other tokens
|
||||
# },
|
||||
# ... # other theme properties
|
||||
# }
|
||||
|
||||
# Default theme configuration - foundation for all themes
|
||||
# This acts as the base theme for all users
|
||||
THEME_DEFAULT: Theme = {
|
||||
"token": {
|
||||
# Brand
|
||||
"brandLogoAlt": "Apache Superset",
|
||||
"brandLogoUrl": APP_ICON,
|
||||
"brandLogoMargin": "18px",
|
||||
"brandLogoHref": "/",
|
||||
"brandLogoHeight": "24px",
|
||||
# Spinner
|
||||
"brandSpinnerUrl": None,
|
||||
"brandSpinnerSvg": None,
|
||||
# Default colors
|
||||
"colorPrimary": "#2893B3", # NOTE: previous lighter primary color was #20a7c9 # noqa: E501
|
||||
"colorLink": "#2893B3",
|
||||
"colorError": "#e04355",
|
||||
"colorWarning": "#fcc700",
|
||||
"colorSuccess": "#5ac189",
|
||||
"colorInfo": "#66bcfe",
|
||||
# Fonts
|
||||
"fontFamily": "Inter, Helvetica, Arial",
|
||||
"fontFamilyCode": "'Fira Code', 'Courier New', monospace",
|
||||
# Extra tokens
|
||||
"transitionTiming": 0.3,
|
||||
"brandIconMaxWidth": 37,
|
||||
"fontSizeXS": "8",
|
||||
"fontSizeXXL": "28",
|
||||
"fontWeightNormal": "400",
|
||||
"fontWeightLight": "300",
|
||||
"fontWeightStrong": "500",
|
||||
},
|
||||
"algorithm": "default",
|
||||
}
|
||||
|
||||
# Dark theme configuration - foundation for dark mode
|
||||
# Inherits all tokens from THEME_DEFAULT and adds dark algorithm
|
||||
# Set to None to disable dark mode
|
||||
THEME_DARK: Optional[Theme] = {
|
||||
**THEME_DEFAULT,
|
||||
"algorithm": "dark",
|
||||
}
|
||||
# Default theme configuration
|
||||
# Leave empty to use Superset's default theme
|
||||
THEME_DEFAULT: Theme = {"algorithm": "default"}
|
||||
|
||||
# Dark theme configuration
|
||||
# Applied when user selects dark mode
|
||||
THEME_DARK: Theme = {"algorithm": "dark"}
|
||||
|
||||
# Theme behavior and user preference settings
|
||||
# To force a single theme on all users, set THEME_DARK = None
|
||||
@@ -2196,14 +2173,6 @@ CATALOGS_SIMPLIFIED_MIGRATION: bool = False
|
||||
# keeping a web API call open for this long.
|
||||
SYNC_DB_PERMISSIONS_IN_ASYNC_MODE: bool = False
|
||||
|
||||
# CUSTOM_DATABASE_ERRORS: Configure custom error messages for database exceptions
|
||||
# in superset/custom_database_errors.py.
|
||||
# Transform raw database errors into user-friendly messages with optional documentation
|
||||
try:
|
||||
from superset.custom_database_errors import CUSTOM_DATABASE_ERRORS
|
||||
except ImportError:
|
||||
CUSTOM_DATABASE_ERRORS = {}
|
||||
|
||||
|
||||
LOCAL_EXTENSIONS: list[str] = []
|
||||
EXTENSIONS_PATH: str | None = None
|
||||
|
||||
@@ -62,7 +62,7 @@ from sqlalchemy.orm import (
|
||||
)
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
from sqlalchemy.sql import column, ColumnElement, literal_column, quoted_name, table
|
||||
from sqlalchemy.sql import column, ColumnElement, literal_column, table
|
||||
from sqlalchemy.sql.elements import ColumnClause, TextClause
|
||||
from sqlalchemy.sql.expression import Label
|
||||
from sqlalchemy.sql.selectable import Alias, TableClause
|
||||
@@ -1403,19 +1403,13 @@ class SqlaTable(
|
||||
# project.dataset.table format
|
||||
if self.catalog and self.database.db_engine_spec.supports_cross_catalog_queries:
|
||||
# SQLAlchemy doesn't have built-in catalog support for TableClause,
|
||||
# so we need to construct the full identifier manually with proper quoting
|
||||
catalog_quoted = self.quote_identifier(self.catalog)
|
||||
table_quoted = self.quote_identifier(self.table_name)
|
||||
|
||||
# so we need to construct the full identifier manually
|
||||
if self.schema:
|
||||
schema_quoted = self.quote_identifier(self.schema)
|
||||
full_name = f"{catalog_quoted}.{schema_quoted}.{table_quoted}"
|
||||
full_name = f"{self.catalog}.{self.schema}.{self.table_name}"
|
||||
else:
|
||||
full_name = f"{catalog_quoted}.{table_quoted}"
|
||||
full_name = f"{self.catalog}.{self.table_name}"
|
||||
|
||||
# Use quoted_name with quote=False to prevent SQLAlchemy from re-quoting
|
||||
# the already-quoted identifier components
|
||||
return table(quoted_name(full_name, quote=False))
|
||||
return table(full_name)
|
||||
|
||||
if self.schema:
|
||||
return table(self.table_name, schema=self.schema)
|
||||
@@ -1436,7 +1430,6 @@ class SqlaTable(
|
||||
metric: AdhocMetric,
|
||||
columns_by_name: dict[str, TableColumn],
|
||||
template_processor: BaseTemplateProcessor | None = None,
|
||||
processed: bool = False,
|
||||
) -> ColumnElement:
|
||||
"""
|
||||
Turn an adhoc metric into a sqlalchemy column.
|
||||
@@ -1444,7 +1437,6 @@ class SqlaTable(
|
||||
:param dict metric: Adhoc metric definition
|
||||
:param dict columns_by_name: Columns for the current table
|
||||
:param template_processor: template_processor instance
|
||||
:param bool processed: Whether the sqlExpression has already been processed
|
||||
:returns: The metric defined as a sqlalchemy column
|
||||
:rtype: sqlalchemy.sql.column
|
||||
"""
|
||||
@@ -1463,20 +1455,16 @@ class SqlaTable(
|
||||
sqla_column = column(column_name)
|
||||
sqla_metric = self.sqla_aggregations[metric["aggregate"]](sqla_column)
|
||||
elif expression_type == utils.AdhocMetricExpressionType.SQL:
|
||||
expression = metric.get("sqlExpression")
|
||||
|
||||
if not processed:
|
||||
try:
|
||||
expression = self._process_sql_expression(
|
||||
expression=expression,
|
||||
database_id=self.database_id,
|
||||
engine=self.database.backend,
|
||||
schema=self.schema,
|
||||
template_processor=template_processor,
|
||||
)
|
||||
except SupersetSecurityException as ex:
|
||||
raise QueryObjectValidationError(ex.message) from ex
|
||||
|
||||
try:
|
||||
expression = self._process_sql_expression(
|
||||
expression=metric["sqlExpression"],
|
||||
database_id=self.database_id,
|
||||
engine=self.database.backend,
|
||||
schema=self.schema,
|
||||
template_processor=template_processor,
|
||||
)
|
||||
except SupersetSecurityException as ex:
|
||||
raise QueryObjectValidationError(ex.message) from ex
|
||||
sqla_metric = literal_column(expression)
|
||||
else:
|
||||
raise QueryObjectValidationError("Adhoc metric expressionType is invalid")
|
||||
@@ -1654,10 +1642,7 @@ class SqlaTable(
|
||||
)
|
||||
db_engine_spec = self.db_engine_spec
|
||||
errors = [
|
||||
dataclasses.asdict(error)
|
||||
for error in db_engine_spec.extract_errors(
|
||||
ex, database_name=self.database.unique_name
|
||||
)
|
||||
dataclasses.asdict(error) for error in db_engine_spec.extract_errors(ex)
|
||||
]
|
||||
error_message = utils.error_msg_from_exception(ex)
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# 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 re
|
||||
from typing import Any
|
||||
|
||||
from flask_babel import gettext as __
|
||||
|
||||
from superset.errors import SupersetErrorType
|
||||
|
||||
# CUSTOM_DATABASE_ERRORS: Configure custom error messages for database exceptions.
|
||||
# Transform raw database errors into user-friendly messages with optional documentation
|
||||
# links using custom_doc_links. Set show_issue_info=False to hide default error codes.
|
||||
# Example:
|
||||
# CUSTOM_DATABASE_ERRORS = {
|
||||
# "database_name": {
|
||||
# re.compile('permission denied for view'): (
|
||||
# __(
|
||||
# 'Permission denied'
|
||||
# ),
|
||||
# SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
|
||||
# {
|
||||
# "custom_doc_links": [
|
||||
# {
|
||||
# "url": "https://example.com/docs/1",
|
||||
# "label": "Check documentation"
|
||||
# },
|
||||
# ],
|
||||
# "show_issue_info": False,
|
||||
# }
|
||||
# )
|
||||
# },
|
||||
# "examples": {
|
||||
# re.compile(r'message="(?P<message>[^"]*)"'): (
|
||||
# __(
|
||||
# 'Unexpected error: "%(message)s"'
|
||||
# ),
|
||||
# SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
|
||||
# {}
|
||||
# )
|
||||
# }
|
||||
# }
|
||||
|
||||
CUSTOM_DATABASE_ERRORS: dict[
|
||||
str, dict[re.Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]]
|
||||
] = {
|
||||
"examples": {
|
||||
re.compile("no such table: a"): (
|
||||
__("This is custom error message for a"),
|
||||
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
|
||||
{
|
||||
"custom_doc_links": [
|
||||
{
|
||||
"url": "https://example.com/docs/1",
|
||||
"label": "Custom documentation link",
|
||||
},
|
||||
],
|
||||
"show_issue_info": False,
|
||||
},
|
||||
),
|
||||
re.compile("no such table: b"): (
|
||||
__("This is custom error message for b"),
|
||||
SupersetErrorType.GENERIC_DB_ENGINE_ERROR,
|
||||
{
|
||||
"show_issue_info": True,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,11 @@ class ThemeDAO(BaseDAO[Theme]):
|
||||
|
||||
@classmethod
|
||||
def find_system_default(cls) -> Optional[Theme]:
|
||||
"""
|
||||
Find the current system default theme.
|
||||
Returns the theme with is_system_default=True if exactly one exists.
|
||||
Returns None if no theme or multiple themes have
|
||||
is_system_default=True, which triggers fallback to config.py theme.
|
||||
"""Find the current system default theme.
|
||||
|
||||
First looks for a theme with is_system_default=True.
|
||||
If not found or multiple found, falls back to is_system=True theme
|
||||
with name 'THEME_DEFAULT'.
|
||||
"""
|
||||
system_defaults = (
|
||||
db.session.query(Theme).filter(Theme.is_system_default.is_(True)).all()
|
||||
@@ -45,15 +45,27 @@ class ThemeDAO(BaseDAO[Theme]):
|
||||
if len(system_defaults) == 1:
|
||||
return system_defaults[0]
|
||||
|
||||
return None
|
||||
if len(system_defaults) > 1:
|
||||
logger.warning(
|
||||
"Multiple system default themes found (%s), "
|
||||
"falling back to config theme",
|
||||
len(system_defaults),
|
||||
)
|
||||
|
||||
# Fallback to is_system=True theme with name 'THEME_DEFAULT'
|
||||
return (
|
||||
db.session.query(Theme)
|
||||
.filter(Theme.is_system.is_(True), Theme.theme_name == "THEME_DEFAULT")
|
||||
.first()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def find_system_dark(cls) -> Optional[Theme]:
|
||||
"""Find the current system dark theme.
|
||||
|
||||
Returns the theme with is_system_dark=True if exactly one exists.
|
||||
Returns None if no theme or multiple themes have is_system_dark=True,
|
||||
which triggers fallback to config.py theme.
|
||||
First looks for a theme with is_system_dark=True.
|
||||
If not found or multiple found, falls back to is_system=True theme
|
||||
with name 'THEME_DARK'.
|
||||
"""
|
||||
system_darks = (
|
||||
db.session.query(Theme).filter(Theme.is_system_dark.is_(True)).all()
|
||||
@@ -62,4 +74,15 @@ class ThemeDAO(BaseDAO[Theme]):
|
||||
if len(system_darks) == 1:
|
||||
return system_darks[0]
|
||||
|
||||
return None
|
||||
if len(system_darks) > 1:
|
||||
logger.warning(
|
||||
"Multiple system dark themes found (%s), falling back to config theme",
|
||||
len(system_darks),
|
||||
)
|
||||
|
||||
# Fallback to is_system=True theme with name 'THEME_DARK'
|
||||
return (
|
||||
db.session.query(Theme)
|
||||
.filter(Theme.is_system.is_(True), Theme.theme_name == "THEME_DARK")
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -1326,44 +1326,21 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
"""Extract error message for queries"""
|
||||
return utils.error_msg_from_exception(ex)
|
||||
|
||||
@classmethod
|
||||
def get_database_custom_errors(
|
||||
cls, database_name: str | None
|
||||
) -> dict[Any, tuple[str, SupersetErrorType, dict[str, Any]]]:
|
||||
config_custom_errors = app.config.get("CUSTOM_DATABASE_ERRORS", {})
|
||||
if not isinstance(config_custom_errors, dict):
|
||||
return {}
|
||||
|
||||
if database_name and database_name in config_custom_errors:
|
||||
database_errors = config_custom_errors[database_name]
|
||||
if isinstance(database_errors, dict):
|
||||
return database_errors
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def extract_errors(
|
||||
cls,
|
||||
ex: Exception,
|
||||
context: dict[str, Any] | None = None,
|
||||
database_name: str | None = None,
|
||||
cls, ex: Exception, context: dict[str, Any] | None = None
|
||||
) -> list[SupersetError]:
|
||||
raw_message = cls._extract_error_message(ex)
|
||||
|
||||
context = context or {}
|
||||
db_engine_custom_errors = cls.get_database_custom_errors(database_name)
|
||||
|
||||
for regex, (message, error_type, extra) in [
|
||||
*db_engine_custom_errors.items(),
|
||||
*cls.custom_errors.items(),
|
||||
]:
|
||||
for regex, (message, error_type, extra) in cls.custom_errors.items():
|
||||
if match := regex.search(raw_message):
|
||||
params = {**context, **match.groupdict()}
|
||||
extra["engine_name"] = cls.engine_name
|
||||
formatted_message = (message % params) if message else raw_message
|
||||
return [
|
||||
SupersetError(
|
||||
error_type=error_type,
|
||||
message=formatted_message,
|
||||
message=message % params,
|
||||
level=ErrorLevel.ERROR,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
@@ -296,15 +296,11 @@ class DatabricksDynamicBaseEngineSpec(BasicParametersMixin, DatabricksBaseEngine
|
||||
|
||||
@classmethod
|
||||
def extract_errors(
|
||||
cls,
|
||||
ex: Exception,
|
||||
context: dict[str, Any] | None = None,
|
||||
database_name: str | None = None,
|
||||
cls, ex: Exception, context: dict[str, Any] | None = None
|
||||
) -> list[SupersetError]:
|
||||
raw_message = cls._extract_error_message(ex)
|
||||
|
||||
context = context or {}
|
||||
|
||||
# access_token isn't currently parseable from the
|
||||
# databricks error response, but adding it in here
|
||||
# for reference if their error message changes
|
||||
@@ -312,14 +308,7 @@ class DatabricksDynamicBaseEngineSpec(BasicParametersMixin, DatabricksBaseEngine
|
||||
for key, value in cls.context_key_mapping.items():
|
||||
context[key] = context.get(value)
|
||||
|
||||
db_engine_custom_errors = cls.get_database_custom_errors(database_name)
|
||||
if not isinstance(db_engine_custom_errors, dict):
|
||||
db_engine_custom_errors = {}
|
||||
|
||||
for regex, (message, error_type, extra) in [
|
||||
*db_engine_custom_errors.items(),
|
||||
*cls.custom_errors.items(),
|
||||
]:
|
||||
for regex, (message, error_type, extra) in cls.custom_errors.items():
|
||||
match = regex.search(raw_message)
|
||||
if match:
|
||||
params = {**context, **match.groupdict()}
|
||||
|
||||
@@ -116,9 +116,7 @@ class DorisEngineSpec(MySQLEngineSpec):
|
||||
)
|
||||
encryption_parameters = {"ssl": "0"}
|
||||
supports_dynamic_schema = True
|
||||
supports_catalog = supports_dynamic_catalog = True
|
||||
# while technically supported by Doris, this generates invalid table identifiers
|
||||
supports_cross_catalog_queries = False
|
||||
supports_catalog = supports_dynamic_catalog = supports_cross_catalog_queries = True
|
||||
|
||||
column_type_mappings = ( # type: ignore
|
||||
(
|
||||
|
||||
@@ -871,40 +871,6 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
raise QueryObjectValidationError(ex.message) from ex
|
||||
return expression
|
||||
|
||||
def _process_orderby_expression(
|
||||
self,
|
||||
expression: Optional[str],
|
||||
database_id: int,
|
||||
engine: str,
|
||||
schema: str,
|
||||
template_processor: Optional[BaseTemplateProcessor],
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Validate and process an ORDER BY clause expression.
|
||||
|
||||
This requires prefixing the expression with a dummy SELECT statement, so it can
|
||||
be properly parsed and validated.
|
||||
"""
|
||||
if expression:
|
||||
expression = f"SELECT 1 ORDER BY {expression}"
|
||||
|
||||
if processed := self._process_sql_expression(
|
||||
expression=expression,
|
||||
database_id=database_id,
|
||||
engine=engine,
|
||||
schema=schema,
|
||||
template_processor=template_processor,
|
||||
):
|
||||
prefix, expression = re.split(
|
||||
r"ORDER\s+BY",
|
||||
processed,
|
||||
maxsplit=1,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
return expression.strip()
|
||||
|
||||
return None
|
||||
|
||||
def make_sqla_column_compatible(
|
||||
self, sqla_col: ColumnElement, label: Optional[str] = None
|
||||
) -> ColumnElement:
|
||||
@@ -1087,10 +1053,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
)
|
||||
db_engine_spec = self.db_engine_spec
|
||||
errors = [
|
||||
dataclasses.asdict(error)
|
||||
for error in db_engine_spec.extract_errors(
|
||||
ex, database_name=self.database.unique_name
|
||||
)
|
||||
dataclasses.asdict(error) for error in db_engine_spec.extract_errors(ex)
|
||||
]
|
||||
error_message = utils.error_msg_from_exception(ex)
|
||||
|
||||
@@ -1176,7 +1139,6 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
metric: AdhocMetric,
|
||||
columns_by_name: dict[str, "TableColumn"], # pylint: disable=unused-argument
|
||||
template_processor: Optional[BaseTemplateProcessor] = None,
|
||||
processed: bool = False,
|
||||
) -> ColumnElement:
|
||||
"""
|
||||
Turn an adhoc metric into a sqlalchemy column.
|
||||
@@ -1184,7 +1146,6 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
:param dict metric: Adhoc metric definition
|
||||
:param dict columns_by_name: Columns for the current table
|
||||
:param template_processor: template_processor instance
|
||||
:param bool processed: Whether the sqlExpression has already been processed
|
||||
:returns: The metric defined as a sqlalchemy column
|
||||
:rtype: sqlalchemy.sql.column
|
||||
"""
|
||||
@@ -1197,17 +1158,13 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
sqla_column = sa.column(column_name)
|
||||
sqla_metric = self.sqla_aggregations[metric["aggregate"]](sqla_column)
|
||||
elif expression_type == utils.AdhocMetricExpressionType.SQL:
|
||||
expression = metric.get("sqlExpression")
|
||||
|
||||
if not processed:
|
||||
expression = self._process_sql_expression(
|
||||
expression=metric["sqlExpression"],
|
||||
database_id=self.database_id,
|
||||
engine=self.database.backend,
|
||||
schema=self.schema,
|
||||
template_processor=template_processor,
|
||||
)
|
||||
|
||||
expression = self._process_sql_expression(
|
||||
expression=metric["sqlExpression"],
|
||||
database_id=self.database_id,
|
||||
engine=self.database.backend,
|
||||
schema=self.schema,
|
||||
template_processor=template_processor,
|
||||
)
|
||||
sqla_metric = literal_column(expression)
|
||||
else:
|
||||
raise QueryObjectValidationError("Adhoc metric expressionType is invalid")
|
||||
@@ -1822,7 +1779,7 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
if isinstance(col, dict):
|
||||
col = cast(AdhocMetric, col)
|
||||
if col.get("sqlExpression"):
|
||||
col["sqlExpression"] = self._process_orderby_expression(
|
||||
col["sqlExpression"] = self._process_sql_expression(
|
||||
expression=col["sqlExpression"],
|
||||
database_id=self.database_id,
|
||||
engine=self.database.backend,
|
||||
@@ -1831,12 +1788,9 @@ class ExploreMixin: # pylint: disable=too-many-public-methods
|
||||
)
|
||||
if utils.is_adhoc_metric(col):
|
||||
# add adhoc sort by column to columns_by_name if not exists
|
||||
col = self.adhoc_metric_to_sqla(
|
||||
col,
|
||||
columns_by_name,
|
||||
processed=True,
|
||||
)
|
||||
# use the existing instance, if possible
|
||||
col = self.adhoc_metric_to_sqla(col, columns_by_name)
|
||||
# if the adhoc metric has been defined before
|
||||
# use the existing instance.
|
||||
col = metrics_exprs_by_expr.get(str(col), col)
|
||||
need_groupby = True
|
||||
elif col in metrics_exprs_by_label:
|
||||
|
||||
@@ -367,9 +367,7 @@ class Slice( # pylint: disable=too-many-public-methods
|
||||
return qry.one_or_none()
|
||||
|
||||
|
||||
def id_or_uuid_filter(id_or_uuid: str | int) -> BinaryExpression:
|
||||
if isinstance(id_or_uuid, int):
|
||||
return Slice.id == id_or_uuid
|
||||
def id_or_uuid_filter(id_or_uuid: str) -> BinaryExpression:
|
||||
if id_or_uuid.isdigit():
|
||||
return Slice.id == int(id_or_uuid)
|
||||
return Slice.uuid == id_or_uuid
|
||||
|
||||
@@ -17,6 +17,5 @@
|
||||
|
||||
from .dremio import Dremio
|
||||
from .firebolt import Firebolt, FireboltOld
|
||||
from .pinot import Pinot
|
||||
|
||||
__all__ = ["Dremio", "Firebolt", "FireboltOld", "Pinot"]
|
||||
__all__ = ["Dremio", "Firebolt", "FireboltOld"]
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
MySQL ANSI dialect for Apache Pinot.
|
||||
|
||||
This dialect is based on MySQL but follows ANSI SQL quoting conventions where
|
||||
double quotes are used for identifiers instead of string literals.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlglot import exp
|
||||
from sqlglot.dialects.mysql import MySQL
|
||||
from sqlglot.helper import seq_get
|
||||
from sqlglot.tokens import TokenType
|
||||
|
||||
|
||||
class Pinot(MySQL):
|
||||
"""
|
||||
MySQL ANSI dialect used by Apache Pinot.
|
||||
|
||||
The main difference from standard MySQL is that double quotes (") are used for
|
||||
identifiers instead of string literals, following ANSI SQL conventions.
|
||||
|
||||
See: https://calcite.apache.org/javadocAggregate/org/apache/calcite/config/Lex.html#MYSQL_ANSI
|
||||
"""
|
||||
|
||||
class Tokenizer(MySQL.Tokenizer):
|
||||
QUOTES = ["'"] # Only single quotes for strings
|
||||
IDENTIFIERS = ['"', "`"] # Backticks and double quotes for identifiers
|
||||
STRING_ESCAPES = ["'", "\\"] # Remove double quote from string escapes
|
||||
KEYWORDS = {
|
||||
**MySQL.Tokenizer.KEYWORDS,
|
||||
"STRING": TokenType.TEXT,
|
||||
"LONG": TokenType.BIGINT,
|
||||
"BYTES": TokenType.VARBINARY,
|
||||
}
|
||||
|
||||
class Parser(MySQL.Parser):
|
||||
FUNCTIONS = {
|
||||
**MySQL.Parser.FUNCTIONS,
|
||||
"DATE_ADD": lambda args: exp.DateAdd(
|
||||
this=seq_get(args, 2),
|
||||
expression=seq_get(args, 1),
|
||||
unit=seq_get(args, 0),
|
||||
),
|
||||
"DATE_SUB": lambda args: exp.DateSub(
|
||||
this=seq_get(args, 2),
|
||||
expression=seq_get(args, 1),
|
||||
unit=seq_get(args, 0),
|
||||
),
|
||||
}
|
||||
|
||||
class Generator(MySQL.Generator):
|
||||
TYPE_MAPPING = {
|
||||
**MySQL.Generator.TYPE_MAPPING,
|
||||
exp.DataType.Type.TINYINT: "INT",
|
||||
exp.DataType.Type.SMALLINT: "INT",
|
||||
exp.DataType.Type.INT: "INT",
|
||||
exp.DataType.Type.BIGINT: "LONG",
|
||||
exp.DataType.Type.FLOAT: "FLOAT",
|
||||
exp.DataType.Type.DOUBLE: "DOUBLE",
|
||||
exp.DataType.Type.BOOLEAN: "BOOLEAN",
|
||||
exp.DataType.Type.TIMESTAMP: "TIMESTAMP",
|
||||
exp.DataType.Type.TIMESTAMPTZ: "TIMESTAMP",
|
||||
exp.DataType.Type.VARCHAR: "STRING",
|
||||
exp.DataType.Type.CHAR: "STRING",
|
||||
exp.DataType.Type.TEXT: "STRING",
|
||||
exp.DataType.Type.BINARY: "BYTES",
|
||||
exp.DataType.Type.VARBINARY: "BYTES",
|
||||
exp.DataType.Type.JSON: "JSON",
|
||||
}
|
||||
|
||||
# Override MySQL's CAST_MAPPING - don't convert integer or string types
|
||||
CAST_MAPPING = {
|
||||
exp.DataType.Type.LONGBLOB: exp.DataType.Type.VARBINARY,
|
||||
exp.DataType.Type.MEDIUMBLOB: exp.DataType.Type.VARBINARY,
|
||||
exp.DataType.Type.TINYBLOB: exp.DataType.Type.VARBINARY,
|
||||
exp.DataType.Type.UBIGINT: "UNSIGNED",
|
||||
}
|
||||
|
||||
TRANSFORMS = {
|
||||
**MySQL.Generator.TRANSFORMS,
|
||||
exp.DateAdd: lambda self, e: self.func(
|
||||
"DATE_ADD",
|
||||
exp.Literal.string(str(e.args.get("unit").name)),
|
||||
e.args.get("expression"),
|
||||
e.this,
|
||||
),
|
||||
exp.DateSub: lambda self, e: self.func(
|
||||
"DATE_SUB",
|
||||
exp.Literal.string(str(e.args.get("unit").name)),
|
||||
e.args.get("expression"),
|
||||
e.this,
|
||||
),
|
||||
exp.Substring: lambda self, e: self.func(
|
||||
"SUBSTR",
|
||||
e.this,
|
||||
e.args.get("start"),
|
||||
e.args.get("length"),
|
||||
),
|
||||
exp.StrPosition: lambda self, e: self.func(
|
||||
"STRPOS",
|
||||
e.this,
|
||||
e.args.get("substr"),
|
||||
e.args.get("position"),
|
||||
),
|
||||
exp.StartsWith: lambda self, e: self.func(
|
||||
"STARTSWITH",
|
||||
e.this,
|
||||
e.args.get("expression"),
|
||||
),
|
||||
exp.Chr: lambda self, e: self.func(
|
||||
"CHR",
|
||||
*e.args.get("expressions", []),
|
||||
),
|
||||
exp.Mod: lambda self, e: self.func(
|
||||
"MOD",
|
||||
e.this,
|
||||
e.args.get("expression"),
|
||||
),
|
||||
exp.ArrayAgg: lambda self, e: self.func(
|
||||
"ARRAY_AGG",
|
||||
e.this,
|
||||
),
|
||||
exp.JSONExtractScalar: lambda self, e: self.func(
|
||||
"JSON_EXTRACT_SCALAR",
|
||||
e.this,
|
||||
e.args.get("expression"),
|
||||
e.args.get("variant"),
|
||||
),
|
||||
}
|
||||
# Remove DATE_TRUNC transformation - Pinot supports standard SQL DATE_TRUNC
|
||||
TRANSFORMS.pop(exp.DateTrunc, None)
|
||||
|
||||
def datatype_sql(self, expression: exp.DataType) -> str:
|
||||
# Don't use MySQL's VARCHAR size requirement logic
|
||||
# Just use TYPE_MAPPING for all types
|
||||
type_value = expression.this
|
||||
type_sql = (
|
||||
self.TYPE_MAPPING.get(type_value, type_value.value)
|
||||
if isinstance(type_value, exp.DataType.Type)
|
||||
else type_value
|
||||
)
|
||||
|
||||
interior = self.expressions(expression, flat=True)
|
||||
nested = f"({interior})" if interior else ""
|
||||
|
||||
if expression.this in self.UNSIGNED_TYPE_MAPPING:
|
||||
return f"{type_sql} UNSIGNED{nested}"
|
||||
|
||||
return f"{type_sql}{nested}"
|
||||
|
||||
def cast_sql(self, expression: exp.Cast, safe_prefix: str | None = None) -> str:
|
||||
# Pinot doesn't support MySQL's TIMESTAMP() function
|
||||
# Use standard CAST syntax instead
|
||||
return super(MySQL.Generator, self).cast_sql(expression, safe_prefix)
|
||||
@@ -44,7 +44,7 @@ from sqlglot.optimizer.scope import (
|
||||
)
|
||||
|
||||
from superset.exceptions import QueryClauseValidationException, SupersetParseError
|
||||
from superset.sql.dialects import Dremio, Firebolt, Pinot
|
||||
from superset.sql.dialects import Dremio, Firebolt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from superset.models.core import Database
|
||||
@@ -94,7 +94,7 @@ SQLGLOT_DIALECTS = {
|
||||
# "odelasticsearch": ???
|
||||
"oracle": Dialects.ORACLE,
|
||||
"parseable": Dialects.POSTGRES,
|
||||
"pinot": Pinot,
|
||||
"pinot": Dialects.MYSQL,
|
||||
"postgresql": Dialects.POSTGRES,
|
||||
"presto": Dialects.PRESTO,
|
||||
"pydoris": Dialects.DORIS,
|
||||
@@ -552,16 +552,14 @@ class SQLStatement(BaseSQLStatement[exp.Expression]):
|
||||
try:
|
||||
statements = sqlglot.parse(script, dialect=dialect)
|
||||
except sqlglot.errors.ParseError as ex:
|
||||
kwargs = (
|
||||
{
|
||||
"highlight": ex.errors[0]["highlight"],
|
||||
"line": ex.errors[0]["line"],
|
||||
"column": ex.errors[0]["col"],
|
||||
}
|
||||
if ex.errors
|
||||
else {}
|
||||
)
|
||||
raise SupersetParseError(script, engine, **kwargs) from ex
|
||||
error = ex.errors[0]
|
||||
raise SupersetParseError(
|
||||
script,
|
||||
engine,
|
||||
highlight=error["highlight"],
|
||||
line=error["line"],
|
||||
column=error["col"],
|
||||
) from ex
|
||||
except sqlglot.errors.SqlglotError as ex:
|
||||
raise SupersetParseError(
|
||||
script,
|
||||
@@ -1482,15 +1480,6 @@ def sanitize_clause(clause: str, engine: str) -> str:
|
||||
Make sure the SQL clause is valid.
|
||||
"""
|
||||
try:
|
||||
statement = SQLStatement(clause, engine)
|
||||
dialect = SQLGLOT_DIALECTS.get(engine)
|
||||
from sqlglot.dialects.dialect import Dialect
|
||||
|
||||
return Dialect.get_or_raise(dialect).generate(
|
||||
statement._parsed, # pylint: disable=protected-access
|
||||
copy=True,
|
||||
comments=False,
|
||||
pretty=False,
|
||||
)
|
||||
return SQLStatement(clause, engine).format()
|
||||
except SupersetParseError as ex:
|
||||
raise QueryClauseValidationException(f"Invalid SQL clause: {clause}") from ex
|
||||
|
||||
@@ -111,9 +111,7 @@ def handle_query_error(
|
||||
elif isinstance(ex, SupersetErrorsException):
|
||||
errors = ex.errors
|
||||
else:
|
||||
errors = query.database.db_engine_spec.extract_errors(
|
||||
str(ex), database_name=query.database.unique_name
|
||||
)
|
||||
errors = query.database.db_engine_spec.extract_errors(str(ex))
|
||||
|
||||
errors_payload = [dataclasses.asdict(error) for error in errors]
|
||||
if errors:
|
||||
|
||||
@@ -21,7 +21,7 @@ import logging
|
||||
import os
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, cast
|
||||
from typing import Any, Callable
|
||||
|
||||
from babel import Locale
|
||||
from flask import (
|
||||
@@ -58,10 +58,8 @@ from superset.daos.theme import ThemeDAO
|
||||
from superset.db_engine_specs import get_available_engine_specs
|
||||
from superset.db_engine_specs.gsheets import GSheetsEngineSpec
|
||||
from superset.extensions import cache_manager
|
||||
from superset.models.core import Theme as ThemeModel
|
||||
from superset.reports.models import ReportRecipientType
|
||||
from superset.superset_typing import FlaskResponse
|
||||
from superset.themes.types import Theme, ThemeMode
|
||||
from superset.themes.utils import (
|
||||
is_valid_theme,
|
||||
)
|
||||
@@ -312,57 +310,6 @@ def menu_data(user: User) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _merge_theme_dicts(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Recursively merge overlay theme dict into base theme dict.
|
||||
Arrays and non-dict values are replaced, not merged.
|
||||
"""
|
||||
result = base.copy()
|
||||
for key, value in overlay.items():
|
||||
if isinstance(result.get(key), dict) and isinstance(value, dict):
|
||||
result[key] = _merge_theme_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _load_theme_from_model(
|
||||
theme_model: ThemeModel | None,
|
||||
fallback_theme: Theme | None,
|
||||
theme_type: ThemeMode,
|
||||
) -> Theme | None:
|
||||
"""Load and parse theme from database model, merging with config theme as base."""
|
||||
if theme_model:
|
||||
try:
|
||||
db_theme = json.loads(theme_model.json_data)
|
||||
if fallback_theme:
|
||||
merged = _merge_theme_dicts(dict(fallback_theme), db_theme)
|
||||
return cast(Theme, merged)
|
||||
return db_theme
|
||||
except json.JSONDecodeError:
|
||||
logger.error(
|
||||
"Invalid JSON in system %s theme %s", theme_type.value, theme_model.id
|
||||
)
|
||||
return fallback_theme
|
||||
return fallback_theme
|
||||
|
||||
|
||||
def _process_theme(theme: Theme | None, theme_type: ThemeMode) -> Theme:
|
||||
"""Process and validate a theme, returning an empty dict if invalid."""
|
||||
if theme is None or theme == {}:
|
||||
# When config theme is None or empty, don't provide a custom theme
|
||||
# The frontend will use base theme only
|
||||
return {}
|
||||
elif not is_valid_theme(cast(dict[str, Any], theme)):
|
||||
logger.warning(
|
||||
"Invalid %s theme configuration: %s, clearing it",
|
||||
theme_type.value,
|
||||
theme,
|
||||
)
|
||||
return {}
|
||||
return theme or {}
|
||||
|
||||
|
||||
def get_theme_bootstrap_data() -> dict[str, Any]:
|
||||
"""
|
||||
Returns the theme data to be sent to the client.
|
||||
@@ -370,30 +317,59 @@ def get_theme_bootstrap_data() -> dict[str, Any]:
|
||||
# Check if UI theme administration is enabled
|
||||
enable_ui_admin = app.config.get("ENABLE_UI_THEME_ADMINISTRATION", False)
|
||||
|
||||
# Get config themes to use as fallback
|
||||
config_theme_default = get_config_value("THEME_DEFAULT")
|
||||
config_theme_dark = get_config_value("THEME_DARK")
|
||||
|
||||
if enable_ui_admin:
|
||||
# Try to load themes from database
|
||||
default_theme_model = ThemeDAO.find_system_default()
|
||||
dark_theme_model = ThemeDAO.find_system_dark()
|
||||
|
||||
# Parse theme JSON from database models
|
||||
default_theme = _load_theme_from_model(
|
||||
default_theme_model, config_theme_default, ThemeMode.DEFAULT
|
||||
)
|
||||
dark_theme = _load_theme_from_model(
|
||||
dark_theme_model, config_theme_dark, ThemeMode.DARK
|
||||
)
|
||||
else:
|
||||
# UI theme administration disabled - use config-based themes
|
||||
default_theme = config_theme_default
|
||||
dark_theme = config_theme_dark
|
||||
default_theme = {}
|
||||
if default_theme_model:
|
||||
try:
|
||||
default_theme = json.loads(default_theme_model.json_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(
|
||||
"Invalid JSON in system default theme %s",
|
||||
default_theme_model.id,
|
||||
)
|
||||
# Fallback to config
|
||||
default_theme = get_config_value("THEME_DEFAULT")
|
||||
else:
|
||||
# No system default theme in database, use config
|
||||
default_theme = get_config_value("THEME_DEFAULT")
|
||||
|
||||
# Process and validate themes
|
||||
default_theme = _process_theme(default_theme, ThemeMode.DEFAULT)
|
||||
dark_theme = _process_theme(dark_theme, ThemeMode.DARK)
|
||||
dark_theme = {}
|
||||
if dark_theme_model:
|
||||
try:
|
||||
dark_theme = json.loads(dark_theme_model.json_data)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(
|
||||
"Invalid JSON in system dark theme %s", dark_theme_model.id
|
||||
)
|
||||
# Fallback to config
|
||||
dark_theme = get_config_value("THEME_DARK")
|
||||
else:
|
||||
# No system dark theme in database, use config
|
||||
dark_theme = get_config_value("THEME_DARK")
|
||||
else:
|
||||
# UI theme administration disabled, use config-based themes
|
||||
default_theme = get_config_value("THEME_DEFAULT")
|
||||
dark_theme = get_config_value("THEME_DARK")
|
||||
|
||||
# Validate theme configurations
|
||||
if not is_valid_theme(default_theme):
|
||||
logger.warning(
|
||||
"Invalid default theme configuration: %s, using empty theme",
|
||||
default_theme,
|
||||
)
|
||||
default_theme = {}
|
||||
|
||||
if not is_valid_theme(dark_theme):
|
||||
logger.warning(
|
||||
"Invalid dark theme configuration: %s, using empty theme",
|
||||
dark_theme,
|
||||
)
|
||||
dark_theme = {}
|
||||
|
||||
return {
|
||||
"theme": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user