Compare commits

..

5 Commits

Author SHA1 Message Date
Maxime Beauchemin
19669a3f91 fix / improve types 2025-06-24 19:14:27 -07:00
Maxime Beauchemin
175b696d68 docs 2025-06-24 13:52:42 -07:00
Maxime Beauchemin
7ace5f1737 adjust the ThemeController 2025-06-24 13:49:26 -07:00
Maxime Beauchemin
312f6c5ed6 move theme loading from bootstrapData in the ThemeController 2025-06-24 13:44:45 -07:00
Maxime Beauchemin
79eddd4807 feat(theming): support user OS-level dark/light mode configuration 2025-06-24 13:24:54 -07:00
536 changed files with 9099 additions and 47409 deletions

View File

@@ -12,10 +12,6 @@ updates:
# not until React >= 18.0.0
- dependency-name: "storybook"
- dependency-name: "@storybook*"
# JSDOM v30 doesn't play well with Jest v30
# Source: https://jestjs.io/blog#known-issues
# GH thread: https://github.com/jsdom/jsdom/issues/3492
- dependency-name: "jest-environment-jsdom"
directory: "/superset-frontend/"
schedule:
interval: "monthly"

View File

@@ -24,12 +24,6 @@ jobs:
submodules: recursive
fetch-depth: 1
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
if: steps.check.outputs.python
uses: ./.github/actions/setup-backend/
@@ -39,20 +33,10 @@ jobs:
run: ./scripts/uv-pip-compile.sh
- name: Check for uncommitted changes
if: steps.check.outputs.python
run: |
echo "Full diff (for logging/debugging):"
git diff
echo "Filtered diff (excluding comments and whitespace):"
filtered_diff=$(git diff -U0 | grep '^[-+]' | grep -vE '^[-+]{3}' | grep -vE '^[-+][[:space:]]*#' | grep -vE '^[-+][[:space:]]*$' || true)
echo "$filtered_diff"
if [[ -n "$filtered_diff" ]]; then
echo
if [[ -n "$(git diff)" ]]; then
echo "ERROR: The pinned dependencies are not up-to-date."
echo "Please run './scripts/uv-pip-compile.sh' and commit the changes."
echo "More info: https://github.com/apache/superset/tree/master/requirements"
exit 1
else
echo "Pinned dependencies are up-to-date."

View File

@@ -54,7 +54,7 @@ jobs:
yarn install --immutable
- name: Cache pre-commit environments
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}

View File

@@ -5,7 +5,7 @@
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
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0

View File

@@ -111,6 +111,7 @@ Here are some of the major database solutions that are supported:
<img src="https://superset.apache.org/img/databases/druid.png" alt="druid" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/firebolt.png" alt="firebolt" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/timescale.png" alt="timescale" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/rockset.png" alt="rockset" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/postgresql.png" alt="postgresql" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/mysql.png" alt="mysql" border="0" width="200" />
<img src="https://superset.apache.org/img/databases/mssql-server.png" alt="mssql-server" border="0" width="200" />

View File

