mirror of
https://github.com/apache/superset.git
synced 2026-06-12 19:19:20 +00:00
Compare commits
113 Commits
theme_dark
...
ts_migrate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c92a78ab35 | ||
|
|
04ba0a00b3 | ||
|
|
f4754641c8 | ||
|
|
7c98c3f4f6 | ||
|
|
5a32777dd0 | ||
|
|
7229e1ccf3 | ||
|
|
30695d75d7 | ||
|
|
75ee4edc6a | ||
|
|
d269e3d187 | ||
|
|
7d0fabe1ab | ||
|
|
9695249976 | ||
|
|
17c1a37afb | ||
|
|
73dfe57ae2 | ||
|
|
0d236c4ade | ||
|
|
bc0a10fc73 | ||
|
|
5efca408eb | ||
|
|
29f638e239 | ||
|
|
ddeb612429 | ||
|
|
0bc214e889 | ||
|
|
d951158ce6 | ||
|
|
85034b9748 | ||
|
|
11215b092a | ||
|
|
2129e22423 | ||
|
|
7ea1fca4f7 | ||
|
|
9c8fdc0fc1 | ||
|
|
e25be0f3d9 | ||
|
|
d633fe47ef | ||
|
|
c25e734407 | ||
|
|
d8fd6de940 | ||
|
|
733f112142 | ||
|
|
f55476034b | ||
|
|
0a5941edd7 | ||
|
|
0fc4119728 | ||
|
|
6adfd33e3a | ||
|
|
829e4d92d9 | ||
|
|
42db43c686 | ||
|
|
a0f9efd45e | ||
|
|
5d6a979cd0 | ||
|
|
d6ed819fe2 | ||
|
|
ef14a5fbb4 | ||
|
|
4718767ddb | ||
|
|
dfb377c636 | ||
|
|
7082933b96 | ||
|
|
11b6263d55 | ||
|
|
b0cf7b61ad | ||
|
|
96a1b33f22 | ||
|
|
5cff87c048 | ||
|
|
b9052fa461 | ||
|
|
0ea2066d5b | ||
|
|
992aa3a4d5 | ||
|
|
36c7b15342 | ||
|
|
2ab85f3b67 | ||
|
|
1b690a9876 | ||
|
|
f18d9b6bf4 | ||
|
|
ebca5169a0 | ||
|
|
b9ba4d6fda | ||
|
|
20371940d3 | ||
|
|
ee4944bc1a | ||
|
|
5a1023da89 | ||
|
|
4b94d25869 | ||
|
|
2ceced71c5 | ||
|
|
9f0523977d | ||
|
|
cb6342fc73 | ||
|
|
5214ee6fd4 | ||
|
|
7b3329f315 | ||
|
|
d1b372f670 | ||
|
|
09c657c899 | ||
|
|
24500e99f8 | ||
|
|
b2a173977e | ||
|
|
8be79f4170 | ||
|
|
debaf8d6e9 | ||
|
|
1133d84775 | ||
|
|
8c4ca60b28 | ||
|
|
90eded1a04 | ||
|
|
093ee37aac | ||
|
|
23b1fe3b9e | ||
|
|
42288c4784 | ||
|
|
c008190a08 | ||
|
|
2734688f4f | ||
|
|
e3d8326d81 | ||
|
|
e1ab27f484 | ||
|
|
d1d43be9d1 | ||
|
|
f9cec3e366 | ||
|
|
f92431176a | ||
|
|
c31daf8c92 | ||
|
|
c74fae9663 | ||
|
|
308007f909 | ||
|
|
9aaab7374e | ||
|
|
057218d87f | ||
|
|
050ccdcb3d | ||
|
|
09d975cc3f | ||
|
|
1d2a40f69b | ||
|
|
1a57601fe6 | ||
|
|
09736ee42c | ||
|
|
2e9939baf2 | ||
|
|
6af8ce6bb8 | ||
|
|
8d2c51c945 | ||
|
|
580738cea0 | ||
|
|
1a54b761c1 | ||
|
|
7f5f577daa | ||
|
|
e3cbbca4f9 | ||
|
|
b7ad8dba89 | ||
|
|
ede3de0ca0 | ||
|
|
8ee5505b61 | ||
|
|
eec563b0b4 | ||
|
|
a56d7252c2 | ||
|
|
8b5afc6f29 | ||
|
|
f802474b07 | ||
|
|
11a9f2fe4c | ||
|
|
0cff7619f2 | ||
|
|
a13a590796 | ||
|
|
e0e3f94949 | ||
|
|
6876a1e684 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -12,6 +12,10 @@ 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"
|
||||
|
||||
18
.github/workflows/check-python-deps.yml
vendored
18
.github/workflows/check-python-deps.yml
vendored
@@ -24,6 +24,12 @@ 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/
|
||||
@@ -33,10 +39,20 @@ jobs:
|
||||
run: ./scripts/uv-pip-compile.sh
|
||||
|
||||
- name: Check for uncommitted changes
|
||||
if: steps.check.outputs.python
|
||||
run: |
|
||||
if [[ -n "$(git diff)" ]]; then
|
||||
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
|
||||
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."
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
||||
yarn install --immutable
|
||||
|
||||
- name: Cache pre-commit environments
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-v2-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -111,7 +111,6 @@ 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" />
|
||||
|
||||
@@ -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_TRANSACTION=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_TRANSLATIONS=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.
|
||||
|
||||
@@ -71,7 +71,6 @@ 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>` |
|
||||
@@ -1174,25 +1173,6 @@ 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
|
||||
|
||||
@@ -51,6 +51,7 @@ if desired. Most endpoints hit are logged as
|
||||
well as key events like query start and end in SQL Lab.
|
||||
|
||||
To setup StatsD logging, it’s 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
|
||||
|
||||
@@ -8,11 +8,11 @@ version: 1
|
||||
|
||||
## CORS
|
||||
|
||||
To configure CORS, or cross-origin resource sharing, the following dependency must be installed:
|
||||
|
||||
```python
|
||||
pip install apache_superset[cors]
|
||||
```
|
||||
:::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.
|
||||
:::
|
||||
|
||||
|
||||
The following keys in `superset_config.py` can be specified to configure CORS:
|
||||
|
||||
|
||||
@@ -275,3 +275,11 @@ 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.
|
||||
|
||||
@@ -22,6 +22,13 @@ 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
|
||||
@@ -94,14 +101,9 @@ These will now be available when pip installing requirements.
|
||||
|
||||
## Python 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`:
|
||||
We highly recommend installing Superset inside of a virtual environment.
|
||||
|
||||
```bash
|
||||
pip install virtualenv
|
||||
```
|
||||
|
||||
You can create and activate a virtual environment using:
|
||||
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
|
||||
# virtualenv is shipped in Python 3.6+ as venv instead of pyvenv.
|
||||
@@ -132,7 +134,7 @@ pip install apache_superset
|
||||
|
||||
Then, define mandatory configurations, SECRET_KEY and FLASK_APP:
|
||||
```bash
|
||||
export SUPERSET_SECRET_KEY=YOUR-SECRET-KEY
|
||||
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 FLASK_APP=superset
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.2",
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@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.25.1",
|
||||
"antd": "^5.26.3",
|
||||
"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.25.2"
|
||||
"swagger-ui-react": "^5.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.8.1",
|
||||
"@docusaurus/tsconfig": "^3.8.0",
|
||||
"@docusaurus/tsconfig": "^3.8.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
|
||||
@@ -50,7 +50,7 @@ export const Databases = [
|
||||
},
|
||||
{
|
||||
title: 'Apache Druid',
|
||||
href: 'http://druid.io/',
|
||||
href: 'https://druid.apache.org/',
|
||||
imgName: 'druid.png',
|
||||
},
|
||||
{
|
||||
@@ -69,17 +69,12 @@ export const Databases = [
|
||||
},
|
||||
{
|
||||
title: 'ClickHouse',
|
||||
href: 'https://clickhouse.tech/',
|
||||
href: 'https://clickhouse.com/',
|
||||
imgName: 'clickhouse.png',
|
||||
},
|
||||
{
|
||||
title: 'Rockset',
|
||||
href: 'https://rockset.com/',
|
||||
imgName: 'rockset.png',
|
||||
},
|
||||
{
|
||||
title: 'Dremio',
|
||||
href: 'https://dremio.com/',
|
||||
href: 'https://www.dremio.com/',
|
||||
imgName: 'dremio.png',
|
||||
},
|
||||
{
|
||||
@@ -104,12 +99,12 @@ export const Databases = [
|
||||
},
|
||||
{
|
||||
title: 'IBM Db2',
|
||||
href: 'https://www.ibm.com/analytics/db2',
|
||||
href: 'https://www.ibm.com/products/db2',
|
||||
imgName: 'ibmdb2.png',
|
||||
},
|
||||
{
|
||||
title: 'SAP Hana',
|
||||
href: 'https://www.sap.com/products/technology-platform/hana.html',
|
||||
href: 'https://www.sap.com/products/data-cloud/hana.html',
|
||||
imgName: 'sap-hana.jpg',
|
||||
},
|
||||
{
|
||||
@@ -139,7 +134,7 @@ export const Databases = [
|
||||
},
|
||||
{
|
||||
title: 'TDengine',
|
||||
href: 'https://www.tdengine.com/',
|
||||
href: 'https://tdengine.com/',
|
||||
imgName: 'tdengine.png',
|
||||
},
|
||||
];
|
||||
|
||||
BIN
docs/static/img/databases/rockset.png
vendored
BIN
docs/static/img/databases/rockset.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB |
119
docs/yarn.lock
119
docs/yarn.lock
@@ -158,13 +158,20 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@jridgewell/trace-mapping" "^0.3.24"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
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"
|
||||
@@ -194,12 +201,17 @@
|
||||
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.5.2", "@ant-design/icons@^5.6.1":
|
||||
"@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==
|
||||
@@ -210,6 +222,16 @@
|
||||
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"
|
||||
@@ -1084,15 +1106,7 @@
|
||||
"@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":
|
||||
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":
|
||||
"@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":
|
||||
version "7.27.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.6.tgz#97644153808a62898e7c05f3361501417db3c48b"
|
||||
integrity sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ==
|
||||
@@ -1965,10 +1979,10 @@
|
||||
fs-extra "^11.1.1"
|
||||
tslib "^2.6.0"
|
||||
|
||||
"@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/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/types@3.8.1":
|
||||
version "3.8.1"
|
||||
@@ -2443,10 +2457,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.6":
|
||||
version "2.2.6"
|
||||
resolved "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz"
|
||||
integrity sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@rc-component/portal" "^1.1.0"
|
||||
@@ -2455,6 +2469,13 @@
|
||||
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"
|
||||
@@ -4017,12 +4038,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.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==
|
||||
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==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.0"
|
||||
"@ant-design/colors" "^7.2.1"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
"@ant-design/cssinjs-utils" "^1.1.3"
|
||||
"@ant-design/fast-color" "^2.0.6"
|
||||
@@ -4033,7 +4054,7 @@ antd@^5.25.1:
|
||||
"@rc-component/mutate-observer" "^1.1.0"
|
||||
"@rc-component/qrcode" "~1.0.0"
|
||||
"@rc-component/tour" "~1.15.1"
|
||||
"@rc-component/trigger" "^2.2.6"
|
||||
"@rc-component/trigger" "^2.2.7"
|
||||
classnames "^2.5.1"
|
||||
copy-to-clipboard "^3.3.3"
|
||||
dayjs "^1.11.11"
|
||||
@@ -4041,7 +4062,7 @@ antd@^5.25.1:
|
||||
rc-checkbox "~3.5.0"
|
||||
rc-collapse "~3.9.0"
|
||||
rc-dialog "~9.6.0"
|
||||
rc-drawer "~7.2.0"
|
||||
rc-drawer "~7.3.0"
|
||||
rc-dropdown "~4.2.1"
|
||||
rc-field-form "~2.7.0"
|
||||
rc-image "~7.12.0"
|
||||
@@ -4057,17 +4078,17 @@ antd@^5.25.1:
|
||||
rc-rate "~2.13.1"
|
||||
rc-resize-observer "^1.4.3"
|
||||
rc-segmented "~2.7.0"
|
||||
rc-select "~14.16.7"
|
||||
rc-select "~14.16.8"
|
||||
rc-slider "~11.1.8"
|
||||
rc-steps "~6.0.1"
|
||||
rc-switch "~4.1.0"
|
||||
rc-table "~7.50.4"
|
||||
rc-table "~7.51.1"
|
||||
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.0"
|
||||
rc-upload "~4.9.2"
|
||||
rc-util "^5.44.4"
|
||||
scroll-into-view-if-needed "^3.1.0"
|
||||
throttle-debounce "^5.0.2"
|
||||
@@ -10377,10 +10398,10 @@ rc-dialog@~9.6.0:
|
||||
rc-motion "^2.3.0"
|
||||
rc-util "^5.21.0"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@rc-component/portal" "^1.1.1"
|
||||
@@ -10552,7 +10573,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.7:
|
||||
rc-select@~14.16.2, rc-select@~14.16.8:
|
||||
version "14.16.8"
|
||||
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.8.tgz#78e6782f1ccc1f03d9003bc3effa4ed609d29a97"
|
||||
integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
|
||||
@@ -10592,10 +10613,10 @@ rc-switch@~4.1.0:
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.30.0"
|
||||
|
||||
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==
|
||||
rc-table@~7.51.1:
|
||||
version "7.51.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.51.1.tgz#cd69ae3262d3b61e4c93c979c12786906e944691"
|
||||
integrity sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/context" "^1.4.0"
|
||||
@@ -10660,10 +10681,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.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.0.tgz#911963ab5a0b538c743765371c05e2de9e3f5436"
|
||||
integrity sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
classnames "^2.2.5"
|
||||
@@ -11998,10 +12019,10 @@ swagger-client@^3.35.5:
|
||||
ramda "^0.30.1"
|
||||
ramda-adjunct "^5.1.0"
|
||||
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/runtime-corejs3" "^7.27.1"
|
||||
"@scarf/scarf" "=1.4.0"
|
||||
|
||||
@@ -40,12 +40,13 @@ 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.7.0, <5.0.0",
|
||||
"flask-appbuilder>=4.8.0, <5.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -64,6 +65,8 @@ 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",
|
||||
@@ -113,7 +116,6 @@ 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 = [
|
||||
@@ -164,7 +166,6 @@ 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"]
|
||||
|
||||
@@ -7,7 +7,14 @@ 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.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
urllib3>=1.26.19, <2.0.0
|
||||
urllib3==2.5.0
|
||||
werkzeug>=3.0.1
|
||||
numexpr>=2.9.0
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ flask==2.3.3
|
||||
# flask-babel
|
||||
# flask-caching
|
||||
# flask-compress
|
||||
# flask-cors
|
||||
# flask-jwt-extended
|
||||
# flask-limiter
|
||||
# flask-login
|
||||
@@ -111,7 +112,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.7.0
|
||||
flask-appbuilder==4.8.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
@@ -119,6 +120,8 @@ 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
|
||||
@@ -209,6 +212,7 @@ markupsafe==3.0.2
|
||||
# wtforms
|
||||
marshmallow==3.26.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
# marshmallow-sqlalchemy
|
||||
marshmallow-sqlalchemy==1.4.0
|
||||
@@ -392,6 +396,8 @@ typing-extensions==4.14.0
|
||||
# alembic
|
||||
# cattrs
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
# selenium
|
||||
# shillelagh
|
||||
tzdata==2025.2
|
||||
@@ -400,7 +406,7 @@ tzdata==2025.2
|
||||
# pandas
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
urllib3==1.26.20
|
||||
urllib3==2.5.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# requests
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
-e .[development,bigquery,cors,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
-e .[development,bigquery,druid,gevent,gsheets,mysql,postgres,presto,prophet,trino,thumbnails]
|
||||
|
||||
@@ -195,7 +195,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.7.0
|
||||
flask-appbuilder==4.8.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -212,7 +212,9 @@ flask-compress==1.17
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-cors==4.0.2
|
||||
# via apache-superset
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
flask-jwt-extended==4.7.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -424,6 +426,7 @@ markupsafe==3.0.2
|
||||
marshmallow==3.26.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
# marshmallow-sqlalchemy
|
||||
marshmallow-sqlalchemy==1.4.0
|
||||
@@ -839,6 +842,8 @@ typing-extensions==4.14.0
|
||||
# apache-superset
|
||||
# cattrs
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
# selenium
|
||||
# shillelagh
|
||||
tzdata==2025.2
|
||||
@@ -852,7 +857,7 @@ url-normalize==2.2.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
urllib3==1.26.20
|
||||
urllib3==2.5.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# docker
|
||||
|
||||
@@ -24,7 +24,8 @@ 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
|
||||
# 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.
|
||||
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
|
||||
|
||||
4
superset-embedded-sdk/package-lock.json
generated
4
superset-embedded-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@superset-ui/embedded-sdk",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@superset-ui/embedded-sdk",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@superset-ui/switchboard": "^0.20.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@superset-ui/embedded-sdk",
|
||||
"version": "0.1.3",
|
||||
"version": "0.2.0",
|
||||
"description": "SDK for embedding resources from Superset into your own application",
|
||||
"access": "public",
|
||||
"keywords": [
|
||||
|
||||
@@ -46,6 +46,7 @@ export type UiConfigType = {
|
||||
urlParams?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
showRowLimitWarning?: boolean;
|
||||
};
|
||||
|
||||
export type EmbedDashboardParams = {
|
||||
@@ -133,6 +134,9 @@ export async function embedDashboard({
|
||||
if (dashboardUiConfig.emitDataMasks) {
|
||||
configNumber += 16;
|
||||
}
|
||||
if (dashboardUiConfig.showRowLimitWarning) {
|
||||
configNumber += 32;
|
||||
}
|
||||
}
|
||||
return configNumber;
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@ 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 = {
|
||||
@@ -82,9 +86,7 @@ module.exports = {
|
||||
],
|
||||
parser: '@babel/eslint-parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
experimentalObjectRestSpread: true,
|
||||
},
|
||||
ecmaVersion: 2018,
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
@@ -123,6 +125,11 @@ 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'],
|
||||
@@ -156,7 +163,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/prefer-optional-chain': 2,
|
||||
'@typescript-eslint/no-unused-vars': 'warn', // downgrade to Warning severity for Jest v30 upgrade
|
||||
camelcase: 0,
|
||||
'class-methods-use-this': 0,
|
||||
'func-names': 0,
|
||||
@@ -391,6 +398,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line no-dupe-keys
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 'error',
|
||||
'icons/no-fa-icons-usage': 'error',
|
||||
|
||||
@@ -46,10 +46,10 @@ module.exports = {
|
||||
plugins: [
|
||||
'lodash',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
['@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-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-transform-runtime', { corejs: 3 }],
|
||||
// only used in packages/superset-ui-core/src/chart/components/reactify.tsx
|
||||
['babel-plugin-typescript-to-proptypes', { loose: true }],
|
||||
|
||||
@@ -27,13 +27,6 @@ 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');
|
||||
|
||||
@@ -62,6 +62,6 @@ describe('Dashboard actions', () => {
|
||||
// Verify the color of the outlined star (gray)
|
||||
cy.get('@starIconOutlinedAfter')
|
||||
.should('have.css', 'color')
|
||||
.and('eq', 'rgb(133, 133, 133)');
|
||||
.and('eq', 'rgba(0, 0, 0, 0.45)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,30 +21,21 @@ 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,
|
||||
@@ -55,19 +46,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', () => {
|
||||
@@ -183,46 +174,139 @@ describe('Native filters', () => {
|
||||
validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
|
||||
});
|
||||
|
||||
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,
|
||||
describe.only('Numerical Range Filter - Display Modes', () => {
|
||||
beforeEach(() => {
|
||||
visitDashboard();
|
||||
});
|
||||
|
||||
// Assert that the URL contains 'native_filters'
|
||||
cy.url().then(u => {
|
||||
const ur = new URL(u);
|
||||
expect(ur.search).to.include('native_filters');
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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')
|
||||
.should('equal', '40');
|
||||
.then(val => {
|
||||
expect(val).to.equal(from);
|
||||
});
|
||||
|
||||
// Assert that the "To" input has the correct value
|
||||
cy.get('[data-test="range-filter-to-input"]')
|
||||
.invoke('val')
|
||||
.should('equal', '50');
|
||||
.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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,37 +16,29 @@
|
||||
* 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,
|
||||
|
||||
@@ -363,9 +363,26 @@ export function saveNativeFilterSettings(charts: ChartSpec[]) {
|
||||
cy.get(nativeFilters.modal.footer)
|
||||
.contains('Save')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
.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
|
||||
cy.get(nativeFilters.modal.container).should('not.exist');
|
||||
charts.forEach(waitForChartLoad);
|
||||
|
||||
// Wait for all charts to load
|
||||
charts.forEach(chart => {
|
||||
waitForChartLoad(chart);
|
||||
});
|
||||
}
|
||||
|
||||
/** ************************************************************************
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
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';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/eyes-index.d.ts"],
|
||||
"files": ["cypress/support/index.d.ts", "./node_modules/@applitools/eyes-cypress/types/index.d.ts"],
|
||||
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ module.exports = {
|
||||
setupFilesAfterEnv: ['<rootDir>/spec/helpers/setup.ts'],
|
||||
snapshotSerializers: ['@emotion/jest/serializer'],
|
||||
testEnvironmentOptions: {
|
||||
globalsCleanup: true,
|
||||
url: 'http://localhost',
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
@@ -55,7 +56,7 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!d3-(interpolate|color|time)|remark-gfm|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|jest-enzyme|geostyler|geostyler-.*)',
|
||||
'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)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
10072
superset-frontend/package-lock.json
generated
10072
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -84,7 +84,7 @@
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@rjsf/core": "^5.21.1",
|
||||
"@rjsf/utils": "^5.24.3",
|
||||
@@ -109,6 +109,7 @@
|
||||
"@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",
|
||||
@@ -154,7 +155,7 @@
|
||||
"json-stringify-pretty-compact": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.5.0",
|
||||
"mapbox-gl": "^2.10.0",
|
||||
"mapbox-gl": "^3.13.0",
|
||||
"markdown-to-jsx": "^7.7.4",
|
||||
"match-sorter": "^6.3.4",
|
||||
"memoize-one": "^5.2.1",
|
||||
@@ -164,8 +165,6 @@
|
||||
"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",
|
||||
@@ -177,7 +176,7 @@
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-json-tree": "^0.17.0",
|
||||
"react-json-tree": "^0.20.0",
|
||||
"react-lines-ellipsis": "^0.15.4",
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-redux": "^7.2.9",
|
||||
@@ -187,7 +186,6 @@
|
||||
"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",
|
||||
@@ -196,7 +194,6 @@
|
||||
"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",
|
||||
@@ -209,16 +206,12 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@applitools/eyes-storybook": "^3.50.9",
|
||||
"@babel/cli": "^7.22.6",
|
||||
"@applitools/eyes-storybook": "^3.55.6",
|
||||
"@babel/cli": "^7.27.2",
|
||||
"@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",
|
||||
@@ -260,13 +253,11 @@
|
||||
"@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",
|
||||
@@ -277,17 +268,16 @@
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/testing-library__jest-dom": "^5.14.9",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/yargs": "12 - 18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"babel-jest": "^30.0.2",
|
||||
"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.0.0-rc.10",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"cheerio": "1.1.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"css-minimizer-webpack-plugin": "^7.0.2",
|
||||
@@ -317,9 +307,9 @@
|
||||
"history": "^5.3.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"imports-loader": "^5.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest": "^30.0.2",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-html-reporter": "^3.10.2",
|
||||
"jest-html-reporter": "^4.3.0",
|
||||
"jest-websocket-mock": "^2.5.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lerna": "^8.2.1",
|
||||
@@ -338,19 +328,19 @@
|
||||
"storybook": "8.1.11",
|
||||
"style-loader": "^4.0.0",
|
||||
"thread-loader": "^4.0.4",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-jest": "^29.4.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "5.1.6",
|
||||
"typescript": "5.4.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-bundle-analyzer": "^4.10.1",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ace-builds": "^1.41.0",
|
||||
|
||||
@@ -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.colors.primary.light2};
|
||||
background-color: ${({ theme }) => theme.colorPrimaryBg};
|
||||
padding: ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
height: ${({ height }) => height}px;
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"yeoman-generator": "^7.4.0",
|
||||
"yeoman-generator": "^7.5.1",
|
||||
"yosay": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"jest": "^29.7.0",
|
||||
"yeoman-test": "^8.3.0"
|
||||
"fs-extra": "^11.3.0",
|
||||
"jest": "^30.0.2",
|
||||
"yeoman-test": "^10.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 4.0.0",
|
||||
|
||||
@@ -27,10 +27,9 @@ import {
|
||||
import { PostProcessingFactory } from './types';
|
||||
import { extractExtraMetrics } from './utils';
|
||||
|
||||
export const sortOperator: PostProcessingFactory<PostProcessingSort> = (
|
||||
formData,
|
||||
queryObject,
|
||||
) => {
|
||||
export const sortOperator: PostProcessingFactory<
|
||||
PostProcessingSort
|
||||
> = formData => {
|
||||
// the sortOperator only used in the barchart v2
|
||||
const sortableLabels = [
|
||||
getXAxisLabel(formData),
|
||||
|
||||
@@ -29,6 +29,7 @@ export const TITLE_POSITION_OPTIONS: [string, string][] = [
|
||||
['Left', t('Left')],
|
||||
['Top', t('Top')],
|
||||
];
|
||||
|
||||
export const titleControls: ControlPanelSectionConfig = {
|
||||
label: t('Chart Title'),
|
||||
tabOverride: 'customize',
|
||||
@@ -43,7 +44,6 @@ export const titleControls: ControlPanelSectionConfig = {
|
||||
label: t('X Axis Title'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -58,7 +58,6 @@ export const titleControls: ControlPanelSectionConfig = {
|
||||
renderTrigger: true,
|
||||
default: TITLE_MARGIN_OPTIONS[0],
|
||||
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -71,7 +70,6 @@ export const titleControls: ControlPanelSectionConfig = {
|
||||
label: t('Y Axis Title'),
|
||||
renderTrigger: true,
|
||||
default: '',
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -86,7 +84,6 @@ export const titleControls: ControlPanelSectionConfig = {
|
||||
renderTrigger: true,
|
||||
default: TITLE_MARGIN_OPTIONS[1],
|
||||
choices: formatSelectOptions(TITLE_MARGIN_OPTIONS),
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -101,7 +98,6 @@ export const titleControls: ControlPanelSectionConfig = {
|
||||
renderTrigger: true,
|
||||
default: TITLE_POSITION_OPTIONS[0][0],
|
||||
choices: TITLE_POSITION_OPTIONS,
|
||||
description: t('Changing this control takes effect instantly'),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { QueryColumn, t, validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
GenericDataType,
|
||||
QueryColumn,
|
||||
t,
|
||||
validateNonEmpty,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ExtraControlProps,
|
||||
SharedControlConfig,
|
||||
@@ -52,6 +57,19 @@ 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
|
||||
@@ -81,14 +99,20 @@ export const dndGroupByControl: SharedControlConfig<
|
||||
const newState: ExtraControlProps = {};
|
||||
const { datasource } = state;
|
||||
if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
|
||||
const options = (datasource as Dataset).columns.filter(c => c.groupby);
|
||||
const options = filterOptions(
|
||||
(datasource as Dataset).columns.filter(c => c.groupby),
|
||||
controlState?.allowedDataTypes,
|
||||
);
|
||||
if (controlState?.includeTime) {
|
||||
options.unshift(DATASET_TIME_COLUMN_OPTION);
|
||||
}
|
||||
newState.options = options;
|
||||
newState.savedMetrics = (datasource as Dataset).metrics || [];
|
||||
} else {
|
||||
const options = (datasource?.columns as QueryColumn[]) || [];
|
||||
const options = filterOptions(
|
||||
(datasource?.columns as QueryColumn[]) || [],
|
||||
controlState?.allowedDataTypes,
|
||||
);
|
||||
if (controlState?.includeTime) {
|
||||
options.unshift(QUERY_TIME_COLUMN_OPTION);
|
||||
}
|
||||
@@ -177,6 +201,19 @@ 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'),
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
isDefined,
|
||||
NO_TIME_RANGE,
|
||||
validateMaxValue,
|
||||
getColumnLabel,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import {
|
||||
@@ -82,6 +83,8 @@ import {
|
||||
dndSeriesControl,
|
||||
dndAdhocMetricControl2,
|
||||
dndXAxisControl,
|
||||
dndTooltipColumnsControl,
|
||||
dndTooltipMetricsControl,
|
||||
} from './dndControls';
|
||||
|
||||
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
|
||||
@@ -373,6 +376,14 @@ 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'),
|
||||
@@ -381,6 +392,26 @@ 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,
|
||||
@@ -392,6 +423,8 @@ export default {
|
||||
secondary_metric: dndSecondaryMetricControl,
|
||||
groupby: dndGroupByControl,
|
||||
columns: dndColumnsControl,
|
||||
tooltip_columns: dndTooltipColumnsControl,
|
||||
tooltip_metrics: dndTooltipMetricsControl,
|
||||
granularity,
|
||||
granularity_sqla: dndGranularitySqlaControl,
|
||||
time_grain_sqla,
|
||||
@@ -417,8 +450,10 @@ 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,
|
||||
};
|
||||
|
||||
@@ -161,6 +161,7 @@ export type InternalControlType =
|
||||
| 'DatasourceControl'
|
||||
| 'DateFilterControl'
|
||||
| 'FixedOrMetricControl'
|
||||
| 'ColorBreakpointsControl'
|
||||
| 'HiddenControl'
|
||||
| 'SelectAsyncControl'
|
||||
| 'SelectControl'
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"@fontsource/fira-code": "^5.0.18",
|
||||
"@fontsource/inter": "^5.0.20",
|
||||
"@fontsource/fira-code": "^5.2.6",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"ace-builds": "^1.41.0",
|
||||
"ace-builds": "^1.43.1",
|
||||
"brace": "^0.11.1",
|
||||
"classnames": "^2.2.5",
|
||||
"csstype": "^3.1.3",
|
||||
@@ -51,13 +51,14 @@
|
||||
"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": "^5.0.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"reselect": "^4.0.0",
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
@@ -65,15 +66,16 @@
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@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.16",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/math-expression-evaluator": "^1.3.3",
|
||||
"@types/node": "^22.10.3",
|
||||
"@types/prop-types": "^15.7.2",
|
||||
@@ -88,7 +90,7 @@
|
||||
"antd": "^5.24.6",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@testing-library/dom": "^8.20.1",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
|
||||
@@ -55,7 +55,11 @@ export enum AppSection {
|
||||
Embedded = 'EMBEDDED',
|
||||
}
|
||||
|
||||
export type FilterState = { value?: any; [key: string]: any };
|
||||
export type FilterState = {
|
||||
value?: any;
|
||||
customColumnLabel?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type DataMask = {
|
||||
extraFormData?: ExtraFormData;
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum VizType {
|
||||
Compare = 'compare',
|
||||
CountryMap = 'country_map',
|
||||
Funnel = 'funnel',
|
||||
Gantt = 'gantt_chart',
|
||||
Gauge = 'gauge_chart',
|
||||
Graph = 'graph_chart',
|
||||
Handlebars = 'handlebars',
|
||||
@@ -54,6 +55,7 @@ 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',
|
||||
|
||||
@@ -27,6 +27,8 @@ 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 {
|
||||
|
||||
@@ -212,7 +212,9 @@ 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;
|
||||
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
defaultValue: { summary: 'false' },
|
||||
},
|
||||
},
|
||||
dropdownRender: {
|
||||
popupRender: {
|
||||
control: false,
|
||||
description:
|
||||
'Custom render function for dropdown content. `(menus: ReactNode) => ReactNode`',
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -38,7 +38,7 @@ const StyledCollapse = styled((props: CollapseProps) => (
|
||||
|
||||
${({ expandIconPosition }) =>
|
||||
expandIconPosition &&
|
||||
expandIconPosition === 'right' &&
|
||||
expandIconPosition === 'end' &&
|
||||
`
|
||||
.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 === 'right' &&
|
||||
expandIconPosition === 'end' &&
|
||||
`
|
||||
.anticon.anticon-right.ant-collapse-arrow > svg {
|
||||
transform: rotate(-90deg) !important;
|
||||
|
||||
@@ -62,12 +62,12 @@ test('Calling "onHide"', async () => {
|
||||
expect(props.onConfirm).toHaveBeenCalledTimes(0);
|
||||
|
||||
// type "del" in the input
|
||||
await userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
|
||||
userEvent.type(screen.getByTestId('delete-modal-input'), 'del');
|
||||
expect(screen.getByTestId('delete-modal-input')).toHaveValue('del');
|
||||
|
||||
// close the modal
|
||||
expect(screen.getByText('×')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('×'));
|
||||
expect(screen.getByTestId('close-modal-btn')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId('close-modal-btn'));
|
||||
expect(props.onHide).toHaveBeenCalledTimes(1);
|
||||
expect(props.onConfirm).toHaveBeenCalledTimes(0);
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export const MenuDotsDropdown = ({
|
||||
iconOrientation = IconOrientation.Vertical,
|
||||
...rest
|
||||
}: MenuDotsDropdownProps) => (
|
||||
<AntdDropdown dropdownRender={() => overlay} {...rest}>
|
||||
<AntdDropdown popupRender={() => overlay} {...rest}>
|
||||
<MenuDotsWrapper data-test="dropdown-trigger">
|
||||
{RenderIcon(iconOrientation)}
|
||||
</MenuDotsWrapper>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Tooltip } from '../Tooltip';
|
||||
import type { DropdownButtonProps } from './types';
|
||||
|
||||
export const DropdownButton = ({
|
||||
dropdownRender,
|
||||
popupRender,
|
||||
tooltip,
|
||||
tooltipPlacement,
|
||||
children,
|
||||
@@ -51,7 +51,7 @@ export const DropdownButton = ({
|
||||
`;
|
||||
const button = (
|
||||
<Dropdown.Button
|
||||
dropdownRender={dropdownRender}
|
||||
popupRender={popupRender}
|
||||
{...rest}
|
||||
css={[
|
||||
defaultBtnCss,
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
CSSProperties,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
ReactElement,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
@@ -26,17 +28,85 @@ 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 '..';
|
||||
import type {
|
||||
DropdownContainerProps,
|
||||
DropdownItem,
|
||||
DropdownRef,
|
||||
} from './types';
|
||||
/**
|
||||
* 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 };
|
||||
|
||||
const MAX_HEIGHT = 500;
|
||||
|
||||
@@ -73,6 +143,37 @@ 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) => {
|
||||
@@ -122,24 +223,7 @@ export const DropdownContainer = forwardRef(
|
||||
childrenArray.map(child => resizeObserver.unobserve(child));
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [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);
|
||||
}
|
||||
};
|
||||
}, [items.length, current, recalculateItemWidths]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (popoverVisible) {
|
||||
@@ -206,6 +290,7 @@ export const DropdownContainer = forwardRef(
|
||||
overflowedItems.length,
|
||||
previousWidth,
|
||||
width,
|
||||
popoverVisible,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -376,5 +461,3 @@ export const DropdownContainer = forwardRef(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export type { DropdownItem, DropdownRef };
|
||||
|
||||
@@ -64,14 +64,14 @@ export const FaveStar = ({
|
||||
<Icons.StarFilled
|
||||
aria-label="starred"
|
||||
iconSize="l"
|
||||
iconColor={theme.colors.warning.base}
|
||||
iconColor={theme.colorWarning}
|
||||
name="favorite-selected"
|
||||
/>
|
||||
) : (
|
||||
<Icons.StarOutlined
|
||||
aria-label="unstarred"
|
||||
iconSize="l"
|
||||
iconColor={theme.colors.grayscale.light1}
|
||||
iconColor={theme.colorTextTertiary}
|
||||
name="favorite-unselected"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { BaseIconComponent } from './BaseIcon';
|
||||
const AsyncIcon = (props: IconType) => {
|
||||
const [, setLoaded] = useState(false);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
|
||||
const { fileName } = props;
|
||||
const { fileName, ...restProps } = props;
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -46,7 +46,7 @@ const AsyncIcon = (props: IconType) => {
|
||||
return (
|
||||
<BaseIconComponent
|
||||
component={ImportedSVG.current || TransparentIcon}
|
||||
{...props}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,11 +43,12 @@ export const BaseIconComponent: React.FC<
|
||||
iconSize,
|
||||
viewBox,
|
||||
customIcons,
|
||||
fileName,
|
||||
...rest
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const whatRole = rest?.onClick ? 'button' : 'img';
|
||||
const ariaLabel = genAriaLabel(rest.fileName || '');
|
||||
const ariaLabel = genAriaLabel(fileName || '');
|
||||
const style = {
|
||||
color: iconColor,
|
||||
fontSize: iconSize
|
||||
|
||||
@@ -37,7 +37,7 @@ export const DatasetTypeLabel: React.FC<DatasetTypeLabelProps> = ({
|
||||
datasetType === 'physical' ? (
|
||||
<Icons.InsertRowAboveOutlined
|
||||
iconSize={SIZE}
|
||||
iconColor={theme.colors.primary.dark1}
|
||||
iconColor={theme.colorPrimary}
|
||||
/>
|
||||
) : (
|
||||
<Icons.ConsoleSqlOutlined iconSize={SIZE} />
|
||||
|
||||
@@ -165,8 +165,10 @@ 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.colors.primary.base));
|
||||
expect(nonClickableColor).toBe(hexToRgb(supersetTheme.colorText));
|
||||
expect(clickableColor).toBe(hexToRgb(supersetTheme.colorPrimary));
|
||||
expect(nonClickableColor.replace(/\s+/g, '')).toBe(
|
||||
supersetTheme.colorTextTertiary.replace(/\s+/g, ''),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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.colorTextBase};
|
||||
color: ${onClick && collapsed ? theme.colorPrimary : theme.colorTextTertiary};
|
||||
padding-right: ${collapsed ? 0 : ICON_PADDING}px;
|
||||
& .anticon {
|
||||
line-height: 0;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { isValidElement, cloneElement, useMemo, useRef, useState } from 'react';
|
||||
import { isNil } from 'lodash';
|
||||
import { css, styled, t } from '@superset-ui/core';
|
||||
import { css, styled, t, useTheme } from '@superset-ui/core';
|
||||
import { Modal as AntdModal, ModalProps as AntdModalProps } from 'antd';
|
||||
import { Resizable } from 're-resizable';
|
||||
import Draggable, {
|
||||
@@ -26,6 +26,7 @@ import Draggable, {
|
||||
DraggableData,
|
||||
DraggableEvent,
|
||||
} from 'react-draggable';
|
||||
import { Icons } from '../Icons';
|
||||
import { Button } from '../Button';
|
||||
import type { ModalProps, StyledModalProps } from './types';
|
||||
|
||||
@@ -45,8 +46,16 @@ export const BaseModal = (props: AntdModalProps) => (
|
||||
);
|
||||
|
||||
export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
${({ theme, responsive, maxWidth }) =>
|
||||
responsive &&
|
||||
${({
|
||||
theme,
|
||||
responsive,
|
||||
maxWidth,
|
||||
resizable,
|
||||
height,
|
||||
draggable,
|
||||
hideFooter,
|
||||
}) => css`
|
||||
${responsive &&
|
||||
css`
|
||||
max-width: ${maxWidth ?? '900px'};
|
||||
padding-left: ${theme.sizeUnit * 3}px;
|
||||
@@ -55,120 +64,120 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
top: 0;
|
||||
`}
|
||||
|
||||
.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 {
|
||||
.ant-modal-content {
|
||||
background-color: ${theme.colorBgContainer};
|
||||
display: flex;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.btn + .btn {
|
||||
margin-left: ${({ theme }) => theme.sizeUnit * 2}px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-content-padding .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${({ draggable, theme }) =>
|
||||
draggable &&
|
||||
`
|
||||
.ant-modal-header {
|
||||
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;
|
||||
.draggable-trigger {
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
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-body {
|
||||
flex: 0 1 auto;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
overflow: auto;
|
||||
${!resizable && height && `height: ${height};`}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
flex: 0 0 1;
|
||||
border-top: ${theme.sizeUnit / 4}px solid ${theme.colorSplit};
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
margin-top: 0;
|
||||
|
||||
.btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn + .btn {
|
||||
margin-left: ${theme.sizeUnit * 2}px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-content-padding .ant-modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${draggable &&
|
||||
css`
|
||||
.ant-modal-header {
|
||||
padding: 0;
|
||||
|
||||
.draggable-trigger {
|
||||
cursor: move;
|
||||
padding: ${theme.sizeUnit * 4}px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`};
|
||||
|
||||
${({ resizable, hideFooter }) =>
|
||||
resizable &&
|
||||
`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.resizable-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
`}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100%;
|
||||
${resizable &&
|
||||
css`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.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);`
|
||||
.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)`};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -214,13 +223,14 @@ const CustomModal = ({
|
||||
resizable = false,
|
||||
resizableConfig = defaultResizableConfig(hideFooter),
|
||||
draggableConfig,
|
||||
destroyOnClose,
|
||||
destroyOnHidden,
|
||||
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();
|
||||
@@ -228,13 +238,16 @@ const CustomModal = ({
|
||||
};
|
||||
|
||||
let FooterComponent;
|
||||
if (isValidElement(footer)) {
|
||||
|
||||
// This safely avoids injecting "closeModal" into native elements like <div> or <span>
|
||||
if (isValidElement(footer) && typeof footer.type === 'function')
|
||||
// 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
|
||||
@@ -309,9 +322,13 @@ const CustomModal = ({
|
||||
open={show}
|
||||
title={<ModalTitle />}
|
||||
closeIcon={
|
||||
<span className="close" aria-hidden="true">
|
||||
×
|
||||
</span>
|
||||
<Icons.CloseOutlined
|
||||
iconColor={theme.colorText}
|
||||
iconSize="l"
|
||||
data-test="close-modal-btn"
|
||||
className="close"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
footer={!hideFooter ? modalFooter : null}
|
||||
hideFooter={hideFooter}
|
||||
@@ -341,7 +358,7 @@ const CustomModal = ({
|
||||
mask={shouldShowMask}
|
||||
draggable={draggable}
|
||||
resizable={resizable}
|
||||
destroyOnClose={destroyOnClose}
|
||||
destroyOnHidden={destroyOnHidden}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface ModalProps {
|
||||
show: boolean;
|
||||
name?: string;
|
||||
title: ReactNode;
|
||||
width?: string;
|
||||
width?: string | number;
|
||||
maxWidth?: string;
|
||||
responsive?: boolean;
|
||||
hideFooter?: boolean;
|
||||
@@ -47,7 +47,7 @@ export interface ModalProps {
|
||||
resizableConfig?: ResizableProps;
|
||||
draggable?: boolean;
|
||||
draggableConfig?: DraggableProps;
|
||||
destroyOnClose?: boolean;
|
||||
destroyOnHidden?: boolean;
|
||||
maskClosable?: boolean;
|
||||
zIndex?: number;
|
||||
bodyStyle?: CSSProperties;
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface ModalTriggerProps {
|
||||
resizableConfig?: any;
|
||||
draggable?: boolean;
|
||||
draggableConfig?: any;
|
||||
destroyOnClose?: boolean;
|
||||
destroyOnHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface ModalTriggerRef {
|
||||
@@ -63,7 +63,7 @@ export const ModalTrigger = forwardRef(
|
||||
tooltip,
|
||||
modalFooter,
|
||||
triggerNode,
|
||||
destroyOnClose = true,
|
||||
destroyOnHidden = true,
|
||||
modalBody,
|
||||
draggableConfig = {},
|
||||
resizableConfig = {},
|
||||
@@ -120,7 +120,7 @@ export const ModalTrigger = forwardRef(
|
||||
resizableConfig={resizableConfig}
|
||||
draggable={draggable}
|
||||
draggableConfig={draggableConfig}
|
||||
destroyOnClose={destroyOnClose}
|
||||
destroyOnHidden={destroyOnHidden}
|
||||
>
|
||||
{modalBody}
|
||||
</Modal>
|
||||
|
||||
@@ -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.colors.primary.dark2};
|
||||
border: 1px solid ${theme.colorPrimary};
|
||||
|
||||
&.ant-btn > span.anticon {
|
||||
line-height: 0;
|
||||
@@ -151,7 +151,7 @@ export const PageHeaderWithActions = ({
|
||||
{showMenuDropdown && (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
dropdownRender={() => additionalActionsMenu}
|
||||
popupRender={() => additionalActionsMenu}
|
||||
{...menuDropdownProps}
|
||||
>
|
||||
<Button
|
||||
@@ -163,7 +163,7 @@ export const PageHeaderWithActions = ({
|
||||
data-test="actions-trigger"
|
||||
>
|
||||
<Icons.EllipsisOutlined
|
||||
iconColor={theme.colors.primary.dark2}
|
||||
iconColor={theme.colorPrimary}
|
||||
iconSize="l"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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';
|
||||
@@ -129,7 +129,7 @@ const AsyncSelect = forwardRef(
|
||||
onError,
|
||||
onChange,
|
||||
onClear,
|
||||
onDropdownVisibleChange,
|
||||
onOpenChange,
|
||||
onDeselect,
|
||||
onSearch,
|
||||
onSelect,
|
||||
@@ -441,12 +441,12 @@ const AsyncSelect = forwardRef(
|
||||
}
|
||||
}
|
||||
|
||||
if (onDropdownVisibleChange) {
|
||||
onDropdownVisibleChange(isDropdownVisible);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(isDropdownVisible);
|
||||
}
|
||||
};
|
||||
|
||||
const dropdownRender = (
|
||||
const popupRender = (
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) =>
|
||||
dropDownRenderHelper(
|
||||
@@ -605,7 +605,7 @@ const AsyncSelect = forwardRef(
|
||||
}
|
||||
data-test={ariaLabel || name}
|
||||
autoClearSearchValue={autoClearSearchValue}
|
||||
dropdownRender={dropdownRender}
|
||||
popupRender={popupRender}
|
||||
filterOption={handleFilterOption}
|
||||
filterSort={sortComparatorWithSearch}
|
||||
getPopupContainer={
|
||||
@@ -618,7 +618,7 @@ const AsyncSelect = forwardRef(
|
||||
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
|
||||
onBlur={handleOnBlur}
|
||||
onDeselect={handleOnDeselect}
|
||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||
onOpenChange={handleOnDropdownVisibleChange}
|
||||
// @ts-ignore
|
||||
onPaste={onPaste}
|
||||
onPopupScroll={handlePagination}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { StoryObj } from '@storybook/react';
|
||||
import { noop } from 'lodash';
|
||||
import { SelectOptionsType, SelectProps } from './types';
|
||||
import { Select } from '.';
|
||||
|
||||
@@ -93,23 +94,26 @@ export const InteractiveSelect: StoryObj = {
|
||||
options,
|
||||
optionsCount,
|
||||
...args
|
||||
}: SelectProps & { header: string; optionsCount: number }) => (
|
||||
<div
|
||||
style={{
|
||||
width: DEFAULT_WIDTH,
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
{...args}
|
||||
options={
|
||||
Array.isArray(options)
|
||||
? generateOptions(options, optionsCount)
|
||||
: options
|
||||
}
|
||||
mode="multiple"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}: 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>
|
||||
);
|
||||
},
|
||||
args: {
|
||||
autoFocus: true,
|
||||
allowNewOptions: false,
|
||||
|
||||
@@ -104,7 +104,7 @@ const Select = forwardRef(
|
||||
onBlur,
|
||||
onChange,
|
||||
onClear,
|
||||
onDropdownVisibleChange,
|
||||
onOpenChange,
|
||||
onDeselect,
|
||||
onSearch,
|
||||
onSelect,
|
||||
@@ -398,8 +398,8 @@ const Select = forwardRef(
|
||||
if (!isDropdownVisible) {
|
||||
setSelectOptions(initialOptionsSorted);
|
||||
}
|
||||
if (onDropdownVisibleChange) {
|
||||
onDropdownVisibleChange(isDropdownVisible);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(isDropdownVisible);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -492,7 +492,7 @@ const Select = forwardRef(
|
||||
],
|
||||
);
|
||||
|
||||
const dropdownRender = (
|
||||
const popupRender = (
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) =>
|
||||
dropDownRenderHelper(
|
||||
@@ -694,7 +694,7 @@ const Select = forwardRef(
|
||||
}
|
||||
data-test={ariaLabel || name}
|
||||
autoClearSearchValue={autoClearSearchValue}
|
||||
dropdownRender={dropdownRender}
|
||||
popupRender={popupRender}
|
||||
filterOption={handleFilterOption}
|
||||
filterSort={sortComparatorWithSearch}
|
||||
getPopupContainer={
|
||||
@@ -708,7 +708,7 @@ const Select = forwardRef(
|
||||
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
|
||||
onBlur={handleOnBlur}
|
||||
onDeselect={handleOnDeselect}
|
||||
onDropdownVisibleChange={handleOnDropdownVisibleChange}
|
||||
onOpenChange={handleOnDropdownVisibleChange}
|
||||
// @ts-ignore
|
||||
onPaste={onPaste}
|
||||
onPopupScroll={undefined}
|
||||
|
||||
@@ -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.colors.primary.base};
|
||||
outline: 2px solid ${theme.colorPrimary};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
flex: ${headerPosition === 'left' ? 1 : 0};
|
||||
|
||||
@@ -62,7 +62,7 @@ export type AntdExposedProps = Pick<
|
||||
| 'onBlur'
|
||||
| 'onPopupScroll'
|
||||
| 'onSearch'
|
||||
| 'onDropdownVisibleChange'
|
||||
| 'onOpenChange'
|
||||
| 'optionRender'
|
||||
| 'placeholder'
|
||||
| 'showArrow'
|
||||
@@ -95,7 +95,7 @@ export interface BaseSelectProps extends AntdExposedProps {
|
||||
/**
|
||||
* Renders the dropdown
|
||||
*/
|
||||
dropdownRender?: (
|
||||
popupRender?: (
|
||||
menu: ReactElement<any, string | JSXElementConstructor<any>>,
|
||||
) => ReactElement<any, string | JSXElementConstructor<any>>;
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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' },
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
@@ -163,4 +163,6 @@ export * from './Steps';
|
||||
export * from './Table';
|
||||
export * from './TableView';
|
||||
export * from './Tag';
|
||||
export * from './UnsavedChangesModal';
|
||||
export * from './constants';
|
||||
export * from './Result';
|
||||
|
||||
@@ -21,6 +21,8 @@ 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;
|
||||
|
||||
@@ -40,7 +40,7 @@ export const buildCustomFormatters = (
|
||||
const actualCurrencyFormat = currencyFormat?.symbol
|
||||
? currencyFormat
|
||||
: savedCurrencyFormats[metric];
|
||||
return actualCurrencyFormat
|
||||
return actualCurrencyFormat?.symbol
|
||||
? {
|
||||
...acc,
|
||||
[metric]: new CurrencyFormatter({
|
||||
|
||||
@@ -20,6 +20,8 @@ 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 {
|
||||
|
||||
@@ -48,6 +48,7 @@ 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(
|
||||
|
||||
@@ -31,6 +31,7 @@ type ExtraFilterQueryField = {
|
||||
granularity_sqla?: string;
|
||||
time_grain_sqla?: TimeGranularity;
|
||||
granularity?: string;
|
||||
time_compare?: string;
|
||||
};
|
||||
|
||||
type ExtractedExtra = ExtraFilterQueryField & {
|
||||
@@ -57,6 +58,7 @@ 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 => {
|
||||
|
||||
@@ -71,6 +71,8 @@ export type QueryObjectExtras = Partial<{
|
||||
where?: string;
|
||||
/** Instant Time Comparison */
|
||||
instant_time_comparison_range?: string;
|
||||
|
||||
time_compare?: string;
|
||||
}>;
|
||||
|
||||
export type ResidualQueryObjectData = {
|
||||
|
||||
@@ -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'
|
||||
'relative_start' | 'relative_end' | 'time_grain_sqla' | 'time_compare'
|
||||
>;
|
||||
|
||||
/** 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'>>;
|
||||
Partial<Pick<QueryObject, 'time_column' | 'time_grain' | 'time_compare'>>;
|
||||
|
||||
/** These parameters override those already present in the form data/query object */
|
||||
export type ExtraFormDataOverride = ExtraFormDataOverrideRegular &
|
||||
|
||||
@@ -30,7 +30,8 @@ export type TimeColumnConfigKey =
|
||||
| '__time_col'
|
||||
| '__time_grain'
|
||||
| '__time_range'
|
||||
| '__granularity';
|
||||
| '__granularity'
|
||||
| '__time_compare';
|
||||
|
||||
export type AppliedTimeExtras = Partial<
|
||||
Record<TimeColumnConfigKey, keyof QueryObject>
|
||||
|
||||
@@ -58,17 +58,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,20 @@ 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 {
|
||||
@@ -35,6 +43,7 @@ import {
|
||||
CacheProvider as EmotionCacheProvider,
|
||||
} from '@emotion/react';
|
||||
import createCache from '@emotion/cache';
|
||||
import { noop } from 'lodash';
|
||||
import { GlobalStyles } from './GlobalStyles';
|
||||
|
||||
import {
|
||||
@@ -66,7 +75,7 @@ export class Theme {
|
||||
brandLogoAlt: 'Apache Superset',
|
||||
brandLogoUrl: '/static/assets/images/superset-logo-horiz.png',
|
||||
brandLogoMargin: '18px',
|
||||
brandLogoHref: 'https://superset.apache.org',
|
||||
brandLogoHref: '/',
|
||||
brandLogoHeight: '24px',
|
||||
|
||||
// Default colors
|
||||
@@ -285,6 +294,7 @@ export class Theme {
|
||||
antdConfig: AntdThemeConfig,
|
||||
emotionCache: any,
|
||||
): void {
|
||||
noop(theme, antdConfig, emotionCache);
|
||||
// Overridden at runtime by SupersetThemeProvider using setThemeState
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import { isRequired } from '../utils';
|
||||
import { TimeFormatFunction } from './types';
|
||||
import stringifyTimeInput from './utils/stringifyTimeInput';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
|
||||
|
||||
export const PREVIEW_TIME = new Date(Date.UTC(2017, 1, 14, 11, 22, 33));
|
||||
|
||||
// Use type augmentation to indicate that
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
import { ExtensibleFunction } from '../models';
|
||||
import { TimeRangeFormatFunction } from './types';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
|
||||
|
||||
// Use type augmentation to indicate that
|
||||
// an instance of TimeFormatter is also a function
|
||||
interface TimeRangeFormatter {
|
||||
|
||||
@@ -60,6 +60,8 @@ export enum FeatureFlag {
|
||||
SlackEnableAvatars = 'SLACK_ENABLE_AVATARS',
|
||||
EnableDashboardScreenshotEndpoints = 'ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS',
|
||||
EnableDashboardDownloadWebDriverScreenshot = 'ENABLE_DASHBOARD_DOWNLOAD_WEBDRIVER_SCREENSHOT',
|
||||
TableV2TimeComparisonEnabled = 'TABLE_V2_TIME_COMPARISON_ENABLED',
|
||||
AgGridTableEnabled = 'AG_GRID_TABLE_ENABLED',
|
||||
}
|
||||
|
||||
export type ScheduleQueriesProps = {
|
||||
|
||||
@@ -35,6 +35,13 @@ const xssFilter = new FilterXSS({
|
||||
'width',
|
||||
'muted',
|
||||
],
|
||||
table: ['width', 'border', 'align', 'valign', 'style'],
|
||||
tr: ['rowspan', 'align', 'valign', 'style'],
|
||||
td: ['width', 'rowspan', 'colspan', 'align', 'valign', 'style'],
|
||||
th: ['width', 'rowspan', 'colspan', 'align', 'valign', 'style'],
|
||||
tbody: ['align', 'valign', 'style'],
|
||||
thead: ['align', 'valign', 'style'],
|
||||
tfoot: ['align', 'valign', 'style'],
|
||||
},
|
||||
stripIgnoreTag: true,
|
||||
css: false,
|
||||
|
||||
@@ -51,8 +51,8 @@ export const TestComponent = ({
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
backgroundColor: theme.colors.primary.light5,
|
||||
color: theme.colors.grayscale.light3,
|
||||
backgroundColor: theme.colorPrimaryBg,
|
||||
color: theme.colorTextSecondary,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
@@ -66,7 +66,7 @@ export const TestComponent = ({
|
||||
{[width, height].join('x')}
|
||||
</div>
|
||||
<div className="formData" style={{ padding: 10 }}>
|
||||
<code style={{ color: theme.colors.primary.light2 }}>
|
||||
<code style={{ color: theme.colorTextSecondary }}>
|
||||
{JSON.stringify(formData)}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import {
|
||||
buildCustomFormatters,
|
||||
Currency,
|
||||
CurrencyFormatter,
|
||||
getCustomFormatter,
|
||||
getNumberFormatter,
|
||||
@@ -116,6 +117,19 @@ test('buildCustomFormatters uses dataset d3 format if not provided in control pa
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCustomFormatters returns NumberFormatter for a d3format with currency set to {}', () => {
|
||||
const customFormatters: Record<string, ValueFormatter> =
|
||||
buildCustomFormatters(
|
||||
['count'],
|
||||
{ count: {} as Currency },
|
||||
{ count: ',.2%' },
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(customFormatters.count).toBeInstanceOf(NumberFormatter);
|
||||
});
|
||||
|
||||
test('getCustomFormatter', () => {
|
||||
const customFormatters = {
|
||||
sum__num: new CurrencyFormatter({
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
|
||||
|
||||
import { ExtensibleFunction } from '@superset-ui/core';
|
||||
|
||||
describe('ExtensibleFunction', () => {
|
||||
|
||||
@@ -150,3 +150,32 @@ test('should sanitize HTML input', () => {
|
||||
|
||||
expect(html).toMatch(expectedHtml);
|
||||
});
|
||||
|
||||
test('should preserve table styling after sanitization (fixes ECharts tooltip formatting)', () => {
|
||||
const tableWithStyles = `
|
||||
<table>
|
||||
<tr style="opacity: 0.8;">
|
||||
<td style="text-align: left; padding-left: 0px;">Label</td>
|
||||
<td style="text-align: right; padding-left: 16px;">Value</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
const sanitized = sanitizeHtml(tableWithStyles);
|
||||
expect(sanitized).toContain('style="opacity: 0.8;"');
|
||||
expect(sanitized).toContain('style="text-align: left; padding-left: 0px;"');
|
||||
expect(sanitized).toContain('style="text-align: right; padding-left: 16px;"');
|
||||
|
||||
const data = [
|
||||
['Metric', 'Value'],
|
||||
['Sales', '$1,234'],
|
||||
];
|
||||
const html = tooltipHtml(data, 'Test Tooltip');
|
||||
|
||||
expect(html).toContain('style="opacity: 0.8;"');
|
||||
expect(html).toContain('text-align: left');
|
||||
expect(html).toContain('text-align: right');
|
||||
expect(html).toContain('padding-left: 0px');
|
||||
expect(html).toContain('padding-left: 16px');
|
||||
expect(html).toContain('max-width: 300px');
|
||||
});
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.14.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mihkeleidast/storybook-addon-source": "^1.0.1",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@storybook/addon-actions": "8.1.11",
|
||||
"@storybook/addon-actions": "9.0.8",
|
||||
"@storybook/addon-controls": "8.1.11",
|
||||
"@storybook/addon-links": "8.1.11",
|
||||
"@storybook/react": "8.1.11",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@storybook/react-webpack5": "8.2.9",
|
||||
"babel-loader": "^10.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
|
||||
@@ -28,9 +28,7 @@ const colorTypes = [
|
||||
'grayscale',
|
||||
];
|
||||
|
||||
const AntDFunctionalColors = ({ antdTheme }) => {
|
||||
const { antd } = supersetTheme;
|
||||
|
||||
const AntDFunctionalColors = () => {
|
||||
// Define color types and variants dynamically
|
||||
const variants = [
|
||||
'active',
|
||||
@@ -44,7 +42,6 @@ const AntDFunctionalColors = ({ antdTheme }) => {
|
||||
'bg',
|
||||
];
|
||||
|
||||
const { colors } = supersetTheme;
|
||||
return (
|
||||
<table
|
||||
style={{ borderCollapse: 'collapse', width: '100%', textAlign: 'left' }}
|
||||
@@ -63,32 +60,29 @@ const AntDFunctionalColors = ({ antdTheme }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{colorTypes.map(type => {
|
||||
const typeKey = `color${type}`;
|
||||
return (
|
||||
<tr key={type}>
|
||||
<td style={{ border: '1px solid #ddd', padding: '8px' }}>
|
||||
<strong>{type}</strong>
|
||||
</td>
|
||||
{variants.map(variant => {
|
||||
const color = themeObject.getColorVariants(type)[variant];
|
||||
return (
|
||||
<td
|
||||
key={variant}
|
||||
style={{
|
||||
border: '1px solid #ddd',
|
||||
padding: '8px',
|
||||
backgroundColor: color || 'transparent',
|
||||
color: [`color${type}${variant}`],
|
||||
}}
|
||||
>
|
||||
{color ? <code>{color}</code> : '-'}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{colorTypes.map(type => (
|
||||
<tr key={type}>
|
||||
<td style={{ border: '1px solid #ddd', padding: '8px' }}>
|
||||
<strong>{type}</strong>
|
||||
</td>
|
||||
{variants.map(variant => {
|
||||
const color = themeObject.getColorVariants(type)[variant];
|
||||
return (
|
||||
<td
|
||||
key={variant}
|
||||
style={{
|
||||
border: '1px solid #ddd',
|
||||
padding: '8px',
|
||||
backgroundColor: color || 'transparent',
|
||||
color: `color${type}${variant}`,
|
||||
}}
|
||||
>
|
||||
{color ? <code>{color}</code> : '-'}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
@@ -159,7 +153,7 @@ export const ThemeColors = () => {
|
||||
</table>
|
||||
<h2>Ant Design Theme Colors</h2>
|
||||
<h3>Functional Colors</h3>
|
||||
<AntDFunctionalColors antdTheme={supersetTheme.antd} />
|
||||
<AntDFunctionalColors />
|
||||
<h2>The supersetTheme object</h2>
|
||||
<pre>
|
||||
<code>{JSON.stringify(supersetTheme, null, 2)}</code>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user