Compare commits

...
Author SHA1 Message Date
Beto Dealmeida 05043fb961 WIP 2025-07-22 11:33:15 -04:00
Đỗ Trọng Hải fe1fddde05 feat(docs): migrate ESLint to v9 (#34207) 2025-07-22 22:09:45 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 7e67deead7 chore(deps-dev): bump form-data from 4.0.0 to 4.0.4 in /superset-embedded-sdk (#34262)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 21:44:07 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6e02603098 chore(deps): bump form-data from 4.0.0 to 4.0.4 in /docs (#34263)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 21:43:02 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4518f6999c chore(deps): bump form-data from 4.0.1 to 4.0.4 in /superset-frontend (#34265)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 21:42:23 +07:00
SBIN2010 aff847b3af fix: database model Collapse state (#34126) 2025-07-22 15:40:19 +03:00
SBIN2010 b24aca0304 fix: bug when updating dashboard (#34193) 2025-07-21 12:02:25 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2c453035e4 chore(deps): bump on-headers and morgan in /superset-websocket/utils/client-ws-app (#34215)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 11:51:18 -07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 4fe11869fc chore(deps): bump on-headers and compression in /superset-frontend (#34216)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 11:38:33 -07:00
Maxime Beauchemin a0a49f9300 feat: add Claude Code GitHub Action integration (#34231) 2025-07-21 09:14:45 -07:00
Mehmet Salih Yavuz 29d2fac485 fix(Chart): Calculate chart height correctly (#34224) 2025-07-21 14:12:16 +03:00
Mehmet Salih Yavuz 0c5da6cb5d fix(theming): World map tooltip color (#34229) 2025-07-19 01:32:53 +03:00
Paul Lavacquery da6947d295 feat(deckgl): add support for OpenStreetMap as our new default and make "tile-providers" more configurable FIX (#34204) 2025-07-18 15:25:32 -07:00
Maxime BeaucheminandBeto Dealmeida 2db8f809ba fix: proper handling of boolean filters with snowflake (#34199)
Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
2025-07-18 12:13:52 -07:00
Erkka TahvanainenandErkka Tahvanainen 5912fad745 fix(dashboard): Fix subitem selection on dashboard download menu (#33933)
Co-authored-by: Erkka Tahvanainen <erkka.tahvanainen@confidently.fi>
2025-07-18 11:16:19 -07:00
mdusmanalviandMehmet Salih Yavuz dc41c45bec feat(pivot-table-chart): Download as pivoted excel (#33569)
Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
2025-07-18 11:12:14 -07:00
Joe Li 88ee90c579 chore: Updates files related to 4.1.3 release (#34217) 2025-07-18 09:59:06 -07:00
Gabriel Torres Ruiz bbb2279644 fix(theming): Superset theme configurations correctly applying to charts (#34218) 2025-07-18 16:25:48 +03:00
Maxime Beauchemin 1958df6b83 fix: dataset endpoint /rowlevelsecurity/related/tables doesn't apply filters as expected (#34192) 2025-07-17 15:51:03 -07:00
eriks47 58bd3bfcf0 fix(chart): update geographical info for latvia (#33450) 2025-07-17 13:32:34 -07:00
Denis Krivenko d6eb6e08d0 style(helm): Minor reformatting of helm chart templates (#33736) 2025-07-17 11:33:09 -07:00
JUST.in DO IT 96cb6030c8 fix(explore): Display missing dataset for denied access (#34129) 2025-07-16 13:36:03 -07:00
Jun Yoneyama 94d47113ea feat(snowflake): Support Snowflake private keys w/o passphrase (#34156) 2025-07-16 20:53:41 +02:00
Mehmet Salih Yavuz f756cee01b fix(theming): Remove leftover antd5 prefix (#34188) 2025-07-16 19:31:14 +03:00
Cesc Bausà e8926f177d feat(i18n): add Catalan (ca) translations (#33953) 2025-07-16 16:38:51 +02:00
Mehmet Salih Yavuz 16f4516903 chore(Oracle): Update oracle column length to 128 (#34179) 2025-07-16 14:58:19 +03:00
Beto Dealmeida 000d353ef3 fix(sqllab): database ID (#34181) 2025-07-15 19:37:24 -04:00
Beto Dealmeida 83b6f672ff fix(databricks): string escaper (#34180) 2025-07-15 19:27:55 -04:00
Beto Dealmeida 0dc48e9b41 fix(sqllab): pass DB id instead of name (#33955) 2025-07-15 18:43:34 -04:00
Maxime Beauchemin fe9eef9198 feat: removing dup logic in sqla/models.py and models/helpers.py (#34177) 2025-07-15 14:02:57 -07:00
Hari Kiran 8a8248b575 docs(development): Fix typo in the documentation (#34163) 2025-07-15 14:30:32 -03:00
Gabriel Torres Ruiz 42d9a78777 feat(theming): Introduce bootstrap-driven Superset theme configurations (#34144) 2025-07-15 09:43:08 -07:00
Mehmet Salih Yavuz 31a15c5162 fix(DrillBy): make drill by work with multi metric charts (#34171) 2025-07-15 15:25:48 +02:00
SBIN2010 67b21c45df feat(filter panel): hide filter panel on all dashboard by default. (#32870) 2025-07-15 10:47:47 +03:00
SBIN2010 b280ab9e1f fix: adding and removing tags does not work in control panel properties modal (#34147) 2025-07-14 23:20:00 +03:00
Maxime Beauchemin c42be77c25 feat(i18n): load language pack asynchronously (#34119) 2025-07-14 10:59:29 -07:00
Maxime Beauchemin 160917eae8 fix: frontend translation framework crashes on string errors (#34118) 2025-07-14 10:23:18 -07:00
Beto Dealmeida 68b84acd93 feat: improve Doris catalog support (#34140) 2025-07-14 12:01:08 -04:00
ongdisheng 0aa48b6564 fix(dataset): trigger onChange when switching to physical dataset to clear SQL (#34153) 2025-07-14 14:30:05 +03:00
Mehmet Salih Yavuz 0fc1955049 chore(Tags): Sort tags by name if possible (#34149) 2025-07-14 11:20:01 +03:00
ongdisheng 8a704d293b docs: remove duplicated line in Running tests with act section (#34145) 2025-07-12 15:25:11 -07:00
138 changed files with 21008 additions and 1450 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ body:
options:
- master / latest-dev
- "5.0.0"
- "4.1.2"
- "4.1.3"
validations:
required: true
- type: dropdown
+82
View File
@@ -0,0 +1,82 @@
name: Claude PR Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
check-permissions:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
runs-on: ubuntu-latest
outputs:
allowed: ${{ steps.check.outputs.allowed }}
steps:
- name: Check if user is allowed
id: check
run: |
# List of allowed users
ALLOWED_USERS="mistercrunch,rusackas"
# Get the commenter's username
COMMENTER="${{ github.event.comment.user.login }}"
echo "Checking permissions for user: $COMMENTER"
# Check if user is in allowed list
if [[ ",$ALLOWED_USERS," == *",$COMMENTER,"* ]]; then
echo "allowed=true" >> $GITHUB_OUTPUT
echo "✅ User $COMMENTER is allowed to use Claude"
else
echo "allowed=false" >> $GITHUB_OUTPUT
echo "❌ User $COMMENTER is not allowed to use Claude"
fi
deny-access:
needs: check-permissions
if: needs.check-permissions.outputs.allowed == 'false'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Comment access denied
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 }}!
Thanks for trying to use Claude Code, but currently only certain team members have access to this feature.
If you believe you should have access, please contact a project maintainer.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
claude-code-action:
needs: check-permissions
if: needs.check-permissions.outputs.allowed == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude PR Action
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
+2
View File
@@ -92,6 +92,7 @@ scripts/*.zip
# IntelliJ
*.iml
venv
.venv
@eaDir/
# PyCharm
@@ -127,3 +128,4 @@ docker/*local*
test-report.html
superset/static/stats/statistics.html
.aider*
CLAUDE.local.md
+58
View File
@@ -0,0 +1,58 @@
<!--
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.
-->
## Change Log
### 4.1.3 (Thu May 29 02:31:07 2025 -0500)
**Database Migrations**
**Features**
**Fixes**
- [#33522](https://github.com/apache/superset/pull/33522) fix(Sqllab): Autocomplete got stuck in UI when open it too fast (@rebenitez1802)
- [#33425](https://github.com/apache/superset/pull/33425) fix(table-chart): time shift is not working (@justinpark)
- [#32414](https://github.com/apache/superset/pull/32414) fix(api): Added uuid to list api calls (@withnale)
- [#33354](https://github.com/apache/superset/pull/33354) fix: loading examples from raw.githubusercontent.com fails with 429 errors (@mistercrunch)
- [#32382](https://github.com/apache/superset/pull/32382) fix(pinot): revert join and subquery flags (@yuribogomolov)
- [#32473](https://github.com/apache/superset/pull/32473) fix(plugin-chart-echarts): remove erroneous upper bound value (@villebro)
- [#33048](https://github.com/apache/superset/pull/33048) fix: improve error type on parse error (@justinpark)
- [#32968](https://github.com/apache/superset/pull/32968) fix(pivot-table): Revert "fix(Pivot Table): Fix column width to respect currency config (#31414)" (@justinpark)
- [#32795](https://github.com/apache/superset/pull/32795) fix(log): store navigation path to get correct logging path (@justinpark)
- [#33216](https://github.com/apache/superset/pull/33216) fix: Downgrade to marshmallow<4 (@amotl)
- [#32866](https://github.com/apache/superset/pull/32866) fix: make packages PEP 625 compliant (@sadpandajoe)
- [#32035](https://github.com/apache/superset/pull/32035) fix(fe/dashboard-list): display modifier info for `Last modified` data (@hainenber)
- [#32708](https://github.com/apache/superset/pull/32708) fix(logging): missing path in event data (@justinpark)
- [#32699](https://github.com/apache/superset/pull/32699) fix: Signature of Celery pruner jobs (@michael-s-molina)
- [#32681](https://github.com/apache/superset/pull/32681) fix(log): Update recent_activity by event name (@justinpark)
- [#32608](https://github.com/apache/superset/pull/32608) fix(welcome): perf on distinct recent activities (@justinpark)
- [#32572](https://github.com/apache/superset/pull/32572) fix: Log table retention policy (@michael-s-molina)
- [#32406](https://github.com/apache/superset/pull/32406) fix(model/helper): represent RLS filter clause in proper textual SQL string (@hainenber)
- [#32240](https://github.com/apache/superset/pull/32240) fix: upgrade to 3.11.11-slim-bookworm to address critical vulnerabilities (@gpchandran)
- [#30858](https://github.com/apache/superset/pull/30858) fix(chart data): removing query from /chart/data payload when accessing as guest user (@fisjac)
**Others**
- [#33612](https://github.com/apache/superset/pull/33612) chore: update Dockerfile - Upgrade to 3.11.12 (@gpchandran)
- [#33435](https://github.com/apache/superset/pull/33435) docs: CVEs fixed on 4.1.2 (@sha174n)
- [#33339](https://github.com/apache/superset/pull/33339) chore(🦾): bump python h11 0.14.0 -> 0.16.0 (@github-actions[bot])
- [#32745](https://github.com/apache/superset/pull/32745) chore(🦾): bump python sqlglot 26.1.3 -> 26.11.1 (@github-actions[bot])
- [#32782](https://github.com/apache/superset/pull/32782) chore: Revert "chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm`" (@sadpandajoe)
- [#32780](https://github.com/apache/superset/pull/32780) chore: bump base image in Dockerfile with `ARG PY_VER=3.11.11-slim-bookworm` (@gpchandran)
+1 -1
View File
@@ -23,7 +23,7 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
- [34204](https://github.com/apache/superset/pull/33603) OpenStreetView has been promoted as the new default for Deck.gl visualization since it can be enabled by default without requiring an API key. If you have Mapbox set up and want to disable OpenStreeView in your environment, please follow the steps documented here [https://superset.apache.org/docs/configuration/map-tiles].
- [33116](https://github.com/apache/superset/pull/33116) In Echarts Series charts (e.g. Line, Area, Bar, etc.) charts, the `x_axis_sort_series` and `x_axis_sort_series_ascending` form data items have been renamed with `x_axis_sort` and `x_axis_sort_asc`.
There's a migration added that can potentially affect a significant number of existing charts.
- [32317](https://github.com/apache/superset/pull/32317) The horizontal filter bar feature is now out of testing/beta development and its feature flag `HORIZONTAL_FILTER_BAR` has been removed.
+78
View File
@@ -0,0 +1,78 @@
---
title: Map Tiles
sidebar_position: 12
version: 1
---
# Map tiles
Superset uses OSM and Mapbox tiles by default. OSM is free but you still need setting your MAPBOX_API_KEY if you want to use mapbox maps.
## Setting map tiles
Map tiles can be set with `DECKGL_BASE_MAP` in your `superset_config.py` or `superset_config_docker.py`
For adding your own map tiles, you can use the following format.
```python
DECKGL_BASE_MAP = [
['tile://https://your_personal_url/{z}/{x}/{y}.png', 'MyTile']
]
```
Openstreetmap tiles url can be added without prefix.
```python
DECKGL_BASE_MAP = [
['https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', 'OpenStreetMap']
]
```
Default values are:
```python
DECKGL_BASE_MAP = [
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
['mapbox://styles/mapbox/streets-v9', 'Streets'],
['mapbox://styles/mapbox/dark-v9', 'Dark'],
['mapbox://styles/mapbox/light-v9', 'Light'],
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets'],
['mapbox://styles/mapbox/satellite-v9', 'Satellite'],
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors'],
]
```
It is possible to set only mapbox by removing osm tiles and other way around.
:::warning
Setting `DECKGL_BASE_MAP` overwrite default values
:::
After defining your map tiles, set them in these variables:
- `CORS_OPTIONS`
- `connect-src` of `TALISMAN_CONFIG` and `TALISMAN_CONFIG_DEV` variables.
```python
ENABLE_CORS = True
CORS_OPTIONS: dict[Any, Any] = {
"origins": [
"https://tile.openstreetmap.org",
"https://tile.osm.ch",
"https://your_personal_url/{z}/{x}/{y}.png",
]
}
.
.
TALISMAN_CONFIG = {
"content_security_policy": {
...
"connect-src": [
"'self'",
"https://api.mapbox.com",
"https://events.mapbox.com",
"https://tile.openstreetmap.org",
"https://tile.osm.ch",
"https://your_personal_url/{z}/{x}/{y}.png",
],
...
}
```
+1 -4
View File
@@ -78,7 +78,7 @@ Affecting the Docker build process:
- **SUPERSET_BUILD_TARGET (default=dev):** which --target to build, either `lean` or `dev` are commonly used
- **INCLUDE_FIREFOX (default=false):** whether to include the Firefox headless browser in the build
- **INCLUDE_CHROMIUM (default=false):** whether to include the Firefox headless browser in the build
- **INCLUDE_CHROMIUM (default=false):** whether to include the Chromium headless browser in the build
- **BUILD_TRANSLATIONS(default=false):** whether to compile the translations from the .po files available
- **SUPERSET_LOAD_EXAMPLES (default=yes):** whether to load the examples into the database upon startup,
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
@@ -614,9 +614,6 @@ act --job test-python-38 --secret GITHUB_TOKEN=$GITHUB_TOKEN --event pull_reques
There is also a utility script included in the Superset codebase to run Python integration tests. The [readme can be found here](https://github.com/apache/superset/tree/master/scripts/tests).
There is also a utility script included in the Superset codebase to run python integration tests. The [readme can be
found here](https://github.com/apache/superset/tree/master/scripts/tests)
To run all integration tests, for example, run this script from the root directory:
```bash
+72
View File
@@ -0,0 +1,72 @@
/* eslint-env node */
/**
* 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.
*/
const typescriptEslintParser = require('@typescript-eslint/parser');
const typescriptEslintPlugin = require('@typescript-eslint/eslint-plugin');
const eslintConfigPrettier = require('eslint-config-prettier');
const prettierEslintPlugin = require('eslint-plugin-prettier');
const js = require('@eslint/js');
const ts = require('typescript-eslint');
const react = require('eslint-plugin-react');
const globals = require('globals');
const { defineConfig, globalIgnores } = require('eslint/config');
module.exports = defineConfig([
globalIgnores(['build/**/*', '.docusaurus/**/*', 'node_modules/**/*']),
js.configs.recommended,
...ts.configs.recommended,
eslintConfigPrettier,
{
files: ['eslint.config.js'],
rules: {
'@typescript-eslint/no-require-imports': 'off',
}
},
{
languageOptions: {
parser: typescriptEslintParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2020,
sourceType: 'module',
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
typescript: typescriptEslintPlugin,
react,
prettier: prettierEslintPlugin,
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
settings: {
react: {
version: 'detect',
},
},
}
])
+8 -5
View File
@@ -40,15 +40,18 @@
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@eslint/js": "^9.31.0",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^8.37.0",
"@typescript-eslint/parser": "^8.37.0",
"eslint": "^9.31.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-react": "^7.37.5",
"prettier": "^2.0.0",
"globals": "^16.3.0",
"prettier": "^3.6.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.37.0",
"webpack": "^5.99.9"
},
"browserslist": {
+1 -1
View File
@@ -111,7 +111,7 @@ const StyledTitleContainer = styled('div')`
}
`;
const StyledButton = styled(Link as React.ComponentType<any>)`
const StyledButton = styled(Link)`
border-radius: 10px;
font-size: 20px;
font-weight: bold;
+12 -8
View File
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable no-undef */
import { useEffect } from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
@@ -50,7 +49,9 @@ export default function Root({ children }) {
// Handle route changes for SPA
const handleRouteChange = () => {
devMode && console.log('Route changed to:', window.location.pathname);
if (devMode) {
console.log('Route changed to:', window.location.pathname);
}
// Short timeout to ensure the page has fully rendered
setTimeout(() => {
@@ -58,11 +59,9 @@ export default function Root({ children }) {
const currentTitle = document.title;
const currentPath = window.location.pathname;
devMode &&
console.log('Tracking page view:', currentPath, currentTitle);
// For testing: impersonate real domain - ONLY FOR DEVELOPMENT
if (devMode) {
console.log('Tracking page view:', currentPath, currentTitle);
window._paq.push(['setDomains', ['superset.apache.org']]);
window._paq.push([
'setCustomUrl',
@@ -85,17 +84,22 @@ export default function Root({ children }) {
'routeDidUpdate',
];
devMode && console.log('Setting up Docusaurus route listeners');
if (devMode) {
console.log('Setting up Docusaurus route listeners');
}
possibleEvents.forEach(eventName => {
document.addEventListener(eventName, () => {
devMode &&
if (devMode) {
console.log(`Docusaurus route update detected via ${eventName}`);
}
handleRouteChange();
});
});
// Also set up manual history tracking as fallback
devMode && console.log('Setting up manual history tracking as fallback');
if (devMode) {
console.log('Setting up manual history tracking as fallback');
}
const originalPushState = window.history.pushState;
window.history.pushState = function () {
originalPushState.apply(this, arguments);
+284 -198
View File
@@ -2157,30 +2157,71 @@
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
"@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"
"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1":
version "4.12.1"
resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0"
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
"@eslint/eslintrc@^2.1.4":
version "2.1.4"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz"
integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
"@eslint/config-array@^0.21.0":
version "0.21.0"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.0.tgz#abdbcbd16b124c638081766392a4d6b509f72636"
integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==
dependencies:
"@eslint/object-schema" "^2.1.6"
debug "^4.3.1"
minimatch "^3.1.2"
"@eslint/config-helpers@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.3.0.tgz#3e09a90dfb87e0005c7694791e58e97077271286"
integrity sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==
"@eslint/core@^0.15.0", "@eslint/core@^0.15.1":
version "0.15.1"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.15.1.tgz#d530d44209cbfe2f82ef86d6ba08760196dd3b60"
integrity sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.1":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz#e55f7f1dd400600dd066dbba349c4c0bac916964"
integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.6.0"
globals "^13.19.0"
espree "^10.0.1"
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.57.1":
version "8.57.1"
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz"
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
"@eslint/js@9.31.0", "@eslint/js@^9.31.0":
version "9.31.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.31.0.tgz#adb1f39953d8c475c4384b67b67541b0d7206ed8"
integrity sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==
"@eslint/object-schema@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.6.tgz#58369ab5b5b3ca117880c0f6c0b0f32f6950f24f"
integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
"@eslint/plugin-kit@^0.3.1":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz#32926b59bd407d58d817941e48b2a7049359b1fd"
integrity sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==
dependencies:
"@eslint/core" "^0.15.1"
levn "^0.4.1"
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
version "9.3.0"
@@ -2194,24 +2235,33 @@
dependencies:
"@hapi/hoek" "^9.0.0"
"@humanwhocodes/config-array@^0.13.0":
version "0.13.0"
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz"
integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
"@humanfs/core@^0.19.1":
version "0.19.1"
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
"@humanfs/node@^0.16.6":
version "0.16.6"
resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.6.tgz#ee2a10eaabd1131987bf0488fd9b820174cd765e"
integrity sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==
dependencies:
"@humanwhocodes/object-schema" "^2.0.3"
debug "^4.3.1"
minimatch "^3.0.5"
"@humanfs/core" "^0.19.1"
"@humanwhocodes/retry" "^0.3.0"
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^2.0.3":
version "2.0.3"
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@humanwhocodes/retry@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.1.tgz#c72a5c76a9fbaf3488e231b13dc52c0da7bab42a"
integrity sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==
"@humanwhocodes/retry@^0.4.2":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
"@iconify/types@^2.0.0":
version "2.0.0"
@@ -2353,7 +2403,7 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@@ -2361,6 +2411,11 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@pkgr/core@^0.2.4":
version "0.2.7"
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.7.tgz#eb5014dfd0b03e7f3ba2eeeff506eed89b028058"
integrity sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==
"@pnpm/config.env-replace@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c"
@@ -3599,11 +3654,6 @@
dependencies:
"@types/node" "*"
"@types/semver@^7.3.12":
version "7.5.8"
resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz"
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
"@types/send@*":
version "0.17.4"
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
@@ -3674,91 +3724,105 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.0.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz"
integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
"@typescript-eslint/eslint-plugin@8.37.0", "@typescript-eslint/eslint-plugin@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.37.0.tgz#332392883f936137cd6252c8eb236d298e514e70"
integrity sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA==
dependencies:
"@eslint-community/regexpp" "^4.4.0"
"@typescript-eslint/scope-manager" "5.62.0"
"@typescript-eslint/type-utils" "5.62.0"
"@typescript-eslint/utils" "5.62.0"
debug "^4.3.4"
"@eslint-community/regexpp" "^4.10.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/type-utils" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
graphemer "^1.4.0"
ignore "^5.2.0"
natural-compare-lite "^1.4.0"
semver "^7.3.7"
tsutils "^3.21.0"
ignore "^7.0.0"
natural-compare "^1.4.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/parser@^5.0.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz"
integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
"@typescript-eslint/parser@8.37.0", "@typescript-eslint/parser@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.37.0.tgz#b87f6b61e25ad5cc5bbf8baf809b8da889c89804"
integrity sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA==
dependencies:
"@typescript-eslint/scope-manager" "5.62.0"
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/typescript-estree" "5.62.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.62.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz"
integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==
"@typescript-eslint/project-service@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.37.0.tgz#0594352e32a4ac9258591b88af77b5653800cdfe"
integrity sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA==
dependencies:
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
"@typescript-eslint/type-utils@5.62.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz"
integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
dependencies:
"@typescript-eslint/typescript-estree" "5.62.0"
"@typescript-eslint/utils" "5.62.0"
"@typescript-eslint/tsconfig-utils" "^8.37.0"
"@typescript-eslint/types" "^8.37.0"
debug "^4.3.4"
tsutils "^3.21.0"
"@typescript-eslint/types@5.62.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz"
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
"@typescript-eslint/typescript-estree@5.62.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz"
integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==
"@typescript-eslint/scope-manager@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.37.0.tgz#a31a3c80ca2ef4ed58de13742debb692e7d4c0a4"
integrity sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA==
dependencies:
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
"@typescript-eslint/tsconfig-utils@8.37.0", "@typescript-eslint/tsconfig-utils@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.37.0.tgz#47a2760d265c6125f8e7864bc5c8537cad2bd053"
integrity sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg==
"@typescript-eslint/type-utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.37.0.tgz#2a682e4c6ff5886712dad57e9787b5e417124507"
integrity sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow==
dependencies:
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
debug "^4.3.4"
globby "^11.1.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/types@8.37.0", "@typescript-eslint/types@^8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.37.0.tgz#09517aa9625eb3c68941dde3ac8835740587b6ff"
integrity sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ==
"@typescript-eslint/typescript-estree@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.37.0.tgz#a07e4574d8e6e4355a558f61323730c987f5fcbc"
integrity sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg==
dependencies:
"@typescript-eslint/project-service" "8.37.0"
"@typescript-eslint/tsconfig-utils" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/visitor-keys" "8.37.0"
debug "^4.3.4"
fast-glob "^3.3.2"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^2.1.0"
"@typescript-eslint/utils@5.62.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz"
integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
"@typescript-eslint/utils@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.37.0.tgz#189ea59b2709f5d898614611f091a776751ee335"
integrity sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "5.62.0"
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/typescript-estree" "5.62.0"
eslint-scope "^5.1.1"
semver "^7.3.7"
"@eslint-community/eslint-utils" "^4.7.0"
"@typescript-eslint/scope-manager" "8.37.0"
"@typescript-eslint/types" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/visitor-keys@5.62.0":
version "5.62.0"
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz"
integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==
"@typescript-eslint/visitor-keys@8.37.0":
version "8.37.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.37.0.tgz#cdb6a6bd3e8d6dd69bd70c1bdda36e2d18737455"
integrity sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w==
dependencies:
"@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/types" "8.37.0"
eslint-visitor-keys "^4.2.1"
"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0":
"@ungap/structured-clone@^1.0.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8"
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
@@ -3914,11 +3978,16 @@ acorn-walk@^8.0.0:
dependencies:
acorn "^8.11.0"
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2, acorn@^8.9.0:
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
version "8.14.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
address@^1.0.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e"
@@ -5006,7 +5075,7 @@ cosmiconfig@^8.1.3, cosmiconfig@^8.3.5:
parse-json "^5.2.0"
path-type "^4.0.0"
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
cross-spawn@^7.0.3, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -5704,13 +5773,6 @@ doctrine@^2.1.0:
dependencies:
esutils "^2.0.2"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
docusaurus-plugin-less@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/docusaurus-plugin-less/-/docusaurus-plugin-less-2.0.2.tgz"
@@ -6104,12 +6166,13 @@ eslint-config-prettier@^10.1.5:
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
eslint-plugin-prettier@^4.0.0:
version "4.2.1"
resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz"
integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
eslint-plugin-prettier@^5.5.1:
version "5.5.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz#470820964de9aedb37e9ce62c3266d2d26d08d15"
integrity sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==
dependencies:
prettier-linter-helpers "^1.0.0"
synckit "^0.11.7"
eslint-plugin-react@^7.37.5:
version "7.37.5"
@@ -6135,7 +6198,7 @@ eslint-plugin-react@^7.37.5:
string.prototype.matchall "^4.0.12"
string.prototype.repeat "^1.0.0"
eslint-scope@5.1.1, eslint-scope@^5.1.1:
eslint-scope@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -6143,80 +6206,82 @@ eslint-scope@5.1.1, eslint-scope@^5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-scope@^7.2.2:
version "7.2.2"
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz"
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
eslint-scope@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@^8.0.0:
version "8.57.1"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
eslint-visitor-keys@^4.2.1:
version "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.31.0:
version "9.31.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.31.0.tgz#9a488e6da75bbe05785cd62e43c5ea99356d21ba"
integrity sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.4"
"@eslint/js" "8.57.1"
"@humanwhocodes/config-array" "^0.13.0"
"@eslint-community/regexpp" "^4.12.1"
"@eslint/config-array" "^0.21.0"
"@eslint/config-helpers" "^0.3.0"
"@eslint/core" "^0.15.0"
"@eslint/eslintrc" "^3.3.1"
"@eslint/js" "9.31.0"
"@eslint/plugin-kit" "^0.3.1"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
"@ungap/structured-clone" "^1.2.0"
"@humanwhocodes/retry" "^0.4.2"
"@types/estree" "^1.0.6"
"@types/json-schema" "^7.0.15"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
cross-spawn "^7.0.6"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.2.2"
eslint-visitor-keys "^3.4.3"
espree "^9.6.1"
esquery "^1.4.2"
eslint-scope "^8.4.0"
eslint-visitor-keys "^4.2.1"
espree "^10.4.0"
esquery "^1.5.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
file-entry-cache "^8.0.0"
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
graphemer "^1.4.0"
ignore "^5.2.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.3"
strip-ansi "^6.0.1"
text-table "^0.2.0"
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
espree@^10.0.1, espree@^10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
dependencies:
acorn "^8.9.0"
acorn "^8.15.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.1"
eslint-visitor-keys "^4.2.1"
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.4.2:
esquery@^1.5.0:
version "1.6.0"
resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7"
integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
dependencies:
estraverse "^5.1.0"
@@ -6411,7 +6476,7 @@ fast-diff@^1.1.2:
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0:
fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.2:
version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
@@ -6484,12 +6549,12 @@ figures@^3.2.0:
dependencies:
escape-string-regexp "^1.0.5"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
file-entry-cache@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
dependencies:
flat-cache "^3.0.4"
flat-cache "^4.0.0"
file-loader@^6.2.0:
version "6.2.0"
@@ -6548,14 +6613,13 @@ find-up@^6.3.0:
locate-path "^7.1.0"
path-exists "^5.0.0"
flat-cache@^3.0.4:
version "3.2.0"
resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz"
integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
flat-cache@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
dependencies:
flatted "^3.2.9"
keyv "^4.5.3"
rimraf "^3.0.2"
keyv "^4.5.4"
flat@^5.0.2:
version "5.0.2"
@@ -6590,12 +6654,14 @@ form-data-encoder@^2.1.2:
integrity sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
format@^0.2.0:
@@ -6781,18 +6847,21 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^13.19.0:
version "13.24.0"
resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz"
integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
dependencies:
type-fest "^0.20.2"
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^15.14.0:
version "15.15.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-15.15.0.tgz#7c4761299d41c32b075715a4ce1ede7897ff72a8"
integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
globals@^16.3.0:
version "16.3.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-16.3.0.tgz#66118e765ddaf9e2d880f7e17658543f93f1f667"
integrity sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==
globalthis@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz"
@@ -7285,6 +7354,11 @@ ignore@^5.2.0, ignore@^5.2.4:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
ignore@^7.0.0:
version "7.0.5"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9"
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
image-size@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/image-size/-/image-size-2.0.2.tgz#84a7b43704db5736f364bf0d1b029821299b4bdc"
@@ -7619,7 +7693,7 @@ is-obj@^2.0.0:
resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
is-path-inside@^3.0.2, is-path-inside@^3.0.3:
is-path-inside@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
@@ -7921,7 +7995,7 @@ katex@^0.16.9:
dependencies:
commander "^8.3.0"
keyv@^4.5.3:
keyv@^4.5.3, keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
@@ -8949,7 +9023,7 @@ minimalistic-assert@^1.0.0:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimatch@3.1.2, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
minimatch@3.1.2, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -8963,6 +9037,13 @@ minimatch@^7.4.3:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.4:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.0:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
@@ -9011,11 +9092,6 @@ nanoid@^3.3.11, nanoid@^3.3.8:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
natural-compare-lite@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz"
integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
@@ -10165,10 +10241,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^2.0.0:
version "2.8.8"
resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
prettier@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
pretty-error@^4.0.0:
version "4.0.0"
@@ -11438,6 +11514,11 @@ semver@^7.3.5, semver@^7.3.7, semver@^7.5.4:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
semver@^7.6.0:
version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
send@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
@@ -12058,6 +12139,13 @@ swagger-ui-react@^5.26.0:
xml-but-prettier "^1.0.1"
zenscroll "^4.0.2"
synckit@^0.11.7:
version "0.11.8"
resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.11.8.tgz#b2aaae998a4ef47ded60773ad06e7cb821f55457"
integrity sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==
dependencies:
"@pkgr/core" "^0.2.4"
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
@@ -12084,11 +12172,6 @@ terser@^5.10.0, terser@^5.15.1, terser@^5.31.1:
commander "^2.20.0"
source-map-support "~0.5.20"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
throttle-debounce@^5.0.0, throttle-debounce@^5.0.2:
version "5.0.2"
resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz"
@@ -12180,6 +12263,11 @@ trough@^2.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f"
integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==
ts-api-utils@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91"
integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
ts-dedent@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
@@ -12195,23 +12283,11 @@ ts-toolbelt@^9.6.0:
resolved "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz"
integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3, tslib@^2.3.0, tslib@^2.6.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
@@ -12306,6 +12382,16 @@ types-ramda@^0.30.0:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.37.0:
version "8.37.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.37.0.tgz#2235ddfa40cdbdadb1afb05f8bda688a2294b4c2"
integrity sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA==
dependencies:
"@typescript-eslint/eslint-plugin" "8.37.0"
"@typescript-eslint/parser" "8.37.0"
"@typescript-eslint/typescript-estree" "8.37.0"
"@typescript-eslint/utils" "8.37.0"
typescript@~5.8.3:
version "5.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
+1 -1
View File
@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.14.2
version: 0.14.3
dependencies:
- name: postgresql
version: 13.4.4
+1 -1
View File
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.14.2](https://img.shields.io/badge/Version-0.14.2-informational?style=flat-square)
![Version: 0.14.3](https://img.shields.io/badge/Version-0.14.3-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
+5 -5
View File
@@ -28,9 +28,9 @@ metadata:
chart: {{ template "superset.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 4 }}
{{- end }}
{{- if .Values.init.jobAnnotations }}
annotations: {{- toYaml .Values.init.jobAnnotations | nindent 4 }}
{{- end }}
@@ -44,10 +44,10 @@ spec:
{{- if or .Values.extraLabels .Values.init.podLabels }}
labels:
{{- if .Values.extraLabels }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- toYaml .Values.extraLabels | nindent 8 }}
{{- end }}
{{- if .Values.init.podLabels }}
{{- toYaml .Values.init.podLabels | nindent 8 }}
{{- toYaml .Values.init.podLabels | nindent 8 }}
{{- end }}
{{- end }}
spec:
+5 -5
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetCeleryBeat.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
@@ -35,12 +35,12 @@ metadata:
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if .maxUnavailable }}
{{- end }}
{{- if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
{{- end }}
selector:
matchLabels:
{{- include "supersetCeleryBeat.selectorLabels" $ | nindent 6 }}
+5 -5
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetCeleryFlower.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
@@ -35,12 +35,12 @@ metadata:
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if .maxUnavailable }}
{{- end }}
{{- if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
{{- end }}
selector:
matchLabels:
{{- include "supersetCeleryFlower.selectorLabels" $ | nindent 6 }}
+5 -5
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetWorker.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
@@ -35,12 +35,12 @@ metadata:
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if .maxUnavailable }}
{{- end }}
{{- if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
{{- end }}
selector:
matchLabels:
{{- include "supersetWorker.selectorLabels" $ | nindent 6 }}
+5 -5
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetWebsockets.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
@@ -35,12 +35,12 @@ metadata:
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if .maxUnavailable }}
{{- end }}
{{- if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
{{- end }}
selector:
matchLabels:
{{- include "supersetWebsockets.selectorLabels" $ | nindent 6 }}
+5 -5
View File
@@ -20,7 +20,7 @@
{{- with .Values.supersetNode.podDisruptionBudget }}
{{- if .enabled -}}
{{- if and .minAvailable .maxUnavailable }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- fail "Only one of minAvailable or maxUnavailable should be set" }}
{{- end}}
apiVersion: policy/v1
kind: PodDisruptionBudget
@@ -35,12 +35,12 @@ metadata:
{{- toYaml $.Values.extraLabels | nindent 4 }}
{{- end }}
spec:
{{- if .minAvailable }}
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- end }}
{{- if .maxUnavailable }}
{{- end }}
{{- if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- end }}
{{- end }}
selector:
matchLabels:
{{- include "supersetNode.selectorLabels" $ | nindent 6 }}
+1
View File
@@ -148,6 +148,7 @@ hive = [
impala = ["impyla>0.16.2, <0.17"]
kusto = ["sqlalchemy-kusto>=3.0.0, <4"]
kylin = ["kylinpy>=2.8.1, <2.9"]
metricflow = ["shillelagh[dbtmetricflowapi]>=1.4.2, <2"]
motherduck = ["duckdb==0.10.2", "duckdb-engine>=0.12.1, <0.13"]
mssql = ["pymssql>=2.2.8, <3"]
mysql = ["mysqlclient>=2.1.0, <3"]
+326 -34
View File
@@ -3251,20 +3251,6 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
@@ -3658,6 +3644,19 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4064,6 +4063,20 @@
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.19",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
@@ -4124,12 +4137,57 @@
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@@ -4590,6 +4648,22 @@
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@@ -4617,10 +4691,13 @@
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
@@ -4640,6 +4717,30 @@
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
@@ -4649,6 +4750,19 @@
"node": ">=8.0.0"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -4711,6 +4825,18 @@
"node": ">=4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -4739,6 +4865,45 @@
"node": ">=4"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -6927,6 +7092,15 @@
"tmpl": "1.0.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -10565,19 +10739,6 @@
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"babel-jest": {
@@ -10862,6 +11023,16 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
},
"call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -11140,6 +11311,17 @@
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
"dev": true
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"electron-to-chromium": {
"version": "1.5.19",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
@@ -11183,12 +11365,45 @@
"is-arrayish": "^0.2.1"
}
},
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true
},
"es-module-lexer": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz",
"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
"dev": true
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"requires": {
"es-errors": "^1.3.0"
}
},
"es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"requires": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
}
},
"escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
@@ -11503,6 +11718,19 @@
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true
},
"form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
}
},
"fs-readdir-recursive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
@@ -11523,9 +11751,9 @@
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true
},
"gensync": {
@@ -11540,12 +11768,40 @@
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"requires": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-package-type": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
"dev": true
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -11588,6 +11844,12 @@
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true
},
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -11609,6 +11871,30 @@
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true
},
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true
},
"has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"requires": {
"has-symbols": "^1.0.3"
}
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"requires": {
"function-bind": "^1.1.2"
}
},
"html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -13219,6 +13505,12 @@
"tmpl": "1.0.5"
}
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+1 -1
View File
@@ -56,7 +56,7 @@ module.exports = {
],
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
transformIgnorePatterns: [
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
'node_modules/(?!d3-(interpolate|color|time|scale)|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)',
],
preset: 'ts-jest',
transform: {
+1332 -73
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -104,12 +104,12 @@
"@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map",
"@superset-ui/legacy-preset-chart-deckgl": "file:./plugins/legacy-preset-chart-deckgl",
"@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
"@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram",
"@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
@@ -203,6 +203,7 @@
"use-event-callback": "^0.1.0",
"use-immer": "^0.9.0",
"use-query-params": "^1.1.9",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"yargs": "^17.7.2"
},
"devDependencies": {
@@ -90,6 +90,7 @@ export interface Dataset {
database?: Record<string, unknown>;
normalize_columns?: boolean;
always_filter_main_dttm?: boolean;
extra?: object | string;
}
export interface ControlPanelState {
@@ -45,7 +45,7 @@ interface HandleSelectProps {
}
const menuItemStyles = (theme: any) => css`
&.antd5-menu-item {
&.ant-menu-item {
height: auto;
line-height: 1.4;
@@ -17,7 +17,7 @@
* under the License.
*/
import {
Radio as Antd5Radio,
Radio as AntRadio,
type CheckboxOptionType,
type RadioGroupProps,
} from 'antd';
@@ -40,19 +40,19 @@ const RadioGroup = ({
...props
}: RadioGroupWrapperProps) => {
const content = options.map((option: CheckboxOptionType) => (
<Antd5Radio key={option.value} value={option.value}>
<AntRadio key={option.value} value={option.value}>
{option.label}
</Antd5Radio>
</AntRadio>
));
return (
<Antd5Radio.Group {...props}>
<AntRadio.Group {...props}>
{spaceConfig ? <Space {...spaceConfig}>{content}</Space> : content}
</Antd5Radio.Group>
</AntRadio.Group>
);
};
export const Radio = Object.assign(Antd5Radio, {
export const Radio = Object.assign(AntRadio, {
GroupWrapper: RadioGroup,
Button: Antd5Radio.Button,
Button: AntRadio.Button,
});
export type {
RadioChangeEvent,
@@ -19,28 +19,28 @@
import { Tooltip } from 'antd';
import { Dropdown, Icons } from '@superset-ui/core/components';
import { t } from '@superset-ui/core';
import { ThemeMode } from '../../theme/types';
import { ThemeAlgorithm, ThemeMode } from '../../theme/types';
export interface ThemeSelectProps {
changeThemeMode: (newMode: ThemeMode) => void;
setThemeMode: (newMode: ThemeMode) => void;
tooltipTitle?: string;
themeMode: ThemeMode;
}
const ThemeSelect: React.FC<ThemeSelectProps> = ({
changeThemeMode,
setThemeMode,
tooltipTitle = 'Select theme',
themeMode,
}) => {
const handleSelect = (mode: ThemeMode) => {
changeThemeMode(mode);
setThemeMode(mode);
};
const themeIconMap: Record<ThemeMode, React.ReactNode> = {
[ThemeMode.LIGHT]: <Icons.SunOutlined />,
[ThemeMode.DARK]: <Icons.MoonOutlined />,
const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = {
[ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
[ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
[ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
[ThemeMode.COMPACT]: <Icons.CompressOutlined />,
[ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
};
return (
@@ -49,9 +49,9 @@ const ThemeSelect: React.FC<ThemeSelectProps> = ({
menu={{
items: [
{
key: ThemeMode.LIGHT,
key: ThemeMode.DEFAULT,
label: t('Light'),
onClick: () => handleSelect(ThemeMode.LIGHT),
onClick: () => handleSelect(ThemeMode.DEFAULT),
icon: <Icons.SunOutlined />,
},
{
@@ -18,7 +18,7 @@
*/
import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
import { AnyThemeConfig } from './types';
import { AnyThemeConfig, ThemeAlgorithm } from './types';
// Mock emotion's cache to avoid actual DOM operations
jest.mock('@emotion/cache', () => ({
@@ -44,7 +44,7 @@ describe('Theme', () => {
const parsedJson = JSON.parse(jsonString);
expect(parsedJson.token?.colorPrimary).toBe('#ff0000');
expect(parsedJson.algorithm).toBe('dark');
expect(parsedJson.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
@@ -91,7 +91,7 @@ describe('Theme', () => {
// Verify dark mode by using the serialized config from the public method
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
@@ -137,7 +137,7 @@ describe('Theme', () => {
// Verify the algorithm was updated
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
@@ -150,7 +150,7 @@ describe('Theme', () => {
// Verify dark algorithm is used
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('switches to default algorithm when toggling dark mode off', () => {
@@ -164,7 +164,7 @@ describe('Theme', () => {
// Verify default algorithm is used
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe('default');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('preserves other algorithms when toggling dark mode', () => {
@@ -181,10 +181,11 @@ describe('Theme', () => {
// Verify default algorithm replaces dark but compact is preserved
const serialized = theme.toSerializedConfig();
expect(Array.isArray(serialized.algorithm)).toBe(true);
expect(serialized.algorithm).toContain('default');
expect(serialized.algorithm).toContain('compact');
expect(serialized.algorithm).not.toContain('dark');
expect(serialized.algorithm).toContain(ThemeAlgorithm.DEFAULT);
expect(serialized.algorithm).toContain(ThemeAlgorithm.COMPACT);
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DARK);
});
});
@@ -218,7 +219,7 @@ describe('Theme', () => {
const serialized = theme.toSerializedConfig();
expect(serialized.token?.colorPrimary).toBe('#ff0000');
expect(serialized.algorithm).toBe('dark');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
});
});
});
@@ -17,7 +17,7 @@
* under the License.
*/
/* eslint-disable theme-colors/no-literal-colors */
import { SerializableThemeConfig } from './types';
import { type SerializableThemeConfig, ThemeAlgorithm } from './types';
const exampleThemes: Record<string, SerializableThemeConfig> = {
superset: {
@@ -27,11 +27,11 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
},
supersetDark: {
token: {},
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
},
supersetCompact: {
token: {},
algorithm: 'compact',
algorithm: ThemeAlgorithm.COMPACT,
},
funky: {
token: {
@@ -43,7 +43,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
borderRadius: 12,
fontFamily: 'Comic Sans MS, cursive',
},
algorithm: 'default',
algorithm: ThemeAlgorithm.DEFAULT,
},
funkyDark: {
token: {
@@ -55,7 +55,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
borderRadius: 12,
fontFamily: 'Comic Sans MS, cursive',
},
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
},
};
export default exampleThemes;
@@ -16,17 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import emotionStyled from '@emotion/styled';
import emotionStyled, { CreateStyled } from '@emotion/styled';
import { useTheme as useThemeBasic } from '@emotion/react';
// import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
import type {
SupersetTheme,
SerializableThemeConfig,
AnyThemeConfig,
ThemeStorage,
ThemeControllerOptions,
ThemeContextType,
import {
type SupersetTheme,
type SerializableThemeConfig,
type AnyThemeConfig,
type ThemeStorage,
type ThemeControllerOptions,
type ThemeContextType,
ThemeAlgorithm,
} from './types';
export {
@@ -56,10 +56,12 @@ export function useTheme() {
return theme;
}
const styled = emotionStyled;
const styled: CreateStyled = emotionStyled;
// launching in in dark mode for now while iterating
const themeObject = Theme.fromConfig({ algorithm: 'default' });
const themeObject: Theme = Theme.fromConfig({
algorithm: ThemeAlgorithm.DEFAULT,
});
const { theme } = themeObject;
const supersetTheme = theme;
@@ -33,6 +33,41 @@ import { Theme } from '.';
export type AntdTokens = ReturnType<typeof antdThemeImport.getDesignToken>;
export type AntdThemeConfig = ThemeConfig;
/**
* Theme algorithms supported by Antd.
* They can be used individually or in combination.
* - DEFAULT: Default light theme
* - DARK: Dark theme
* - COMPACT: Compact theme (smaller spacing)
*/
export enum ThemeAlgorithm {
DEFAULT = 'default',
DARK = 'dark',
COMPACT = 'compact',
}
/**
* Represents the current theme mode of the app.
* It can be one of the following:
* - DEFAULT: Light theme
* - DARK: Dark theme
* - SYSTEM: System theme (auto-detects based on system settings)
*/
export enum ThemeMode {
DEFAULT = 'default',
DARK = 'dark',
SYSTEM = 'system',
}
/**
* All valid algorithm values that can be used in theme config.
*/
export type ThemeAlgorithmOption =
| ThemeAlgorithm.DEFAULT
| ThemeAlgorithm.DARK
| ThemeAlgorithm.COMPACT
| ThemeAlgorithm[];
/**
* A serializable version of Ant Design's ThemeConfig
* Compatible with theme editor exports
@@ -40,11 +75,7 @@ export type AntdThemeConfig = ThemeConfig;
export type SerializableThemeConfig = {
token?: Record<string, any>;
components?: Record<string, any>;
algorithm?:
| 'default'
| 'dark'
| 'compact'
| ('default' | 'dark' | 'compact')[];
algorithm?: ThemeAlgorithmOption;
hashed?: boolean;
inherit?: boolean;
};
@@ -358,13 +389,6 @@ export type AllowedAntdTokenKeys = Extract<
keyof AntdTokens
>;
export enum ThemeMode {
LIGHT = 'light',
DARK = 'dark',
SYSTEM = 'system',
COMPACT = 'compact',
}
export type SharedAntdTokens = Pick<AntdTokens, AllowedAntdTokenKeys>;
/** The final shape for our custom theme object, combining old theme + shared antd + superset specifics. */
@@ -379,7 +403,7 @@ export interface ThemeStorage {
}
export interface ThemeControllerOptions {
themeObject: Theme;
themeObject?: Theme;
storage?: ThemeStorage;
storageKey?: string;
modeStorageKey?: string;
@@ -393,6 +417,6 @@ export interface ThemeContextType {
theme: Theme;
themeMode: ThemeMode;
setTheme: (config: AnyThemeConfig) => void;
changeThemeMode: (newMode: ThemeMode) => void;
setThemeMode: (newMode: ThemeMode) => void;
resetTheme: () => void;
}
@@ -28,9 +28,10 @@ import {
genDeprecatedColorVariations,
} from './utils';
import {
AnyThemeConfig,
SerializableThemeConfig,
AntdThemeConfig,
type AnyThemeConfig,
type SerializableThemeConfig,
type AntdThemeConfig,
ThemeAlgorithm,
} from './types';
// Mock tinycolor2 for consistent testing
@@ -50,22 +51,25 @@ describe('Theme utilities', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
expect(isSerializableConfig(config)).toBe(true);
});
it('returns true when algorithm is a string', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
};
expect(isSerializableConfig(config)).toBe(true);
});
it('returns true when algorithm is an array of strings', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: ['dark', 'compact'],
algorithm: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
};
expect(isSerializableConfig(config)).toBe(true);
});
@@ -74,15 +78,19 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.darkAlgorithm,
};
expect(isSerializableConfig(config)).toBe(false);
});
it('returns false when algorithm is an array containing a function', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
// @ts-ignore
algorithm: [antdThemeImport.darkAlgorithm, 'compact'],
algorithm: [
antdThemeImport.darkAlgorithm,
antdThemeImport.compactAlgorithm,
],
};
expect(isSerializableConfig(config)).toBe(false);
});
});
@@ -91,18 +99,22 @@ describe('Theme utilities', () => {
it('converts string algorithm to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
});
it('converts array of string algorithms to function references', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: ['dark', 'compact'],
algorithm: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
};
const result = deserializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toContain(antdThemeImport.darkAlgorithm);
expect(result.algorithm).toContain(antdThemeImport.compactAlgorithm);
@@ -111,10 +123,12 @@ describe('Theme utilities', () => {
it('preserves other configuration properties', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
hashed: true,
};
const result = deserializeThemeConfig(config);
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
expect(result.hashed).toBe(true);
});
@@ -123,25 +137,31 @@ describe('Theme utilities', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBeUndefined();
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
});
it('converts default algorithm string to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'default',
algorithm: ThemeAlgorithm.DEFAULT,
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
});
it('converts compact algorithm string to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'compact',
algorithm: ThemeAlgorithm.COMPACT,
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.compactAlgorithm);
});
});
@@ -152,8 +172,10 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.darkAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('dark');
expect(result.algorithm).toBe(ThemeAlgorithm.DARK);
});
it('converts array of function algorithms to strings', () => {
@@ -164,10 +186,12 @@ describe('Theme utilities', () => {
antdThemeImport.compactAlgorithm,
],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toContain('dark');
expect(result.algorithm).toContain('compact');
expect(result.algorithm).toContain(ThemeAlgorithm.DARK);
expect(result.algorithm).toContain(ThemeAlgorithm.COMPACT);
});
it('preserves other configuration properties', () => {
@@ -176,7 +200,9 @@ describe('Theme utilities', () => {
algorithm: antdThemeImport.darkAlgorithm,
hashed: true,
};
const result = serializeThemeConfig(config);
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
expect(result.hashed).toBe(true);
});
@@ -185,7 +211,9 @@ describe('Theme utilities', () => {
const config: AntdThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBeUndefined();
});
@@ -196,8 +224,10 @@ describe('Theme utilities', () => {
// @ts-ignore
algorithm: unknownAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('default');
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('converts default algorithm function to string', () => {
@@ -205,8 +235,10 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.defaultAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('default');
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
});
it('converts compact algorithm function to string', () => {
@@ -214,8 +246,10 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.compactAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe('compact');
expect(result.algorithm).toBe(ThemeAlgorithm.COMPACT);
});
it('defaults each unknown algorithm in array to "default"', () => {
@@ -225,9 +259,14 @@ describe('Theme utilities', () => {
// @ts-ignore
algorithm: [antdThemeImport.darkAlgorithm, unknownAlgorithm],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toEqual(['dark', 'default']);
expect(result.algorithm).toEqual([
ThemeAlgorithm.DARK,
ThemeAlgorithm.DEFAULT,
]);
});
it('handles mixed known and unknown algorithms in array', () => {
@@ -244,13 +283,15 @@ describe('Theme utilities', () => {
unknownAlgorithm2,
],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toEqual([
'dark',
'default',
'compact',
'default',
ThemeAlgorithm.DARK,
ThemeAlgorithm.DEFAULT,
ThemeAlgorithm.COMPACT,
ThemeAlgorithm.DEFAULT,
]);
});
});
@@ -261,16 +302,20 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.darkAlgorithm,
};
const result = normalizeThemeConfig(config);
expect(result).toBe(config);
});
it('deserializes serializable configs', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: 'dark',
algorithm: ThemeAlgorithm.DARK,
};
const result = normalizeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
});
});
@@ -278,14 +323,18 @@ describe('Theme utilities', () => {
describe('getAntdConfig', () => {
it('returns config with default algorithm for light mode', () => {
const seed = { colorPrimary: '#ff0000' };
const result = getAntdConfig(seed, false);
expect(result.token).toBe(seed);
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
});
it('returns config with dark algorithm for dark mode', () => {
const seed = { colorPrimary: '#ff0000' };
const result = getAntdConfig(seed, true);
expect(result.token).toBe(seed);
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
});
@@ -301,7 +350,9 @@ describe('Theme utilities', () => {
colorInfo: '#info',
otherToken: 'ignore-me',
};
const result = getSystemColors(tokens);
expect(result).toEqual({
colorPrimary: '#primary',
colorError: '#error',
@@ -315,6 +366,7 @@ describe('Theme utilities', () => {
describe('genDeprecatedColorVariations', () => {
it('generates color variations for light mode', () => {
const result = genDeprecatedColorVariations('#base-color', false);
expect(result.base).toBe('#base-color');
expect(result.light1).toBe('#mixed-color');
expect(result.dark1).toBe('#mixed-color');
@@ -322,6 +374,7 @@ describe('Theme utilities', () => {
it('generates color variations for dark mode', () => {
const result = genDeprecatedColorVariations('#base-color', true);
expect(result.base).toBe('#base-color');
expect(result.light1).toBe('#mixed-color');
expect(result.dark1).toBe('#mixed-color');
@@ -337,7 +390,9 @@ describe('Theme utilities', () => {
colorSuccess: '#success',
colorInfo: '#info',
};
const result = getDeprecatedColors(systemColors, false);
expect(result.primary.base).toBe('#primary');
expect(result.error.base).toBe('#error');
expect(result.warning.base).toBe('#warning');
@@ -19,13 +19,14 @@
import { theme as antdThemeImport } from 'antd';
import tinycolor from 'tinycolor2';
import {
AntdThemeConfig,
AnyThemeConfig,
SerializableThemeConfig,
DeprecatedColorVariations,
DeprecatedThemeColors,
SystemColors,
SupersetTheme,
type AntdThemeConfig,
type AnyThemeConfig,
type SerializableThemeConfig,
type DeprecatedColorVariations,
type DeprecatedThemeColors,
type SystemColors,
type SupersetTheme,
ThemeAlgorithm,
} from './types';
/**
@@ -38,9 +39,8 @@ export function isSerializableConfig(
if (algorithm === undefined) return true;
if (Array.isArray(algorithm)) {
if (Array.isArray(algorithm))
return (algorithm as unknown[]).every(alg => typeof alg === 'string');
}
return typeof algorithm === 'string';
}
@@ -60,9 +60,21 @@ export function deserializeThemeConfig(
let resolvedAlgorithm;
if (Array.isArray(algorithm)) {
resolvedAlgorithm = algorithm.map(alg => algorithmMap[alg]);
} else if (algorithm) {
const validAlgorithms = algorithm
.map((alg: ThemeAlgorithm) => algorithmMap[alg])
.filter(Boolean);
// If we have valid algorithms, use them; otherwise fallback to default
if (validAlgorithms.length > 0) {
resolvedAlgorithm =
validAlgorithms.length === 1 ? validAlgorithms[0] : validAlgorithms;
} else {
resolvedAlgorithm = antdThemeImport.defaultAlgorithm;
}
} else if (algorithm && algorithmMap[algorithm]) {
resolvedAlgorithm = algorithmMap[algorithm];
} else {
resolvedAlgorithm = antdThemeImport.defaultAlgorithm;
}
return {
@@ -83,19 +95,21 @@ export function serializeThemeConfig(
if (Array.isArray(algorithm)) {
serializedAlgorithm = algorithm.map(alg => {
if (alg === antdThemeImport.defaultAlgorithm) return 'default';
if (alg === antdThemeImport.darkAlgorithm) return 'dark';
if (alg === antdThemeImport.compactAlgorithm) return 'compact';
return 'default'; // Fallback
}) as ('default' | 'dark' | 'compact')[];
if (alg === antdThemeImport.defaultAlgorithm)
return ThemeAlgorithm.DEFAULT;
if (alg === antdThemeImport.darkAlgorithm) return ThemeAlgorithm.DARK;
if (alg === antdThemeImport.compactAlgorithm)
return ThemeAlgorithm.COMPACT;
return ThemeAlgorithm.DEFAULT; // Fallback
}) as ThemeAlgorithm[];
} else if (algorithm) {
if (algorithm === antdThemeImport.defaultAlgorithm)
serializedAlgorithm = 'default';
serializedAlgorithm = ThemeAlgorithm.DEFAULT;
else if (algorithm === antdThemeImport.darkAlgorithm)
serializedAlgorithm = 'dark';
serializedAlgorithm = ThemeAlgorithm.DARK;
else if (algorithm === antdThemeImport.compactAlgorithm)
serializedAlgorithm = 'compact';
else serializedAlgorithm = 'default'; // Fallback
serializedAlgorithm = ThemeAlgorithm.COMPACT;
else serializedAlgorithm = ThemeAlgorithm.DEFAULT; // Fallback
}
return {
@@ -109,9 +123,8 @@ export function serializeThemeConfig(
* This automatically detects and converts serializable configs
*/
export function normalizeThemeConfig(config: AnyThemeConfig): AntdThemeConfig {
if (isSerializableConfig(config)) {
return deserializeThemeConfig(config);
}
if (isSerializableConfig(config)) return deserializeThemeConfig(config);
return config as AntdThemeConfig;
}
@@ -164,7 +177,7 @@ export function getDeprecatedColors(
systemColors: SystemColors,
isDark: boolean,
): DeprecatedThemeColors {
const sc = systemColors;
const sc: SystemColors = systemColors;
return {
primary: genDeprecatedColorVariations(sc.colorPrimary, isDark),
error: genDeprecatedColorVariations(sc.colorError, isDark),
@@ -86,20 +86,33 @@ export default class Translator {
}
translate(input: string, ...args: unknown[]): string {
return this.i18n.translate(input).fetch(...args);
try {
return this.i18n.translate(input).fetch(...args);
} catch (err) {
logging.warn(`Translation failed for key "${input}" with args:`, args);
return input;
}
}
translateWithNumber(key: string, ...args: unknown[]): string {
const [plural, num, ...rest] = args;
if (typeof plural === 'number') {
try {
const [plural, num, ...rest] = args;
if (typeof plural === 'number') {
return this.i18n
.translate(key)
.ifPlural(plural, key)
.fetch(plural, num, ...rest);
}
return this.i18n
.translate(key)
.ifPlural(plural, key)
.fetch(plural, num, ...args);
.ifPlural(num as number, plural as string)
.fetch(...rest);
} catch (err) {
logging.warn(
`Plural translation failed for key "${key}" with args:`,
args,
);
}
return this.i18n
.translate(key)
.ifPlural(num as number, plural as string)
.fetch(...rest);
return key;
}
}
@@ -45,6 +45,7 @@ export enum FeatureFlag {
EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING',
EscapeMarkdownHtml = 'ESCAPE_MARKDOWN_HTML',
EstimateQueryCost = 'ESTIMATE_QUERY_COST',
FilterBarClosedByDefault = 'FILTERBAR_CLOSED_BY_DEFAULT',
GlobalAsyncQueries = 'GLOBAL_ASYNC_QUERIES',
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
ScheduledQueries = 'SCHEDULED_QUERIES',
@@ -19,18 +19,27 @@
import { t } from '../translation';
const VALIDE_OSM_URLS = ['https://tile.osm', 'https://tile.openstreetmap'];
/**
* Validate a [Mapbox styles URL](https://docs.mapbox.com/help/glossary/style-url/)
* @param v
*/
export default function validateMapboxStylesUrl(v: unknown) {
if (
typeof v === 'string' &&
v.trim().length > 0 &&
v.trim().startsWith('mapbox://styles/')
) {
return false;
if (typeof v === 'string') {
const trimmed_v = v.trim();
if (
typeof v === 'string' &&
trimmed_v.length > 0 &&
(trimmed_v.startsWith('mapbox://styles/') ||
trimmed_v.startsWith('tile://http') ||
VALIDE_OSM_URLS.some(s => trimmed_v.startsWith(s)))
) {
return false;
}
}
return t('is expected to be a Mapbox URL');
return t(
'is expected to be a Mapbox/OSM URL (eg. mapbox://styles/...) or a tile server URL (eg. tile://http...)',
);
}
@@ -29,6 +29,11 @@ describe('validateMapboxStylesUrl', () => {
'mapbox://styles/foobar/clp2dr5r4008a01pcg4ad45m8',
),
).toEqual(false);
expect(
validateMapboxStylesUrl(
'tile://https://c.tile.openstreetmap.org/{z}/{x}/{y}.png',
),
).toEqual(false);
});
[
@@ -40,7 +45,7 @@ describe('validateMapboxStylesUrl', () => {
].forEach(value => {
it(`should not validate ${value}`, () => {
expect(validateMapboxStylesUrl(value)).toEqual(
'is expected to be a Mapbox URL',
'is expected to be a Mapbox/OSM URL (eg. mapbox://styles/...) or a tile server URL (eg. tile://http...)',
);
});
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -42,4 +42,8 @@ export default styled(WorldMapComponent)`
background-color: ${({ theme }) => theme.colors.grayscale.light5};
}
}
.hoverinfo {
background-color: ${({ theme }) => theme.colorBgElevated};
color: ${({ theme }) => theme.colorTextSecondary};
}
`;
@@ -24,10 +24,17 @@
"lib"
],
"dependencies": {
"@deck.gl/aggregation-layers": "^9.0.38",
"@deck.gl/core": "^9.0.37",
"@deck.gl/layers": "^9.0.38",
"@deck.gl/react": "^9.1.4",
"@deck.gl/aggregation-layers": "^9.1.13",
"@deck.gl/core": "^9.1.13",
"@deck.gl/geo-layers": "^9.1.13",
"@deck.gl/layers": "^9.1.13",
"@deck.gl/react": "^9.1.13",
"@luma.gl/constants": "^9.1.9",
"@luma.gl/core": "^9.1.9",
"@luma.gl/engine": "^9.1.9",
"@luma.gl/shadertools": "^9.1.9",
"@luma.gl/webgl": "^9.1.9",
"@mapbox/tiny-sdf": "^2.0.6",
"@mapbox/geojson-extent": "^1.0.1",
"@math.gl/web-mercator": "^4.1.0",
"@types/d3-array": "^2.0.0",
@@ -39,6 +39,12 @@ import { JsonObject, JsonValue, styled, usePrevious } from '@superset-ui/core';
import Tooltip, { TooltipProps } from './components/Tooltip';
import 'mapbox-gl/dist/mapbox-gl.css';
import { Viewport } from './utils/fitViewport';
import {
MAPBOX_LAYER_PREFIX,
OSM_LAYER_KEYWORDS,
TILE_LAYER_PREFIX,
buildTileLayer,
} from './utils';
const TICK = 250; // milliseconds
@@ -102,6 +108,20 @@ export const DeckGLContainer = memo(
);
const layers = useCallback(() => {
if (
(props.mapStyle?.startsWith(TILE_LAYER_PREFIX) ||
OSM_LAYER_KEYWORDS.some(tilek => props.mapStyle?.includes(tilek))) &&
props.layers.some(
l => typeof l !== 'function' && l?.id === 'tile-layer',
) === false
) {
props.layers.unshift(
buildTileLayer(
(props.mapStyle ?? '').replace(TILE_LAYER_PREFIX, ''),
'tile-layer',
),
);
}
// Support for layer factory
if (props.layers.some(l => typeof l === 'function')) {
return props.layers.map(l =>
@@ -110,7 +130,7 @@ export const DeckGLContainer = memo(
}
return props.layers as Layer[];
}, [props.layers]);
}, [props.layers, props.mapStyle]);
const { children = null, height, width } = props;
@@ -134,11 +154,13 @@ export const DeckGLContainer = memo(
glContextRef.current = context.gl;
}}
>
<StaticMap
preserveDrawingBuffer
mapStyle={props.mapStyle || 'light'}
mapboxApiAccessToken={props.mapboxApiAccessToken}
/>
{props.mapStyle?.startsWith(MAPBOX_LAYER_PREFIX) && (
<StaticMap
preserveDrawingBuffer
mapStyle={props.mapStyle || 'light'}
mapboxApiAccessToken={props.mapboxApiAccessToken}
/>
)}
</DeckGL>
{children}
</div>
@@ -48,6 +48,30 @@ const sequentialSchemeRegistry = getSequentialSchemeRegistry();
export const DEFAULT_DECKGL_COLOR = { r: 158, g: 158, b: 158, a: 1 };
let deckglTiles: string[][];
export const DEFAULT_DECKGL_TILES = [
['https://tile.openstreetmap.org/{z}/{x}/{y}.png', 'Streets (OSM)'],
['https://tile.osm.ch/osm-swiss-style/{z}/{x}/{y}.png', 'Topography (OSM)'],
['mapbox://styles/mapbox/streets-v9', 'Streets (Mapbox)'],
['mapbox://styles/mapbox/dark-v9', 'Dark (Mapbox)'],
['mapbox://styles/mapbox/light-v9', 'Light (Mapbox)'],
['mapbox://styles/mapbox/satellite-streets-v9', 'Satellite Streets (Mapbox)'],
['mapbox://styles/mapbox/satellite-v9', 'Satellite (Mapbox)'],
['mapbox://styles/mapbox/outdoors-v9', 'Outdoors (Mapbox)'],
];
const getDeckGLTiles = () => {
if (!deckglTiles) {
const appContainer = document.getElementById('app');
const { common } = JSON.parse(
appContainer?.getAttribute('data-bootstrap') || '{}',
);
deckglTiles = common?.deckgl_tiles ?? DEFAULT_DECKGL_TILES;
}
return deckglTiles;
};
const DEFAULT_VIEWPORT = {
longitude: 6.85236157047845,
latitude: 31.222656842808707,
@@ -395,17 +419,11 @@ export const mapboxStyle = {
renderTrigger: true,
freeForm: true,
validators: [validateMapboxStylesUrl],
choices: [
['mapbox://styles/mapbox/streets-v9', t('Streets')],
['mapbox://styles/mapbox/dark-v9', t('Dark')],
['mapbox://styles/mapbox/light-v9', t('Light')],
['mapbox://styles/mapbox/satellite-streets-v9', t('Satellite Streets')],
['mapbox://styles/mapbox/satellite-v9', t('Satellite')],
['mapbox://styles/mapbox/outdoors-v9', t('Outdoors')],
],
default: 'mapbox://styles/mapbox/light-v9',
choices: getDeckGLTiles(),
default: getDeckGLTiles()[0][0],
description: t(
'Base layer map style. See Mapbox documentation: %s',
'Mapbox base layer map style (see Mapbox documentation: %s) or tile server URL.',
'https://docs.mapbox.com/help/glossary/style-url/',
),
},
@@ -26,11 +26,17 @@ import {
SequentialScheme,
} from '@superset-ui/core';
import { Color } from '@deck.gl/core';
import { GeoBoundingBox, TileLayer } from '@deck.gl/geo-layers';
import { BitmapLayer, PathLayer } from '@deck.gl/layers';
import { hexToRGB } from './utils/colors';
import { ColorBreakpointType } from './types';
const DEFAULT_NUM_BUCKETS = 10;
export const MAPBOX_LAYER_PREFIX = 'mapbox://';
export const TILE_LAYER_PREFIX = 'tile://';
export const OSM_LAYER_KEYWORDS = ['openstreetmap', 'osm'];
export type Buckets = {
break_points: string[];
num_buckets: string;
@@ -215,3 +221,42 @@ export function getColorBreakpointsBuckets(
return buckets;
}
export function buildTileLayer(url: string, id: string) {
interface TileLayerProps {
id: string;
data: string;
minZoom: number;
maxZoom: number;
tileSize: number;
renderSubLayers: (props: any) => (BitmapLayer | PathLayer)[];
}
interface RenderSubLayerProps {
tile: {
bbox: GeoBoundingBox;
};
data: any;
}
return new TileLayer({
data: url,
id,
minZoom: 0,
maxZoom: 19,
tileSize: 256,
renderSubLayers: (props: RenderSubLayerProps): BitmapLayer[] => {
const { west, north, east, south } = props.tile.bbox as GeoBoundingBox;
// Ajouter une BitmapLayer
const bitmapLayer = new BitmapLayer(props, {
data: undefined,
image: props.data,
bounds: [west, south, east, north],
});
return [bitmapLayer];
},
} as TileLayerProps);
}
@@ -209,12 +209,18 @@ export default function EchartsTimeseries({
}),
);
groupBy.forEach((dimension, i) => {
const val = labelMap[seriesName][i];
const dimensionValues = labelMap[seriesName] ?? [];
// Skip the metric values at the beginning and get the actual dimension value
// If we have multiple metrics, they come first, then the dimension values
const metricsCount = dimensionValues.length - groupBy.length;
const val = dimensionValues[metricsCount + i];
drillByFilters.push({
col: dimension,
op: '==',
val,
formattedVal: formatSeriesName(values[i], {
formattedVal: formatSeriesName(val, {
timeFormatter: getTimeFormatter(formData.dateFormat),
numberFormatter: getNumberFormatter(formData.numberFormat),
coltype: coltypeMapping?.[getColumnLabel(dimension)],
+3 -1
View File
@@ -22,6 +22,8 @@
set -e
export NODE_NO_WARNINGS=1
for file in $( find ../superset/translations/** -name '*.po' );
do
extension=${file##*.}
@@ -29,7 +31,7 @@ do
if [ $extension == "po" ]
then
echo "po2json --domain superset --format jed1.x $file $filename.json"
po2json --domain superset --format jed1.x $file $filename.json
po2json --domain superset --format jed1.x --fuzzy $file $filename.json
prettier --write $filename.json
fi
done
@@ -926,7 +926,7 @@ const SqlEditor: FC<Props> = ({
css={css`
margin-bottom: ${theme.sizeUnit * 2}px;
padding-top: ${theme.sizeUnit * 4}px;
.antd5-alert-action {
.ant-alert-action {
align-self: center;
}
`}
@@ -353,7 +353,7 @@ test('Sends the correct db when changing the database', async () => {
await waitFor(() =>
expect(props.onDbChange).toHaveBeenCalledWith(
expect.objectContaining({
id: `mysql-test-mysql-2`,
value: 2,
database_name: 'test-mysql',
backend: 'mysql',
}),
@@ -319,11 +319,14 @@ export function DatabaseSelector({
value: { label: string; value: number },
database: DatabaseValue,
) {
setCurrentDb(database);
// the database id is actually stored in the value property; the ID is used
// for the DOM, so it can't be an integer
const databaseWithId = { ...database, id: database.value };
setCurrentDb(databaseWithId);
setCurrentCatalog(undefined);
setCurrentSchema(undefined);
if (onDbChange) {
onDbChange(database);
onDbChange(databaseWithId);
}
if (onCatalogChange) {
onCatalogChange(undefined);
@@ -717,7 +717,9 @@ class DatasourceEditor extends PureComponent {
}
onDatasourceTypeChange(datasourceType) {
this.setState({ datasourceType });
// Call onChange after setting datasourceType to ensure
// SQL is cleared when switching to a physical dataset
this.setState({ datasourceType }, this.onChange);
}
setColumns(obj) {
@@ -22,6 +22,7 @@ import {
screen,
waitFor,
userEvent,
cleanup,
} from 'spec/helpers/testing-library';
import mockDatasource from 'spec/fixtures/mockDatasource';
import { isFeatureEnabled } from '@superset-ui/core';
@@ -244,4 +245,36 @@ describe('DatasourceEditor Source Tab', () => {
expect(physicalRadioBtn).toBeDisabled();
expect(virtualRadioBtn).toBeDisabled();
});
it('calls onChange with empty SQL when switching to physical dataset', async () => {
// Clean previous render
cleanup();
props.onChange.mockClear();
await asyncRender({
...props,
datasource: {
...props.datasource,
table_name: 'Vehicle Sales +',
datasourceType: 'virtual',
sql: 'SELECT * FROM users',
},
});
// Enable edit mode
const getLockBtn = screen.getByRole('img', { name: /lock/i });
await userEvent.click(getLockBtn);
// Switch to physical dataset
const physicalRadioBtn = screen.getByRole('radio', {
name: /physical \(table or view\)/i,
});
await userEvent.click(physicalRadioBtn);
// Assert that the latest onChange call has empty SQL
expect(props.onChange).toHaveBeenCalled();
const updatedDatasource = props.onChange.mock.calls[0];
expect(updatedDatasource[0].sql).toBe('');
});
});
@@ -39,3 +39,10 @@ export interface TagType {
css?: SerializedStyles;
closable?: boolean;
}
export enum TagTypeEnum {
Custom = 1,
Type = 2,
Owner = 3,
FavoritedBy = 4,
}
@@ -47,6 +47,15 @@ export const TagsList = ({
onDelete,
maxTags,
}: TagsListProps) => {
const sortedTags = useMemo(
() =>
tags.toSorted((a, b) => {
const nameA = a.name ?? '';
const nameB = b.name ?? '';
return nameA.localeCompare(nameB);
}),
[tags],
);
const [tempMaxTags, setTempMaxTags] = useState<number | undefined>(maxTags);
const handleDelete = (index: number) => {
@@ -58,21 +67,23 @@ export const TagsList = ({
const collapse = () => setTempMaxTags(maxTags);
const tagsIsLong: boolean | null = useMemo(
() => (tempMaxTags ? tags.length > tempMaxTags : null),
[tags.length, tempMaxTags],
() => (tempMaxTags ? sortedTags.length > tempMaxTags : null),
[sortedTags.length, tempMaxTags],
);
const extraTags: number | null = useMemo(
() =>
typeof tempMaxTags === 'number' ? tags.length - tempMaxTags + 1 : null,
[tagsIsLong, tags.length, tempMaxTags],
typeof tempMaxTags === 'number'
? sortedTags.length - tempMaxTags + 1
: null,
[tagsIsLong, sortedTags.length, tempMaxTags],
);
return (
<TagsDiv className="tag-list">
{tagsIsLong && typeof tempMaxTags === 'number' ? (
<>
{tags.slice(0, tempMaxTags - 1).map((tag: TagType, index) => (
{sortedTags.slice(0, tempMaxTags - 1).map((tag: TagType, index) => (
<Tag
id={tag.id}
key={tag.id}
@@ -82,17 +93,17 @@ export const TagsList = ({
editable={editable}
/>
))}
{tags.length > tempMaxTags ? (
{sortedTags.length > tempMaxTags ? (
<Tag
name={`+${extraTags}...`}
onClick={expand}
toolTipTitle={tags.map(t => t.name).join(', ')}
toolTipTitle={sortedTags.map(t => t.name).join(', ')}
/>
) : null}
</>
) : (
<>
{tags.map((tag: TagType, index) => (
{sortedTags.map((tag: TagType, index) => (
<Tag
id={tag.id}
key={tag.id}
@@ -103,7 +114,7 @@ export const TagsList = ({
/>
))}
{maxTags ? (
tags.length > maxTags ? (
sortedTags.length > maxTags ? (
<Tag name="..." onClick={collapse} />
) : null
) : null}
+5 -2
View File
@@ -17,7 +17,6 @@
* under the License.
*/
import { DEFAULT_D3_FORMAT, DEFAULT_D3_TIME_FORMAT } from '@superset-ui/core';
import { BootstrapData, CommonBootstrapData } from './types/bootstrapTypes';
export const DATETIME_WITH_TIME_ZONE = 'YYYY-MM-DD HH:mm:ssZ';
@@ -141,7 +140,11 @@ export const DEFAULT_COMMON_BOOTSTRAP_DATA: CommonBootstrapData = {
},
extra_categorical_color_schemes: [],
extra_sequential_color_schemes: [],
theme: {},
theme: {
default: {},
dark: {},
settings: {},
},
menu_data: {
menu: [],
brand: {
@@ -294,6 +294,7 @@ export function saveDashboardRequest(data, id, saveType) {
owners,
roles,
slug,
tags,
} = data;
const hasId = item => item.id !== undefined;
@@ -315,6 +316,9 @@ export function saveDashboardRequest(data, id, saveType) {
? undefined
: ensureIsArray(roles).map(r => (hasId(r) ? r.id : r)),
slug: slug || null,
tags: !isFeatureEnabled(FeatureFlag.TaggingSystem)
? undefined
: ensureIsArray(tags || []).map(r => (hasId(r) ? r.id : r)),
metadata: {
...data.metadata,
color_namespace: getColorNamespace(data.metadata?.color_namespace),
@@ -445,6 +449,7 @@ export function saveDashboardRequest(data, id, saveType) {
slug: cleanedData.slug,
owners: cleanedData.owners,
roles: cleanedData.roles,
tags: cleanedData.tags || [],
json_metadata: safeStringify({
...(cleanedData?.metadata || {}),
default_filters: safeStringify(serializedFilters),
@@ -23,6 +23,11 @@ import {
within,
screen,
} from 'spec/helpers/testing-library';
import { FeatureFlag } from '@superset-ui/core';
import {
OPEN_FILTER_BAR_WIDTH,
CLOSED_FILTER_BAR_WIDTH,
} from 'src/dashboard/constants';
import DashboardBuilder from 'src/dashboard/components/DashboardBuilder/DashboardBuilder';
import useStoredSidebarWidth from 'src/components/ResizableSidebar/useStoredSidebarWidth';
import {
@@ -268,6 +273,46 @@ describe('DashboardBuilder', () => {
expect(filterbar).toHaveStyleRule('width', `${expectedValue}px`);
});
it('filter panel state when featureflag is true', () => {
window.featureFlags = {
[FeatureFlag.FilterBarClosedByDefault]: true,
};
const setter = jest.fn();
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
CLOSED_FILTER_BAR_WIDTH,
setter,
]);
const { getByTestId } = setup({
dashboardInfo: {
...mockState.dashboardInfo,
dash_edit_perm: true,
},
});
const filterbar = getByTestId('dashboard-filters-panel');
expect(filterbar).toHaveStyleRule('width', `${CLOSED_FILTER_BAR_WIDTH}px`);
});
it('filter panel state when featureflag is false', () => {
window.featureFlags = {
[FeatureFlag.FilterBarClosedByDefault]: false,
};
const setter = jest.fn();
(useStoredSidebarWidth as jest.Mock).mockImplementation(() => [
OPEN_FILTER_BAR_WIDTH,
setter,
]);
const { getByTestId } = setup({
dashboardInfo: {
...mockState.dashboardInfo,
dash_edit_perm: true,
},
});
const filterbar = getByTestId('dashboard-filters-panel');
expect(filterbar).toHaveStyleRule('width', `${OPEN_FILTER_BAR_WIDTH}px`);
});
it('should not render the filter bar when nativeFiltersEnabled is false', () => {
jest.spyOn(useNativeFiltersModule, 'useNativeFilters').mockReturnValue({
showDashboard: true,
@@ -21,6 +21,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { RootState } from 'src/dashboard/types';
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
import {
useFilters,
useNativeFiltersDataMask,
@@ -71,6 +72,8 @@ export const useNativeFilters = () => {
useEffect(() => {
if (
(isFeatureEnabled(FeatureFlag.FilterBarClosedByDefault) &&
expandFilters === null) ||
expandFilters === false ||
(filterValues.length === 0 && nativeFiltersEnabled)
) {
@@ -53,6 +53,7 @@ import {
DASHBOARD_POSITION_DATA_LIMIT,
DASHBOARD_HEADER_ID,
} from 'src/dashboard/util/constants';
import { TagTypeEnum } from 'src/components/Tag/TagType';
import setPeriodicRunner, {
stopPeriodicRender,
} from 'src/dashboard/util/setPeriodicRunner';
@@ -415,6 +416,9 @@ const Header = () => {
owners: dashboardInfo.owners,
roles: dashboardInfo.roles,
slug,
tags: (dashboardInfo.tags || []).filter(
item => item.type === TagTypeEnum.Custom || !item.type,
),
metadata: {
...dashboardInfo?.metadata,
color_namespace: currentColorNamespace,
@@ -459,6 +463,7 @@ const Header = () => {
dashboardInfo.metadata,
dashboardInfo.owners,
dashboardInfo.roles,
dashboardInfo.tags,
dashboardTitle,
layout,
refreshFrequency,
@@ -525,6 +530,7 @@ const Header = () => {
certification_details: updates.certificationDetails,
owners: updates.owners,
roles: updates.roles,
tags: updates.tags,
});
boundActionCreators.setUnsavedChanges(true);
boundActionCreators.dashboardTitleChanged(updates.title);
@@ -383,7 +383,7 @@ const PropertiesModal = ({
currentJsonMetadata = jsonStringify(metadata);
const moreOnSubmitProps: { roles?: Roles } = {};
const moreOnSubmitProps: { roles?: Roles; tags?: TagType[] } = {};
const morePutProps: {
roles?: number[];
tags?: (string | number | undefined)[];
@@ -393,6 +393,7 @@ const PropertiesModal = ({
morePutProps.roles = (roles || []).map(r => r.id);
}
if (isFeatureEnabled(FeatureFlag.TaggingSystem)) {
moreOnSubmitProps.tags = tags;
morePutProps.tags = tags.map(tag => tag.id);
}
const onSubmitProps = {
@@ -600,6 +601,10 @@ const PropertiesModal = ({
setTags(parsedTags);
};
const handleClearTags = () => {
setTags([]);
};
return (
<Modal
show={show}
@@ -725,6 +730,7 @@ const PropertiesModal = ({
value={tagsAsSelectValues}
options={loadTags}
onChange={handleChangeTags}
onClear={handleClearTags}
allowClear
/>
</FormItem>
@@ -59,6 +59,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & {
formData: object;
width: number;
height: number;
exportPivotExcel?: (arg0: string) => void;
};
const annotationsLoading = t('Annotation layers are still loading.');
@@ -167,6 +168,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
formData,
width,
height,
exportPivotExcel = () => ({}),
},
ref,
) => {
@@ -344,6 +346,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
formData={formData}
exploreUrl={exploreUrl}
crossFiltersEnabled={isCrossFiltersEnabled}
exportPivotExcel={exportPivotExcel}
/>
)}
</>
@@ -33,6 +33,7 @@ const createProps = (viz_type = VizType.Sunburst) =>
exportFullCSV: jest.fn(),
exportXLSX: jest.fn(),
exportFullXLSX: jest.fn(),
exportPivotExcel: jest.fn(),
forceRefresh: jest.fn(),
handleToggleFullSize: jest.fn(),
toggleExpandSlice: jest.fn(),
@@ -254,6 +255,20 @@ test('Should not show export full Excel if report is not table', async () => {
expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument();
});
test('Should export to pivoted Excel if report is pivot table', async () => {
const props = createProps(VizType.PivotTable);
renderWrapper(props);
openMenu();
expect(props.exportPivotExcel).toHaveBeenCalledTimes(0);
userEvent.hover(screen.getByText('Download'));
userEvent.click(await screen.findByText('Export to Pivoted Excel'));
expect(props.exportPivotExcel).toHaveBeenCalledTimes(1);
expect(props.exportPivotExcel).toHaveBeenCalledWith(
'.pvtTable',
props.slice.slice_name,
);
});
test('Should "Show chart description"', () => {
const props = createProps();
renderWrapper(props);
@@ -128,6 +128,7 @@ export interface SliceHeaderControlsProps {
exportXLSX?: (sliceId: number) => void;
exportFullXLSX?: (sliceId: number) => void;
handleToggleFullSize: () => void;
exportPivotExcel?: (tableSelector: string, sliceName: string) => void;
addDangerToast: (message: string) => void;
addSuccessToast: (message: string) => void;
@@ -255,6 +256,10 @@ const SliceHeaderControls = (
});
break;
}
case MenuKeys.ExportPivotXlsx: {
props.exportPivotExcel?.('.pvtTable', props.slice.slice_name);
break;
}
case MenuKeys.CrossFilterScoping: {
openScopingModal();
break;
@@ -468,6 +473,15 @@ const SliceHeaderControls = (
{t('Export to Excel')}
</Menu.Item>
{isPivotTable && (
<Menu.Item
key={MenuKeys.ExportPivotXlsx}
icon={<Icons.FileOutlined css={dropdownIconsStyles} />}
>
{t('Export to Pivoted Excel')}
</Menu.Item>
)}
{isFeatureEnabled(FeatureFlag.AllowFullCsvExport) &&
props.supersetCanCSV &&
isTable && (
@@ -37,6 +37,7 @@ import {
import { postFormData } from 'src/explore/exploreUtils/formData';
import { URL_PARAMS } from 'src/constants';
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
import SliceHeader from '../SliceHeader';
import MissingChart from '../MissingChart';
@@ -213,11 +214,15 @@ const Chart = props => {
const getHeaderHeight = useCallback(() => {
if (headerRef.current) {
const computedStyle = getComputedStyle(
const computedMarginBottom = getComputedStyle(
headerRef.current,
).getPropertyValue('margin-bottom');
const marginBottom = parseInt(computedStyle, 10) || 0;
return headerRef.current.offsetHeight + marginBottom;
const marginBottom = parseInt(computedMarginBottom, 10) || 0;
const computedHeight = getComputedStyle(
headerRef.current,
).getPropertyValue('height');
const height = parseInt(computedHeight, 10) || DEFAULT_HEADER_HEIGHT;
return height + marginBottom;
}
return DEFAULT_HEADER_HEIGHT;
}, [headerRef]);
@@ -471,6 +476,7 @@ const Chart = props => {
formData={formData}
width={width}
height={getHeaderHeight()}
exportPivotExcel={exportPivotExcel}
/>
{/*
@@ -27,6 +27,7 @@ export default function DownloadAsImage({
text,
logEvent,
dashboardTitle,
...props
}: {
text: string;
dashboardTitle: string;
@@ -50,6 +51,7 @@ export default function DownloadAsImage({
onClick={e => {
onDownloadImage(e.domEvent);
}}
{...props}
>
{text}
</Menu.Item>
@@ -27,6 +27,7 @@ export default function DownloadAsPdf({
text,
logEvent,
dashboardTitle,
...props
}: {
text: string;
dashboardTitle: string;
@@ -50,6 +51,7 @@ export default function DownloadAsPdf({
onClick={e => {
onDownloadPdf(e.domEvent);
}}
{...props}
>
{text}
</Menu.Item>
+1
View File
@@ -289,4 +289,5 @@ export enum MenuKeys {
ToggleFullscreen = 'toggle_fullscreen',
ManageEmbedded = 'manage_embedded',
ManageEmailReports = 'manage_email_reports',
ExportPivotXlsx = 'export_pivot_xlsx',
}
@@ -46,6 +46,7 @@ import { Icons } from '@superset-ui/core/components/Icons';
import Chart, { Slice } from 'src/types/Chart';
import withToasts from 'src/components/MessageToasts/withToasts';
import { type TagType } from 'src/components';
import { TagTypeEnum } from 'src/components/Tag/TagType';
import { loadTags } from 'src/components/Tag/utils';
export type PropertiesModalProps = {
@@ -125,7 +126,7 @@ function PropertiesModal({
);
if (isFeatureEnabled(FeatureFlag.TaggingSystem)) {
const customTags = chart.tags?.filter(
(tag: TagType) => tag.type === 1,
(tag: TagType) => tag.type === TagTypeEnum.Custom,
);
setTags(customTags);
}
@@ -479,3 +479,30 @@ test('should show missing dataset state', () => {
),
).toBeVisible();
});
test('should show forbidden dataset state', () => {
// @ts-ignore
delete window.location;
// @ts-ignore
window.location = { search: '?slice_id=152' };
const error = {
error_type: 'TABLE_SECURITY_ACCESS_ERROR',
statusText: 'FORBIDDEN',
message: 'You do not have access to the following tables: blocked_table',
extra: {
datasource: 152,
datasource_name: 'forbidden dataset',
},
};
const props = createProps({
datasource: {
...fallbackExploreInitialData.dataset,
extra: {
error,
},
},
});
render(<DatasourceControl {...props} />, { useRedux: true, useRouter: true });
expect(screen.getByText(error.message)).toBeInTheDocument();
expect(screen.getByText(error.statusText)).toBeVisible();
});
@@ -50,6 +50,7 @@ import {
userHasPermission,
isUserAdmin,
} from 'src/dashboard/util/permissionUtils';
import { ErrorMessageWithStackTrace } from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModalFooter';
import ViewQuery from 'src/explore/components/controls/ViewQuery';
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
@@ -281,7 +282,17 @@ class DatasourceControl extends PureComponent {
showSaveDatasetModal,
} = this.state;
const { datasource, onChange, theme } = this.props;
const isMissingDatasource = !datasource?.id;
let extra;
if (datasource?.extra) {
if (typeof datasource.extra === 'string') {
try {
extra = JSON.parse(datasource.extra);
} catch {} // eslint-disable-line no-empty
} else {
extra = datasource.extra; // eslint-disable-line prefer-destructuring
}
}
const isMissingDatasource = !datasource?.id || Boolean(extra?.error);
let isMissingParams = false;
if (isMissingDatasource) {
const datasourceId = getUrlParam(URL_PARAMS.datasourceId);
@@ -386,20 +397,10 @@ class DatasourceControl extends PureComponent {
const { health_check_message: healthCheckMessage } = datasource;
let extra;
if (datasource?.extra) {
if (typeof datasource.extra === 'string') {
try {
extra = JSON.parse(datasource.extra);
} catch {} // eslint-disable-line no-empty
} else {
extra = datasource.extra; // eslint-disable-line prefer-destructuring
}
}
const titleText = isMissingDatasource
? t('Missing dataset')
: getDatasourceTitle(datasource);
const titleText =
isMissingDatasource && !datasource.name
? t('Missing dataset')
: getDatasourceTitle(datasource);
const tooltip = titleText;
@@ -452,31 +453,44 @@ class DatasourceControl extends PureComponent {
)}
{isMissingDatasource && !isMissingParams && (
<div className="error-alert">
<ErrorAlert
type="warning"
errorType={t('Missing dataset')}
descriptionPre={false}
descriptionDetailsCollapsed={false}
descriptionDetails={
<>
<p>
{t(
'The dataset linked to this chart may have been deleted.',
)}
</p>
<p>
<Button
buttonStyle="warning"
onClick={() =>
this.handleMenuItemClick({ key: CHANGE_DATASET })
}
>
{t('Swap dataset')}
</Button>
</p>
</>
}
/>
{extra?.error ? (
<div className="error-alert">
<ErrorMessageWithStackTrace
title={extra.error.statusText || extra.error.message}
subtitle={
extra.error.statusText ? extra.error.message : undefined
}
error={extra.error}
source="explore"
/>
</div>
) : (
<ErrorAlert
type="warning"
errorType={t('Missing dataset')}
descriptionPre={false}
descriptionDetailsCollapsed={false}
descriptionDetails={
<>
<p>
{t(
'The dataset linked to this chart may have been deleted.',
)}
</p>
<p>
<Button
buttonStyle="warning"
onClick={() =>
this.handleMenuItemClick({ key: CHANGE_DATASET })
}
>
{t('Swap dataset')}
</Button>
</p>
</>
}
/>
)}
</div>
)}
{showEditDatasourceModal && (
@@ -90,7 +90,10 @@ const DndFilterSelect = (props: DndFilterSelectProps) => {
let extra = {};
if (datasource?.extra) {
try {
extra = JSON.parse(datasource.extra);
extra =
typeof datasource.extra === 'string'
? JSON.parse(datasource.extra)
: datasource.extra;
} catch {} // eslint-disable-line no-empty
}
return extra;
@@ -43,6 +43,7 @@ import {
LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED,
LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS,
} from 'src/logger/LogUtils';
import exportPivotExcel from 'src/utils/downloadAsPivotExcel';
import ViewQueryModal from '../controls/ViewQueryModal';
import EmbedCodeContent from '../EmbedCodeContent';
import DashboardsSubMenu from './DashboardsSubMenu';
@@ -67,6 +68,7 @@ const MENU_KEYS = {
DELETE_REPORT: 'delete_report',
VIEW_QUERY: 'view_query',
RUN_IN_SQL_LAB: 'run_in_sql_lab',
EXPORT_TO_PIVOT_XLSX: 'export_to_pivot_xlsx',
};
const VIZ_TYPES_PIVOTABLE = [VizType.PivotTable];
@@ -248,6 +250,16 @@ export const useExploreAdditionalActionsMenu = (
}),
);
break;
case MENU_KEYS.EXPORT_TO_PIVOT_XLSX:
exportPivotExcel('.pvtTable', slice?.slice_name ?? t('pivoted_xlsx'));
setIsDropdownVisible(false);
dispatch(
logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS, {
chartId: slice?.slice_id,
chartName: slice?.slice_name,
}),
);
break;
case MENU_KEYS.DOWNLOAD_AS_IMAGE:
downloadAsImage(
'.panel-body .chart-container',
@@ -365,6 +377,13 @@ export const useExploreAdditionalActionsMenu = (
>
{t('Export to Excel')}
</Menu.Item>
<Menu.Item
key={MENU_KEYS.EXPORT_TO_PIVOT_XLSX}
icon={<Icons.FileOutlined />}
disabled={!canDownloadCSV}
>
{t('Export to Pivoted Excel')}
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu title={t('Share')} key={MENU_KEYS.SHARE_SUBMENU}>
<Menu.Item key={MENU_KEYS.COPY_PERMALINK}>
+2 -2
View File
@@ -159,8 +159,8 @@ export const fallbackExploreInitialData: ExplorePageInitialData = {
verbose_map: {},
main_dttm_col: '',
owners: [],
datasource_name: 'missing_datasource',
name: 'missing_datasource',
datasource_name: '',
name: '',
description: null,
},
slice: null,
+1 -1
View File
@@ -69,7 +69,7 @@ export type Datasource = Dataset & {
catalog?: string | null;
schema?: string;
is_sqllab_view?: boolean;
extra?: string;
extra?: string | object;
};
export interface ExplorePageInitialData {
@@ -191,4 +191,32 @@ describe('ExtraOptions Component', () => {
fireEvent.change(input, { target: { value: '1000' } });
expect(onExtraInputChange).toHaveBeenCalled();
});
it('renders the collaps tab correctly and resets to default tab after closing', () => {
const { rerender } = renderComponent();
const sqlLabTab = screen.getByRole('tab', {
name: /SQL Lab./i,
});
expect(sqlLabTab).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(sqlLabTab);
expect(sqlLabTab).toHaveAttribute('aria-expanded', 'true');
const customDb = {
...defaultDb,
expose_in_sqllab: false,
};
rerender(
<ExtraOptions
db={customDb as unknown as DatabaseObject}
onInputChange={onInputChange}
onTextChange={onTextChange}
onEditorChange={onEditorChange}
onExtraInputChange={onExtraInputChange}
onExtraEditorChange={onExtraEditorChange}
extraExtension={undefined}
/>,
);
expect(sqlLabTab).toHaveAttribute('aria-expanded', 'false');
});
});
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChangeEvent, EventHandler } from 'react';
import { ChangeEvent, EventHandler, useState, useEffect } from 'react';
import cx from 'classnames';
import {
t,
@@ -88,12 +88,21 @@ const ExtraOptions = ({
const isAllowRunAsyncDisabled = isFeatureEnabled(
FeatureFlag.ForceSqlLabRunAsync,
);
const [activeKey, setActiveKey] = useState<string[] | undefined>();
useEffect(() => {
if (!expandableModalIsOpen && activeKey !== undefined) {
setActiveKey(undefined);
}
}, [expandableModalIsOpen, activeKey]);
return (
<Collapse
expandIconPosition="end"
accordion
modalMode
activeKey={activeKey}
onChange={key => setActiveKey(key)}
items={[
{
key: 'sql-lab',
@@ -132,7 +132,7 @@ function GroupListModal({
value: role.id,
label: role.name,
}))}
getPopupContainer={trigger => trigger.closest('.antd5-modal-content')}
getPopupContainer={trigger => trigger.closest('.ant-modal-content')}
/>
</FormItem>
<FormItem name="users" label={t('Users')}>
@@ -187,7 +187,7 @@ const RightMenu = ({
const {
theme: themeEditorTheme,
setTheme,
changeThemeMode,
setThemeMode,
themeMode,
} = useThemeContext();
const dropdownItems: MenuObjectProps[] = [
@@ -501,10 +501,7 @@ const RightMenu = ({
)}
{isFeatureEnabled(FeatureFlag.ThemeEnableDarkThemeSwitch) && (
<span>
<ThemeSelect
changeThemeMode={changeThemeMode}
themeMode={themeMode}
/>
<ThemeSelect setThemeMode={setThemeMode} themeMode={themeMode} />
</span>
)}
+6 -1
View File
@@ -19,6 +19,7 @@
import { JsonObject, SupersetClient } from '@superset-ui/core';
import rison from 'rison';
import { TagType } from 'src/components';
import { TagTypeEnum } from 'src/components/Tag/TagType';
export const OBJECT_TYPES_VALUES = Object.freeze([
'dashboard',
@@ -93,7 +94,11 @@ export function fetchTags(
endpoint: `/api/v1/${objectType}/${objectId}`,
})
.then(({ json }) =>
callback(json.result.tags.filter((tag: TagType) => tag.type === 1)),
callback(
json.result.tags.filter(
(tag: TagType) => tag.type === TagTypeEnum.Custom,
),
),
)
.catch(response => error(response));
}
@@ -54,7 +54,7 @@ const Wrapper = styled.div`
justify-content: space-between;
width: 100%;
.antd5-input-number {
.ant-input-number {
min-width: 80px;
position: relative;
}
@@ -196,10 +196,10 @@ function ActionLogList() {
}: any) => (
<Typography.Text
css={css`
.antd5-typography-copy {
.ant-typography-copy {
visibility: hidden;
}
&:hover .antd5-typography-copy {
&:hover .ant-typography-copy {
visibility: visible;
}
`}
@@ -224,10 +224,10 @@ function ActionLogList() {
}: any) => (
<Typography.Text
css={css`
.antd5-typography-copy {
.ant-typography-copy {
visibility: hidden;
}
&:hover .antd5-typography-copy {
&:hover .ant-typography-copy {
visibility: visible;
}
`}
@@ -31,6 +31,7 @@ import getFormDataWithExtraFilters from 'src/dashboard/util/charts/getFormDataWi
import { URL_PARAMS } from 'src/constants';
import { JsonObject, VizType } from '@superset-ui/core';
import { useUnsavedChangesPrompt } from 'src/hooks/useUnsavedChangesPrompt';
import { getParsedExploreURLParams } from 'src/explore/exploreUtils/getParsedExploreURLParams';
import ChartPage from '.';
jest.mock('src/hooks/useUnsavedChangesPrompt', () => ({
@@ -49,6 +50,9 @@ jest.mock(
),
);
jest.mock('src/dashboard/util/charts/getFormDataWithExtraFilters');
jest.mock('src/explore/exploreUtils/getParsedExploreURLParams', () => ({
getParsedExploreURLParams: jest.fn(),
}));
describe('ChartPage', () => {
beforeEach(() => {
@@ -89,6 +93,91 @@ describe('ChartPage', () => {
);
});
test('displays the dataset name and error when it is prohibited', async () => {
const chartApiRoute = `glob:*/api/v1/chart/*`;
const exploreApiRoute = 'glob:*/api/v1/explore/*';
const expectedDatasourceName = 'failed datasource name';
(getParsedExploreURLParams as jest.Mock).mockReturnValue(
new Map([['datasource_id', 1]]),
);
fetchMock.get(exploreApiRoute, () => {
class Extra {
datasource = 123;
datasource_name = expectedDatasourceName;
}
class SupersetSecurityError {
message = 'You do not have a permission to the table';
extra = new Extra();
}
throw new SupersetSecurityError();
});
fetchMock.get(chartApiRoute, 200);
const { getByTestId } = render(<ChartPage />, {
useRouter: true,
useRedux: true,
useDnd: true,
});
await waitFor(
() =>
expect(getByTestId('mock-explore-chart-panel')).toHaveTextContent(
JSON.stringify({ datasource_name: expectedDatasourceName }).slice(
1,
-1,
),
),
{
timeout: 5000,
},
);
expect(fetchMock.calls(chartApiRoute).length).toEqual(0);
expect(fetchMock.calls(exploreApiRoute).length).toBeGreaterThanOrEqual(1);
});
test('fetches the chart api when explore metadata is prohibited and access from the chart link', async () => {
const expectedChartId = 7;
const expectedChartName = 'Unauthorized dataset owned chart name';
(getParsedExploreURLParams as jest.Mock).mockReturnValue(
new Map([['slice_id', expectedChartId]]),
);
const chartApiRoute = `glob:*/api/v1/chart/${expectedChartId}`;
const exploreApiRoute = 'glob:*/api/v1/explore/*';
fetchMock.get(exploreApiRoute, () => {
class Extra {
datasource = 123;
}
class SupersetSecurityError {
message = 'You do not have a permission to the table';
extra = new Extra();
}
throw new SupersetSecurityError();
});
fetchMock.get(chartApiRoute, {
result: {
id: expectedChartId,
slice_name: expectedChartName,
url: 'chartid',
},
});
const { getByTestId, getByText } = render(<ChartPage />, {
useRouter: true,
useRedux: true,
useDnd: true,
});
await waitFor(() => expect(fetchMock.calls(chartApiRoute).length).toBe(1), {
timeout: 5000,
});
expect(fetchMock.calls(exploreApiRoute).length).toBeGreaterThanOrEqual(1);
expect(getByTestId('mock-explore-chart-panel')).toBeInTheDocument();
expect(getByTestId('mock-explore-chart-panel')).toHaveTextContent(
JSON.stringify({ datasource: 123 }).slice(1, -1),
);
expect(getByText(expectedChartName)).toBeInTheDocument();
});
describe('with dashboardContextFormData', () => {
const dashboardPageId = 'mockPageId';
+78 -34
View File
@@ -41,6 +41,7 @@ import { ExploreResponsePayload, SaveActionType } from 'src/explore/types';
import { fallbackExploreInitialData } from 'src/explore/fixtures';
import { getItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { getFormDataWithDashboardContext } from 'src/explore/controlUtils/getFormDataWithDashboardContext';
import type Chart from 'src/types/Chart';
const isValidResult = (rv: JsonObject): boolean =>
rv?.result?.form_data && rv?.result?.dataset;
@@ -49,41 +50,31 @@ const hasDatasetId = (rv: JsonObject): boolean =>
isDefined(rv?.result?.dataset?.id);
const fetchExploreData = async (exploreUrlParams: URLSearchParams) => {
try {
const rv = await makeApi<{}, ExploreResponsePayload>({
method: 'GET',
endpoint: 'api/v1/explore/',
})(exploreUrlParams);
if (isValidResult(rv)) {
if (hasDatasetId(rv)) {
return rv;
}
// Since there's no dataset id but the API responded with a valid payload,
// we assume the dataset was deleted, so we preserve some values from previous
// state so if the user decide to swap the datasource, the chart config remains
fallbackExploreInitialData.form_data = {
...rv.result.form_data,
...fallbackExploreInitialData.form_data,
};
if (rv.result?.slice) {
fallbackExploreInitialData.slice = rv.result.slice;
}
const rv = await makeApi<{}, ExploreResponsePayload>({
method: 'GET',
endpoint: 'api/v1/explore/',
})(exploreUrlParams);
if (isValidResult(rv)) {
if (hasDatasetId(rv)) {
return rv;
}
let message = t('Failed to load chart data');
const responseError = rv?.result?.message;
if (responseError) {
message = `${message}:\n${responseError}`;
// Since there's no dataset id but the API responded with a valid payload,
// we assume the dataset was deleted, so we preserve some values from previous
// state so if the user decide to swap the datasource, the chart config remains
fallbackExploreInitialData.form_data = {
...rv.result.form_data,
...fallbackExploreInitialData.form_data,
};
if (rv.result?.slice) {
fallbackExploreInitialData.slice = rv.result.slice;
}
throw new Error(message);
} catch (err) {
// todo: encapsulate the error handler
const clientError = await getClientErrorObject(err);
throw new Error(
clientError.message ||
clientError.error ||
t('Failed to load chart data.'),
);
}
let message = t('Failed to load chart data');
const responseError = rv?.result?.message;
if (responseError) {
message = `${message}:\n${responseError}`;
}
throw new Error(message);
};
const getDashboardPageContext = (pageId?: string | null) => {
@@ -166,9 +157,62 @@ export default function ExplorePage() {
}),
);
})
.catch(err => {
.catch(err => Promise.all([getClientErrorObject(err), err]))
.then(resolved => {
const [clientError, err] = resolved || [];
if (!err) {
return Promise.resolve();
}
const errorMesage =
clientError?.message ||
clientError?.error ||
t('Failed to load chart data.');
dispatch(addDangerToast(errorMesage));
if (err.extra?.datasource) {
const exploreData = {
...fallbackExploreInitialData,
dataset: {
...fallbackExploreInitialData.dataset,
id: err.extra?.datasource,
name: err.extra?.datasource_name,
extra: {
error: err,
},
},
};
const chartId = exploreUrlParams.get('slice_id');
return (
chartId
? makeApi<void, { result: Chart }>({
method: 'GET',
endpoint: `api/v1/chart/${chartId}`,
})()
: Promise.reject()
)
.then(
({ result: { id, url, owners, form_data: _, ...data } }) => {
const slice = {
...data,
datasource: err.extra?.datasource_name,
slice_id: id,
slice_url: url,
owners: owners?.map(({ id }) => id),
};
dispatch(
hydrateExplore({
...exploreData,
slice,
}),
);
},
)
.catch(() => {
dispatch(hydrateExplore(exploreData));
});
}
dispatch(hydrateExplore(fallbackExploreInitialData));
dispatch(addDangerToast(err.message));
return Promise.resolve();
})
.finally(() => {
setIsLoaded(true);
@@ -71,6 +71,7 @@ import PropertiesModal from 'src/explore/components/PropertiesModal';
import Chart from 'src/types/Chart';
import { Icons } from '@superset-ui/core/components/Icons';
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
import { TagTypeEnum } from 'src/components/Tag/TagType';
import { loadTags } from 'src/components/Tag/utils';
import ChartCard from 'src/features/charts/ChartCard';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
@@ -417,7 +418,8 @@ function ChartList(props: ChartListProps) {
<TagsList
tags={tags.filter((tag: TagType) =>
tag.type
? tag.type === 1 || tag.type === 'TagTypes.custom'
? tag.type === TagTypeEnum.Custom ||
tag.type === 'TagTypes.custom'
: true,
)}
maxTags={3}
@@ -67,6 +67,7 @@ import {
Dashboard as CRUDDashboard,
QueryObjectColumns,
} from 'src/views/CRUD/types';
import { TagTypeEnum } from 'src/components/Tag/TagType';
import { loadTags } from 'src/components/Tag/utils';
import DashboardCard from 'src/features/dashboards/DashboardCard';
import { DashboardStatus } from 'src/features/dashboards/types';
@@ -376,7 +377,8 @@ function DashboardList(props: DashboardListProps) {
<TagsList
tags={tags.filter(
(tag: TagType) =>
tag.type === 'TagTypes.custom' || tag.type === 1,
tag.type === 'TagTypes.custom' ||
tag.type === TagTypeEnum.Custom,
)}
maxTags={3}
/>
+1 -1
View File
@@ -62,7 +62,7 @@ const StyledCard = styled(Card)`
margin-top: ${theme.marginXL}px;
color: ${theme.colorBgContainer};
background: ${theme.colorBgBase};
.antd5-form-item-label label {
.ant-form-item-label label {
color: ${theme.colorPrimary};
}
`}
@@ -45,7 +45,7 @@ const StyledCard = styled(Card)`
width: 50%;
margin-top: ${theme.marginXL}px;
background: ${theme.colorBgBase};
.antd5-form-item-label label {
.ant-form-item-label label {
color: ${theme.colorPrimary};
}
`}
@@ -58,6 +58,7 @@ import handleResourceExport from 'src/utils/export';
import SubMenu, { ButtonProps, SubMenuProps } from 'src/features/home/SubMenu';
import { commonMenuData } from 'src/features/home/commonMenuData';
import { QueryObjectColumns, SavedQueryObject } from 'src/views/CRUD/types';
import { TagTypeEnum } from 'src/components/Tag/TagType';
import { loadTags } from 'src/components/Tag/utils';
import { Icons } from '@superset-ui/core/components/Icons';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
@@ -393,7 +394,11 @@ function SavedQueryList({
},
}: any) => (
// Only show custom type tags
<TagsList tags={tags.filter((tag: TagType) => tag.type === 1)} />
<TagsList
tags={tags.filter(
(tag: TagType) => tag.type === TagTypeEnum.Custom,
)}
/>
),
Header: t('Tags'),
accessor: 'tags',
@@ -54,11 +54,11 @@ const DescriptionsContainer = styled.div`
const StyledLayout = styled.div`
${({ theme }) => css`
.antd5-row {
.ant-row {
margin: 0px ${theme.sizeUnit * 3}px ${theme.sizeUnit * 6}px
${theme.sizeUnit * 3}px;
}
&& .menu > .antd5-menu {
&& .menu > .ant-menu {
padding: 0px;
}
&& .nav-right {
+57 -41
View File
@@ -19,7 +19,13 @@
import { setConfig as setHotLoaderConfig } from 'react-hot-loader';
import dayjs from 'dayjs';
// eslint-disable-next-line no-restricted-imports
import { configure, makeApi, initFeatureFlags } from '@superset-ui/core';
import {
configure,
makeApi,
initFeatureFlags,
SupersetClient,
LanguagePack,
} from '@superset-ui/core';
import setupClient from './setup/setupClient';
import setupColors from './setup/setupColors';
import setupFormatters from './setup/setupFormatters';
@@ -27,57 +33,67 @@ import setupDashboardComponents from './setup/setupDashboardComponents';
import { User } from './types/bootstrapTypes';
import getBootstrapData, { applicationRoot } from './utils/getBootstrapData';
configure();
// Set hot reloader config
if (process.env.WEBPACK_MODE === 'development') {
setHotLoaderConfig({ logLevel: 'debug', trackTailUpdates: false });
}
// eslint-disable-next-line import/no-mutable-exports
// Grab initial bootstrap data
const bootstrapData = getBootstrapData();
// Configure translation
if (typeof window !== 'undefined') {
configure({ languagePack: bootstrapData.common.language_pack });
dayjs.locale(bootstrapData.common.locale);
} else {
configure();
}
// Configure feature flags
initFeatureFlags(bootstrapData.common.feature_flags);
// Setup SupersetClient
// Setup SupersetClient early so we can fetch language pack
setupClient({ appRoot: applicationRoot() });
setupColors(
bootstrapData.common.extra_categorical_color_schemes,
bootstrapData.common.extra_sequential_color_schemes,
);
// Load language pack before anything else
(async () => {
const lang = bootstrapData.common.locale || 'en';
if (lang !== 'en') {
try {
// Second call to configure to set the language pack
const { json } = await SupersetClient.get({
endpoint: `/superset/language_pack/${lang}/`,
});
configure({ languagePack: json as LanguagePack });
dayjs.locale(lang);
} catch (err) {
console.warn(
'Failed to fetch language pack, falling back to default.',
err,
);
configure();
dayjs.locale('en');
}
}
// Setup number formatters
setupFormatters(
bootstrapData.common.d3_format,
bootstrapData.common.d3_time_format,
);
// Continue with rest of setup
initFeatureFlags(bootstrapData.common.feature_flags);
setupDashboardComponents();
setupColors(
bootstrapData.common.extra_categorical_color_schemes,
bootstrapData.common.extra_sequential_color_schemes,
);
const getMe = makeApi<void, User>({
method: 'GET',
endpoint: '/api/v1/me/',
});
setupFormatters(
bootstrapData.common.d3_format,
bootstrapData.common.d3_time_format,
);
/**
* When you re-open the window, we check if you are still logged in.
* If your session expired or you signed out, we'll redirect to login.
* If you aren't logged in in the first place (!isActive), then we shouldn't do this.
*/
if (bootstrapData.user?.isActive) {
document.addEventListener('visibilitychange', () => {
// we only care about the tab becoming visible, not vice versa
if (document.visibilityState !== 'visible') return;
setupDashboardComponents();
getMe().catch(() => {
// ignore error, SupersetClient will redirect to login on a 401
});
const getMe = makeApi<void, User>({
method: 'GET',
endpoint: '/api/v1/me/',
});
}
if (bootstrapData.user?.isActive) {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
getMe().catch(() => {
// SupersetClient will redirect to login on 401
});
}
});
}
})();
@@ -0,0 +1,596 @@
/**
* 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 {
Theme,
AnyThemeConfig,
ThemeStorage,
ThemeControllerOptions,
themeObject as supersetThemeObject,
} from '@superset-ui/core';
import { SupersetTheme, ThemeMode } from '@superset-ui/core/theme/types';
import {
getAntdConfig,
normalizeThemeConfig,
} from '@superset-ui/core/theme/utils';
import type {
BootstrapThemeData,
BootstrapThemeDataConfig,
SerializableThemeSettings,
} from 'src/types/bootstrapTypes';
import getBootstrapData from 'src/utils/getBootstrapData';
const DEFAULT_THEME_SETTINGS = {
enforced: false,
allowSwitching: true,
allowOSPreference: true,
} as const;
const STORAGE_KEYS = {
THEME_MODE: 'superset-theme-mode',
} as const;
const MEDIA_QUERY_DARK_SCHEME = '(prefers-color-scheme: dark)';
export class LocalStorageAdapter implements ThemeStorage {
getItem(key: string): string | null {
try {
return localStorage.getItem(key);
} catch (error) {
console.warn('Failed to read from localStorage:', error);
return null;
}
}
setItem(key: string, value: string): void {
try {
localStorage.setItem(key, value);
} catch (error) {
console.warn('Failed to write to localStorage:', error);
}
}
removeItem(key: string): void {
try {
localStorage.removeItem(key);
} catch (error) {
console.warn('Failed to remove from localStorage:', error);
}
}
}
export class ThemeController {
private themeObject: Theme;
private storage: ThemeStorage;
private modeStorageKey: string;
private defaultTheme: AnyThemeConfig;
private darkTheme: AnyThemeConfig | null;
private themeSettings: SerializableThemeSettings;
private systemMode: ThemeMode.DARK | ThemeMode.DEFAULT;
private currentMode: ThemeMode;
private readonly hasBootstrapThemes: boolean;
private onChangeCallbacks: Set<(theme: Theme) => void> = new Set();
private mediaQuery: MediaQueryList;
constructor(options: ThemeControllerOptions = {}) {
const {
storage = new LocalStorageAdapter(),
modeStorageKey = STORAGE_KEYS.THEME_MODE,
themeObject = supersetThemeObject,
defaultTheme = (supersetThemeObject.theme as AnyThemeConfig) ?? {},
onChange = null,
} = options;
this.storage = storage;
this.modeStorageKey = modeStorageKey;
this.themeObject = themeObject;
// Initialize bootstrap data and themes
const {
bootstrapDefaultTheme,
bootstrapDarkTheme,
bootstrapThemeSettings,
hasBootstrapThemes,
}: BootstrapThemeData = this.loadBootstrapData();
this.hasBootstrapThemes = hasBootstrapThemes;
this.themeSettings = bootstrapThemeSettings || {};
// Set themes based on bootstrap data availability
if (this.hasBootstrapThemes) {
this.darkTheme = bootstrapDarkTheme || bootstrapDefaultTheme || null;
this.defaultTheme =
bootstrapDefaultTheme || bootstrapDarkTheme || defaultTheme;
} else {
this.darkTheme = null;
this.defaultTheme = defaultTheme;
}
// Initialize system theme detection
this.systemMode = ThemeController.getSystemPreferredMode();
// Only initialize media query listener if OS preference is allowed
if (this.shouldInitializeMediaQueryListener())
this.initializeMediaQueryListener();
// Initialize theme and mode
this.currentMode = this.determineInitialMode();
const initialTheme =
this.getThemeForMode(this.currentMode) || this.defaultTheme;
// Setup change callback
if (onChange) this.onChangeCallbacks.add(onChange);
// Apply initial theme and persist mode
this.applyTheme(initialTheme);
this.persistMode();
}
// Public Methods
/**
* Cleans up listeners and references. Should be called when the controller is no longer needed.
*/
public destroy(): void {
if (this.mediaQuery)
this.mediaQuery.removeEventListener(
'change',
this.handleSystemThemeChange,
);
this.onChangeCallbacks.clear();
}
/**
* Check if the user can update the theme.
*/
public canSetTheme(): boolean {
return !this.themeSettings?.enforced;
}
/**
* Check if the user can update the theme mode.
*/
public canSetMode(): boolean {
return this.isModeUpdatable();
}
/**
* Returns the current theme object.
*/
public getTheme(): Theme {
return this.themeObject;
}
/**
* Returns the current theme mode.
*/
public getCurrentMode(): ThemeMode {
return this.currentMode;
}
/**
* Sets new theme.
* @param theme - The new theme to apply
* @throws {Error} If the user does not have permission to update the theme
*/
public setTheme(theme: AnyThemeConfig): void {
this.validateThemeUpdatePermission();
const normalizedTheme = this.normalizeTheme(theme);
this.currentMode = this.determineInitialMode();
this.updateTheme(normalizedTheme);
}
/**
* Sets the theme mode (light, dark, or system).
* @param mode - The new theme mode to apply
* @throws {Error} If the user does not have permission to update the theme mode
*/
public setThemeMode(mode: ThemeMode): void {
this.validateModeUpdatePermission(mode);
if (this.currentMode === mode) return;
const theme: AnyThemeConfig | null = this.getThemeForMode(mode);
if (!theme) {
console.warn(`Theme for mode ${mode} not found, falling back to default`);
this.fallbackToDefaultMode();
return;
}
this.currentMode = mode;
this.updateTheme(theme);
}
/**
* Resets the theme to the default theme.
*/
public resetTheme(): void {
this.currentMode = ThemeMode.DEFAULT;
const defaultTheme: AnyThemeConfig =
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme;
this.updateTheme(defaultTheme);
}
/**
* Handles system theme changes with error recovery.
*/
private handleSystemThemeChange = (): void => {
try {
const newSystemMode: ThemeMode.DEFAULT | ThemeMode.DARK =
ThemeController.getSystemPreferredMode();
// Update systemMode regardless of current mode
const oldSystemMode: ThemeMode.DEFAULT | ThemeMode.DARK = this.systemMode;
this.systemMode = newSystemMode;
// Only update theme if currently in SYSTEM mode and the preference changed
if (
this.currentMode === ThemeMode.SYSTEM &&
oldSystemMode !== newSystemMode
) {
const newTheme: AnyThemeConfig | null = this.getThemeForMode(
ThemeMode.SYSTEM,
);
if (newTheme) this.updateTheme(newTheme);
}
} catch (error) {
console.error('Failed to handle system theme change:', error);
}
};
/**
* Updates the theme.
* @param theme - The new theme to apply
*/
private updateTheme(theme?: AnyThemeConfig): void {
try {
// If no config provided, use current mode to get 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) {
console.error('Failed to update theme:', error);
this.fallbackToDefaultMode();
}
}
/**
* Fallback to default mode with error recovery.
*/
private fallbackToDefaultMode(): void {
this.currentMode = ThemeMode.DEFAULT;
// Get the default theme which will have the correct algorithm
const defaultTheme: AnyThemeConfig =
this.getThemeForMode(ThemeMode.DEFAULT) || this.defaultTheme;
this.applyTheme(defaultTheme);
this.persistMode();
this.notifyListeners();
}
/**
* Registers a callback to be called when the theme changes.
* @param callback - The callback to be called on theme change
* @returns A function to unsubscribe from the theme change events
*/
public onChange(callback: (theme: Theme) => void): () => void {
this.onChangeCallbacks.add(callback);
return () => this.onChangeCallbacks.delete(callback);
}
// Private Helper Methods
/**
* Determines whether the MediaQueryList listener for system theme changes should be initialized.
* This checks if OS preference detection is enabled in the theme settings.
* @returns {boolean} True if the media query listener should be initialized, false otherwise
*/
private shouldInitializeMediaQueryListener(): boolean {
const { allowOSPreference = DEFAULT_THEME_SETTINGS.allowOSPreference } =
this.themeSettings || {};
return allowOSPreference === true;
}
/**
* Initializes media query listeners if OS preference is allowed
*/
private initializeMediaQueryListener(): void {
try {
this.mediaQuery = window.matchMedia(MEDIA_QUERY_DARK_SCHEME);
this.mediaQuery.addEventListener('change', this.handleSystemThemeChange);
} catch (error) {
console.warn('Failed to initialize media query listener:', error);
}
}
/**
* Loads and validates bootstrap theme data.
*/
private loadBootstrapData(): BootstrapThemeData {
const {
common: { theme = {} as BootstrapThemeDataConfig },
} = getBootstrapData();
const {
default: defaultTheme,
dark: darkTheme,
settings: themeSettings,
} = theme;
const hasValidDefault: boolean = this.isNonEmptyObject(defaultTheme);
const hasValidDark: boolean = this.isNonEmptyObject(darkTheme);
const hasValidSettings: boolean = this.isNonEmptyObject(themeSettings);
return {
bootstrapDefaultTheme: hasValidDefault ? defaultTheme : null,
bootstrapDarkTheme: hasValidDark ? darkTheme : null,
bootstrapThemeSettings: hasValidSettings ? themeSettings : null,
hasBootstrapThemes: hasValidDefault || hasValidDark,
};
}
/**
* Checks if an object is non-empty (has at least one property).
*/
private isNonEmptyObject(
obj: Record<string, any> | undefined | null,
): boolean {
return Boolean(
obj && typeof obj === 'object' && Object.keys(obj).length > 0,
);
}
/**
* Determines if mode updates are allowed.
*/
private isModeUpdatable(): boolean {
const {
enforced = DEFAULT_THEME_SETTINGS.enforced,
allowSwitching = DEFAULT_THEME_SETTINGS.allowSwitching,
} = this.themeSettings || {};
return !enforced && allowSwitching;
}
/**
* Normalizes the theme configuration to ensure it has a valid algorithm.
* @param theme - The theme configuration to normalize
* @returns An object with normalized mode and algorithm.
*/
private normalizeTheme(theme: AnyThemeConfig): AnyThemeConfig {
const normalizedTheme = normalizeThemeConfig(theme);
return normalizedTheme;
}
/**
* Returns the appropriate theme configuration for a given mode.
* @param mode - The theme mode to get the configuration for
* @returns The theme configuration for the specified mode or null if not available
*/
private getThemeForMode(mode: ThemeMode): AnyThemeConfig | null {
const { allowOSPreference = DEFAULT_THEME_SETTINGS.allowOSPreference } =
this.themeSettings;
let resolvedMode: ThemeMode = mode;
if (mode === ThemeMode.SYSTEM) {
if (!allowOSPreference) return null;
resolvedMode = ThemeController.getSystemPreferredMode();
}
if (!this.hasBootstrapThemes) {
const baseTheme = this.defaultTheme.token as Partial<SupersetTheme>;
return getAntdConfig(baseTheme, resolvedMode === ThemeMode.DARK);
}
// 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 {
const {
enforced = DEFAULT_THEME_SETTINGS.enforced,
allowOSPreference = DEFAULT_THEME_SETTINGS.allowOSPreference,
allowSwitching = DEFAULT_THEME_SETTINGS.allowSwitching,
} = this.themeSettings;
// Enforced mode always takes precedence
if (enforced) {
this.storage.removeItem(this.modeStorageKey);
return ThemeMode.DEFAULT;
}
// When OS preference is allowed but switching is not
// This means the user MUST follow OS preference and cannot override it
if (allowOSPreference && !allowSwitching) {
// Clear any saved preference since switching is not allowed
this.storage.removeItem(this.modeStorageKey);
return ThemeMode.SYSTEM;
}
// Try to restore saved mode
const savedMode: ThemeMode | null = this.loadSavedMode();
if (savedMode && this.isValidThemeMode(savedMode)) return savedMode;
// Fallback to system preference if allowed and available
if (allowOSPreference && this.getThemeForMode(this.systemMode))
return ThemeMode.SYSTEM;
return ThemeMode.DEFAULT;
}
/**
* Safely loads saved theme mode from storage.
*/
private loadSavedMode(): ThemeMode | null {
try {
const stored: string | null = this.storage.getItem(this.modeStorageKey);
if (stored && Object.values(ThemeMode).includes(stored as ThemeMode))
return stored as ThemeMode;
return null;
} catch (error) {
console.warn('Failed to load saved theme mode:', error);
return null;
}
}
/**
* Validates if a theme mode is valid and supported.
* This checks if the mode is one of the known ThemeMode values.
* @param mode - The theme mode to validate
* @returns {boolean} True if the mode is valid, false otherwise
*/
private isValidThemeMode(mode: ThemeMode): boolean {
if (!Object.values(ThemeMode).includes(mode)) return false;
// Validate that we have the required theme data for the mode
switch (mode) {
case ThemeMode.DARK:
return !!(this.darkTheme || this.defaultTheme);
case ThemeMode.DEFAULT:
return !!this.defaultTheme;
case ThemeMode.SYSTEM:
return this.themeSettings?.allowOSPreference !== false;
default:
return true;
}
}
/**
* Validates permission to update theme.
*/
private validateThemeUpdatePermission(): void {
if (!this.canSetTheme())
throw new Error('User does not have permission to update the theme');
}
/**
* Validates permission to update mode.
* @param newMode - The new mode to validate
* @throws {Error} If the user does not have permission to update the theme mode
* @throws {Error} If the new mode is SYSTEM and OS preference is not allowed
*/
private validateModeUpdatePermission(newMode: ThemeMode): void {
const {
allowOSPreference = DEFAULT_THEME_SETTINGS.allowOSPreference,
allowSwitching = DEFAULT_THEME_SETTINGS.allowSwitching,
} = this.themeSettings;
// If OS preference is allowed but switching is not,
// don't allow any mode changes
if (allowOSPreference && !allowSwitching)
throw new Error(
'Theme mode changes are not allowed when OS preference is enforced',
);
// Check if user can set a new theme mode
if (!this.canSetMode())
throw new Error('User does not have permission to update the theme mode');
// Check if user has permissions to set OS preference as a theme mode
if (newMode === ThemeMode.SYSTEM && !allowOSPreference)
throw new Error('System theme mode is not allowed');
}
/**
* Applies the current theme configuration.
* This method sets the theme on the themeObject and applies it to Theme.
* It also handles any errors that may occur during the application of the theme.
* @param theme - The theme configuration to apply
*/
private applyTheme(theme: AnyThemeConfig): void {
try {
const normalizedConfig = normalizeThemeConfig(theme);
this.themeObject.setConfig(normalizedConfig);
} catch (error) {
console.error('Failed to apply theme:', error);
this.fallbackToDefaultMode();
}
}
/**
* Persists the current theme mode to storage.
*/
private persistMode(): void {
try {
this.storage.setItem(this.modeStorageKey, this.currentMode);
} catch (error) {
console.warn('Failed to persist theme mode:', error);
}
}
/**
* Notifies all registered listeners about theme changes.
*/
private notifyListeners(): void {
this.onChangeCallbacks.forEach(callback => {
try {
callback(this.themeObject);
} catch (error) {
console.error('Error in theme change callback:', error);
}
});
}
/**
* Gets the system's preferred theme mode.
* @returns {ThemeMode.DARK | ThemeMode.DEFAULT} The system's preferred theme mode
*/
static getSystemPreferredMode(): ThemeMode.DARK | ThemeMode.DEFAULT {
try {
return window.matchMedia(MEDIA_QUERY_DARK_SCHEME).matches
? ThemeMode.DARK
: ThemeMode.DEFAULT;
} catch (error) {
console.warn('Failed to detect system theme preference:', error);
return ThemeMode.DEFAULT;
}
}
}
@@ -1,246 +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 {
Theme,
AnyThemeConfig,
ThemeStorage,
ThemeControllerOptions,
themeObject,
} from '@superset-ui/core';
import { ThemeMode } from '@superset-ui/core/theme/types';
export class LocalStorageAdapter implements ThemeStorage {
getItem(key: string): string | null {
return localStorage.getItem(key);
}
setItem(key: string, value: string): void {
localStorage.setItem(key, value);
}
removeItem(key: string): void {
localStorage.removeItem(key);
}
}
export class ThemeController {
private themeObject: Theme;
private storage: ThemeStorage;
private storageKey: string;
private modeStorageKey: string;
private defaultTheme: AnyThemeConfig;
private systemMode: ThemeMode.DARK | ThemeMode.LIGHT;
private currentMode: ThemeMode;
private customizations: AnyThemeConfig = {};
private onChangeCallbacks: Set<(theme: Theme) => void> = new Set();
private canUpdateThemeFn: () => boolean;
private canUpdateModeFn: () => boolean;
private mediaQuery: MediaQueryList;
constructor(options: ThemeControllerOptions = { themeObject }) {
this.storage = options.storage || new LocalStorageAdapter();
this.storageKey = options.storageKey || 'superset-theme';
this.modeStorageKey = options.modeStorageKey || `${this.storageKey}-mode`;
this.defaultTheme = options.defaultTheme || {};
this.themeObject = options.themeObject;
// Load customizations from storage
const savedThemeJson = this.storage.getItem(this.storageKey);
if (savedThemeJson) {
try {
this.customizations = JSON.parse(savedThemeJson);
} catch (e) {
console.error('Failed to parse saved theme:', e);
this.storage.removeItem(this.storageKey);
}
}
// Determine initial mode
this.systemMode = ThemeController.getSystemMode();
const savedMode = this.storage.getItem(this.modeStorageKey) as ThemeMode;
this.currentMode = savedMode || ThemeMode.SYSTEM;
// Apply the initial theme and mode
this.applyTheme();
if (options.onChange) {
this.onChangeCallbacks.add(options.onChange);
}
this.canUpdateThemeFn = options.canUpdateTheme || (() => true);
this.canUpdateModeFn = options.canUpdateMode || (() => true);
// Listen for system theme changes to enable dynamic `SYSTEM` mode
this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.mediaQuery.addEventListener('change', this.handleSystemThemeChange);
}
/**
* Cleans up listeners. Should be called when the controller is no longer needed.
*/
public destroy(): void {
this.mediaQuery.removeEventListener('change', this.handleSystemThemeChange);
}
public canUpdateTheme(): boolean {
return this.canUpdateThemeFn();
}
public canUpdateMode(): boolean {
return this.canUpdateModeFn();
}
public getTheme(): Theme {
return this.themeObject;
}
public getCurrentMode(): ThemeMode {
return this.currentMode;
}
/**
* Sets new theme customizations (e.g., from a JSON editor).
* This method updates the theme's appearance but preserves the current mode.
*/
public setTheme(newCustomizations: AnyThemeConfig): void {
if (!this.canUpdateTheme()) {
throw new Error('User does not have permission to update the theme');
}
this.customizations = newCustomizations;
if (!newCustomizations.algorithm) {
this.currentMode = ThemeMode.LIGHT;
}
if (newCustomizations?.algorithm) {
this.currentMode = newCustomizations.algorithm as ThemeMode;
}
this.applyTheme();
this.persist();
this.notifyListeners();
}
/**
* Changes the theme mode (light, dark, or system).
* This is for the mode switch.
*/
public changeThemeMode(newMode: ThemeMode): void {
if (!this.canUpdateMode()) {
throw new Error('User does not have permission to update the theme mode');
}
if (this.currentMode === newMode) return;
this.currentMode = newMode;
this.applyTheme();
this.persist();
this.notifyListeners();
}
public resetTheme(): void {
this.customizations = this.defaultTheme;
this.applyTheme();
this.persist();
this.notifyListeners();
}
public onChange(callback: (theme: Theme) => void): () => void {
this.onChangeCallbacks.add(callback);
return () => {
this.onChangeCallbacks.delete(callback);
};
}
private handleSystemThemeChange = (): void => {
const newSystemMode = ThemeController.getSystemMode();
if (this.systemMode === newSystemMode) return;
this.systemMode = newSystemMode;
// If the current mode is SYSTEM, we need to re-apply the theme
if (this.currentMode === ThemeMode.SYSTEM) {
this.applyTheme();
this.notifyListeners();
}
};
/**
* Centralized method to apply the current customizations and mode.
*/
private applyTheme(): void {
const newConfig = { ...this.customizations };
switch (this.currentMode) {
case ThemeMode.DARK:
newConfig.algorithm = 'dark';
break;
case ThemeMode.LIGHT:
newConfig.algorithm = 'default';
break;
case ThemeMode.SYSTEM:
newConfig.algorithm =
this.systemMode === ThemeMode.DARK ? 'dark' : 'default';
break;
case ThemeMode.COMPACT:
newConfig.algorithm = 'compact';
break;
default:
newConfig.algorithm = 'default';
break;
}
this.themeObject.setConfig(newConfig);
}
private persist(): void {
this.storage.setItem(this.modeStorageKey, this.currentMode);
const { algorithm, ...persistedCustomizations } = this.customizations;
this.storage.setItem(
this.storageKey,
JSON.stringify(persistedCustomizations),
);
}
private notifyListeners(): void {
this.onChangeCallbacks.forEach(callback => {
try {
callback(this.themeObject);
} catch (e) {
console.error('Error in theme change callback:', e);
}
});
}
static getSystemMode(): ThemeMode.DARK | ThemeMode.LIGHT {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return isDark ? ThemeMode.DARK : ThemeMode.LIGHT;
}
}
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
createContext,
useCallback,
@@ -62,8 +61,8 @@ export function SupersetThemeProvider({
[themeController],
);
const changeThemeMode = useCallback(
(newMode: ThemeMode) => themeController.changeThemeMode(newMode),
const setThemeMode = useCallback(
(newMode: ThemeMode) => themeController.setThemeMode(newMode),
[themeController],
);
@@ -77,10 +76,10 @@ export function SupersetThemeProvider({
theme: currentTheme,
themeMode: currentThemeMode,
setTheme,
changeThemeMode,
setThemeMode,
resetTheme,
}),
[currentTheme, currentThemeMode, setTheme, changeThemeMode, resetTheme],
[currentTheme, currentThemeMode, setTheme, setThemeMode, resetTheme],
);
return (
@@ -96,9 +95,10 @@ export function SupersetThemeProvider({
* React hook to use the theme context
*/
export function useThemeContext(): ThemeContextType {
const context = useContext(ThemeContext);
if (!context) {
const context: ThemeContextType | null = useContext(ThemeContext);
if (!context)
throw new Error('useThemeContext must be used within a ThemeProvider');
}
return context;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,239 @@
/**
* 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 { ReactNode } from 'react';
import { Theme } from '@superset-ui/core';
import { ThemeContextType, ThemeMode } from '@superset-ui/core/theme/types';
import { act, render, screen } from '@superset-ui/core/spec';
import { renderHook } from '@testing-library/react-hooks';
import { SupersetThemeProvider, useThemeContext } from '../ThemeProvider';
import { ThemeController } from '../ThemeController';
jest.mock('../ThemeController');
const MockThemeController = ThemeController as jest.MockedClass<
typeof ThemeController
>;
// Mock theme objects
const mockTheme = {
SupersetThemeProvider: ({ children }: { children: ReactNode }) => (
<div data-test="superset-theme-provider">{children}</div>
),
theme: {
token: {
colorPrimary: '#1890ff',
colorBgBase: '#ffffff',
},
},
} as unknown as Theme;
const mockDarkTheme = {
SupersetThemeProvider: ({ children }: { children: ReactNode }) => (
<div data-test="superset-dark-theme-provider">{children}</div>
),
theme: {
token: {
colorPrimary: '#1890ff',
colorBgBase: '#000000',
},
},
} as unknown as Theme;
const createWrapper =
(controller: jest.Mocked<ThemeController>) =>
({ children }: { children: ReactNode }) => (
<SupersetThemeProvider themeController={controller}>
{children}
</SupersetThemeProvider>
);
describe('SupersetThemeProvider', () => {
let mockThemeController: jest.Mocked<ThemeController>;
let mockOnChangeCallback: jest.Mock;
beforeEach(() => {
mockOnChangeCallback = jest.fn();
mockThemeController = {
getTheme: jest.fn().mockReturnValue(mockTheme),
getCurrentMode: jest.fn().mockReturnValue(ThemeMode.DEFAULT),
setTheme: jest.fn(),
setThemeMode: jest.fn(),
resetTheme: jest.fn(),
onChange: jest.fn().mockReturnValue(jest.fn()),
canUpdateTheme: jest.fn().mockReturnValue(true),
canUpdateMode: jest.fn().mockReturnValue(true),
destroy: jest.fn(),
} as unknown as jest.Mocked<ThemeController>;
mockThemeController.onChange.mockImplementation(callback => {
mockOnChangeCallback.mockImplementation(callback);
return jest.fn();
});
MockThemeController.mockImplementation(() => mockThemeController);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Provider Initialization', () => {
it('should render children within theme provider wrapper', () => {
render(
<SupersetThemeProvider themeController={mockThemeController}>
<div data-test="test-child">Hello Superset</div>
</SupersetThemeProvider>,
);
expect(screen.getByTestId('superset-theme-provider')).toBeInTheDocument();
expect(screen.getByTestId('test-child')).toBeInTheDocument();
});
it('should initialize theme state from controller', () => {
const wrapper = createWrapper(mockThemeController);
const { result } = renderHook((): ThemeContextType => useThemeContext(), {
wrapper,
});
expect(mockThemeController.getTheme).toHaveBeenCalled();
expect(mockThemeController.getCurrentMode).toHaveBeenCalled();
expect(result.current.theme).toBe(mockTheme);
expect(result.current.themeMode).toBe(ThemeMode.DEFAULT);
});
it('should register onChange listener on mount', () => {
const wrapper = createWrapper(mockThemeController);
renderHook((): ThemeContextType => useThemeContext(), { wrapper });
expect(mockThemeController.onChange).toHaveBeenCalledWith(
expect.any(Function),
);
});
it('should unregister onChange listener on unmount', () => {
const unsubscribeMock = jest.fn();
mockThemeController.onChange.mockReturnValue(unsubscribeMock);
const wrapper = createWrapper(mockThemeController);
const { unmount } = renderHook(
(): ThemeContextType => useThemeContext(),
{ wrapper },
);
unmount();
expect(unsubscribeMock).toHaveBeenCalled();
});
});
describe('Theme State Updates', () => {
it('should update theme state when controller notifies change', () => {
const wrapper = createWrapper(mockThemeController);
const { result } = renderHook((): ThemeContextType => useThemeContext(), {
wrapper,
});
// Initial state
expect(result.current.themeMode).toBe(ThemeMode.DEFAULT);
expect(result.current.theme).toBe(mockTheme);
// Simulate theme change from controller
act(() => {
mockThemeController.getCurrentMode.mockReturnValue(ThemeMode.DARK);
mockOnChangeCallback(mockDarkTheme);
});
// State should update
expect(result.current.themeMode).toBe(ThemeMode.DARK);
expect(result.current.theme).toBe(mockDarkTheme);
});
it('should update both theme and mode when controller changes', () => {
const wrapper = createWrapper(mockThemeController);
const { result } = renderHook((): ThemeContextType => useThemeContext(), {
wrapper,
});
act(() => {
mockThemeController.getCurrentMode.mockReturnValue(ThemeMode.SYSTEM);
mockOnChangeCallback(mockDarkTheme);
});
expect(result.current.themeMode).toBe(ThemeMode.SYSTEM);
expect(result.current.theme).toBe(mockDarkTheme);
});
});
describe('Theme Actions', () => {
it('should call setTheme when invoked', () => {
const wrapper = createWrapper(mockThemeController);
const { result } = renderHook((): ThemeContextType => useThemeContext(), {
wrapper,
});
const customTheme = {
token: {
colorPrimary: '#ff0000',
colorBgBase: '#000000',
},
};
act(() => {
result.current.setTheme(customTheme);
});
expect(mockThemeController.setTheme).toHaveBeenCalledWith(customTheme);
});
it('should call setThemeMode when invoked', () => {
const wrapper = createWrapper(mockThemeController);
const { result } = renderHook((): ThemeContextType => useThemeContext(), {
wrapper,
});
act(() => {
result.current.setThemeMode(ThemeMode.DARK);
});
expect(mockThemeController.setThemeMode).toHaveBeenCalledWith(
ThemeMode.DARK,
);
});
it('should call resetTheme when invoked', () => {
const wrapper = createWrapper(mockThemeController);
const { result } = renderHook((): ThemeContextType => useThemeContext(), {
wrapper,
});
act(() => {
result.current.resetTheme();
});
expect(mockThemeController.resetTheme).toHaveBeenCalled();
});
});
});
+38 -15
View File
@@ -1,17 +1,3 @@
import {
ColorSchemeConfig,
FeatureFlagMap,
JsonObject,
LanguagePack,
Locale,
SequentialSchemeConfig,
} from '@superset-ui/core';
import { FormatLocaleDefinition } from 'd3-format';
import { TimeLocaleDefinition } from 'd3-time-format';
import { isPlainObject } from 'lodash';
import { Languages } from 'src/features/home/LanguagePicker';
import type { FlashMessage } from 'src/components';
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -30,6 +16,24 @@ import type { FlashMessage } from 'src/components';
* specific language governing permissions and limitations
* under the License.
*/
import {
ColorSchemeConfig,
FeatureFlagMap,
JsonObject,
LanguagePack,
Locale,
SequentialSchemeConfig,
} from '@superset-ui/core';
import { FormatLocaleDefinition } from 'd3-format';
import { TimeLocaleDefinition } from 'd3-time-format';
import { isPlainObject } from 'lodash';
import { Languages } from 'src/features/home/LanguagePicker';
import type { FlashMessage } from 'src/components';
import type {
AnyThemeConfig,
SerializableThemeConfig,
} from '@superset-ui/core/theme/types';
export type User = {
createdOn?: string;
email?: string;
@@ -143,6 +147,18 @@ export interface MenuData {
};
}
export interface SerializableThemeSettings {
enforced?: boolean;
allowSwitching?: boolean;
allowOSPreference?: boolean;
}
export interface BootstrapThemeDataConfig {
default: SerializableThemeConfig | {};
dark: SerializableThemeConfig | {};
settings: SerializableThemeSettings | {};
}
export interface CommonBootstrapData {
application_root: string;
static_assets_prefix: string;
@@ -153,7 +169,7 @@ export interface CommonBootstrapData {
language_pack: LanguagePack;
extra_categorical_color_schemes: ColorSchemeConfig[];
extra_sequential_color_schemes: SequentialSchemeConfig[];
theme: JsonObject;
theme: BootstrapThemeDataConfig;
menu_data: MenuData;
d3_format: Partial<FormatLocaleDefinition>;
d3_time_format: Partial<TimeLocaleDefinition>;
@@ -169,6 +185,13 @@ export interface BootstrapData {
requested_query?: JsonObject;
}
export interface BootstrapThemeData {
bootstrapDefaultTheme: AnyThemeConfig | null;
bootstrapDarkTheme: AnyThemeConfig | null;
bootstrapThemeSettings: SerializableThemeSettings | null;
hasBootstrapThemes: boolean;
}
export function isUser(user: any): user is User {
return isPlainObject(user) && 'username' in user;
}
@@ -1,4 +1,3 @@
/* eslint-env node */
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -17,31 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:prettier/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'react', 'prettier'],
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
settings: {
react: {
version: 'detect',
},
},
ignorePatterns: ['build/**/*', '.docusaurus/**/*', 'node_modules/**/*'],
};
import { utils, writeFile } from 'xlsx';
export default function exportPivotExcel(
tableSelector: string,
fileName: string,
) {
const table = document.querySelector(tableSelector);
const workbook = utils.table_to_book(table);
writeFile(workbook, `${fileName}.xlsx`);
}
@@ -18,7 +18,7 @@
*/
import { Route } from 'react-router-dom';
import { getExtensionsRegistry, themeObject } from '@superset-ui/core';
import { getExtensionsRegistry } from '@superset-ui/core';
import { Provider as ReduxProvider } from 'react-redux';
import { QueryParamProvider } from 'use-query-params';
import { DndProvider } from 'react-dnd';
@@ -32,17 +32,14 @@ import { store } from './store';
import '../preamble';
const { common } = getBootstrapData();
if (Object.keys(common?.theme || {}).length > 0) {
themeObject.setConfig(common.theme);
}
const themeController = new ThemeController({ themeObject });
const themeController = new ThemeController();
const extensionsRegistry = getExtensionsRegistry();
export const RootContextProviders: React.FC = ({ children }) => {
const RootContextProviderExtension = extensionsRegistry.get(
'root.context.provider',
);
return (
<SupersetThemeProvider themeController={themeController}>
<ReduxProvider store={store}>
+15 -15
View File
@@ -13,7 +13,7 @@
"express": "~5.1.0",
"http-errors": "~2.0.0",
"jsonwebtoken": "^9.0.2",
"morgan": "~1.10.0",
"morgan": "~1.10.1",
"pug": "~3.0.3"
}
},
@@ -863,15 +863,15 @@
}
},
"node_modules/morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
"on-headers": "~1.1.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -936,9 +936,9 @@
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"engines": {
"node": ">= 0.8"
}
@@ -2045,15 +2045,15 @@
}
},
"morgan": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"requires": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.0.2"
"on-headers": "~1.1.0"
},
"dependencies": {
"debug": {
@@ -2100,9 +2100,9 @@
}
},
"on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="
},
"once": {
"version": "1.4.0",

Some files were not shown because too many files have changed in this diff Show More