Compare commits

..

2 Commits

Author SHA1 Message Date
Elizabeth Thompson
3d2c332165 Merge remote-tracking branch 'origin/master' into docs/testing-guidelines-test-function 2025-09-29 13:13:49 -07:00
Elizabeth Thompson
572f3392d7 docs(testing): add guidelines for using test() instead of describe()/it()
Added comprehensive testing structure guidelines to LLMS.md that explain why and how to use test() instead of describe() and it(), following the "avoid nesting when testing" principle for better test isolation and readability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 10:25:15 -07:00
111 changed files with 2731 additions and 6796 deletions

2
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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: |

View File

@@ -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 }}!

View File

@@ -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'

View File

@@ -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'

View File

@@ -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: |

View File

@@ -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: |

View File

@@ -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'

View File

@@ -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

View File

@@ -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: |

View File

@@ -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'

View File

@@ -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'

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}';

View File

@@ -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

View File

@@ -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

View File

@@ -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('/')

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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" \

View File

@@ -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

View File

@@ -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": [

View File

@@ -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 && {

View File

@@ -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"
]
}
}

View File

@@ -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:

View File

@@ -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"]

View File

@@ -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

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -22,7 +22,7 @@
"typescript": "^5.0.0"
},
"peerDependencies": {
"antd": "^5.24.9",
"antd": "^5.24.6",
"react": "^17.0.2"
},
"scripts": {

View File

@@ -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();
});
});

View 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"
}
}

View File

@@ -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",

View File

@@ -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),
},
]);
});

View File

@@ -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>[]);

View File

@@ -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', () => {

View File

@@ -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 = () => {

View File

@@ -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);
});
});
});

View File

@@ -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 */

View File

@@ -78,7 +78,6 @@ export type SerializableThemeConfig = {
algorithm?: ThemeAlgorithmOption;
hashed?: boolean;
inherit?: boolean;
cssVar?: boolean | { key?: string; prefix?: string };
};
/**

View File

@@ -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);
});
});
});

View File

@@ -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

View File

@@ -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: [

View File

@@ -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);

View File

@@ -505,7 +505,6 @@ export const tooltipTemplate = {
config: {
type: TooltipTemplateControl,
label: t('Customize tooltips template'),
renderTrigger: true,
debounceDelay: 30,
default: '',
description: '',

View File

@@ -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}

View File

@@ -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%' },

View File

@@ -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: {

View File

@@ -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: [

View File

@@ -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
}
}

View 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"
}
}

View File

@@ -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 } });

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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"

View File

@@ -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');
});

View File

@@ -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();
});

View File

@@ -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 && (
<>

View File

@@ -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>
</>
);

View File

@@ -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`}

View File

@@ -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);
});
});

View File

@@ -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();
});

View File

@@ -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}

View File

@@ -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();
});

View File

@@ -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>
),

View File

@@ -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) {

View File

@@ -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?.();
}, []);

View File

@@ -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);
});

View File

@@ -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>
)}
</>
);
}

View File

@@ -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);

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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

View File

@@ -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:

View File

@@ -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 := [

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
},
),
}
}

View File

@@ -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()
)

View File

@@ -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,
)

View File

@@ -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()}

View File

@@ -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
(

View File

@@ -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:

View File

@@ -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

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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

View File

@@ -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:

View File

@@ -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