@@ -43,7 +43,7 @@ assists people when migrating to a new version.
- [31198](https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName".
- [29798](https://github.com/apache/superset/pull/29798) Since 3.1.0, the intial schedule for an alert or report was mistakenly offset by the specified timezone's relation to UTC. The initial schedule should now begin at the correct time.
- [30021](https://github.com/apache/superset/pull/30021) The `dev` layer in our Dockerfile no long includes firefox binaries, only Chromium to reduce bloat/docker-build-time.
- [30099](https://github.com/apache/superset/pull/30099) Translations are no longer included in the default docker image builds. If your environment requires translations, you'll want to set the docker build arg `BUILD_TRANSLATIONS=true`.
- [30099](https://github.com/apache/superset/pull/30099) Translations are no longer included in the default docker image builds. If your environment requires translations, you'll want to set the docker build arg `BUILD_TRANSACTION=true`.
- [31262](https://github.com/apache/superset/pull/31262) NOTE: deprecated `pylint` in favor of `ruff` as our only python linter. Only affect development workflows positively (not the release itself). It should cover most important rules, be much faster, but some things linting rules that were enforced before may not be enforce in the exact same way as before.
- [31173](https://github.com/apache/superset/pull/31173) Modified `fetch_csrf_token` to align with HTTP standards, particularly regarding how cookies are handled. If you encounter any issues related to CSRF functionality, please report them as a new issue and reference this PR for context.
- [31413](https://github.com/apache/superset/pull/31413) Enable the DATE_FORMAT_IN_EMAIL_SUBJECT feature flag to allow users to specify a date format for the email subject, which will then be replaced with the actual date.

View File

@@ -71,6 +71,7 @@ are compatible with Superset.
| [Parseable](/docs/configuration/databases#parseable) | `pip install sqlalchemy-parseable` | `parseable://<UserName>:<DBPassword>@<Database Host>/<Stream Name>` |
| [PostgreSQL](/docs/configuration/databases#postgres) | `pip install psycopg2` | `postgresql://<UserName>:<DBPassword>@<Database Host>/<Database Name>` |
| [Presto](/docs/configuration/databases#presto) | `pip install pyhive` | `presto://{username}:{password}@{hostname}:{port}/{database}` |
| [Rockset](/docs/configuration/databases#rockset) | `pip install rockset-sqlalchemy` | `rockset://<api_key>:@<api_server>` |
| [SAP Hana](/docs/configuration/databases#hana) | `pip install hdbcli sqlalchemy-hana` or `pip install apache_superset[hana]` | `hana://{username}:{password}@{host}:{port}` |
| [SingleStore](/docs/configuration/databases#singlestore) | `pip install sqlalchemy-singlestoredb` | `singlestoredb://{username}:{password}@{host}:{port}/{database}` |
| [StarRocks](/docs/configuration/databases#starrocks) | `pip install starrocks` | `starrocks://<User>:<Password>@<Host>:<Port>/<Catalog>.<Database>` |
@@ -1173,6 +1174,25 @@ The expected connection string is formatted as follows:
risingwave://root@{hostname}:{port}/{database}?sslmode=disable
```
#### Rockset
The connection string for Rockset is:
```
rockset://{api key}:@{api server}
```
Get your API key from the [Rockset console](https://console.rockset.com/apikeys).
Find your API server from the [API reference](https://rockset.com/docs/rest-api/#introduction). Omit the `https://` portion of the URL.
To target to a specific virtual instance, use this URI format:
```
rockset://{api key}:@{api server}/{VI ID}
```
For more complete instructions, we recommend the [Rockset documentation](https://docs.rockset.com/apache-superset/).
#### Snowflake
##### Install Snowflake Driver

View File

@@ -51,7 +51,6 @@ if desired. Most endpoints hit are logged as
well as key events like query start and end in SQL Lab.
To setup StatsD logging, its a matter of configuring the logger in your `superset_config.py`.
If not already present, you need to ensure that the `statsd`-package is installed in Superset's python environment.
```python
from superset.stats_logger import StatsdStatsLogger

View File

@@ -8,11 +8,11 @@ version: 1
## CORS
To configure CORS, or cross-origin resource sharing, the following dependency must be installed:
:::note
In Superset versions prior to `5.x` you have to install to install `flask-cors` with `pip install flask-cors` to enable CORS support.
:::
```python
pip install apache_superset[cors]
```
The following keys in `superset_config.py` can be specified to configure CORS:

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 Chromium headless browser in the build
- **INCLUDE_CHROMIUM (default=false):** whether to include the Firefox 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,6 +614,9 @@ 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

View File

@@ -275,11 +275,3 @@ No. Currently, there is no way to recover a deleted Superset dashboard/chart/dat
Hence, it is recommended to take periodic backups of the metadata database. For recovery, you can launch a recovery instance of a Superset server with the backed-up copy of the DB attached and use the Export Dashboard button in the Superset UI (or the `superset export-dashboards` CLI command). Then, take the .zip file and import it into the current Superset instance.
Alternatively, you can programmatically take regular exports of the assets as a backup.
## I ran a security scan of the Superset container image and it showed dozens of "high" and "critical" vulnerabilities! Can you release a version of Superset without these?
You are talking about dependency CVEs: identified vulnerabilities in software that Superset uses. Most of these CVEs are in the Linux kernel or Python, both of which have many other people working on their security.
We address these dependency CVEs as best we can by regularly updating our dependencies to newer versions. We use bots to assist with that and cheerfully welcome pull requests from humans that fix dependency CVEs.
The Superset [security team](https://superset.apache.org/docs/security/#reporting-security-vulnerabilities) focuses primarily on vulnerabilities _in Superset itself_. See our [CVEs page](https://superset.apache.org/docs/security/cves) for a list of past Superset CVEs.

View File

@@ -22,13 +22,6 @@ level dependencies.
**Debian and Ubuntu**
Ubuntu **24.04** uses python 3.12 per default, which currently is not supported by Superset. You need to add a second python installation of 3.11 and install the required additional dependencies.
```bash
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt update
sudo apt install python3.11 python3.11-dev python3.11-venv build-essential libssl-dev libffi-dev libsasl2-dev libldap2-dev default-libmysqlclient-dev
```
In Ubuntu **20.04 and 22.04** the following command will ensure that the required dependencies are installed:
```bash
@@ -101,9 +94,14 @@ These will now be available when pip installing requirements.
## Python Virtual Environment
We highly recommend installing Superset inside of a virtual environment.
We highly recommend installing Superset inside of a virtual environment. Python ships with
`virtualenv` out of the box. If you're using [pyenv](https://github.com/pyenv/pyenv), you can install [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv). Or you can install it with `pip`:
You can create and activate a virtual environment using the following commands. Ensure you are using a compatible version of python. You might have to explicitly use for example `python3.11` instead of `python3`.
```bash
pip install virtualenv
```
You can create and activate a virtual environment using:
```bash
# virtualenv is shipped in Python 3.6+ as venv instead of pyvenv.
@@ -134,7 +132,7 @@ pip install apache_superset
Then, define mandatory configurations, SECRET_KEY and FLASK_APP:
```bash
export SUPERSET_SECRET_KEY=YOUR-SECRET-KEY # For production use, make sure this is a strong key, for example generated using `openssl rand -base64 42`. See https://superset.apache.org/docs/configuration/configuring-superset#specifying-a-secret_key
export SUPERSET_SECRET_KEY=YOUR-SECRET-KEY
export FLASK_APP=superset
```

View File

@@ -340,8 +340,8 @@ TALISMAN_CONFIG = {
}
```
For more information on setting up Talisman, please refer to
https://superset.apache.org/docs/configuration/networking-settings/#changing-flask-talisman-csp.
# For more information on setting up Talisman, please refer to
https://superset.apache.org/docs/configuration/networking-settings/#changing-flask-talisman-csp
### Reporting Security Vulnerabilities

View File

@@ -18,7 +18,7 @@
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@ant-design/icons": "^5.5.2",
"@docusaurus/core": "3.8.1",
"@docusaurus/plugin-client-redirects": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
@@ -26,7 +26,7 @@
"@emotion/styled": "^10.0.27",
"@saucelabs/theme-github-codeblock": "^0.3.0",
"@superset-ui/style": "^0.14.23",
"antd": "^5.26.3",
"antd": "^5.25.1",
"docusaurus-plugin-less": "^2.0.2",
"less": "^4.3.0",
"less-loader": "^12.3.0",
@@ -35,11 +35,11 @@
"react-dom": "^18.3.1",
"react-github-btn": "^1.4.0",
"react-svg-pan-zoom": "^3.13.1",
"swagger-ui-react": "^5.26.0"
"swagger-ui-react": "^5.25.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.1",
"@docusaurus/tsconfig": "^3.8.0",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",

View File

@@ -50,7 +50,7 @@ export const Databases = [
},
{
title: 'Apache Druid',
href: 'https://druid.apache.org/',
href: 'http://druid.io/',
imgName: 'druid.png',
},
{
@@ -69,12 +69,17 @@ export const Databases = [
},
{
title: 'ClickHouse',
href: 'https://clickhouse.com/',
href: 'https://clickhouse.tech/',
imgName: 'clickhouse.png',
},
{
title: 'Rockset',
href: 'https://rockset.com/',
imgName: 'rockset.png',
},
{
title: 'Dremio',
href: 'https://www.dremio.com/',
href: 'https://dremio.com/',
imgName: 'dremio.png',
},
{
@@ -99,12 +104,12 @@ export const Databases = [
},
{
title: 'IBM Db2',
href: 'https://www.ibm.com/products/db2',
href: 'https://www.ibm.com/analytics/db2',
imgName: 'ibmdb2.png',
},
{
title: 'SAP Hana',
href: 'https://www.sap.com/products/data-cloud/hana.html',
href: 'https://www.sap.com/products/technology-platform/hana.html',
imgName: 'sap-hana.jpg',
},
{
@@ -134,7 +139,7 @@ export const Databases = [
},
{
title: 'TDengine',
href: 'https://tdengine.com/',
href: 'https://www.tdengine.com/',
imgName: 'tdengine.png',
},
];

BIN
docs/static/img/databases/rockset.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -158,20 +158,13 @@
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.2.1":
version "7.2.1"
resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-7.2.1.tgz#3bbc1c6c18550020d1622a0067ff03492318df98"
integrity sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==
"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.2.0":
version "7.2.0"
resolved "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz"
integrity sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==
dependencies:
"@ant-design/fast-color" "^2.0.6"
"@ant-design/colors@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-8.0.0.tgz#92b5aa1cd44896b62c7b67133b4d5a6a00266162"
integrity sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==
dependencies:
"@ant-design/fast-color" "^3.0.0"
"@ant-design/cssinjs-utils@^1.1.3":
version "1.1.3"
resolved "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz"
@@ -201,17 +194,12 @@
dependencies:
"@babel/runtime" "^7.24.7"
"@ant-design/fast-color@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@ant-design/fast-color/-/fast-color-3.0.0.tgz#fb5178203de825f284809538f5142203d0ef3d80"
integrity sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==
"@ant-design/icons-svg@^4.4.0":
version "4.4.2"
resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
"@ant-design/icons@^5.6.1":
"@ant-design/icons@^5.5.2", "@ant-design/icons@^5.6.1":
version "5.6.1"
resolved "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz"
integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==
@@ -222,16 +210,6 @@
classnames "^2.2.6"
rc-util "^5.31.1"
"@ant-design/icons@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.0.0.tgz#302c935b8b0b429e4444cbc45809247276186d94"
integrity sha512-o0aCCAlHc1o4CQcapAwWzHeaW2x9F49g7P3IDtvtNXgHowtRWYb7kiubt8sQPFvfVIVU/jLw2hzeSlNt0FU+Uw==
dependencies:
"@ant-design/colors" "^8.0.0"
"@ant-design/icons-svg" "^4.4.0"
"@rc-component/util" "^1.2.1"
classnames "^2.2.6"
"@ant-design/react-slick@~1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz"
@@ -1106,7 +1084,15 @@
"@babel/plugin-transform-modules-commonjs" "^7.26.3"
"@babel/plugin-transform-typescript" "^7.27.0"
"@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.25.9", "@babel/runtime-corejs3@^7.26.10", "@babel/runtime-corejs3@^7.27.1":
"@babel/runtime-corejs3@^7.20.7", "@babel/runtime-corejs3@^7.22.15", "@babel/runtime-corejs3@^7.25.9", "@babel/runtime-corejs3@^7.26.10":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.0.tgz#c766df350ec7a2caf3ed64e3659b100954589413"
integrity sha512-UWjX6t+v+0ckwZ50Y5ShZLnlk95pP5MyW/pon9tiYzl3+18pkTHTFNTKr7rQbfRXPkowt2QAn30o1b6oswszew==
dependencies:
core-js-pure "^3.30.2"
regenerator-runtime "^0.14.0"
"@babel/runtime-corejs3@^7.27.1":
version "7.27.6"
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.6.tgz#97644153808a62898e7c05f3361501417db3c48b"
integrity sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ==
@@ -1979,10 +1965,10 @@
fs-extra "^11.1.1"
tslib "^2.6.0"
"@docusaurus/tsconfig@^3.8.1":
version "3.8.1"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.8.1.tgz#a1f7daadfc93455289200647f4ee10cdca540f7b"
integrity sha512-XBWCcqhRHhkhfolnSolNL+N7gj3HVE3CoZVqnVjfsMzCoOsuQw2iCLxVVHtO+rePUUfouVZHURDgmqIySsF66A==
"@docusaurus/tsconfig@^3.8.0":
version "3.8.0"
resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.8.0.tgz#ea7ee0917e1562cf0a6e95e049c42f1f61351f32"
integrity sha512-utLl48nNjSYBoq47RKukZ9fPLEX3nJWThzrujb0ndQQ1jc/gh4RhTRaAqItH9nImnsgGKmLMnyoMBpfGmoop+w==
"@docusaurus/types@3.8.1":
version "3.8.1"
@@ -2457,10 +2443,10 @@
classnames "^2.3.2"
rc-util "^5.24.4"
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.7":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@rc-component/trigger/-/trigger-2.2.7.tgz#a2b97ecbb93280a3c424e51fa415b371b355d76a"
integrity sha512-Qggj4Z0AA2i5dJhzlfFSmg1Qrziu8dsdHOihROL5Kl18seO2Eh/ZaTYt2c8a/CyGaTChnFry7BEYew1+/fhSbA==
"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.2.6":
version "2.2.6"
resolved "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz"
integrity sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==
dependencies:
"@babel/runtime" "^7.23.2"
"@rc-component/portal" "^1.1.0"
@@ -2469,13 +2455,6 @@
rc-resize-observer "^1.3.1"
rc-util "^5.44.0"
"@rc-component/util@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@rc-component/util/-/util-1.2.1.tgz#2c3158f11a4193478cec44ca42915da31f67d8a0"
integrity sha512-AUVu6jO+lWjQnUOOECwu8iR0EdElQgWW5NBv5vP/Uf9dWbAX3udhMutRlkVXjuac2E40ghkFy+ve00mc/3Fymg==
dependencies:
react-is "^18.2.0"
"@saucelabs/theme-github-codeblock@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@saucelabs/theme-github-codeblock/-/theme-github-codeblock-0.3.0.tgz"
@@ -4038,12 +4017,12 @@ ansi-styles@^6.1.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
antd@^5.26.3:
version "5.26.3"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.26.3.tgz#cbbb7e1b48a972dc7b6ee8b6948f51cc91c263f8"
integrity sha512-M/s9Q39h/+G7AWnS6fbNxmAI9waTH4ti022GVEXBLq2j810V1wJ3UOQps13nEilzDNcyxnFN/EIbqIgS7wSYaA==
antd@^5.25.1:
version "5.25.1"
resolved "https://registry.yarnpkg.com/antd/-/antd-5.25.1.tgz#859b419a18d113492304ccd66c29074a71902241"
integrity sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==
dependencies:
"@ant-design/colors" "^7.2.1"
"@ant-design/colors" "^7.2.0"
"@ant-design/cssinjs" "^1.23.0"
"@ant-design/cssinjs-utils" "^1.1.3"
"@ant-design/fast-color" "^2.0.6"
@@ -4054,7 +4033,7 @@ antd@^5.26.3:
"@rc-component/mutate-observer" "^1.1.0"
"@rc-component/qrcode" "~1.0.0"
"@rc-component/tour" "~1.15.1"
"@rc-component/trigger" "^2.2.7"
"@rc-component/trigger" "^2.2.6"
classnames "^2.5.1"
copy-to-clipboard "^3.3.3"
dayjs "^1.11.11"
@@ -4062,7 +4041,7 @@ antd@^5.26.3:
rc-checkbox "~3.5.0"
rc-collapse "~3.9.0"
rc-dialog "~9.6.0"
rc-drawer "~7.3.0"
rc-drawer "~7.2.0"
rc-dropdown "~4.2.1"
rc-field-form "~2.7.0"
rc-image "~7.12.0"
@@ -4078,17 +4057,17 @@ antd@^5.26.3:
rc-rate "~2.13.1"
rc-resize-observer "^1.4.3"
rc-segmented "~2.7.0"
rc-select "~14.16.8"
rc-select "~14.16.7"
rc-slider "~11.1.8"
rc-steps "~6.0.1"
rc-switch "~4.1.0"
rc-table "~7.51.1"
rc-table "~7.50.4"
rc-tabs "~15.6.1"
rc-textarea "~1.10.0"
rc-tooltip "~6.4.0"
rc-tree "~5.13.1"
rc-tree-select "~5.27.0"
rc-upload "~4.9.2"
rc-upload "~4.9.0"
rc-util "^5.44.4"
scroll-into-view-if-needed "^3.1.0"
throttle-debounce "^5.0.2"
@@ -10398,10 +10377,10 @@ rc-dialog@~9.6.0:
rc-motion "^2.3.0"
rc-util "^5.21.0"
rc-drawer@~7.3.0:
version "7.3.0"
resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-7.3.0.tgz#1bb5fe5f9da38b6a2b2a7dffc9fcb647252a328f"
integrity sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==
rc-drawer@~7.2.0:
version "7.2.0"
resolved "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz"
integrity sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==
dependencies:
"@babel/runtime" "^7.23.9"
"@rc-component/portal" "^1.1.1"
@@ -10573,7 +10552,7 @@ rc-segmented@~2.7.0:
rc-motion "^2.4.4"
rc-util "^5.17.0"
rc-select@~14.16.2, rc-select@~14.16.8:
rc-select@~14.16.2, rc-select@~14.16.7:
version "14.16.8"
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.8.tgz#78e6782f1ccc1f03d9003bc3effa4ed609d29a97"
integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
@@ -10613,10 +10592,10 @@ rc-switch@~4.1.0:
classnames "^2.2.1"
rc-util "^5.30.0"
rc-table@~7.51.1:
version "7.51.1"
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.51.1.tgz#cd69ae3262d3b61e4c93c979c12786906e944691"
integrity sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==
rc-table@~7.50.4:
version "7.50.4"
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.50.4.tgz#687b5bf76d1a94168f75481cbc83be9442010432"
integrity sha512-Y+YuncnQqoS5e7yHvfvlv8BmCvwDYDX/2VixTBEhkMDk9itS9aBINp4nhzXFKiBP/frG4w0pS9d9Rgisl0T1Bw==
dependencies:
"@babel/runtime" "^7.10.1"
"@rc-component/context" "^1.4.0"
@@ -10681,10 +10660,10 @@ rc-tree@~5.13.0, rc-tree@~5.13.1:
rc-util "^5.16.1"
rc-virtual-list "^3.5.1"
rc-upload@~4.9.2:
version "4.9.2"
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.2.tgz#297f52fd1b1c2a4b570c3e42444609b7530531bb"
integrity sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==
rc-upload@~4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.0.tgz#911963ab5a0b538c743765371c05e2de9e3f5436"
integrity sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==
dependencies:
"@babel/runtime" "^7.18.3"
classnames "^2.2.5"
@@ -12019,10 +11998,10 @@ swagger-client@^3.35.5:
ramda "^0.30.1"
ramda-adjunct "^5.1.0"
swagger-ui-react@^5.26.0:
version "5.26.0"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.26.0.tgz#b15a903d556cc0ec2a56a969beb9d5bc9ea52910"
integrity sha512-4e6bP9bdJyh+SqQW0lxulPn/SDno4+oWrKXsuon5Z9kjtV0zeoWEJ1c70Qxp8kN/c3caFwec8OyxDNhvo14pkw==
swagger-ui-react@^5.25.2:
version "5.25.2"
resolved "https://registry.yarnpkg.com/swagger-ui-react/-/swagger-ui-react-5.25.2.tgz#27e8570c7225a0beffcfae26b569c8dd1274c545"
integrity sha512-derGtL7NKvF0w4XIULgiWmS1y3EMEVQAJwLVLTAzVo6/ilLhwP9HCz2yjkVVHzK58gvOzrhO3DKk/Effpr5LJw==
dependencies:
"@babel/runtime-corejs3" "^7.27.1"
"@scarf/scarf" "=1.4.0"

View File

@@ -40,13 +40,12 @@ dependencies = [
"click>=8.0.3",
"click-option-group",
"colorama",
"flask-cors>=4.0.2, <7.0",
"croniter>=0.3.28",
"cron-descriptor",
"cryptography>=42.0.4, <45.0.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <3.0.0",
"flask-appbuilder>=4.8.0, <5.0.0",
"flask-appbuilder>=4.7.0, <5.0.0",
"flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
@@ -65,8 +64,6 @@ dependencies = [
"jsonpath-ng>=1.6.1, <2",
"Mako>=1.2.2",
"markdown>=3.0",
# marshmallow>=4 has issues: https://github.com/apache/superset/issues/33162
"marshmallow>=3.0, <4",
"msgpack>=1.0.0, <1.1",
"nh3>=0.2.11, <0.3",
"numpy>1.23.5, <2.3",
@@ -116,6 +113,7 @@ bigquery = [
]
clickhouse = ["clickhouse-connect>=0.5.14, <1.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
cors = ["flask-cors>=4.0.2, <5.0"]
crate = ["sqlalchemy-cratedb>=0.40.1, <1"]
databend = ["databend-sqlalchemy>=0.3.2, <1.0"]
databricks = [
@@ -166,6 +164,7 @@ presto = ["pyhive[presto]>=0.6.5"]
trino = ["trino>=0.328.0"]
prophet = ["prophet>=1.1.5, <2"]
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
rockset = ["rockset-sqlalchemy>=0.0.1, <1"]
risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.2.18, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]

View File

@@ -7,14 +7,7 @@ To alter the pinned dependency, you can edit/alter the `.in` and `pyproject.toml
```bash
./scripts/uv-pip-compile.sh
```
:::warning
The pinned dependencies are based on the `current` version of python supported in Superset.
Output of `./scripts/uv-pip-compile.sh` may vary slightly based on the python version you are using to run the command.
Check the `pyproject.toml` file for the current version of python supported.
:::
This will generate the pinned requirements in the `.txt` files, which will be used in our CI/CD pipelines and in the Docker images.
We recommend to everyone in the community to use the pinned requirements in their local development environments, to ensure consistency across different environments, though we don't force requirements as part of our python package semantics to allow flexibility for users to install different versions of the dependencies if they wish.
Note that `development.txt` is a superset of what's in `base.txt`, and all version numbers for shared library should fully match at all times. `translations.txt` is meant as a supplemental file to be used in conjunction with the other requirements files, and is not meant to be used standalone.

View File

@@ -16,7 +16,7 @@
# specific language governing permissions and limitations
# under the License.
#
urllib3==2.5.0
urllib3>=1.26.19, <2.0.0
werkzeug>=3.0.1
numexpr>=2.9.0

View File

@@ -104,7 +104,6 @@ flask==2.3.3
# flask-babel
# flask-caching
# flask-compress
# flask-cors
# flask-jwt-extended
# flask-limiter
# flask-login
@@ -112,7 +111,7 @@ flask==2.3.3
# flask-session
# flask-sqlalchemy
# flask-wtf
flask-appbuilder==4.8.0
flask-appbuilder==4.7.0
# via apache-superset (pyproject.toml)
flask-babel==2.0.0
# via flask-appbuilder
@@ -120,8 +119,6 @@ flask-caching==2.3.1
# via apache-superset (pyproject.toml)
flask-compress==1.17
# via apache-superset (pyproject.toml)
flask-cors==4.0.2
# via apache-superset (pyproject.toml)
flask-jwt-extended==4.7.1
# via flask-appbuilder
flask-limiter==3.12
@@ -212,7 +209,6 @@ markupsafe==3.0.2
# wtforms
marshmallow==3.26.1
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
# marshmallow-sqlalchemy
marshmallow-sqlalchemy==1.4.0
@@ -396,8 +392,6 @@ typing-extensions==4.14.0
# alembic
# cattrs
# limits
# pyopenssl
# referencing
# selenium
# shillelagh
tzdata==2025.2
@@ -406,7 +400,7 @@ tzdata==2025.2
# pandas
url-normalize==2.2.1
# via requests-cache
urllib3==2.5.0
urllib3==1.26.20
# via
# -r requirements/base.in
# requests

View File

@@ -16,4 +16,4 @@
# specific language governing permissions and limitations
# under the License.
#
-e .[development,bigquery,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
-e .[development,bigquery,cors,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]

View File

@@ -195,7 +195,7 @@ flask==2.3.3
# flask-sqlalchemy
# flask-testing
# flask-wtf
flask-appbuilder==4.8.0
flask-appbuilder==4.7.0
# via
# -c requirements/base.txt
# apache-superset
@@ -212,9 +212,7 @@ flask-compress==1.17
# -c requirements/base.txt
# apache-superset
flask-cors==4.0.2
# via
# -c requirements/base.txt
# apache-superset
# via apache-superset
flask-jwt-extended==4.7.1
# via
# -c requirements/base.txt
@@ -426,7 +424,6 @@ markupsafe==3.0.2
marshmallow==3.26.1
# via
# -c requirements/base.txt
# apache-superset
# flask-appbuilder
# marshmallow-sqlalchemy
marshmallow-sqlalchemy==1.4.0
@@ -842,8 +839,6 @@ typing-extensions==4.14.0
# apache-superset
# cattrs
# limits
# pyopenssl
# referencing
# selenium
# shillelagh
tzdata==2025.2
@@ -857,7 +852,7 @@ url-normalize==2.2.1
# via
# -c requirements/base.txt
# requests-cache
urllib3==2.5.0
urllib3==1.26.20
# via
# -c requirements/base.txt
# docker

View File

@@ -24,8 +24,7 @@ ADDITIONAL_ARGS="$@"
# Generate the requirements/base.txt file
uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt $ADDITIONAL_ARGS
# Generate the requirements/development.txt file, making sure requirements/base.txt is a constraint to keep the versions in sync. Note that `development.txt` is a Superset of `base.txt` where version for the shared libs should match their version.
# Generate the requirements/development.txt file, making sure requirements/base.txt is a constraint to keep the versions in sync
uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt $ADDITIONAL_ARGS
# NOTE translation is intended as a "supplemental" set of pins that can be combined with either base or dev as needed
uv pip compile requirements/translations.in -o requirements/translations.txt $ADDITIONAL_ARGS

View File

@@ -1,12 +1,12 @@
{
"name": "@superset-ui/embedded-sdk",
"version": "0.2.0",
"version": "0.1.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@superset-ui/embedded-sdk",
"version": "0.2.0",
"version": "0.1.3",
"license": "Apache-2.0",
"dependencies": {
"@superset-ui/switchboard": "^0.20.3",

View File

@@ -1,6 +1,6 @@
{
"name": "@superset-ui/embedded-sdk",
"version": "0.2.0",
"version": "0.1.3",
"description": "SDK for embedding resources from Superset into your own application",
"access": "public",
"keywords": [

View File

@@ -46,7 +46,6 @@ export type UiConfigType = {
urlParams?: {
[key: string]: any;
};
showRowLimitWarning?: boolean;
};
export type EmbedDashboardParams = {
@@ -134,9 +133,6 @@ export async function embedDashboard({
if (dashboardUiConfig.emitDataMasks) {
configNumber += 16;
}
if (dashboardUiConfig.showRowLimitWarning) {
configNumber += 32;
}
}
return configNumber;
}

View File

@@ -69,10 +69,6 @@ const restrictedImportsRules = {
message:
'Please use the theme directly from the ThemeProvider rather than importing supersetTheme.',
},
'no-query-string': {
name: 'query-string',
message: 'Please use the URLSearchParams API instead of query-string.',
},
};
module.exports = {
@@ -86,7 +82,9 @@ module.exports = {
],
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2018,
ecmaFeatures: {
experimentalObjectRestSpread: true,
},
},
env: {
browser: true,
@@ -125,11 +123,6 @@ module.exports = {
'react-prefer-function-component',
'prettier',
],
// Add this TS ESlint rule in separate `rules` section to avoid breakages with JS/TS files in /cypress-base.
// TODO(hainenber): merge it to below `rules` section.
rules: {
'@typescript-eslint/prefer-optional-chain': 'error',
},
overrides: [
{
files: ['*.ts', '*.tsx'],
@@ -163,7 +156,7 @@ module.exports = {
'@typescript-eslint/no-non-null-assertion': 0, // disabled temporarily
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/explicit-module-boundary-types': 0, // re-enable up for discussion
'@typescript-eslint/no-unused-vars': 'warn', // downgrade to Warning severity for Jest v30 upgrade
'@typescript-eslint/prefer-optional-chain': 2,
camelcase: 0,
'class-methods-use-this': 0,
'func-names': 0,
@@ -398,7 +391,6 @@ module.exports = {
},
},
],
// eslint-disable-next-line no-dupe-keys
rules: {
'theme-colors/no-literal-colors': 'error',
'icons/no-fa-icons-usage': 'error',

View File

@@ -46,10 +46,10 @@ module.exports = {
plugins: [
'lodash',
'@babel/plugin-syntax-dynamic-import',
['@babel/plugin-transform-class-properties', { loose: true }],
['@babel/plugin-transform-optional-chaining', { loose: true }],
['@babel/plugin-transform-private-methods', { loose: true }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }],
['@babel/plugin-proposal-class-properties', { loose: true }],
['@babel/plugin-proposal-optional-chaining', { loose: true }],
['@babel/plugin-proposal-private-methods', { loose: true }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose: true }],
['@babel/plugin-transform-runtime', { corejs: 3 }],
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx
['babel-plugin-typescript-to-proptypes', { loose: true }],

View File

@@ -27,6 +27,13 @@ describe('Login view', () => {
cy.visit(LOGIN);
});
it('should load login page', () => {
cy.getBySel('login-form').should('be.visible');
cy.getBySel('username-input').should('be.visible');
cy.getBySel('password-input').should('be.visible');
cy.getBySel('login-button').should('be.visible');
});
it('should redirect to login with incorrect username and password', () => {
interceptLogin();
cy.getBySel('login-form').should('be.visible');

View File

@@ -1,4 +1,3 @@
/* eslint-disable camelcase */
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -17,18 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ValueGetterParams } from 'ag-grid-community';
const filterValueGetter = (params: ValueGetterParams) => {
const raw = params.data[params.colDef.field as string];
const formatter = params.colDef.valueFormatter as Function;
if (!raw || !formatter) return null;
const formatted = formatter({
value: raw,
import { REGISTER } from 'cypress/utils/urls';
describe('Register view', () => {
beforeEach(() => {
cy.visit(REGISTER);
});
const numeric = parseFloat(String(formatted).replace('%', '').trim());
return Number.isNaN(numeric) ? null : numeric;
};
export default filterValueGetter;
it('should load register page', () => {
cy.getBySel('register-form').should('be.visible');
cy.getBySel('username-input').should('be.visible');
cy.getBySel('first-name-input').should('be.visible');
cy.getBySel('last-name-input').should('be.visible');
cy.getBySel('email-input').should('be.visible');
cy.getBySel('password-input').should('be.visible');
cy.getBySel('confirm-password-input').should('be.visible');
cy.getBySel('register-button').should('be.visible');
});
});

View File

@@ -62,6 +62,6 @@ describe('Dashboard actions', () => {
// Verify the color of the outlined star (gray)
cy.get('@starIconOutlinedAfter')
.should('have.css', 'color')
.and('eq', 'rgba(0, 0, 0, 0.45)');
.and('eq', 'rgb(133, 133, 133)');
});
});

View File

@@ -21,21 +21,30 @@ import qs from 'querystring';
import {
dashboardView,
nativeFilters,
exploreView,
dataTestChartName,
} from 'cypress/support/directories';
import {
addCountryNameFilter,
addParentFilterWithValue,
applyAdvancedTimeRangeFilterOnDashboard,
applyNativeFilterValueWithIndex,
cancelNativeFilterSettings,
checkNativeFilterTooltip,
clickOnAddFilterInModal,
collapseFilterOnLeftPanel,
deleteNativeFilter,
enterNativeFilterEditModal,
expandFilterOnLeftPanel,
fillNativeFilterForm,
getNativeFilterPlaceholderWithIndex,
inputNativeFilterDefaultValue,
saveNativeFilterSettings,
nativeFilterTooltips,
undoDeleteNativeFilter,
validateFilterContentOnDashboard,
valueNativeFilterOptions,
validateFilterNameOnDashboard,
testItems,
WORLD_HEALTH_CHARTS,
@@ -46,19 +55,19 @@ import {
visitDashboard,
} from './shared_dashboard_functions';
// function selectFilter(index: number) {
// cy.get("[data-test='filter-title-container'] [draggable='true']")
// .eq(index)
// .click();
// }
function selectFilter(index: number) {
cy.get("[data-test='filter-title-container'] [draggable='true']")
.eq(index)
.click();
}
// function closeFilterModal() {
// cy.get('body').then($body => {
// if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
// cy.getBySel('native-filter-modal-cancel-button').click();
// }
// });
// }
function closeFilterModal() {
cy.get('body').then($body => {
if ($body.find('[data-test="native-filter-modal-cancel-button"]').length) {
cy.getBySel('native-filter-modal-cancel-button').click();
}
});
}
describe('Native filters', () => {
describe('Nativefilters initial state not required', () => {
@@ -174,139 +183,46 @@ describe('Native filters', () => {
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
});
describe.only('Numerical Range Filter - Display Modes', () => {
beforeEach(() => {
visitDashboard();
it('User can create a numerical range filter', () => {
visitDashboard();
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
saveNativeFilterSettings([]);
// Assertions
cy.get('[data-test="range-filter-from-input"]')
.should('be.visible')
.click();
cy.get('[data-test="range-filter-from-input"]').type('{selectall}40');
cy.get('[data-test="range-filter-to-input"]')
.should('be.visible')
.click();
cy.get('[data-test="range-filter-to-input"]').type('{selectall}50');
cy.get(nativeFilters.applyFilter).click({
force: true,
});
const expandFilterConfiguration = () => {
cy.get('.ant-collapse-header')
.contains('Filter Configuration')
.should('be.visible')
.then($header => {
cy.wrap($header)
.closest('.ant-collapse-item')
.invoke('hasClass', 'ant-collapse-item-active')
.then(isExpanded => {
if (!isExpanded) cy.wrap($header).click();
});
});
// Assert that the URL contains 'native_filters'
cy.url().then(u => {
const ur = new URL(u);
expect(ur.search).to.include('native_filters');
cy.get('.ant-collapse-content-box').should('be.visible');
};
const selectRangeTypeOption = (label: string) => {
cy.contains('Range Type')
.should('be.visible')
.closest('.ant-form-item')
.within(() => {
cy.get('.ant-select-selector').click();
});
cy.get('.ant-select-dropdown:visible')
.contains('.ant-select-item-option', label)
.click();
};
const applyAndAssertInputs = (from: string, to: string) => {
// Set 'from' input
cy.get('[data-test="range-filter-from-input"]').clear();
cy.get('[data-test="range-filter-from-input"]').type(from);
cy.get('[data-test="range-filter-from-input"]').blur();
// Set 'to' input
cy.get('[data-test="range-filter-to-input"]').clear();
cy.get('[data-test="range-filter-to-input"]').type(to);
cy.get('[data-test="range-filter-to-input"]').blur();
// Assert values without chaining after .invoke()
cy.get('[data-test="range-filter-from-input"]')
.invoke('val')
.then(val => {
expect(val).to.equal(from);
});
.should('equal', '40');
// Assert that the "To" input has the correct value
cy.get('[data-test="range-filter-to-input"]')
.invoke('val')
.then(val => {
expect(val).to.equal(to);
});
};
it('User can create a numerical range filter with "Range Inputs" display mode', () => {
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
expandFilterConfiguration();
selectRangeTypeOption('Range Inputs');
saveNativeFilterSettings([]);
cy.wait(500); // allow filter to mount
applyAndAssertInputs('40', '70');
});
it('User can change the display mode to "Slider"', () => {
enterNativeFilterEditModal(false);
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
expandFilterConfiguration();
cy.contains('Range Type')
.should('be.visible')
.closest('.ant-form-item')
.within(() => {
cy.get('.ant-select-selector').click({ force: true });
});
cy.get('.ant-select-dropdown:visible .ant-select-item-option')
.contains(/^Slider$/)
.click({ force: true });
cy.get('.ant-select-selector').should('contain.text', 'Slider');
saveNativeFilterSettings([]);
cy.get('.ant-slider', { timeout: 10000 }).should('be.visible');
cy.get('[data-test="range-filter-from-input"]', {
timeout: 5000,
}).should('not.exist');
cy.get('[data-test="range-filter-to-input"]', { timeout: 5000 }).should(
'not.exist',
);
});
it('User can change the display mode to "Slider and range input"', () => {
enterNativeFilterEditModal(false);
// Re-create filter
fillNativeFilterForm(
testItems.filterType.numerical,
testItems.filterNumericalColumn,
testItems.datasetForNativeFilter,
testItems.filterNumericalColumn,
);
expandFilterConfiguration();
selectRangeTypeOption('Slider and range input');
saveNativeFilterSettings([]);
cy.wait(500);
applyAndAssertInputs('40', '70');
.should('equal', '50');
});
});

View File

@@ -16,29 +16,37 @@
* specific language governing permissions and limitations
* under the License.
*/
import qs from 'querystring';
import {
dashboardView,
nativeFilters,
exploreView,
dataTestChartName,
} from 'cypress/support/directories';
import {
addCountryNameFilter,
addParentFilterWithValue,
applyAdvancedTimeRangeFilterOnDashboard,
applyNativeFilterValueWithIndex,
cancelNativeFilterSettings,
checkNativeFilterTooltip,
clickOnAddFilterInModal,
collapseFilterOnLeftPanel,
deleteNativeFilter,
enterNativeFilterEditModal,
expandFilterOnLeftPanel,
fillNativeFilterForm,
getNativeFilterPlaceholderWithIndex,
inputNativeFilterDefaultValue,
saveNativeFilterSettings,
nativeFilterTooltips,
undoDeleteNativeFilter,
validateFilterContentOnDashboard,
valueNativeFilterOptions,
validateFilterNameOnDashboard,
testItems,
WORLD_HEALTH_CHARTS,
} from './utils';
import {
prepareDashboardFilters,

View File

@@ -363,26 +363,9 @@ export function saveNativeFilterSettings(charts: ChartSpec[]) {
cy.get(nativeFilters.modal.footer)
.contains('Save')
.should('be.visible')
.click({ force: true });
// Wait for modal to either close or remain open
cy.get('body').should($body => {
const modalExists = $body.find(nativeFilters.modal.container).length > 0;
if (modalExists) {
cy.get(nativeFilters.modal.footer)
.contains('Save')
.should('be.visible')
.click({ force: true });
}
});
// Ensure modal is closed
.click();
cy.get(nativeFilters.modal.container).should('not.exist');
// Wait for all charts to load
charts.forEach(chart => {
waitForChartLoad(chart);
});
charts.forEach(waitForChartLoad);
}
/** ************************************************************************

View File

@@ -18,6 +18,7 @@
*/
import '@cypress/code-coverage/support';
import '@applitools/eyes-cypress/commands';
import failOnConsoleError from 'cypress-fail-on-console-error';
import { expect } from 'chai';
import rison from 'rison';

View File

@@ -10,7 +10,7 @@
"allowJs": true,
"noEmit": true
},
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/types/index.d.ts"],
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/eyes-index.d.ts"],
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
"exclude": ["node_modules"]
}

View File

@@ -37,7 +37,6 @@ module.exports = {
setupFilesAfterEnv: ['<rootDir>/spec/helpers/setup.ts'],
snapshotSerializers: ['@emotion/jest/serializer'],
testEnvironmentOptions: {
globalsCleanup: true,
url: 'http://localhost',
},
collectCoverageFrom: [
@@ -56,7 +55,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)|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|jest-enzyme|geostyler|geostyler-.*)',
],
preset: 'ts-jest',
transform: {

File diff suppressed because it is too large Load Diff

View File

@@ -84,7 +84,7 @@
"dependencies": {
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@emotion/styled": "^11.3.0",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^5.21.1",
"@rjsf/utils": "^5.24.3",
@@ -109,7 +109,6 @@
"@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",
@@ -155,7 +154,7 @@
"json-stringify-pretty-compact": "^2.0.0",
"lodash": "^4.17.21",
"luxon": "^3.5.0",
"mapbox-gl": "^3.13.0",
"mapbox-gl": "^2.10.0",
"markdown-to-jsx": "^7.7.4",
"match-sorter": "^6.3.4",
"memoize-one": "^5.2.1",
@@ -165,6 +164,8 @@
"ol": "^7.5.2",
"polished": "^4.3.1",
"prop-types": "^15.8.1",
"query-string": "^6.13.7",
"rc-trigger": "^5.3.4",
"re-resizable": "^6.10.1",
"react": "^17.0.2",
"react-checkbox-tree": "^1.8.0",
@@ -176,7 +177,7 @@
"react-google-recaptcha": "^3.1.0",
"react-hot-loader": "^4.13.1",
"react-intersection-observer": "^9.16.0",
"react-json-tree": "^0.20.0",
"react-json-tree": "^0.17.0",
"react-lines-ellipsis": "^0.15.4",
"react-loadable": "^5.5.0",
"react-redux": "^7.2.9",
@@ -186,6 +187,7 @@
"react-search-input": "^0.11.3",
"react-sortable-hoc": "^2.0.0",
"react-split": "^2.0.9",
"react-syntax-highlighter": "^15.4.5",
"react-table": "^7.8.0",
"react-transition-group": "^4.4.5",
"react-virtualized-auto-sizer": "^1.0.25",
@@ -194,6 +196,7 @@
"redux-localstorage": "^0.4.1",
"redux-thunk": "^2.1.0",
"redux-undo": "^1.0.0-beta9-9-7",
"rimraf": "^6.0.1",
"rison": "^0.1.1",
"scroll-into-view-if-needed": "^3.1.0",
"simple-zstd": "^1.4.2",
@@ -206,12 +209,16 @@
"yargs": "^17.7.2"
},
"devDependencies": {
"@applitools/eyes-storybook": "^3.55.6",
"@babel/cli": "^7.27.2",
"@applitools/eyes-storybook": "^3.50.9",
"@babel/cli": "^7.22.6",
"@babel/compat-data": "^7.26.8",
"@babel/core": "^7.26.0",
"@babel/eslint-parser": "^7.25.9",
"@babel/node": "^7.22.6",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
"@babel/plugin-transform-runtime": "^7.27.1",
@@ -253,11 +260,13 @@
"@types/node": "^22.12.0",
"@types/react": "^17.0.83",
"@types/react-dom": "^17.0.26",
"@types/react-gravatar": "^2.6.14",
"@types/react-json-tree": "^0.6.11",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-transition-group": "^4.4.12",
"@types/react-ultimate-pagination": "^1.2.4",
"@types/react-virtualized-auto-sizer": "^1.0.4",
@@ -268,16 +277,17 @@
"@types/sinon": "^17.0.3",
"@types/testing-library__jest-dom": "^5.14.9",
"@types/tinycolor2": "^1.4.3",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"babel-jest": "^30.0.2",
"@types/yargs": "12 - 18",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"babel-jest": "^29.7.0",
"babel-loader": "^10.0.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-typescript-to-proptypes": "^2.0.0",
"cheerio": "1.1.0",
"copy-webpack-plugin": "^13.0.0",
"cheerio": "1.0.0-rc.10",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
@@ -307,9 +317,9 @@
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.3",
"imports-loader": "^5.0.0",
"jest": "^30.0.2",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-html-reporter": "^4.3.0",
"jest-html-reporter": "^3.10.2",
"jest-websocket-mock": "^2.5.0",
"jsdom": "^26.0.0",
"lerna": "^8.2.1",
@@ -328,19 +338,19 @@
"storybook": "8.1.11",
"style-loader": "^4.0.0",
"thread-loader": "^4.0.4",
"ts-jest": "^29.4.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.1",
"tscw-config": "^1.1.2",
"tsx": "^4.19.2",
"typescript": "5.4.5",
"typescript": "5.1.6",
"vm-browserify": "^1.1.2",
"webpack": "^5.99.9",
"webpack": "^5.98.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1",
"webpack-manifest-plugin": "^5.0.1",
"webpack-sources": "^3.3.3",
"webpack-visualizer-plugin2": "^1.2.0"
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.15.1",
"webpack-manifest-plugin": "^5.0.0",
"webpack-sources": "^3.2.3",
"webpack-visualizer-plugin2": "^1.1.0"
},
"peerDependencies": {
"ace-builds": "^1.41.0",

View File

@@ -28,7 +28,7 @@ import { <%= packageLabel %>Props, <%= packageLabel %>StylesProps } from './type
// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/theme/index.ts
const Styles = styled.div<<%= packageLabel %>StylesProps>`
background-color: ${({ theme }) => theme.colorPrimaryBg};
background-color: ${({ theme }) => theme.colors.primary.light2};
padding: ${({ theme }) => theme.sizeUnit * 4}px;
border-radius: ${({ theme }) => theme.borderRadius}px;
height: ${({ height }) => height}px;

View File

@@ -30,14 +30,14 @@
"dependencies": {
"chalk": "^5.4.1",
"lodash-es": "^4.17.21",
"yeoman-generator": "^7.5.1",
"yeoman-generator": "^7.4.0",
"yosay": "^3.0.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"fs-extra": "^11.3.0",
"jest": "^30.0.2",
"yeoman-test": "^10.1.1"
"fs-extra": "^11.2.0",
"jest": "^29.7.0",
"yeoman-test": "^8.3.0"
},
"engines": {
"npm": ">= 4.0.0",

View File

@@ -27,9 +27,10 @@ import {
import { PostProcessingFactory } from './types';
import { extractExtraMetrics } from './utils';
export const sortOperator: PostProcessingFactory<
PostProcessingSort
> = formData => {
export const sortOperator: PostProcessingFactory<PostProcessingSort> = (
formData,
queryObject,
) => {
// the sortOperator only used in the barchart v2
const sortableLabels = [
getXAxisLabel(formData),

View File

@@ -29,7 +29,6 @@ export const TITLE_POSITION_OPTIONS: [string, string][] = [
['Left', t('Left')],
['Top', t('Top')],
];
export const titleControls: ControlPanelSectionConfig = {
label: t('Chart Title'),
tabOverride: 'customize',
@@ -44,6 +43,7 @@ export const titleControls: ControlPanelSectionConfig = {
label: t('X Axis Title'),
renderTrigger: true,
default: '',
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -58,6 +58,7 @@ export const titleControls: ControlPanelSectionConfig = {
renderTrigger: true,
default: TITLE_MARGIN_OPTIONS[0],
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -70,6 +71,7 @@ export const titleControls: ControlPanelSectionConfig = {
label: t('Y Axis Title'),
renderTrigger: true,
default: '',
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -84,6 +86,7 @@ export const titleControls: ControlPanelSectionConfig = {
renderTrigger: true,
default: TITLE_MARGIN_OPTIONS[1],
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
description: t('Changing this control takes effect instantly'),
},
},
],
@@ -98,6 +101,7 @@ export const titleControls: ControlPanelSectionConfig = {
renderTrigger: true,
default: TITLE_POSITION_OPTIONS[0][0],
choices: TITLE_POSITION_OPTIONS,
description: t('Changing this control takes effect instantly'),
},
},
],

View File

@@ -17,12 +17,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
GenericDataType,
QueryColumn,
t,
validateNonEmpty,
} from '@superset-ui/core';
import { QueryColumn, t, validateNonEmpty } from '@superset-ui/core';
import {
ExtraControlProps,
SharedControlConfig,
@@ -57,19 +52,6 @@ type Control = {
* feature flags are set and when they're checked.
*/
function filterOptions(
options: (ColumnMeta | QueryColumn)[],
allowedDataTypes?: GenericDataType[],
) {
if (!allowedDataTypes) {
return options;
}
return options.filter(
o =>
o.type_generic !== undefined && allowedDataTypes.includes(o.type_generic),
);
}
export const dndGroupByControl: SharedControlConfig<
'DndColumnSelect' | 'SelectControl',
ColumnMeta
@@ -99,20 +81,14 @@ export const dndGroupByControl: SharedControlConfig<
const newState: ExtraControlProps = {};
const { datasource } = state;
if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
const options = filterOptions(
(datasource as Dataset).columns.filter(c => c.groupby),
controlState?.allowedDataTypes,
);
const options = (datasource as Dataset).columns.filter(c => c.groupby);
if (controlState?.includeTime) {
options.unshift(DATASET_TIME_COLUMN_OPTION);
}
newState.options = options;
newState.savedMetrics = (datasource as Dataset).metrics || [];
} else {
const options = filterOptions(
(datasource?.columns as QueryColumn[]) || [],
controlState?.allowedDataTypes,
);
const options = (datasource?.columns as QueryColumn[]) || [];
if (controlState?.includeTime) {
options.unshift(QUERY_TIME_COLUMN_OPTION);
}
@@ -201,19 +177,6 @@ export const dndAdhocMetricControl: typeof dndAdhocMetricsControl = {
),
};
export const dndTooltipColumnsControl: typeof dndColumnsControl = {
...dndColumnsControl,
label: t('Tooltip (columns)'),
description: t('Columns to show in the tooltip.'),
};
export const dndTooltipMetricsControl: typeof dndAdhocMetricsControl = {
...dndAdhocMetricsControl,
label: t('Tooltip (metrics)'),
description: t('Metrics to show in the tooltip.'),
validators: [],
};
export const dndAdhocMetricControl2: typeof dndAdhocMetricControl = {
...dndAdhocMetricControl,
label: t('Right Axis Metric'),

View File

@@ -45,7 +45,6 @@ import {
isDefined,
NO_TIME_RANGE,
validateMaxValue,
getColumnLabel,
} from '@superset-ui/core';
import {
@@ -83,8 +82,6 @@ import {
dndSeriesControl,
dndAdhocMetricControl2,
dndXAxisControl,
dndTooltipColumnsControl,
dndTooltipMetricsControl,
} from './dndControls';
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
@@ -376,14 +373,6 @@ const temporal_columns_lookup: SharedControlConfig<'HiddenControl'> = {
),
};
const zoomable: SharedControlConfig<'CheckboxControl'> = {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: false,
renderTrigger: true,
description: t('Enable data zooming controls'),
};
const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
type: 'CheckboxControl',
label: t('Sort by metric'),
@@ -392,26 +381,6 @@ const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
),
};
const order_by_cols: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl',
label: t('Ordering'),
description: t('Order results by selected columns'),
multi: true,
default: [],
shouldMapStateToProps: () => true,
mapStateToProps: ({ datasource }) => ({
choices: (datasource?.columns || [])
.map(col =>
[true, false].map(asc => [
JSON.stringify([col.column_name, asc]),
`${getColumnLabel(col.column_name)} [${asc ? 'asc' : 'desc'}]`,
]),
)
.flat(),
}),
resetOnHide: false,
};
export default {
metrics: dndAdhocMetricsControl,
metric: dndAdhocMetricControl,
@@ -423,8 +392,6 @@ export default {
secondary_metric: dndSecondaryMetricControl,
groupby: dndGroupByControl,
columns: dndColumnsControl,
tooltip_columns: dndTooltipColumnsControl,
tooltip_metrics: dndTooltipMetricsControl,
granularity,
granularity_sqla: dndGranularitySqlaControl,
time_grain_sqla,
@@ -450,10 +417,8 @@ export default {
legacy_order_by: dndSortByControl,
truncate_metric,
x_axis: dndXAxisControl,
zoomable,
show_empty_columns,
temporal_columns_lookup,
currency_format,
sort_by_metric,
order_by_cols,
};

View File

@@ -90,7 +90,6 @@ export interface Dataset {
database?: Record<string, unknown>;
normalize_columns?: boolean;
always_filter_main_dttm?: boolean;
extra?: object | string;
}
export interface ControlPanelState {
@@ -162,7 +161,6 @@ export type InternalControlType =
| 'DatasourceControl'
| 'DateFilterControl'
| 'FixedOrMetricControl'
| 'ColorBreakpointsControl'
| 'HiddenControl'
| 'SelectAsyncControl'
| 'SelectControl'

View File

@@ -26,10 +26,10 @@
"dependencies": {
"@ant-design/icons": "^5.2.6",
"@babel/runtime": "^7.25.6",
"@fontsource/fira-code": "^5.2.6",
"@fontsource/inter": "^5.2.6",
"@fontsource/fira-code": "^5.0.18",
"@fontsource/inter": "^5.0.20",
"@types/json-bigint": "^1.0.4",
"ace-builds": "^1.43.1",
"ace-builds": "^1.41.0",
"brace": "^0.11.1",
"classnames": "^2.2.5",
"csstype": "^3.1.3",
@@ -51,14 +51,13 @@
"react-js-cron": "^5.2.0",
"react-draggable": "^4.4.6",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^15.4.5",
"react-ultimate-pagination": "^1.3.2",
"react-error-boundary": "^6.0.0",
"react-error-boundary": "^5.0.0",
"react-markdown": "^8.0.7",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-gfm": "^3.0.1",
"reselect": "^4.0.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
@@ -66,16 +65,15 @@
"xss": "^1.0.14"
},
"devDependencies": {
"@emotion/styled": "^11.14.1",
"@emotion/styled": "^11.3.0",
"@types/d3-format": "^3.0.4",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale": "^2.1.1",
"@types/d3-time": "^3.0.4",
"@types/d3-time-format": "^4.0.3",
"@types/react-table": "^7.7.20",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/jquery": "^3.5.8",
"@types/lodash": "^4.17.20",
"@types/lodash": "^4.17.16",
"@types/math-expression-evaluator": "^1.3.3",
"@types/node": "^22.10.3",
"@types/prop-types": "^15.7.2",
@@ -90,7 +88,7 @@
"antd": "^5.24.6",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.14.1",
"@emotion/styled": "^11.3.0",
"@testing-library/dom": "^8.20.1",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^12.1.5",

View File

@@ -16,11 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
export const ELEMENT_HEIGHT_SCALE = 0.85 as const;
export enum Dimension {
StartTime = 'startTime',
EndTime = 'endTime',
Index = 'index',
SeriesCount = 'seriesCount',
// /superset/sqllab_viz
interface SqlLabPostRequest {
data: {
schema: string;
sql: string;
dbId: number;
templateParams?: string | undefined;
datasourceName: string;
metrics?: string[];
columns?: string[];
};
}

View File

@@ -55,11 +55,7 @@ export enum AppSection {
Embedded = 'EMBEDDED',
}
export type FilterState = {
value?: any;
customColumnLabel?: string;
[key: string]: any;
};
export type FilterState = { value?: any; [key: string]: any };
export type DataMask = {
extraFormData?: ExtraFormData;

View File

@@ -31,7 +31,6 @@ export enum VizType {
Compare = 'compare',
CountryMap = 'country_map',
Funnel = 'funnel',
Gantt = 'gantt_chart',
Gauge = 'gauge_chart',
Graph = 'graph_chart',
Handlebars = 'handlebars',
@@ -55,7 +54,6 @@ export enum VizType {
Step = 'echarts_timeseries_step',
Sunburst = 'sunburst_v2',
Table = 'table',
TableAgGrid = 'ag-grid-table',
TimePivot = 'time_pivot',
TimeTable = 'time_table',
Timeseries = 'echarts_timeseries',

View File

@@ -27,8 +27,6 @@ import getLabelsColorMap, {
import { getAnalogousColors } from './utils';
import { FeatureFlag, isFeatureEnabled } from '../utils';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
// Use type augmentation to correct the fact that
// an instance of CategoricalScale is also a function
interface CategoricalColorScale {

View File

@@ -212,9 +212,7 @@ export function AsyncAceEditor(
background-color: ${token.colorBgElevated} !important;
color: ${token.colorTextSecondary} !important;
}
.ace_editor.ace_editor .ace_gutter .ace_gutter-active-line {
background-color: ${token.colorBorderSecondary};
}
/* Adjust selection color */
.ace_editor .ace_selection {
background-color: ${token.colorPrimaryBgHover} !important;

View File

@@ -165,7 +165,7 @@ export default {
defaultValue: { summary: 'false' },
},
},
popupRender: {
dropdownRender: {
control: false,
description:
'Custom render function for dropdown content. `(menus: ReactNode) => ReactNode`',

View File

@@ -1,320 +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 { Typography, Flex, Space } from '@superset-ui/core/components';
import CodeSyntaxHighlighter from '.';
import type { CodeSyntaxHighlighterProps, SupportedLanguage } from '.';
const { Title, Text, Paragraph } = Typography;
const languages: SupportedLanguage[] = ['sql', 'json', 'htmlbars', 'markdown'];
// Sample code for each language
const sampleCode = {
sql: `-- Complex SQL Query Example
SELECT
u.id,
u.username,
u.email,
COUNT(o.id) as total_orders,
SUM(o.amount) as total_spent,
AVG(o.amount) as avg_order_value
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2023-01-01'
AND u.status = 'active'
GROUP BY u.id, u.username, u.email
HAVING COUNT(o.id) > 0
ORDER BY total_spent DESC, total_orders DESC
LIMIT 50;`,
json: `{
"user": {
"id": 12345,
"username": "john_doe",
"email": "john@example.com",
"profile": {
"firstName": "John",
"lastName": "Doe",
"age": 30,
"preferences": {
"theme": "dark",
"language": "en",
"notifications": true
}
},
"orders": [
{
"id": "order_001",
"amount": 99.99,
"status": "completed",
"items": ["laptop", "mouse"]
},
{
"id": "order_002",
"amount": 49.99,
"status": "pending",
"items": ["keyboard"]
}
]
}
}`,
htmlbars: `{{!-- Handlebars Template Example --}}
<div class="user-profile">
<h1>Welcome, {{user.firstName}} {{user.lastName}}!</h1>
{{#if user.orders}}
<div class="orders-section">
<h2>Your Orders ({{user.orders.length}})</h2>
{{#each user.orders}}
<div class="order-card {{status}}">
<h3>Order #{{id}}</h3>
<p class="amount">\${{amount}}</p>
<p class="status">Status: {{capitalize status}}</p>
{{#if items}}
<ul class="items">
{{#each items}}
<li>{{this}}</li>
{{/each}}
</ul>
{{/if}}
</div>
{{/each}}
</div>
{{else}}
<p class="no-orders">No orders found.</p>
{{/if}}
</div>`,
markdown: `# CodeSyntaxHighlighter Component
A **themed syntax highlighter** for Superset that supports multiple languages and automatic theme switching.
## Features
- 🎨 **Automatic theming** - Adapts to light/dark modes
- ⚡ **Lazy loading** - Languages load on-demand for better performance
- 🔧 **TypeScript support** - Full type safety
- 📱 **Responsive** - Works on all screen sizes
## Supported Languages
| Language | Extension | Use Case |
|----------|-----------|----------|
| SQL | \`.sql\` | Database queries |
| JSON | \`.json\` | Data interchange |
| HTML/Handlebars | \`.hbs\` | Templates |
| Markdown | \`.md\` | Documentation |
## Usage
\`\`\`typescript
import CodeSyntaxHighlighter from '@superset-ui/core/components/CodeSyntaxHighlighter';
<CodeSyntaxHighlighter language="sql">
SELECT * FROM users WHERE active = true;
</CodeSyntaxHighlighter>
\`\`\`
> **Note**: Languages are loaded lazily for optimal performance!`,
};
export default {
title: 'Components/CodeSyntaxHighlighter',
component: CodeSyntaxHighlighter,
parameters: {
docs: {
description: {
component:
"A themed syntax highlighter component that automatically adapts to Superset's light/dark themes and supports lazy loading of languages.",
},
},
},
};
// Gallery showing all supported languages
export const LanguageGallery = () => (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{languages.map(language => (
<div key={language}>
<Title
level={3}
style={{ textTransform: 'capitalize', marginBottom: 16 }}
>
{language.toUpperCase()} Example
</Title>
<CodeSyntaxHighlighter language={language}>
{sampleCode[language]}
</CodeSyntaxHighlighter>
</div>
))}
</Space>
);
// Interactive playground
export const InteractivePlayground = (args: CodeSyntaxHighlighterProps) => (
<CodeSyntaxHighlighter {...args}>
{args.children || sampleCode[args.language || 'sql']}
</CodeSyntaxHighlighter>
);
InteractivePlayground.args = {
language: 'sql',
showLineNumbers: false,
wrapLines: true,
children: sampleCode.sql,
};
InteractivePlayground.argTypes = {
language: {
control: { type: 'select' },
options: languages,
description: 'Programming language for syntax highlighting',
},
showLineNumbers: {
control: { type: 'boolean' },
description: 'Display line numbers alongside the code',
},
wrapLines: {
control: { type: 'boolean' },
description: 'Wrap long lines instead of showing horizontal scroll',
},
children: {
control: { type: 'text' },
description: 'Code content to highlight',
},
customStyle: {
control: { type: 'object' },
description: 'Custom CSS styles to apply to the syntax highlighter',
},
};
// Showcase different styling options
export const StylingExamples = () => (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* Default styling */}
<div>
<Title level={3}>Default Styling</Title>
<CodeSyntaxHighlighter language="sql">
SELECT id, name FROM users WHERE active = true;
</CodeSyntaxHighlighter>
</div>
{/* With line numbers */}
<div>
<Title level={3}>With Line Numbers</Title>
<CodeSyntaxHighlighter language="sql" showLineNumbers>
{sampleCode.sql}
</CodeSyntaxHighlighter>
</div>
{/* Custom styling */}
<div>
<Title level={3}>Custom Styling (Compact)</Title>
<CodeSyntaxHighlighter
language="json"
customStyle={{
fontSize: '12px',
padding: '12px',
maxHeight: '200px',
overflow: 'auto',
}}
>
{sampleCode.json}
</CodeSyntaxHighlighter>
</div>
{/* No line wrapping */}
<div>
<Title level={3}>No Line Wrapping</Title>
<CodeSyntaxHighlighter
language="sql"
wrapLines={false}
customStyle={{ maxWidth: '400px' }}
>
{`SELECT very_long_column_name, another_very_long_column_name, yet_another_extremely_long_column_name FROM very_long_table_name WHERE condition = 'this is a very long condition';`}
</CodeSyntaxHighlighter>
</div>
</Space>
);
// Performance and edge cases
export const EdgeCases = () => (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{/* Very long single line */}
<div>
<Title level={3}>Very Long Single Line</Title>
<CodeSyntaxHighlighter language="sql">
{`SELECT ${'very_long_column_name, '.repeat(20)}id FROM users;`}
</CodeSyntaxHighlighter>
</div>
{/* Special characters */}
<div>
<Title level={3}>Special Characters & Escaping</Title>
<CodeSyntaxHighlighter language="sql">
{`SELECT * FROM "table-name" WHERE field = 'O\\'Brien' AND data = '{"key": "value"}';`}
</CodeSyntaxHighlighter>
</div>
{/* Multiple languages showcase */}
<div>
<Title level={3}>Quick Language Comparison</Title>
<Flex gap="middle">
<div style={{ flex: 1 }}>
<Title level={4}>SQL</Title>
<CodeSyntaxHighlighter language="sql">
SELECT id, name FROM users;
</CodeSyntaxHighlighter>
</div>
<div style={{ flex: 1 }}>
<Title level={4}>JSON</Title>
<CodeSyntaxHighlighter language="json">
{`{"users": [{"id": 1, "name": "John"}]}`}
</CodeSyntaxHighlighter>
</div>
</Flex>
</div>
</Space>
);
// Theme testing helper
export const ThemeShowcase = () => (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Paragraph>
<Text strong>Theme Testing:</Text> Switch between light and dark themes in
Storybook to see automatic adaptation.
</Paragraph>
<Space direction="vertical" size="large" style={{ width: '100%' }}>
{languages.map(language => (
<div key={language}>
<Title level={4} style={{ textTransform: 'uppercase' }}>
{language}
</Title>
<CodeSyntaxHighlighter language={language}>
{sampleCode[language].split('\n').slice(0, 5).join('\n')}
</CodeSyntaxHighlighter>
</div>
))}
</Space>
</Space>
);

View File

@@ -1,156 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen } from '../../spec';
import CodeSyntaxHighlighter from './index';
// Simple mock that just returns the content
jest.mock(
'react-syntax-highlighter/dist/cjs/light',
() =>
function MockSyntaxHighlighter({ children, ...props }: any) {
return (
<pre data-testid="syntax-highlighter" data-language={props.language}>
{children}
</pre>
);
},
);
// Mock the language modules
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/sql',
() => 'sql-mock',
);
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/json',
() => 'json-mock',
);
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars',
() => 'html-mock',
);
jest.mock(
'react-syntax-highlighter/dist/cjs/languages/hljs/markdown',
() => 'md-mock',
);
// Mock the styles
jest.mock('react-syntax-highlighter/dist/cjs/styles/hljs/github', () => ({}));
jest.mock(
'react-syntax-highlighter/dist/cjs/styles/hljs/atom-one-dark',
() => ({}),
);
describe('CodeSyntaxHighlighter', () => {
it('renders code content', () => {
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('renders with default SQL language', () => {
render(<CodeSyntaxHighlighter>SELECT * FROM users;</CodeSyntaxHighlighter>);
// Should show content (the important thing is content is visible)
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('renders with specified language', () => {
render(
<CodeSyntaxHighlighter language="json">
{`{ "key": "value" }`}
</CodeSyntaxHighlighter>,
);
// Should show content regardless of which element renders it
expect(screen.getByText('{ "key": "value" }')).toBeInTheDocument();
});
it('supports all expected languages', () => {
const languages = ['sql', 'json', 'htmlbars', 'markdown'] as const;
languages.forEach(language => {
const { unmount } = render(
<CodeSyntaxHighlighter language={language}>
{`Test content for ${language}`}
</CodeSyntaxHighlighter>,
);
// Should render the content (either in fallback or syntax highlighter)
expect(
screen.getByText(`Test content for ${language}`),
).toBeInTheDocument();
unmount();
});
});
it('renders fallback pre element initially', () => {
render(
<CodeSyntaxHighlighter language="sql">
SELECT COUNT(*) FROM table;
</CodeSyntaxHighlighter>,
);
// Should render the content in some form
expect(screen.getByText('SELECT COUNT(*) FROM table;')).toBeInTheDocument();
});
it('handles special characters', () => {
const specialContent = "SELECT * FROM `users` WHERE name = 'O\\'Brien';";
render(
<CodeSyntaxHighlighter language="sql">
{specialContent}
</CodeSyntaxHighlighter>,
);
expect(screen.getByText(specialContent)).toBeInTheDocument();
});
it('accepts custom styles', () => {
render(
<CodeSyntaxHighlighter language="sql" customStyle={{ fontSize: '16px' }}>
SELECT * FROM users;
</CodeSyntaxHighlighter>,
);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('accepts showLineNumbers prop', () => {
render(
<CodeSyntaxHighlighter language="sql" showLineNumbers>
SELECT * FROM users;
</CodeSyntaxHighlighter>,
);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
it('accepts wrapLines prop', () => {
render(
<CodeSyntaxHighlighter language="sql" wrapLines={false}>
SELECT * FROM users;
</CodeSyntaxHighlighter>,
);
expect(screen.getByText('SELECT * FROM users;')).toBeInTheDocument();
});
});

View File

@@ -1,149 +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 { useEffect, useState } from 'react';
import SyntaxHighlighterBase from 'react-syntax-highlighter/dist/cjs/light';
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import tomorrow from 'react-syntax-highlighter/dist/cjs/styles/hljs/tomorrow-night';
import { themeObject } from '@superset-ui/core';
export type SupportedLanguage = 'sql' | 'htmlbars' | 'markdown' | 'json';
export interface CodeSyntaxHighlighterProps {
children: string;
language?: SupportedLanguage;
customStyle?: React.CSSProperties;
showLineNumbers?: boolean;
wrapLines?: boolean;
style?: any; // Override theme style if needed
}
// Track which languages have been registered to avoid duplicate registrations
const registeredLanguages = new Set<SupportedLanguage>();
// Language import functions - these will be called lazily
const languageImporters = {
sql: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/sql'),
htmlbars: () =>
import('react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'),
markdown: () =>
import('react-syntax-highlighter/dist/cjs/languages/hljs/markdown'),
json: () => import('react-syntax-highlighter/dist/cjs/languages/hljs/json'),
};
/**
* Lazily register a language for syntax highlighting
*/
const registerLanguage = async (language: SupportedLanguage): Promise<void> => {
if (registeredLanguages.has(language)) {
return; // Already registered
}
try {
const languageModule = await languageImporters[language]();
SyntaxHighlighterBase.registerLanguage(language, languageModule.default);
registeredLanguages.add(language);
} catch (error) {
console.warn(`Failed to load language ${language}:`, error);
}
};
/**
* A themed syntax highlighter component that automatically adapts to Superset's current theme.
* Supports light/dark mode switching and provides consistent styling across the application.
* Languages are loaded lazily to improve initial page load performance.
* Uses ultra-neutral themes for professional, consistent appearance.
*/
export const CodeSyntaxHighlighter: React.FC<CodeSyntaxHighlighterProps> = ({
children,
language = 'sql',
customStyle = {},
showLineNumbers = false,
wrapLines = true,
style: overrideStyle,
}) => {
const [isLanguageReady, setIsLanguageReady] = useState(
registeredLanguages.has(language),
);
useEffect(() => {
const loadLanguage = async () => {
if (!registeredLanguages.has(language)) {
await registerLanguage(language);
setIsLanguageReady(true);
}
};
loadLanguage();
}, [language]);
const isDark = themeObject.isThemeDark();
const themeStyle = overrideStyle || (isDark ? tomorrow : github);
const defaultCustomStyle: React.CSSProperties = {
background: themeObject.theme.colorBgElevated,
padding: themeObject.theme.sizeUnit * 4,
border: 0,
borderRadius: themeObject.theme.borderRadius,
...customStyle,
};
// Show a simple pre-formatted text while language is loading
if (!isLanguageReady) {
return (
<pre
style={{
...defaultCustomStyle,
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
margin: 0,
}}
>
{children}
</pre>
);
}
return (
<SyntaxHighlighterBase
language={language}
style={themeStyle}
customStyle={defaultCustomStyle}
showLineNumbers={showLineNumbers}
wrapLines={wrapLines}
>
{children}
</SyntaxHighlighterBase>
);
};
/**
* Utility function to preload specific languages if needed
* This can be called strategically in components that know they'll need certain languages
*/
export const preloadLanguages = async (
languages: SupportedLanguage[],
): Promise<void> => {
const promises = languages
.filter(lang => !registeredLanguages.has(lang))
.map(registerLanguage);
await Promise.all(promises);
};
export default CodeSyntaxHighlighter;

View File

@@ -38,7 +38,7 @@ const StyledCollapse = styled((props: CollapseProps) => (
${({ expandIconPosition }) =>
expandIconPosition &&
expandIconPosition === 'end' &&
expandIconPosition === 'right' &&
`
.anticon.anticon-right.ant-collapse-arrow > svg {
transform: rotate(90deg) !important;
@@ -72,7 +72,7 @@ const StyledCollapse = styled((props: CollapseProps) => (
.ant-collapse-header {
${({ expandIconPosition }) =>
expandIconPosition &&
expandIconPosition === 'end' &&
expandIconPosition === 'right' &&
`
.anticon.anticon-right.ant-collapse-arrow > svg {
transform: rotate(-90deg) !important;

View File

@@ -62,12 +62,12 @@ test('Calling "onHide"', async () => {
expect(props.onConfirm).toHaveBeenCalledTimes(0);
// type "del" in the input
userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
await userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
expect(screen.getByTestId('delete-modal-input')).toHaveValue('del');
// close the modal
expect(screen.getByTestId('close-modal-btn')).toBeInTheDocument();
userEvent.click(screen.getByTestId('close-modal-btn'));
expect(screen.getByText('×')).toBeInTheDocument();
await userEvent.click(screen.getByText('×'));
expect(props.onHide).toHaveBeenCalledTimes(1);
expect(props.onConfirm).toHaveBeenCalledTimes(0);

View File

@@ -89,7 +89,7 @@ export const MenuDotsDropdown = ({
iconOrientation = IconOrientation.Vertical,
...rest
}: MenuDotsDropdownProps) => (
<AntdDropdown popupRender={() => overlay} {...rest}>
<AntdDropdown dropdownRender={() => overlay} {...rest}>
<MenuDotsWrapper data-test="dropdown-trigger">
{RenderIcon(iconOrientation)}
</MenuDotsWrapper>

View File

@@ -23,7 +23,7 @@ import { Tooltip } from '../Tooltip';
import type { DropdownButtonProps } from './types';
export const DropdownButton = ({
popupRender,
dropdownRender,
tooltip,
tooltipPlacement,
children,
@@ -51,7 +51,7 @@ export const DropdownButton = ({
`;
const button = (
<Dropdown.Button
popupRender={popupRender}
dropdownRender={dropdownRender}
{...rest}
css={[
defaultBtnCss,

View File

@@ -17,10 +17,8 @@
* under the License.
*/
import {
CSSProperties,
cloneElement,
forwardRef,
ReactElement,
RefObject,
useEffect,
useImperativeHandle,
@@ -28,85 +26,17 @@ import {
useMemo,
useState,
useRef,
ReactNode,
useCallback,
} from 'react';
import { Global } from '@emotion/react';
import { css, t, useTheme, usePrevious } from '@superset-ui/core';
import { useResizeDetector } from 'react-resize-detector';
import { Badge, Icons, Button, Tooltip, Popover } from '..';
/**
* Container item.
*/
export interface DropdownItem {
/**
* String that uniquely identifies the item.
*/
id: string;
/**
* The element to be rendered.
*/
element: ReactElement;
}
/**
* Horizontal container that displays overflowed items in a dropdown.
* It shows an indicator of how many items are currently overflowing.
*/
export interface DropdownContainerProps {
/**
* Array of items. The id property is used to uniquely identify
* the elements when rendering or dealing with event handlers.
*/
items: DropdownItem[];
/**
* Event handler called every time an element moves between
* main container and dropdown.
*/
onOverflowingStateChange?: (overflowingState: {
notOverflowed: string[];
overflowed: string[];
}) => void;
/**
* Option to customize the content of the dropdown.
*/
dropdownContent?: (overflowedItems: DropdownItem[]) => ReactElement;
/**
* Dropdown ref.
*/
dropdownRef?: RefObject<HTMLDivElement>;
/**
* Dropdown additional style properties.
*/
dropdownStyle?: CSSProperties;
/**
* Displayed count in the dropdown trigger.
*/
dropdownTriggerCount?: number;
/**
* Icon of the dropdown trigger.
*/
dropdownTriggerIcon?: ReactElement;
/**
* Text of the dropdown trigger.
*/
dropdownTriggerText?: string;
/**
* Text of the dropdown trigger tooltip
*/
dropdownTriggerTooltip?: ReactNode | null;
/**
* Main container additional style properties.
*/
style?: CSSProperties;
/**
* Force render popover content before it's first opened
*/
forceRender?: boolean;
}
export type DropdownRef = HTMLDivElement & { open: () => void };
import type {
DropdownContainerProps,
DropdownItem,
DropdownRef,
} from './types';
const MAX_HEIGHT = 500;
@@ -143,37 +73,6 @@ export const DropdownContainer = forwardRef(
const [showOverflow, setShowOverflow] = useState(false);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = useCallback(() => {
const mainItemsContainerNode = current?.children.item(0);
if (mainItemsContainerNode) {
const visibleChildrenElements = Array.from(
mainItemsContainerNode.children,
);
setItemsWidth(prevGlobalWidths => {
if (prevGlobalWidths.length !== items.length) {
return prevGlobalWidths;
}
const newGlobalWidths = [...prevGlobalWidths];
let changed = false;
visibleChildrenElements.forEach((child, indexInVisible) => {
const originalItemIndex = indexInVisible;
if (originalItemIndex < newGlobalWidths.length) {
const newWidth = child.getBoundingClientRect().width;
if (newGlobalWidths[originalItemIndex] !== newWidth) {
newGlobalWidths[originalItemIndex] = newWidth;
changed = true;
}
}
});
return changed ? newGlobalWidths : prevGlobalWidths;
});
}
}, [current?.children, items.length]);
const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
items.reduce(
([items, ids], item) => {
@@ -223,7 +122,24 @@ export const DropdownContainer = forwardRef(
childrenArray.map(child => resizeObserver.unobserve(child));
resizeObserver.disconnect();
};
}, [items.length, current, recalculateItemWidths]);
}, [items.length]);
// callback to update item widths so that the useLayoutEffect runs whenever
// width of any of the child changes
const recalculateItemWidths = () => {
const container = current?.children.item(0);
if (container) {
const { children } = container;
const childrenArray = Array.from(children);
const currentWidths = childrenArray.map(
child => child.getBoundingClientRect().width,
);
// Update state with new widths
setItemsWidth(currentWidths);
}
};
useLayoutEffect(() => {
if (popoverVisible) {
@@ -290,7 +206,6 @@ export const DropdownContainer = forwardRef(
overflowedItems.length,
previousWidth,
width,
popoverVisible,
]);
useEffect(() => {
@@ -461,3 +376,5 @@ export const DropdownContainer = forwardRef(
);
},
);
export type { DropdownItem, DropdownRef };

View File

@@ -64,14 +64,14 @@ export const FaveStar = ({
<Icons.StarFilled
aria-label="starred"
iconSize="l"
iconColor={theme.colorWarning}
iconColor={theme.colors.warning.base}
name="favorite-selected"
/>
) : (
<Icons.StarOutlined
aria-label="unstarred"
iconSize="l"
iconColor={theme.colorTextTertiary}
iconColor={theme.colors.grayscale.light1}
name="favorite-unselected"
/>
)}

View File

@@ -25,7 +25,7 @@ import { BaseIconComponent } from './BaseIcon';
const AsyncIcon = (props: IconType) => {
const [, setLoaded] = useState(false);
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
const { fileName, ...restProps } = props;
const { fileName } = props;
useEffect(() => {
let cancelled = false;
@@ -46,7 +46,7 @@ const AsyncIcon = (props: IconType) => {
return (
<BaseIconComponent
component={ImportedSVG.current || TransparentIcon}
{...restProps}
{...props}
/>
);
};

View File

@@ -43,12 +43,11 @@ export const BaseIconComponent: React.FC<
iconSize,
viewBox,
customIcons,
fileName,
...rest
}) => {
const theme = useTheme();
const whatRole = rest?.onClick ? 'button' : 'img';
const ariaLabel = genAriaLabel(fileName || '');
const ariaLabel = genAriaLabel(rest.fileName || '');
const style = {
color: iconColor,
fontSize: iconSize

View File

@@ -37,7 +37,7 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
datasetType === 'physical' ? (
<Icons.InsertRowAboveOutlined
iconSize={SIZE}
iconColor={theme.colorPrimary}
iconColor={theme.colors.primary.dark1}
/>
) : (
<Icons.ConsoleSqlOutlined iconSize={SIZE} />

View File

@@ -165,10 +165,8 @@ test('renders clickable items with blue icons when the bar is collapsed', async
const images = screen.getAllByRole('img');
const clickableColor = window.getComputedStyle(images[0]).color;
const nonClickableColor = window.getComputedStyle(images[1]).color;
expect(clickableColor).toBe(hexToRgb(supersetTheme.colorPrimary));
expect(nonClickableColor.replace(/\s+/g, '')).toBe(
supersetTheme.colorTextTertiary.replace(/\s+/g, ''),
);
expect(clickableColor).toBe(hexToRgb(supersetTheme.colors.primary.base));
expect(nonClickableColor).toBe(hexToRgb(supersetTheme.colorText));
});
});

View File

@@ -80,7 +80,7 @@ const StyledItem = styled.div<{
padding-right: ${last ? 0 : SPACE_BETWEEN_ITEMS}px;
cursor: ${onClick ? 'pointer' : 'default'};
& .metadata-icon {
color: ${onClick && collapsed ? theme.colorPrimary : theme.colorTextTertiary};
color: ${onClick && collapsed ? theme.colorPrimary : theme.colorTextBase};
padding-right: ${collapsed ? 0 : ICON_PADDING}px;
& .anticon {
line-height: 0;

View File

@@ -18,7 +18,7 @@
*/
import { isValidElement, cloneElement, useMemo, useRef, useState } from 'react';
import { isNil } from 'lodash';
import { css, styled, t, useTheme } from '@superset-ui/core';
import { css, styled, t } from '@superset-ui/core';
import { Modal as AntdModal, ModalProps as AntdModalProps } from 'antd';
import { Resizable } from 're-resizable';
import Draggable, {
@@ -26,7 +26,6 @@ import Draggable, {
DraggableData,
DraggableEvent,
} from 'react-draggable';
import { Icons } from '../Icons';
import { Button } from '../Button';
import type { ModalProps, StyledModalProps } from './types';
@@ -46,16 +45,8 @@ export const BaseModal = (props: AntdModalProps) => (
);
export const StyledModal = styled(BaseModal)<StyledModalProps>`
${({
theme,
responsive,
maxWidth,
resizable,
height,
draggable,
hideFooter,
}) => css`
${responsive &&
${({ theme, responsive, maxWidth }) =>
responsive &&
css`
max-width: ${maxWidth ?? '900px'};
padding-left: ${theme.sizeUnit * 3}px;
@@ -64,120 +55,120 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
top: 0;
`}
.ant-modal-content {
background-color: ${theme.colorBgContainer};
display: flex;
flex-direction: column;
max-height: calc(100vh - ${theme.sizeUnit * 8}px);
margin-bottom: ${theme.sizeUnit * 4}px;
margin-top: ${theme.sizeUnit * 4}px;
padding: 0;
}
.ant-modal-header {
flex: 0 0 auto;
border-radius: ${theme.borderRadius}px ${theme.borderRadius}px 0 0;
padding: ${theme.sizeUnit * 4}px ${theme.sizeUnit * 6}px;
.ant-modal-title {
font-weight: ${theme.fontWeightStrong};
}
.ant-modal-title h4 {
display: flex;
margin: 0;
align-items: center;
}
}
.ant-modal-close {
width: ${theme.sizeUnit * 14}px;
height: ${theme.sizeUnit * 14}px;
padding: ${theme.sizeUnit * 6}px ${theme.sizeUnit * 4}px
${theme.sizeUnit * 4}px;
top: 0;
right: 0;
display: flex;
justify-content: center;
}
.ant-modal-close:hover {
background: transparent;
}
.ant-modal-close-x {
.ant-modal-content {
background-color: ${({ theme }) => theme.colorBgContainer};
display: flex;
flex-direction: column;
max-height: ${({ theme }) => `calc(100vh - ${theme.sizeUnit * 8}px)`};
margin-bottom: ${({ theme }) => theme.sizeUnit * 4}px;
margin-top: ${({ theme }) => theme.sizeUnit * 4}px;
padding: 0;
}
.ant-modal-header {
flex: 0 0 auto;
border-radius: ${({ theme }) => theme.borderRadius}px
${({ theme }) => theme.borderRadius}px 0 0;
padding: ${({ theme }) => theme.sizeUnit * 4}px
${({ theme }) => theme.sizeUnit * 6}px;
.ant-modal-title {
font-weight: ${({ theme }) => theme.fontWeightStrong};
}
.ant-modal-title h4 {
display: flex;
margin: 0;
align-items: center;
[data-test='close-modal-btn'] {
justify-content: center;
}
.close {
flex: 1 1 auto;
margin-bottom: ${theme.sizeUnit}px;
color: ${theme.colorPrimaryText};
font-weight: ${theme.fontWeightLight};
}
}
}
.ant-modal-close {
width: ${({ theme }) => theme.sizeUnit * 14}px;
height: ${({ theme }) => theme.sizeUnit * 14}px;
top: 0;
right: 0;
}
.ant-modal-close:hover {
background: transparent;
}
.ant-modal-close-x {
display: flex;
align-items: center;
.close {
flex: 1 1 auto;
margin-bottom: ${({ theme }) => theme.sizeUnit}px;
color: ${({ theme }) => theme.colorPrimaryText};
font-size: 32px;
font-weight: ${({ theme }) => theme.fontWeightLight};
}
}
.ant-modal-body {
flex: 0 1 auto;
padding: ${({ theme }) => theme.sizeUnit * 4}px;
overflow: auto;
${({ resizable, height }) => !resizable && height && `height: ${height};`}
}
.ant-modal-footer {
flex: 0 0 1;
border-top: ${({ theme }) => theme.sizeUnit / 4}px solid
${({ theme }) => theme.colorSplit};
padding: ${({ theme }) => theme.sizeUnit * 4}px;
margin-top: 0;
.btn {
font-size: 12px;
}
.ant-modal-body {
flex: 0 1 auto;
padding: ${theme.sizeUnit * 4}px;
overflow: auto;
${!resizable && height && `height: ${height};`}
.btn + .btn {
margin-left: ${({ theme }) => theme.sizeUnit * 2}px;
}
}
.ant-modal-footer {
flex: 0 0 1;
border-top: ${theme.sizeUnit / 4}px solid ${theme.colorSplit};
padding: ${theme.sizeUnit * 4}px;
margin-top: 0;
&.no-content-padding .ant-modal-body {
padding: 0;
}
.btn {
font-size: 12px;
}
.btn + .btn {
margin-left: ${theme.sizeUnit * 2}px;
}
}
&.no-content-padding .ant-modal-body {
${({ draggable, theme }) =>
draggable &&
`
.ant-modal-header {
padding: 0;
}
${draggable &&
css`
.ant-modal-header {
padding: 0;
.draggable-trigger {
.draggable-trigger {
cursor: move;
padding: ${theme.sizeUnit * 4}px;
width: 100%;
}
}
`};
${({ resizable, hideFooter }) =>
resizable &&
`
.resizable {
pointer-events: all;
.resizable-wrapper {
height: 100%;
}
`}
${resizable &&
css`
.resizable {
pointer-events: all;
.ant-modal-content {
height: 100%;
.resizable-wrapper {
height: 100%;
}
.ant-modal-content {
height: 100%;
.ant-modal-body {
height: ${hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`};
.ant-modal-body {
/* 100% - header height - footer height */
height: ${
hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px);`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px);`
}
}
}
`}
}
`}
`;
@@ -223,14 +214,13 @@ const CustomModal = ({
resizable = false,
resizableConfig = defaultResizableConfig(hideFooter),
draggableConfig,
destroyOnHidden,
destroyOnClose,
openerRef,
...rest
}: ModalProps) => {
const draggableRef = useRef<HTMLDivElement>(null);
const [bounds, setBounds] = useState<DraggableBounds>();
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
const theme = useTheme();
const handleOnHide = () => {
openerRef?.current?.focus();
@@ -238,16 +228,13 @@ const CustomModal = ({
};
let FooterComponent;
// This safely avoids injecting "closeModal" into native elements like <div> or <span>
if (isValidElement(footer) && typeof footer.type === 'function')
if (isValidElement(footer)) {
// If a footer component is provided inject a closeModal function
// so the footer can provide a "close" button if desired
FooterComponent = cloneElement(footer, {
closeModal: handleOnHide,
} as Partial<unknown>);
else FooterComponent = footer;
}
const modalFooter = isNil(FooterComponent)
? [
<Button
@@ -322,13 +309,9 @@ const CustomModal = ({
open={show}
title={<ModalTitle />}
closeIcon={
<Icons.CloseOutlined
iconColor={theme.colorText}
iconSize="l"
data-test="close-modal-btn"
className="close"
aria-hidden="true"
/>
<span className="close" aria-hidden="true">
×
</span>
}
footer={!hideFooter ? modalFooter : null}
hideFooter={hideFooter}
@@ -358,7 +341,7 @@ const CustomModal = ({
mask={shouldShowMask}
draggable={draggable}
resizable={resizable}
destroyOnHidden={destroyOnHidden}
destroyOnClose={destroyOnClose}
{...rest}
>
{children}

View File

@@ -34,7 +34,7 @@ export interface ModalProps {
show: boolean;
name?: string;
title: ReactNode;
width?: string | number;
width?: string;
maxWidth?: string;
responsive?: boolean;
hideFooter?: boolean;
@@ -47,7 +47,7 @@ export interface ModalProps {
resizableConfig?: ResizableProps;
draggable?: boolean;
draggableConfig?: DraggableProps;
destroyOnHidden?: boolean;
destroyOnClose?: boolean;
maskClosable?: boolean;
zIndex?: number;
bodyStyle?: CSSProperties;

View File

@@ -39,7 +39,7 @@ export interface ModalTriggerProps {
resizableConfig?: any;
draggable?: boolean;
draggableConfig?: any;
destroyOnHidden?: boolean;
destroyOnClose?: boolean;
}
export interface ModalTriggerRef {
@@ -63,7 +63,7 @@ export const ModalTrigger = forwardRef(
tooltip,
modalFooter,
triggerNode,
destroyOnHidden = true,
destroyOnClose = true,
modalBody,
draggableConfig = {},
resizableConfig = {},
@@ -120,7 +120,7 @@ export const ModalTrigger = forwardRef(
resizableConfig={resizableConfig}
draggable={draggable}
draggableConfig={draggableConfig}
destroyOnHidden={destroyOnHidden}
destroyOnClose={destroyOnClose}
>
{modalBody}
</Modal>

View File

@@ -34,7 +34,7 @@ export const menuTriggerStyles = (theme: SupersetTheme) => css`
width: ${theme.sizeUnit * 8}px;
height: ${theme.sizeUnit * 8}px;
padding: 0;
border: 1px solid ${theme.colorPrimary};
border: 1px solid ${theme.colors.primary.dark2};
&.ant-btn > span.anticon {
line-height: 0;
@@ -151,7 +151,7 @@ export const PageHeaderWithActions = ({
{showMenuDropdown && (
<Dropdown
trigger={['click']}
popupRender={() => additionalActionsMenu}
dropdownRender={() => additionalActionsMenu}
{...menuDropdownProps}
>
<Button
@@ -163,7 +163,7 @@ export const PageHeaderWithActions = ({
data-test="actions-trigger"
>
<Icons.EllipsisOutlined
iconColor={theme.colorPrimary}
iconColor={theme.colors.primary.dark2}
iconSize="l"
/>
</Button>

View File

@@ -45,7 +45,7 @@ interface HandleSelectProps {
}
const menuItemStyles = (theme: any) => css`
&.ant-menu-item {
&.antd5-menu-item {
height: auto;
line-height: 1.4;

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import {
Radio as AntRadio,
Radio as Antd5Radio,
type CheckboxOptionType,
type RadioGroupProps,
} from 'antd';
@@ -40,19 +40,19 @@ const RadioGroup = ({
...props
}: RadioGroupWrapperProps) => {
const content = options.map((option: CheckboxOptionType) => (
<AntRadio key={option.value} value={option.value}>
<Antd5Radio key={option.value} value={option.value}>
{option.label}
</AntRadio>
</Antd5Radio>
));
return (
<AntRadio.Group {...props}>
<Antd5Radio.Group {...props}>
{spaceConfig ? <Space {...spaceConfig}>{content}</Space> : content}
</AntRadio.Group>
</Antd5Radio.Group>
);
};
export const Radio = Object.assign(AntRadio, {
export const Radio = Object.assign(Antd5Radio, {
GroupWrapper: RadioGroup,
Button: AntRadio.Button,
Button: Antd5Radio.Button,
});
export type {
RadioChangeEvent,

View File

@@ -1,21 +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.
*/
export { Result } from 'antd';
export type { ResultProps } from 'antd';

View File

@@ -129,7 +129,7 @@ const AsyncSelect = forwardRef(
onError,
onChange,
onClear,
onOpenChange,
onDropdownVisibleChange,
onDeselect,
onSearch,
onSelect,
@@ -441,12 +441,12 @@ const AsyncSelect = forwardRef(
}
}
if (onOpenChange) {
onOpenChange(isDropdownVisible);
if (onDropdownVisibleChange) {
onDropdownVisibleChange(isDropdownVisible);
}
};
const popupRender = (
const dropdownRender = (
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
) =>
dropDownRenderHelper(
@@ -605,7 +605,7 @@ const AsyncSelect = forwardRef(
}
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
popupRender={popupRender}
dropdownRender={dropdownRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
getPopupContainer={
@@ -618,7 +618,7 @@ const AsyncSelect = forwardRef(
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
onOpenChange={handleOnDropdownVisibleChange}
onDropdownVisibleChange={handleOnDropdownVisibleChange}
// @ts-ignore
onPaste={onPaste}
onPopupScroll={handlePagination}

View File

@@ -17,7 +17,6 @@
* under the License.
*/
import { StoryObj } from '@storybook/react';
import { noop } from 'lodash';
import { SelectOptionsType, SelectProps } from './types';
import { Select } from '.';
@@ -94,26 +93,23 @@ export const InteractiveSelect: StoryObj = {
options,
optionsCount,
...args
}: SelectProps & { header: string; optionsCount: number }) => {
noop(header);
return (
<div
style={{
width: DEFAULT_WIDTH,
}}
>
<Select
{...args}
options={
Array.isArray(options)
? generateOptions(options, optionsCount)
: options
}
mode="multiple"
/>
</div>
);
},
}: SelectProps & { header: string; optionsCount: number }) => (
<div
style={{
width: DEFAULT_WIDTH,
}}
>
<Select
{...args}
options={
Array.isArray(options)
? generateOptions(options, optionsCount)
: options
}
mode="multiple"
/>
</div>
),
args: {
autoFocus: true,
allowNewOptions: false,

View File

@@ -104,7 +104,7 @@ const Select = forwardRef(
onBlur,
onChange,
onClear,
onOpenChange,
onDropdownVisibleChange,
onDeselect,
onSearch,
onSelect,
@@ -398,8 +398,8 @@ const Select = forwardRef(
if (!isDropdownVisible) {
setSelectOptions(initialOptionsSorted);
}
if (onOpenChange) {
onOpenChange(isDropdownVisible);
if (onDropdownVisibleChange) {
onDropdownVisibleChange(isDropdownVisible);
}
};
@@ -492,7 +492,7 @@ const Select = forwardRef(
],
);
const popupRender = (
const dropdownRender = (
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
) =>
dropDownRenderHelper(
@@ -694,7 +694,7 @@ const Select = forwardRef(
}
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
popupRender={popupRender}
dropdownRender={dropdownRender}
filterOption={handleFilterOption}
filterSort={sortComparatorWithSearch}
getPopupContainer={
@@ -708,7 +708,7 @@ const Select = forwardRef(
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onBlur={handleOnBlur}
onDeselect={handleOnDeselect}
onOpenChange={handleOnDropdownVisibleChange}
onDropdownVisibleChange={handleOnDropdownVisibleChange}
// @ts-ignore
onPaste={onPaste}
onPopupScroll={undefined}

View File

@@ -45,7 +45,7 @@ export const StyledSelect = styled(Select, {
})<{ headerPosition?: string; oneLine?: boolean }>`
${({ theme, headerPosition, oneLine }) => `
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
outline: 2px solid ${theme.colorPrimary};
outline: 2px solid ${theme.colors.primary.base};
outline-offset: -2px;
}
flex: ${headerPosition === 'left' ? 1 : 0};

View File

@@ -62,7 +62,7 @@ export type AntdExposedProps = Pick<
| 'onBlur'
| 'onPopupScroll'
| 'onSearch'
| 'onOpenChange'
| 'onDropdownVisibleChange'
| 'optionRender'
| 'placeholder'
| 'showArrow'
@@ -95,7 +95,7 @@ export interface BaseSelectProps extends AntdExposedProps {
/**
* Renders the dropdown
*/
popupRender?: (
dropdownRender?: (
menu: ReactElement<any, string | JSXElementConstructor<any>>,
) => ReactElement<any, string | JSXElementConstructor<any>>;
/**

View File

@@ -19,28 +19,28 @@
import { Tooltip } from 'antd';
import { Dropdown, Icons } from '@superset-ui/core/components';
import { t } from '@superset-ui/core';
import { ThemeAlgorithm, ThemeMode } from '../../theme/types';
import { ThemeMode } from '../../theme/types';
export interface ThemeSelectProps {
setThemeMode: (newMode: ThemeMode) => void;
changeThemeMode: (newMode: ThemeMode) => void;
tooltipTitle?: string;
themeMode: ThemeMode;
}
const ThemeSelect: React.FC<ThemeSelectProps> = ({
setThemeMode,
changeThemeMode,
tooltipTitle = 'Select theme',
themeMode,
}) => {
const handleSelect = (mode: ThemeMode) => {
setThemeMode(mode);
changeThemeMode(mode);
};
const themeIconMap: Record<ThemeAlgorithm | ThemeMode, React.ReactNode> = {
[ThemeAlgorithm.DEFAULT]: <Icons.SunOutlined />,
[ThemeAlgorithm.DARK]: <Icons.MoonOutlined />,
const themeIconMap: Record<ThemeMode, React.ReactNode> = {
[ThemeMode.LIGHT]: <Icons.SunOutlined />,
[ThemeMode.DARK]: <Icons.MoonOutlined />,
[ThemeMode.SYSTEM]: <Icons.FormatPainterOutlined />,
[ThemeAlgorithm.COMPACT]: <Icons.CompressOutlined />,
[ThemeMode.COMPACT]: <Icons.CompressOutlined />,
};
return (
@@ -49,9 +49,9 @@ const ThemeSelect: React.FC<ThemeSelectProps> = ({
menu={{
items: [
{
key: ThemeMode.DEFAULT,
key: ThemeMode.LIGHT,
label: t('Light'),
onClick: () => handleSelect(ThemeMode.DEFAULT),
onClick: () => handleSelect(ThemeMode.LIGHT),
icon: <Icons.SunOutlined />,
},
{

View File

@@ -1,47 +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 { ReactElement } from 'react';
import { UnsavedChangesModal, type UnsavedChangesModalProps } from '.';
export default {
title: 'Components/UnsavedChangesModal',
component: UnsavedChangesModal,
};
export const InteractiveUnsavedChangesModal = (
props: UnsavedChangesModalProps,
): ReactElement => (
<UnsavedChangesModal {...props}>
If you don't save, changes will be lost.
</UnsavedChangesModal>
);
InteractiveUnsavedChangesModal.args = {
showModal: true,
onHide: () => {},
handleSave: () => {},
onConfirmNavigation: () => {},
title: 'Unsaved Changes',
};
InteractiveUnsavedChangesModal.argTypes = {
onHide: { action: 'onHide' },
handleSave: { action: 'handleSave' },
onConfirmNavigation: { action: 'onConfirmNavigation' },
};

View File

@@ -1,96 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { UnsavedChangesModal } from '.';
test('should render nothing if showModal is false', () => {
const { queryByRole } = render(
<UnsavedChangesModal
showModal={false}
onHide={() => {}}
handleSave={() => {}}
onConfirmNavigation={() => {}}
/>,
);
expect(queryByRole('dialog')).not.toBeInTheDocument();
});
test('should render the UnsavedChangesModal component if showModal is true', async () => {
const { queryByRole } = render(
<UnsavedChangesModal
showModal
onHide={() => {}}
handleSave={() => {}}
onConfirmNavigation={() => {}}
/>,
);
expect(queryByRole('dialog')).toBeInTheDocument();
});
test('should only call onConfirmNavigation when clicking the Discard button', async () => {
const mockOnHide = jest.fn();
const mockHandleSave = jest.fn();
const mockOnConfirmNavigation = jest.fn();
render(
<UnsavedChangesModal
showModal
onHide={mockOnHide}
handleSave={mockHandleSave}
onConfirmNavigation={mockOnConfirmNavigation}
/>,
);
const discardButton: HTMLElement = await screen.findByRole('button', {
name: /discard/i,
});
userEvent.click(discardButton);
expect(mockOnConfirmNavigation).toHaveBeenCalled();
expect(mockHandleSave).not.toHaveBeenCalled();
expect(mockOnHide).not.toHaveBeenCalled();
});
test('should only call handleSave when clicking the Save button', async () => {
const mockOnHide = jest.fn();
const mockHandleSave = jest.fn();
const mockOnConfirmNavigation = jest.fn();
render(
<UnsavedChangesModal
showModal
onHide={mockOnHide}
handleSave={mockHandleSave}
onConfirmNavigation={mockOnConfirmNavigation}
/>,
);
const saveButton: HTMLElement = await screen.findByRole('button', {
name: /save/i,
});
userEvent.click(saveButton);
expect(mockHandleSave).toHaveBeenCalled();
expect(mockOnHide).not.toHaveBeenCalled();
expect(mockOnConfirmNavigation).not.toHaveBeenCalled();
});

View File

@@ -1,129 +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 { t, styled, css } from '@superset-ui/core';
import { Icons, Modal, Typography } from '@superset-ui/core/components';
import { Button } from '@superset-ui/core/components/Button';
import type { FC, ReactElement } from 'react';
const StyledModalTitle = styled(Typography.Title)`
&& {
font-weight: 600;
margin: 0;
}
`;
const StyledModalBody = styled(Typography.Text)`
${({ theme }) => css`
padding: 0 ${theme.sizeUnit * 2}px;
&& {
margin: 0;
}
`}
`;
const StyledDiscardBtn = styled(Button)`
${({ theme }) => css`
min-width: ${theme.sizeUnit * 22}px;
height: ${theme.sizeUnit * 8}px;
`}
`;
const StyledSaveBtn = styled(Button)`
${({ theme }) => css`
min-width: ${theme.sizeUnit * 17}px;
height: ${theme.sizeUnit * 8}px;
span > :first-of-type {
margin-right: 0;
}
`}
`;
const StyledWarningIcon = styled(Icons.WarningOutlined)`
${({ theme }) => css`
color: ${theme.colorWarning};
margin-right: ${theme.sizeUnit * 4}px;
`}
`;
export type UnsavedChangesModalProps = {
showModal: boolean;
onHide: () => void;
handleSave: () => void;
onConfirmNavigation: () => void;
title?: string;
body?: string;
};
export const UnsavedChangesModal: FC<UnsavedChangesModalProps> = ({
showModal,
onHide,
handleSave,
onConfirmNavigation,
title = 'Unsaved Changes',
body = "If you don't save, changes will be lost.",
}: UnsavedChangesModalProps): ReactElement => (
<Modal
centered
responsive
onHide={onHide}
show={showModal}
width="444px"
title={
<div
css={css`
align-items: center;
display: flex;
`}
>
<StyledWarningIcon iconSize="xl" />
<StyledModalTitle type="secondary" level={5}>
{title}
</StyledModalTitle>
</div>
}
footer={
<div
css={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
<StyledDiscardBtn
htmlType="button"
buttonSize="small"
onClick={onConfirmNavigation}
>
{t('Discard')}
</StyledDiscardBtn>
<StyledSaveBtn
htmlType="button"
buttonSize="small"
buttonStyle="primary"
onClick={handleSave}
>
{t('Save')}
</StyledSaveBtn>
</div>
}
>
<StyledModalBody type="secondary">{body}</StyledModalBody>
</Modal>
);

View File

@@ -163,6 +163,4 @@ export * from './Steps';
export * from './Table';
export * from './TableView';
export * from './Tag';
export * from './UnsavedChangesModal';
export * from './constants';
export * from './Result';

View File

@@ -21,8 +21,6 @@ import { ExtensibleFunction } from '../models';
import { getNumberFormatter, NumberFormats } from '../number-format';
import { Currency } from '../query';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
interface CurrencyFormatterConfig {
d3Format?: string;
currency: Currency;

View File

@@ -40,7 +40,7 @@ export const buildCustomFormatters = (
const actualCurrencyFormat = currencyFormat?.symbol
? currencyFormat
: savedCurrencyFormats[metric];
return actualCurrencyFormat?.symbol
return actualCurrencyFormat
? {
...acc,
[metric]: new CurrencyFormatter({

View File

@@ -20,8 +20,6 @@ import { ExtensibleFunction } from '../models';
import { isRequired } from '../utils';
import { NumberFormatFunction } from './types';
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
export const PREVIEW_VALUE = 12345.432;
export interface NumberFormatterConfig {

View File

@@ -48,7 +48,6 @@ export const EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS: Record<
time_column: 'time_column',
time_grain: 'time_grain',
time_range: 'time_range',
time_compare: 'time_compare',
};
export const EXTRA_FORM_DATA_OVERRIDE_REGULAR_KEYS = Object.keys(

View File

@@ -31,7 +31,6 @@ type ExtraFilterQueryField = {
granularity_sqla?: string;
time_grain_sqla?: TimeGranularity;
granularity?: string;
time_compare?: string;
};
type ExtractedExtra = ExtraFilterQueryField & {
@@ -58,7 +57,6 @@ export default function extractExtras(formData: QueryFormData): ExtractedExtra {
__time_col: 'granularity_sqla',
__time_grain: 'time_grain_sqla',
__granularity: 'granularity',
__time_compare: 'time_compare',
};
(formData.extra_filters || []).forEach(filter => {

View File

@@ -71,8 +71,6 @@ export type QueryObjectExtras = Partial<{
where?: string;
/** Instant Time Comparison */
instant_time_comparison_range?: string;
time_compare?: string;
}>;
export type ResidualQueryObjectData = {

View File

@@ -122,7 +122,7 @@ export type ExtraFormDataAppend = {
* filter clauses can't be overridden */
export type ExtraFormDataOverrideExtras = Pick<
QueryObjectExtras,
'relative_start' | 'relative_end' | 'time_grain_sqla' | 'time_compare'
'relative_start' | 'relative_end' | 'time_grain_sqla'
>;
/** These parameters override those already present in the form data/query object */
@@ -131,7 +131,7 @@ export type ExtraFormDataOverrideRegular = Partial<
> &
Partial<Pick<SqlaFormData, 'granularity'>> &
Partial<Pick<BaseFormData, 'time_range'>> &
Partial<Pick<QueryObject, 'time_column' | 'time_grain' | 'time_compare'>>;
Partial<Pick<QueryObject, 'time_column' | 'time_grain'>>;
/** These parameters override those already present in the form data/query object */
export type ExtraFormDataOverride = ExtraFormDataOverrideRegular &

View File

@@ -30,8 +30,7 @@ export type TimeColumnConfigKey =
| '__time_col'
| '__time_grain'
| '__time_range'
| '__granularity'
| '__time_compare';
| '__granularity';
export type AppliedTimeExtras = Partial<
Record<TimeColumnConfigKey, keyof QueryObject>

View File

@@ -58,6 +58,17 @@ export const GlobalStyles = () => {
display: none !important;
}
.ant-dropdown,
.ant-dropdown,
.ant-select-dropdown,
.ant-modal-wrap,
.ant-modal-mask,
.ant-picker-dropdown,
.ant-popover,
.ant-popover {
z-index: ${theme.zIndexPopupBase} !important;
}
.no-wrap {
white-space: nowrap;
}

View File

@@ -18,7 +18,7 @@
*/
import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
import { AnyThemeConfig, ThemeAlgorithm } from './types';
import { AnyThemeConfig } 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(ThemeAlgorithm.DARK);
expect(parsedJson.algorithm).toBe('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(ThemeAlgorithm.DARK);
expect(serialized.algorithm).toBe('dark');
});
});
@@ -137,7 +137,7 @@ describe('Theme', () => {
// Verify the algorithm was updated
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
expect(serialized.algorithm).toBe('dark');
});
});
@@ -150,7 +150,7 @@ describe('Theme', () => {
// Verify dark algorithm is used
const serialized = theme.toSerializedConfig();
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
expect(serialized.algorithm).toBe('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(ThemeAlgorithm.DEFAULT);
expect(serialized.algorithm).toBe('default');
});
it('preserves other algorithms when toggling dark mode', () => {
@@ -181,11 +181,10 @@ 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(ThemeAlgorithm.DEFAULT);
expect(serialized.algorithm).toContain(ThemeAlgorithm.COMPACT);
expect(serialized.algorithm).not.toContain(ThemeAlgorithm.DARK);
expect(serialized.algorithm).toContain('default');
expect(serialized.algorithm).toContain('compact');
expect(serialized.algorithm).not.toContain('dark');
});
});
@@ -219,7 +218,7 @@ describe('Theme', () => {
const serialized = theme.toSerializedConfig();
expect(serialized.token?.colorPrimary).toBe('#ff0000');
expect(serialized.algorithm).toBe(ThemeAlgorithm.DARK);
expect(serialized.algorithm).toBe('dark');
});
});
});

View File

@@ -22,20 +22,12 @@ import React from 'react';
import { theme as antdThemeImport, ConfigProvider } from 'antd';
import tinycolor from 'tinycolor2';
// @fontsource/* v5.1+ doesn't play nice with eslint-import plugin v2.31+
/* eslint-disable import/extensions */
import '@fontsource/inter/200.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/400.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/500.css';
/* eslint-disable import/extensions */
import '@fontsource/inter/600.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/400.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/500.css';
/* eslint-disable import/extensions */
import '@fontsource/fira-code/600.css';
import {
@@ -43,7 +35,6 @@ import {
CacheProvider as EmotionCacheProvider,
} from '@emotion/react';
import createCache from '@emotion/cache';
import { noop } from 'lodash';
import { GlobalStyles } from './GlobalStyles';
import {
@@ -75,7 +66,7 @@ export class Theme {
brandLogoAlt: 'Apache Superset',
brandLogoUrl: '/static/assets/images/superset-logo-horiz.png',
brandLogoMargin: '18px',
brandLogoHref: '/',
brandLogoHref: 'https://superset.apache.org',
brandLogoHeight: '24px',
// Default colors
@@ -294,7 +285,6 @@ export class Theme {
antdConfig: AntdThemeConfig,
emotionCache: any,
): void {
noop(theme, antdConfig, emotionCache);
// Overridden at runtime by SupersetThemeProvider using setThemeState
}

View File

@@ -17,7 +17,7 @@
* under the License.
*/
/* eslint-disable theme-colors/no-literal-colors */
import { type SerializableThemeConfig, ThemeAlgorithm } from './types';
import { SerializableThemeConfig } from './types';
const exampleThemes: Record<string, SerializableThemeConfig> = {
superset: {
@@ -27,11 +27,11 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
},
supersetDark: {
token: {},
algorithm: ThemeAlgorithm.DARK,
algorithm: 'dark',
},
supersetCompact: {
token: {},
algorithm: ThemeAlgorithm.COMPACT,
algorithm: 'compact',
},
funky: {
token: {
@@ -43,7 +43,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
borderRadius: 12,
fontFamily: 'Comic Sans MS, cursive',
},
algorithm: ThemeAlgorithm.DEFAULT,
algorithm: 'default',
},
funkyDark: {
token: {
@@ -55,7 +55,7 @@ const exampleThemes: Record<string, SerializableThemeConfig> = {
borderRadius: 12,
fontFamily: 'Comic Sans MS, cursive',
},
algorithm: ThemeAlgorithm.DARK,
algorithm: 'dark',
},
};
export default exampleThemes;

View File

@@ -16,17 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import emotionStyled, { CreateStyled } from '@emotion/styled';
import emotionStyled from '@emotion/styled';
import { useTheme as useThemeBasic } from '@emotion/react';
// import { theme as antdThemeImport } from 'antd';
import { Theme } from './Theme';
import {
type SupersetTheme,
type SerializableThemeConfig,
type AnyThemeConfig,
type ThemeStorage,
type ThemeControllerOptions,
type ThemeContextType,
ThemeAlgorithm,
import type {
SupersetTheme,
SerializableThemeConfig,
AnyThemeConfig,
ThemeStorage,
ThemeControllerOptions,
ThemeContextType,
} from './types';
export {
@@ -56,12 +56,10 @@ export function useTheme() {
return theme;
}
const styled: CreateStyled = emotionStyled;
const styled = emotionStyled;
// launching in in dark mode for now while iterating
const themeObject: Theme = Theme.fromConfig({
algorithm: ThemeAlgorithm.DEFAULT,
});
const themeObject = Theme.fromConfig({ algorithm: 'default' });
const { theme } = themeObject;
const supersetTheme = theme;

View File

@@ -33,41 +33,6 @@ 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
@@ -75,7 +40,11 @@ export type ThemeAlgorithmOption =
export type SerializableThemeConfig = {
token?: Record<string, any>;
components?: Record<string, any>;
algorithm?: ThemeAlgorithmOption;
algorithm?:
| 'default'
| 'dark'
| 'compact'
| ('default' | 'dark' | 'compact')[];
hashed?: boolean;
inherit?: boolean;
};
@@ -389,6 +358,13 @@ 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. */
@@ -403,7 +379,7 @@ export interface ThemeStorage {
}
export interface ThemeControllerOptions {
themeObject?: Theme;
themeObject: Theme;
storage?: ThemeStorage;
storageKey?: string;
modeStorageKey?: string;
@@ -417,6 +393,6 @@ export interface ThemeContextType {
theme: Theme;
themeMode: ThemeMode;
setTheme: (config: AnyThemeConfig) => void;
setThemeMode: (newMode: ThemeMode) => void;
changeThemeMode: (newMode: ThemeMode) => void;
resetTheme: () => void;
}

View File

@@ -28,10 +28,9 @@ import {
genDeprecatedColorVariations,
} from './utils';
import {
type AnyThemeConfig,
type SerializableThemeConfig,
type AntdThemeConfig,
ThemeAlgorithm,
AnyThemeConfig,
SerializableThemeConfig,
AntdThemeConfig,
} from './types';
// Mock tinycolor2 for consistent testing
@@ -51,25 +50,22 @@ 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: ThemeAlgorithm.DARK,
algorithm: 'dark',
};
expect(isSerializableConfig(config)).toBe(true);
});
it('returns true when algorithm is an array of strings', () => {
const config: AnyThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
algorithm: ['dark', 'compact'],
};
expect(isSerializableConfig(config)).toBe(true);
});
@@ -78,19 +74,15 @@ 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' },
algorithm: [
antdThemeImport.darkAlgorithm,
antdThemeImport.compactAlgorithm,
],
// @ts-ignore
algorithm: [antdThemeImport.darkAlgorithm, 'compact'],
};
expect(isSerializableConfig(config)).toBe(false);
});
});
@@ -99,22 +91,18 @@ describe('Theme utilities', () => {
it('converts string algorithm to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: ThemeAlgorithm.DARK,
algorithm: '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: [ThemeAlgorithm.DARK, ThemeAlgorithm.COMPACT],
algorithm: ['dark', 'compact'],
};
const result = deserializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toContain(antdThemeImport.darkAlgorithm);
expect(result.algorithm).toContain(antdThemeImport.compactAlgorithm);
@@ -123,12 +111,10 @@ describe('Theme utilities', () => {
it('preserves other configuration properties', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: ThemeAlgorithm.DARK,
algorithm: 'dark',
hashed: true,
};
const result = deserializeThemeConfig(config);
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
expect(result.hashed).toBe(true);
});
@@ -137,31 +123,25 @@ describe('Theme utilities', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.defaultAlgorithm);
expect(result.algorithm).toBeUndefined();
});
it('converts default algorithm string to function reference', () => {
const config: SerializableThemeConfig = {
token: { colorPrimary: '#ff0000' },
algorithm: ThemeAlgorithm.DEFAULT,
algorithm: '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: ThemeAlgorithm.COMPACT,
algorithm: 'compact',
};
const result = deserializeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.compactAlgorithm);
});
});
@@ -172,10 +152,8 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.darkAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe(ThemeAlgorithm.DARK);
expect(result.algorithm).toBe('dark');
});
it('converts array of function algorithms to strings', () => {
@@ -186,12 +164,10 @@ describe('Theme utilities', () => {
antdThemeImport.compactAlgorithm,
],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toContain(ThemeAlgorithm.DARK);
expect(result.algorithm).toContain(ThemeAlgorithm.COMPACT);
expect(result.algorithm).toContain('dark');
expect(result.algorithm).toContain('compact');
});
it('preserves other configuration properties', () => {
@@ -200,9 +176,7 @@ describe('Theme utilities', () => {
algorithm: antdThemeImport.darkAlgorithm,
hashed: true,
};
const result = serializeThemeConfig(config);
expect(result.token).toEqual({ colorPrimary: '#ff0000' });
expect(result.hashed).toBe(true);
});
@@ -211,9 +185,7 @@ describe('Theme utilities', () => {
const config: AntdThemeConfig = {
token: { colorPrimary: '#ff0000' },
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBeUndefined();
});
@@ -224,10 +196,8 @@ describe('Theme utilities', () => {
// @ts-ignore
algorithm: unknownAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
expect(result.algorithm).toBe('default');
});
it('converts default algorithm function to string', () => {
@@ -235,10 +205,8 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.defaultAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe(ThemeAlgorithm.DEFAULT);
expect(result.algorithm).toBe('default');
});
it('converts compact algorithm function to string', () => {
@@ -246,10 +214,8 @@ describe('Theme utilities', () => {
token: { colorPrimary: '#ff0000' },
algorithm: antdThemeImport.compactAlgorithm,
};
const result = serializeThemeConfig(config);
expect(result.algorithm).toBe(ThemeAlgorithm.COMPACT);
expect(result.algorithm).toBe('compact');
});
it('defaults each unknown algorithm in array to "default"', () => {
@@ -259,14 +225,9 @@ describe('Theme utilities', () => {
// @ts-ignore
algorithm: [antdThemeImport.darkAlgorithm, unknownAlgorithm],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toEqual([
ThemeAlgorithm.DARK,
ThemeAlgorithm.DEFAULT,
]);
expect(result.algorithm).toEqual(['dark', 'default']);
});
it('handles mixed known and unknown algorithms in array', () => {
@@ -283,15 +244,13 @@ describe('Theme utilities', () => {
unknownAlgorithm2,
],
};
const result = serializeThemeConfig(config);
expect(Array.isArray(result.algorithm)).toBe(true);
expect(result.algorithm).toEqual([
ThemeAlgorithm.DARK,
ThemeAlgorithm.DEFAULT,
ThemeAlgorithm.COMPACT,
ThemeAlgorithm.DEFAULT,
'dark',
'default',
'compact',
'default',
]);
});
});
@@ -302,20 +261,16 @@ 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: ThemeAlgorithm.DARK,
algorithm: 'dark',
};
const result = normalizeThemeConfig(config);
expect(result.algorithm).toBe(antdThemeImport.darkAlgorithm);
});
});
@@ -323,18 +278,14 @@ 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);
});
@@ -350,9 +301,7 @@ describe('Theme utilities', () => {
colorInfo: '#info',
otherToken: 'ignore-me',
};
const result = getSystemColors(tokens);
expect(result).toEqual({
colorPrimary: '#primary',
colorError: '#error',
@@ -366,7 +315,6 @@ 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');
@@ -374,7 +322,6 @@ 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');
@@ -390,9 +337,7 @@ 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');

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