mirror of
https://github.com/apache/superset.git
synced 2026-06-25 01:19:17 +00:00
Compare commits
59 Commits
fix_exampl
...
titan-filt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72f4a52c97 | ||
|
|
c85229d9b4 | ||
|
|
fdea4e21b0 | ||
|
|
e20a08cb14 | ||
|
|
429935a277 | ||
|
|
a4bb11c755 | ||
|
|
f0b6e87091 | ||
|
|
ea5a609d0b | ||
|
|
0abe6eed89 | ||
|
|
e205846845 | ||
|
|
deef923825 | ||
|
|
0fa3feb088 | ||
|
|
1393f7d3d2 | ||
|
|
b7ba50033a | ||
|
|
ce9759785a | ||
|
|
8de58b9848 | ||
|
|
cc8ab2c556 | ||
|
|
1409b1a25b | ||
|
|
bdfb698aa4 | ||
|
|
57183da315 | ||
|
|
c928f23e1b | ||
|
|
0c89914a6d | ||
|
|
630e0e0240 | ||
|
|
513047c3bb | ||
|
|
d932837a3c | ||
|
|
38868f9ff4 | ||
|
|
8013b32f0e | ||
|
|
adeed60fe0 | ||
|
|
546945e7a6 | ||
|
|
5b2f1bbf9e | ||
|
|
875f538d54 | ||
|
|
b7d3ff1e85 | ||
|
|
c03964dc5f | ||
|
|
950a3313d8 | ||
|
|
e2a22d481c | ||
|
|
b4e2406385 | ||
|
|
ca9e74edd8 | ||
|
|
39b3de6b5d | ||
|
|
26563bb330 | ||
|
|
0653e123cc | ||
|
|
76358ed64e | ||
|
|
217f11a8f7 | ||
|
|
af21ef2497 | ||
|
|
51c25831e8 | ||
|
|
be41e0526a | ||
|
|
0f240ea1b2 | ||
|
|
e520538af6 | ||
|
|
e03d840d06 | ||
|
|
1921ba993e | ||
|
|
b050897ebd | ||
|
|
0bdd8a223d | ||
|
|
d12f86363f | ||
|
|
9f680a63f8 | ||
|
|
928a052440 | ||
|
|
fbc84a1f9a | ||
|
|
fa1693dc5f | ||
|
|
8a8fb49617 | ||
|
|
dc4474889d | ||
|
|
29ac507d56 |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -27,6 +27,8 @@ updates:
|
||||
- package-ecosystem: "uv"
|
||||
directory: "requirements/"
|
||||
open-pull-requests-limit: 10
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- uv
|
||||
- dependabot
|
||||
|
||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -48,6 +48,8 @@ jobs:
|
||||
allow-dependencies-licenses: pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils, pkg:npm/node-forge@1.3.1, pkg:npm/rgbcolor, pkg:npm/jszip@3.10.1
|
||||
|
||||
python-dependency-liccheck:
|
||||
# NOTE: Configuration for liccheck lives in our pyproject.yml.
|
||||
# You cannot use a liccheck.ini file in this workflow.
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
|
||||
2
.github/workflows/superset-docs-verify.yml
vendored
2
.github/workflows/superset-docs-verify.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Do not bump this linkinator-action version without opening
|
||||
# an ASF Infra ticket to allow the new verison first!
|
||||
# an ASF Infra ticket to allow the new version first!
|
||||
- uses: JustinBeckwith/linkinator-action@v1.11.0
|
||||
continue-on-error: true # This will make the job advisory (non-blocking, no red X)
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
######################################################################
|
||||
# Node stage to deal with static asset construction
|
||||
######################################################################
|
||||
ARG PY_VER=3.11.11-slim-bookworm
|
||||
ARG PY_VER=3.11.12-slim-bookworm
|
||||
|
||||
# If BUILDPLATFORM is null, set it to 'amd64' (or leave as is otherwise).
|
||||
ARG BUILDPLATFORM=${BUILDPLATFORM:-amd64}
|
||||
|
||||
@@ -43,6 +43,7 @@ Join our growing community!
|
||||
- [Cape Crypto](https://capecrypto.com)
|
||||
- [Capital Service S.A.](https://capitalservice.pl) [@pkonarzewski]
|
||||
- [Clark.de](https://clark.de/)
|
||||
- [Europace](https://europace.de)
|
||||
- [KarrotPay](https://www.daangnpay.com/)
|
||||
- [Remita](https://remita.net) [@mujibishola]
|
||||
- [Taveo](https://www.taveo.com) [@codek]
|
||||
|
||||
@@ -250,6 +250,14 @@ Will be rendered as:
|
||||
SELECT * FROM users WHERE role IN ('admin', 'viewer')
|
||||
```
|
||||
|
||||
**Current User RLS Rules**
|
||||
|
||||
The `{{ current_user_rls_rules() }}` macro returns an array of RLS rules applied to the current dataset for the logged in user.
|
||||
|
||||
If you have caching enabled in your Superset configuration, then the list of RLS Rules will be used
|
||||
by Superset when calculating the cache key. A cache key is a unique identifier that determines if there's a
|
||||
cache hit in the future and Superset can retrieve cached data.
|
||||
|
||||
**Custom URL Parameters**
|
||||
|
||||
The `{{ url_param('custom_variable') }}` macro lets you define arbitrary URL
|
||||
|
||||
@@ -64,6 +64,56 @@ check the [supersetbot docker](https://github.com/apache-superset/supersetbot)
|
||||
subcommand and the [docker.yml](https://github.com/apache/superset/blob/master/.github/workflows/docker.yml)
|
||||
GitHub action.
|
||||
|
||||
## Building your own production Docker image
|
||||
|
||||
Every Superset deployment will require its own set of drivers depending on the data warehouse(s),
|
||||
etc. so we recommend that users build their own Docker image by extending the `lean` image.
|
||||
|
||||
Here's an example Dockerfile that does this. Follow the in-line comments to customize it for
|
||||
your desired Superset version and database drivers. The comments also note that a certain feature flag will
|
||||
have to be enabled in your config file.
|
||||
|
||||
You would build the image with `docker build -t mysuperset:latest .` or `docker build -t ourcompanysuperset:4.1.2 .`
|
||||
|
||||
```Dockerfile
|
||||
# change this to apache/superset:4.1.2 or whatever version you want to build from;
|
||||
# otherwise the default is the latest commit on GitHub master branch
|
||||
FROM apache/superset:master
|
||||
|
||||
USER root
|
||||
|
||||
# Set environment variable for Playwright
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers
|
||||
|
||||
# Install packages using uv into the virtual environment
|
||||
# Superset started using uv after the 4.1 branch; if you are building from apache/superset:4.1.x,
|
||||
# replace the first two lines with RUN pip install \
|
||||
RUN . /app/.venv/bin/activate && \
|
||||
uv pip install \
|
||||
# install psycopg2 for using PostgreSQL metadata store - could be a MySQL package if using that backend:
|
||||
psycopg2-binary \
|
||||
# add the driver(s) for your data warehouse(s), in this example we're showing for Microsoft SQL Server:
|
||||
pymssql \
|
||||
# package needed for using single-sign on authentication:
|
||||
Authlib \
|
||||
# openpyxl to be able to upload Excel files
|
||||
openpyxl \
|
||||
# Pillow for Alerts & Reports to generate PDFs of dashboards
|
||||
Pillow \
|
||||
# install Playwright for taking screenshots for Alerts & Reports. This assumes the feature flag PLAYWRIGHT_REPORTS_AND_THUMBNAILS is enabled
|
||||
# That feature flag will default to True starting in 6.0.0
|
||||
# Playwright works only with Chrome.
|
||||
# If you are still using Selenium instead of Playwright, you would instead install here the selenium package and a headless browser & webdriver
|
||||
playwright \
|
||||
&& playwright install-deps \
|
||||
&& PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/playwright-browsers playwright install chromium
|
||||
|
||||
# Switch back to the superset user
|
||||
USER superset
|
||||
|
||||
CMD ["/app/docker/entrypoints/run-server.sh"]
|
||||
```
|
||||
|
||||
## Key ARGs in Dockerfile
|
||||
|
||||
- `BUILD_TRANSLATIONS`: whether to build the translations into the image. For the
|
||||
|
||||
@@ -27,9 +27,7 @@ You will need to back up your metadata DB. That could mean backing up the servic
|
||||
|
||||
You will also need to extend the Superset docker image. The default `lean` images do not contain drivers needed to access your metadata database (Postgres or MySQL), nor to access your data warehouse, nor the headless browser needed for Alerts & Reports. You could run a `-dev` image while demoing Superset, which has some of this, but you'll still need to install the driver for your data warehouse. The `-dev` images run as root, which is not recommended for production.
|
||||
|
||||
Ideally you will build your own image of Superset that extends `lean`, adding what your deployment needs.
|
||||
|
||||
See [Docker Build Presets](/docs/installation/docker-builds/#build-presets) for more information about the different image versions you can extend.
|
||||
Ideally you will build your own image of Superset that extends `lean`, adding what your deployment needs. See [Building your own production Docker image](/docs/installation/docker-builds/#building-your-own-production-docker-image).
|
||||
|
||||
## [Kubernetes (K8s)](/docs/installation/kubernetes.mdx)
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
title: CVEs fixed by release
|
||||
sidebar_position: 2
|
||||
---
|
||||
#### Version 4.1.2
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|:---------------|:-----------------------------------------------------------------------------------|---------:|
|
||||
| CVE-2025-27696 | Improper authorization leading to resource ownership takeover | < 4.1.2 |
|
||||
|
||||
#### Version 4.1.0
|
||||
|
||||
| CVE | Title | Affected |
|
||||
|
||||
@@ -26,10 +26,10 @@
|
||||
"@emotion/styled": "^10.0.27",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@superset-ui/style": "^0.14.23",
|
||||
"antd": "^5.24.9",
|
||||
"antd": "^5.25.1",
|
||||
"docusaurus-plugin-less": "^2.0.2",
|
||||
"less": "^4.3.0",
|
||||
"less-loader": "^11.0.0",
|
||||
"less-loader": "^12.3.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -44,12 +44,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"prettier": "^2.0.0",
|
||||
"typescript": "~5.8.3",
|
||||
"webpack": "^5.99.7"
|
||||
"webpack": "^5.99.8"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -4179,10 +4179,10 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
|
||||
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
|
||||
|
||||
antd@^5.24.9:
|
||||
version "5.24.9"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.24.9.tgz#c5862e02ed770bd95e312961f4f0b7b158a004d9"
|
||||
integrity sha512-liB+Y/JwD5/KSKbK1Z1EVAbWcoWYvWJ1s97AbbT+mOdigpJQuWwH7kG8IXNEljI7onvj0DdD43TXhSRLUu9AMA==
|
||||
antd@^5.25.1:
|
||||
version "5.25.1"
|
||||
resolved "https://registry.yarnpkg.com/antd/-/antd-5.25.1.tgz#859b419a18d113492304ccd66c29074a71902241"
|
||||
integrity sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^7.2.0"
|
||||
"@ant-design/cssinjs" "^1.23.0"
|
||||
@@ -4199,7 +4199,7 @@ antd@^5.24.9:
|
||||
classnames "^2.5.1"
|
||||
copy-to-clipboard "^3.3.3"
|
||||
dayjs "^1.11.11"
|
||||
rc-cascader "~3.33.1"
|
||||
rc-cascader "~3.34.0"
|
||||
rc-checkbox "~3.5.0"
|
||||
rc-collapse "~3.9.0"
|
||||
rc-dialog "~9.6.0"
|
||||
@@ -4219,7 +4219,7 @@ antd@^5.24.9:
|
||||
rc-rate "~2.13.1"
|
||||
rc-resize-observer "^1.4.3"
|
||||
rc-segmented "~2.7.0"
|
||||
rc-select "~14.16.6"
|
||||
rc-select "~14.16.7"
|
||||
rc-slider "~11.1.8"
|
||||
rc-steps "~6.0.1"
|
||||
rc-switch "~4.1.0"
|
||||
@@ -4229,7 +4229,7 @@ antd@^5.24.9:
|
||||
rc-tooltip "~6.4.0"
|
||||
rc-tree "~5.13.1"
|
||||
rc-tree-select "~5.27.0"
|
||||
rc-upload "~4.8.1"
|
||||
rc-upload "~4.9.0"
|
||||
rc-util "^5.44.4"
|
||||
scroll-into-view-if-needed "^3.1.0"
|
||||
throttle-debounce "^5.0.2"
|
||||
@@ -6247,10 +6247,10 @@ escape-string-regexp@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
|
||||
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
|
||||
|
||||
eslint-config-prettier@^10.1.2:
|
||||
version "10.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276"
|
||||
integrity sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==
|
||||
eslint-config-prettier@^10.1.5:
|
||||
version "10.1.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz#00c18d7225043b6fbce6a665697377998d453782"
|
||||
integrity sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==
|
||||
|
||||
eslint-plugin-prettier@^4.0.0:
|
||||
version "4.2.1"
|
||||
@@ -8211,10 +8211,10 @@ layout-base@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285"
|
||||
integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
|
||||
|
||||
less-loader@^11.0.0:
|
||||
version "11.1.4"
|
||||
resolved "https://registry.npmjs.org/less-loader/-/less-loader-11.1.4.tgz"
|
||||
integrity sha512-6/GrYaB6QcW6Vj+/9ZPgKKs6G10YZai/l/eJ4SLwbzqNTBsAqt5hSLVF47TgsiBxV1P6eAU0GYRH3YRuQU9V3A==
|
||||
less-loader@^12.3.0:
|
||||
version "12.3.0"
|
||||
resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-12.3.0.tgz#d4a00361568be86a97da3df4f16954b0d4c15340"
|
||||
integrity sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==
|
||||
|
||||
less@^4.3.0:
|
||||
version "4.3.0"
|
||||
@@ -10605,10 +10605,10 @@ raw-body@2.5.2:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
rc-cascader@~3.33.1:
|
||||
version "3.33.1"
|
||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.33.1.tgz#19e01462ef5ef51b723c1f562c7b9cde4691e7ee"
|
||||
integrity sha512-Kyl4EJ7ZfCBuidmZVieegcbFw0RcU5bHHSbtEdmuLYd0fYHCAiYKZ6zon7fWAVyC6rWWOOib0XKdTSf7ElC9rg==
|
||||
rc-cascader@~3.34.0:
|
||||
version "3.34.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.34.0.tgz#56f936ab6b1229bab7d558701ce9b9e96536582c"
|
||||
integrity sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.25.7"
|
||||
classnames "^2.3.1"
|
||||
@@ -10821,10 +10821,10 @@ rc-segmented@~2.7.0:
|
||||
rc-motion "^2.4.4"
|
||||
rc-util "^5.17.0"
|
||||
|
||||
rc-select@~14.16.2, rc-select@~14.16.6:
|
||||
version "14.16.6"
|
||||
resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz"
|
||||
integrity sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==
|
||||
rc-select@~14.16.2, rc-select@~14.16.7:
|
||||
version "14.16.8"
|
||||
resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.16.8.tgz#78e6782f1ccc1f03d9003bc3effa4ed609d29a97"
|
||||
integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
"@rc-component/trigger" "^2.1.1"
|
||||
@@ -10929,10 +10929,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.8.1:
|
||||
version "4.8.1"
|
||||
resolved "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz"
|
||||
integrity sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==
|
||||
rc-upload@~4.9.0:
|
||||
version "4.9.0"
|
||||
resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.9.0.tgz#911963ab5a0b538c743765371c05e2de9e3f5436"
|
||||
integrity sha512-pAzlPnyiFn1GCtEybEG2m9nXNzQyWXqWV2xFYCmDxjN9HzyjS5Pz2F+pbNdYw8mMJsixLEKLG0wVy9vOGxJMJA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
classnames "^2.2.5"
|
||||
@@ -13033,10 +13033,10 @@ webpack-sources@^3.2.3:
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||
|
||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.7:
|
||||
version "5.99.7"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.7.tgz#60201c1ca66da046b07d006c2f6e0cc5e8a7bdba"
|
||||
integrity sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==
|
||||
webpack@^5.88.1, webpack@^5.95.0, webpack@^5.99.8:
|
||||
version "5.99.8"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.99.8.tgz#dd31a020b7c092d30c4c6d9a4edb95809e7f5946"
|
||||
integrity sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==
|
||||
dependencies:
|
||||
"@types/eslint-scope" "^3.7.7"
|
||||
"@types/estree" "^1.0.6"
|
||||
|
||||
@@ -44,7 +44,7 @@ dependencies = [
|
||||
"cryptography>=42.0.4, <45.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <3.0.0",
|
||||
"flask-appbuilder>=4.6.3, <5.0.0",
|
||||
"flask-appbuilder>=4.7.0, <5.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -240,6 +240,12 @@ disallow_untyped_calls = false
|
||||
disallow_untyped_defs = false
|
||||
disable_error_code = "annotation-unchecked"
|
||||
|
||||
# TODO: remove this once cryptography is fixed, introduced in cryptography 44.0.3
|
||||
[[tool.mypy.overrides]]
|
||||
module = "cryptography.*"
|
||||
ignore_errors = true
|
||||
follow_imports = "skip"
|
||||
|
||||
[tool.ruff]
|
||||
# Exclude a variety of commonly ignored directories.
|
||||
exclude = [
|
||||
@@ -272,7 +278,6 @@ exclude = [
|
||||
"venv",
|
||||
]
|
||||
|
||||
|
||||
# Same as Black.
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
@@ -367,6 +372,7 @@ docstring-code-line-length = "dynamic"
|
||||
requirement_txt_file = "requirements/base.txt"
|
||||
authorized_licenses = [
|
||||
"academic free license (afl)",
|
||||
"any-osi",
|
||||
"apache license 2.0",
|
||||
"apache software",
|
||||
"apache software, bsd",
|
||||
@@ -380,6 +386,7 @@ authorized_licenses = [
|
||||
"osi approved",
|
||||
"psf-2.0",
|
||||
"python software foundation",
|
||||
"simplified bsd",
|
||||
"the unlicense (unlicense)",
|
||||
"the unlicense",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml requirements/base.in -o requirements/base.txt
|
||||
alembic==1.15.1
|
||||
alembic==1.15.2
|
||||
# via flask-migrate
|
||||
amqp==5.3.1
|
||||
# via kombu
|
||||
@@ -8,7 +8,7 @@ apispec==6.6.1
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# flask-appbuilder
|
||||
apsw==3.49.1.0
|
||||
apsw==3.49.2.0
|
||||
# via shillelagh
|
||||
async-timeout==4.0.3
|
||||
# via
|
||||
@@ -32,7 +32,7 @@ billiard==4.2.1
|
||||
# via celery
|
||||
blinker==1.9.0
|
||||
# via flask
|
||||
bottleneck==1.4.2
|
||||
bottleneck==1.5.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
brotli==1.1.0
|
||||
# via flask-compress
|
||||
@@ -42,11 +42,11 @@ cachelib==0.13.0
|
||||
# flask-session
|
||||
cachetools==5.5.2
|
||||
# via google-auth
|
||||
cattrs==24.1.2
|
||||
cattrs==24.1.3
|
||||
# via requests-cache
|
||||
celery==5.5.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
certifi==2025.1.31
|
||||
certifi==2025.4.26
|
||||
# via
|
||||
# requests
|
||||
# selenium
|
||||
@@ -54,9 +54,9 @@ cffi==1.17.1
|
||||
# via
|
||||
# cryptography
|
||||
# pynacl
|
||||
charset-normalizer==3.4.1
|
||||
charset-normalizer==3.4.2
|
||||
# via requests
|
||||
click==8.1.8
|
||||
click==8.2.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# celery
|
||||
@@ -99,7 +99,7 @@ email-validator==2.2.0
|
||||
# via flask-appbuilder
|
||||
et-xmlfile==2.0.0
|
||||
# via openpyxl
|
||||
exceptiongroup==1.2.2
|
||||
exceptiongroup==1.3.0
|
||||
# via
|
||||
# cattrs
|
||||
# trio
|
||||
@@ -118,7 +118,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-babel==2.0.0
|
||||
# via flask-appbuilder
|
||||
@@ -152,13 +152,12 @@ geographiclib==2.0
|
||||
# via geopy
|
||||
geopy==2.4.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
google-auth==2.38.0
|
||||
google-auth==2.40.1
|
||||
# via shillelagh
|
||||
greenlet==3.1.1
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==23.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
@@ -174,6 +173,7 @@ idna==3.10
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
isodate==0.7.2
|
||||
@@ -190,7 +190,7 @@ jsonpath-ng==1.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
jsonschema==4.23.0
|
||||
# via flask-appbuilder
|
||||
jsonschema-specifications==2024.10.1
|
||||
jsonschema-specifications==2025.4.1
|
||||
# via jsonschema
|
||||
kombu==5.5.3
|
||||
# via celery
|
||||
@@ -243,7 +243,9 @@ openpyxl==3.1.5
|
||||
ordered-set==4.1.0
|
||||
# via flask-limiter
|
||||
outcome==1.3.0.post0
|
||||
# via trio
|
||||
# via
|
||||
# trio
|
||||
# trio-websocket
|
||||
packaging==25.0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
@@ -263,7 +265,7 @@ parsedatetime==2.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
pgsanity==0.2.9
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.7
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
ply==3.11
|
||||
# via jsonpath-ng
|
||||
@@ -279,7 +281,7 @@ pyasn1==0.6.1
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# rsa
|
||||
pyasn1-modules==0.4.1
|
||||
pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
@@ -336,13 +338,13 @@ requests-cache==1.2.1
|
||||
# via shillelagh
|
||||
rich==13.9.4
|
||||
# via flask-limiter
|
||||
rpds-py==0.23.1
|
||||
rpds-py==0.25.0
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
rsa==4.9
|
||||
rsa==4.9.1
|
||||
# via google-auth
|
||||
selenium==4.27.1
|
||||
selenium==4.32.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
shillelagh==1.3.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -352,7 +354,6 @@ six==1.17.0
|
||||
# via
|
||||
# prison
|
||||
# python-dateutil
|
||||
# url-normalize
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -373,7 +374,7 @@ sqlalchemy-utils==0.38.3
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# flask-appbuilder
|
||||
sqlglot==26.16.4
|
||||
sqlglot==26.17.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
sqlparse==0.5.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -381,17 +382,18 @@ sshtunnel==0.4.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
tabulate==0.8.10
|
||||
# via apache-superset (pyproject.toml)
|
||||
trio==0.28.0
|
||||
trio==0.30.0
|
||||
# via
|
||||
# selenium
|
||||
# trio-websocket
|
||||
trio-websocket==0.11.1
|
||||
trio-websocket==0.12.2
|
||||
# via selenium
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.13.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# alembic
|
||||
# cattrs
|
||||
# exceptiongroup
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
@@ -402,7 +404,7 @@ tzdata==2025.2
|
||||
# via
|
||||
# kombu
|
||||
# pandas
|
||||
url-normalize==1.4.3
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
urllib3==1.26.20
|
||||
# via
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# uv pip compile requirements/development.in -c requirements/base.txt -o requirements/development.txt
|
||||
-e .
|
||||
# via -r requirements/development.in
|
||||
alembic==1.15.1
|
||||
alembic==1.15.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-migrate
|
||||
@@ -14,7 +14,7 @@ apispec==6.6.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-appbuilder
|
||||
apsw==3.49.1.0
|
||||
apsw==3.49.2.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# shillelagh
|
||||
@@ -51,7 +51,7 @@ blinker==1.9.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask
|
||||
bottleneck==1.4.2
|
||||
bottleneck==1.5.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -68,7 +68,7 @@ cachetools==5.5.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
cattrs==24.1.2
|
||||
cattrs==24.1.3
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
@@ -76,7 +76,7 @@ celery==5.5.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
certifi==2025.1.31
|
||||
certifi==2025.4.26
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests
|
||||
@@ -88,11 +88,11 @@ cffi==1.17.1
|
||||
# pynacl
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
charset-normalizer==3.4.1
|
||||
charset-normalizer==3.4.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests
|
||||
click==8.1.8
|
||||
click==8.2.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -176,7 +176,7 @@ et-xmlfile==2.0.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# openpyxl
|
||||
exceptiongroup==1.2.2
|
||||
exceptiongroup==1.3.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# cattrs
|
||||
@@ -202,7 +202,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==4.6.3
|
||||
flask-appbuilder==4.7.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -280,7 +280,7 @@ google-api-core==2.23.0
|
||||
# google-cloud-core
|
||||
# pandas-gbq
|
||||
# sqlalchemy-bigquery
|
||||
google-auth==2.38.0
|
||||
google-auth==2.40.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-api-core
|
||||
@@ -318,7 +318,6 @@ greenlet==3.1.1
|
||||
# apache-superset
|
||||
# gevent
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
grpcio==1.71.0
|
||||
# via
|
||||
# apache-superset
|
||||
@@ -355,6 +354,7 @@ idna==3.10
|
||||
# email-validator
|
||||
# requests
|
||||
# trio
|
||||
# url-normalize
|
||||
importlib-metadata==8.7.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -389,7 +389,7 @@ jsonschema==4.23.0
|
||||
# openapi-spec-validator
|
||||
jsonschema-path==0.3.4
|
||||
# via openapi-spec-validator
|
||||
jsonschema-specifications==2024.10.1
|
||||
jsonschema-specifications==2025.4.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonschema
|
||||
@@ -495,6 +495,7 @@ outcome==1.3.0.post0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# trio
|
||||
# trio-websocket
|
||||
packaging==25.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
@@ -542,7 +543,7 @@ pillow==10.3.0
|
||||
# via
|
||||
# apache-superset
|
||||
# matplotlib
|
||||
platformdirs==4.3.7
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
@@ -598,7 +599,7 @@ pyasn1==0.6.1
|
||||
# pyasn1-modules
|
||||
# python-ldap
|
||||
# rsa
|
||||
pyasn1-modules==0.4.1
|
||||
pyasn1-modules==0.4.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
@@ -731,22 +732,22 @@ rich==13.9.4
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# flask-limiter
|
||||
rpds-py==0.23.1
|
||||
rpds-py==0.25.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# jsonschema
|
||||
# referencing
|
||||
rsa==4.9
|
||||
rsa==4.9.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# google-auth
|
||||
ruff==0.8.0
|
||||
# via apache-superset
|
||||
selenium==4.27.1
|
||||
selenium==4.32.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
setuptools==75.6.0
|
||||
setuptools==80.7.1
|
||||
# via
|
||||
# nodeenv
|
||||
# pandas-gbq
|
||||
@@ -767,7 +768,6 @@ six==1.17.0
|
||||
# prison
|
||||
# python-dateutil
|
||||
# rfc3339-validator
|
||||
# url-normalize
|
||||
# wtforms-json
|
||||
slack-sdk==3.35.0
|
||||
# via
|
||||
@@ -799,7 +799,7 @@ sqlalchemy-utils==0.38.3
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
# flask-appbuilder
|
||||
sqlglot==26.16.4
|
||||
sqlglot==26.17.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# apache-superset
|
||||
@@ -829,21 +829,22 @@ tqdm==4.67.1
|
||||
# prophet
|
||||
trino==0.330.0
|
||||
# via apache-superset
|
||||
trio==0.28.0
|
||||
trio==0.30.0
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# selenium
|
||||
# trio-websocket
|
||||
trio-websocket==0.11.1
|
||||
trio-websocket==0.12.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# selenium
|
||||
typing-extensions==4.12.2
|
||||
typing-extensions==4.13.2
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# alembic
|
||||
# apache-superset
|
||||
# cattrs
|
||||
# exceptiongroup
|
||||
# limits
|
||||
# pyopenssl
|
||||
# referencing
|
||||
@@ -857,7 +858,7 @@ tzdata==2025.2
|
||||
# pandas
|
||||
tzlocal==5.2
|
||||
# via trino
|
||||
url-normalize==1.4.3
|
||||
url-normalize==2.2.1
|
||||
# via
|
||||
# -c requirements/base.txt
|
||||
# requests-cache
|
||||
|
||||
995
superset-frontend/package-lock.json
generated
995
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -122,7 +122,7 @@
|
||||
"@visx/tooltip": "^3.0.0",
|
||||
"@visx/xychart": "^3.5.1",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"ace-builds": "^1.36.3",
|
||||
"ace-builds": "^1.41.0",
|
||||
"ag-grid-community": "33.1.1",
|
||||
"ag-grid-react": "33.1.1",
|
||||
"antd": "4.10.3",
|
||||
@@ -137,6 +137,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dom-to-image-more": "^3.2.0",
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"dompurify": "^3.2.4",
|
||||
"echarts": "^5.6.0",
|
||||
"emotion-rgba": "0.0.12",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
@@ -230,7 +231,7 @@
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.26.3",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
"@babel/register": "^7.23.7",
|
||||
@@ -292,7 +293,7 @@
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"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",
|
||||
|
||||
@@ -30,7 +30,7 @@ export const aggregationOperator: PostProcessingFactory<
|
||||
> = (formData: QueryFormData, queryObject) => {
|
||||
const { aggregation = 'LAST_VALUE' } = formData;
|
||||
|
||||
if (aggregation === 'LAST_VALUE') {
|
||||
if (aggregation === 'LAST_VALUE' || aggregation === 'raw') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ export const aggregationControl = {
|
||||
clearable: false,
|
||||
renderTrigger: false,
|
||||
choices: [
|
||||
['raw', t('None')],
|
||||
['LAST_VALUE', t('Last Value')],
|
||||
['sum', t('Total (Sum)')],
|
||||
['mean', t('Average (Mean)')],
|
||||
@@ -77,7 +78,9 @@ export const aggregationControl = {
|
||||
['max', t('Maximum')],
|
||||
['median', t('Median')],
|
||||
],
|
||||
description: t('Select an aggregation method to apply to the metric.'),
|
||||
description: t(
|
||||
'Aggregation method used to compute the Big Number from the Trendline.For non-additive metrics like ratios, averages, distinct counts, etc use NONE.',
|
||||
),
|
||||
provideFormDataToProps: true,
|
||||
mapStateToProps: ({ form_data }: ControlPanelState) => ({
|
||||
value: form_data.aggregation || 'LAST_VALUE',
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface ChartMetadataConfig {
|
||||
label?: ChartLabel | null;
|
||||
labelExplanation?: string | null;
|
||||
queryObjectCount?: number;
|
||||
dynamicQueryObjectCount?: boolean;
|
||||
parseMethod?: ParseMethod;
|
||||
// suppressContextMenu: true hides the default context menu for the chart.
|
||||
// This is useful for viz plugins that define their own context menu.
|
||||
@@ -92,6 +93,8 @@ export default class ChartMetadata {
|
||||
|
||||
queryObjectCount: number;
|
||||
|
||||
dynamicQueryObjectCount: boolean;
|
||||
|
||||
parseMethod: ParseMethod;
|
||||
|
||||
suppressContextMenu?: boolean;
|
||||
@@ -115,6 +118,7 @@ export default class ChartMetadata {
|
||||
label = null,
|
||||
labelExplanation = null,
|
||||
queryObjectCount = 1,
|
||||
dynamicQueryObjectCount = false,
|
||||
parseMethod = 'json-bigint',
|
||||
suppressContextMenu = false,
|
||||
} = config;
|
||||
@@ -145,6 +149,7 @@ export default class ChartMetadata {
|
||||
this.label = label;
|
||||
this.labelExplanation = labelExplanation;
|
||||
this.queryObjectCount = queryObjectCount;
|
||||
this.dynamicQueryObjectCount = dynamicQueryObjectCount;
|
||||
this.parseMethod = parseMethod;
|
||||
this.suppressContextMenu = suppressContextMenu;
|
||||
}
|
||||
|
||||
@@ -58,17 +58,18 @@ export default async function parseResponse<T extends ParseMethod = 'json'>(
|
||||
const result: JsonResponse = {
|
||||
response,
|
||||
json: cloneDeepWith(json, (value: any) => {
|
||||
// `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
||||
// TODO: clean up after json-bigint>1.0.1 is released
|
||||
if (value?.isInteger?.() === false) {
|
||||
return Number(value);
|
||||
}
|
||||
if (
|
||||
value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER)
|
||||
value?.isInteger?.() === true &&
|
||||
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
|
||||
) {
|
||||
return BigInt(value);
|
||||
}
|
||||
// // `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
||||
// // TODO: clean up after json-bigint>1.0.1 is released
|
||||
if (value?.isNaN?.() === false) {
|
||||
return value?.toNumber?.();
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -21,10 +21,18 @@ import { t } from '../translation';
|
||||
export default function validateServerPagination(
|
||||
v: unknown,
|
||||
serverPagination: boolean,
|
||||
max: number,
|
||||
maxValueWithoutServerPagination: number,
|
||||
maxServer: number,
|
||||
) {
|
||||
if (Number(v) > +max && !serverPagination) {
|
||||
return t('Server pagination needs to be enabled for values over %s', max);
|
||||
if (
|
||||
Number(v) > +maxValueWithoutServerPagination &&
|
||||
Number(v) <= maxServer &&
|
||||
!serverPagination
|
||||
) {
|
||||
return t(
|
||||
'Server pagination needs to be enabled for values over %s',
|
||||
maxValueWithoutServerPagination,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('parseResponse()', () => {
|
||||
const mockBigIntUrl = '/mock/get/bigInt';
|
||||
const mockGetBigIntPayload = `{
|
||||
"value": 9223372036854775807, "minus": { "value": -483729382918228373892, "str": "something" },
|
||||
"number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223 },
|
||||
"number": 1234, "floatValue": { "plus": 0.3452211361231223, "minus": -0.3452211361231223, "even": 1234567890123456.0000000 },
|
||||
"string.constructor": "data.constructor",
|
||||
"constructor": "constructor"
|
||||
}`;
|
||||
@@ -161,6 +161,7 @@ describe('parseResponse()', () => {
|
||||
expect(responseBigNumber.json.floatValue.minus).toEqual(
|
||||
-0.3452211361231223,
|
||||
);
|
||||
expect(responseBigNumber.json.floatValue.even).toEqual(1234567890123456);
|
||||
expect(
|
||||
responseBigNumber.json.floatValue.plus +
|
||||
responseBigNumber.json.floatValue.minus,
|
||||
|
||||
@@ -20,27 +20,134 @@
|
||||
import { validateServerPagination } from '@superset-ui/core';
|
||||
import './setup';
|
||||
|
||||
test('validateServerPagination returns warning message when server pagination is disabled and value exceeds max', () => {
|
||||
expect(validateServerPagination(100001, false, 100000)).toBeTruthy();
|
||||
expect(validateServerPagination('150000', false, 100000)).toBeTruthy();
|
||||
expect(validateServerPagination(200000, false, 100000)).toBeTruthy();
|
||||
const DEFAULT_MAX_ROW = 100000;
|
||||
const DEFAULT_MAX_ROW_TABLE_SERVER = 500000;
|
||||
|
||||
test('validateServerPagination returns warning message only when value is between max thresholds and server pagination is disabled', () => {
|
||||
// Should show warning - value between thresholds and server pagination disabled
|
||||
expect(
|
||||
validateServerPagination(
|
||||
200000,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
300000,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// Should not show warning - value above max server threshold
|
||||
expect(
|
||||
validateServerPagination(
|
||||
600000,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
|
||||
// Should not show warning - value below max without server threshold
|
||||
expect(
|
||||
validateServerPagination(
|
||||
50000,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('validateServerPagination returns false when server pagination is enabled', () => {
|
||||
expect(validateServerPagination(100001, true, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(150000, true, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('200000', true, 100000)).toBeFalsy();
|
||||
test('validateServerPagination returns false when server pagination is enabled regardless of value', () => {
|
||||
expect(
|
||||
validateServerPagination(
|
||||
200000,
|
||||
true,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
300000,
|
||||
true,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
600000,
|
||||
true,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('validateServerPagination returns false when value is below max', () => {
|
||||
expect(validateServerPagination(50000, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('75000', false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(99999, false, 100000)).toBeFalsy();
|
||||
test('validateServerPagination handles string inputs correctly', () => {
|
||||
expect(
|
||||
validateServerPagination(
|
||||
'200000',
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
'600000',
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
'50000',
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
test('validateServerPagination handles edge cases', () => {
|
||||
expect(validateServerPagination(undefined, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(null, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination(NaN, false, 100000)).toBeFalsy();
|
||||
expect(validateServerPagination('invalid', false, 100000)).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
undefined,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
null,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
NaN,
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
validateServerPagination(
|
||||
'invalid',
|
||||
false,
|
||||
DEFAULT_MAX_ROW,
|
||||
DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -54,11 +54,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.7",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@storybook/react-webpack5": "8.2.9",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-loader": "^10.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"ts-loader": "^9.5.2",
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
@@ -38,9 +38,20 @@ import {
|
||||
} from '../DeckGLContainer';
|
||||
import { getExploreLongUrl } from '../utils/explore';
|
||||
import layerGenerators from '../layers';
|
||||
import { Viewport } from '../utils/fitViewport';
|
||||
import fitViewport, { Viewport } from '../utils/fitViewport';
|
||||
import { TooltipProps } from '../components/Tooltip';
|
||||
|
||||
import { getPoints as getPointsArc } from '../layers/Arc/Arc';
|
||||
import { getPoints as getPointsPath } from '../layers/Path/Path';
|
||||
import { getPoints as getPointsPolygon } from '../layers/Polygon/Polygon';
|
||||
import { getPoints as getPointsGrid } from '../layers/Grid/Grid';
|
||||
import { getPoints as getPointsScatter } from '../layers/Scatter/Scatter';
|
||||
import { getPoints as getPointsContour } from '../layers/Contour/Contour';
|
||||
import { getPoints as getPointsHeatmap } from '../layers/Heatmap/Heatmap';
|
||||
import { getPoints as getPointsHex } from '../layers/Hex/Hex';
|
||||
import { getPoints as getPointsGeojson } from '../layers/Geojson/Geojson';
|
||||
import { getPoints as getPointsScreengrid } from '../layers/Screengrid/Screengrid';
|
||||
|
||||
export type DeckMultiProps = {
|
||||
formData: QueryFormData;
|
||||
payload: JsonObject;
|
||||
@@ -56,7 +67,35 @@ export type DeckMultiProps = {
|
||||
const DeckMulti = (props: DeckMultiProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
|
||||
const [viewport, setViewport] = useState<Viewport>();
|
||||
const getAdjustedViewport = useCallback(() => {
|
||||
let viewport = { ...props.viewport };
|
||||
const points = [
|
||||
...getPointsPolygon(props.payload.data.features.deck_polygon || []),
|
||||
...getPointsPath(props.payload.data.features.deck_path || []),
|
||||
...getPointsGrid(props.payload.data.features.deck_grid || []),
|
||||
...getPointsScatter(props.payload.data.features.deck_scatter || []),
|
||||
...getPointsContour(props.payload.data.features.deck_contour || []),
|
||||
...getPointsHeatmap(props.payload.data.features.deck_heatmap || []),
|
||||
...getPointsHex(props.payload.data.features.deck_hex || []),
|
||||
...getPointsArc(props.payload.data.features.deck_arc || []),
|
||||
...getPointsGeojson(props.payload.data.features.deck_geojson || []),
|
||||
...getPointsScreengrid(props.payload.data.features.deck_screengrid || []),
|
||||
];
|
||||
|
||||
if (props.formData) {
|
||||
viewport = fitViewport(viewport, {
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
points,
|
||||
});
|
||||
}
|
||||
if (viewport.zoom < 0) {
|
||||
viewport.zoom = 0;
|
||||
}
|
||||
return viewport;
|
||||
}, [props]);
|
||||
|
||||
const [viewport, setViewport] = useState<Viewport>(getAdjustedViewport());
|
||||
const [subSlicesLayers, setSubSlicesLayers] = useState<Record<number, Layer>>(
|
||||
{},
|
||||
);
|
||||
@@ -70,23 +109,31 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
|
||||
const loadLayers = useCallback(
|
||||
(formData: QueryFormData, payload: JsonObject, viewport?: Viewport) => {
|
||||
setViewport(viewport);
|
||||
setViewport(getAdjustedViewport());
|
||||
setSubSlicesLayers({});
|
||||
payload.data.slices.forEach(
|
||||
(subslice: { slice_id: number } & JsonObject) => {
|
||||
// Filters applied to multi_deck are passed down to underlying charts
|
||||
// note that dashboard contextual information (filter_immune_slices and such) aren't
|
||||
// taken into consideration here
|
||||
const filters = [
|
||||
...(subslice.form_data.filters || []),
|
||||
...(formData.filters || []),
|
||||
const extra_filters = [
|
||||
...(subslice.form_data.extra_filters || []),
|
||||
...(formData.extra_filters || []),
|
||||
...(formData.extra_form_data?.filters || []),
|
||||
];
|
||||
|
||||
const adhoc_filters = [
|
||||
...(formData.adhoc_filters || []),
|
||||
...(subslice.formData?.adhoc_filters || []),
|
||||
...(formData.extra_form_data?.adhoc_filters || []),
|
||||
];
|
||||
|
||||
const subsliceCopy = {
|
||||
...subslice,
|
||||
form_data: {
|
||||
...subslice.form_data,
|
||||
filters,
|
||||
extra_filters,
|
||||
adhoc_filters,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -117,7 +164,13 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
},
|
||||
);
|
||||
},
|
||||
[props.datasource, props.onAddFilter, props.onSelect, setTooltip],
|
||||
[
|
||||
props.datasource,
|
||||
props.onAddFilter,
|
||||
props.onSelect,
|
||||
setTooltip,
|
||||
getAdjustedViewport,
|
||||
],
|
||||
);
|
||||
|
||||
const prevDeckSlices = usePrevious(props.formData.deck_slices);
|
||||
@@ -136,7 +189,7 @@ const DeckMulti = (props: DeckMultiProps) => {
|
||||
<DeckGLContainerStyledWrapper
|
||||
ref={containerRef}
|
||||
mapboxApiAccessToken={payload.data.mapboxApiKey}
|
||||
viewport={viewport || props.viewport}
|
||||
viewport={viewport}
|
||||
layers={layers}
|
||||
mapStyle={formData.mapbox_style}
|
||||
setControlValue={setControlValue}
|
||||
|
||||
@@ -29,7 +29,7 @@ import TooltipRow from '../../TooltipRow';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
const points: Point[] = [];
|
||||
data.forEach(d => {
|
||||
points.push(d.sourcePosition);
|
||||
|
||||
@@ -97,7 +97,7 @@ export const getLayer: getLayerType<unknown> = function (
|
||||
});
|
||||
};
|
||||
|
||||
function getPoints(data: any[]) {
|
||||
export function getPoints(data: any[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import { commonLayerProps } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
import { Point } from '../../types';
|
||||
|
||||
type ProcessedFeature = Feature<Geometry, GeoJsonProperties> & {
|
||||
properties: JsonObject;
|
||||
@@ -172,6 +173,17 @@ export type DeckGLGeoJsonProps = {
|
||||
width: number;
|
||||
};
|
||||
|
||||
export function getPoints(data: Point[]) {
|
||||
return data.reduce((acc: Array<any>, feature: any) => {
|
||||
const bounds = geojsonExtent(feature);
|
||||
if (bounds) {
|
||||
return [...acc, [bounds[0], bounds[1]], [bounds[2], bounds[3]]];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
|
||||
@@ -186,24 +198,13 @@ const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
|
||||
const viewport: Viewport = useMemo(() => {
|
||||
if (formData.autozoom) {
|
||||
const points =
|
||||
payload?.data?.features?.reduce?.(
|
||||
(acc: [number, number, number, number][], feature: any) => {
|
||||
const bounds = geojsonExtent(feature);
|
||||
if (bounds) {
|
||||
return [...acc, [bounds[0], bounds[1]], [bounds[2], bounds[3]]];
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
) || [];
|
||||
const points = getPoints(payload.data.features) || [];
|
||||
|
||||
if (points.length) {
|
||||
return fitViewport(props.viewport, {
|
||||
width,
|
||||
height,
|
||||
points,
|
||||
points: getPoints(payload.data.features) || [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function getLayer(
|
||||
});
|
||||
}
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ export const getLayer: getLayerType<unknown> = (
|
||||
});
|
||||
};
|
||||
|
||||
function getPoints(data: any[]) {
|
||||
export function getPoints(data: any[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export function getLayer(
|
||||
});
|
||||
}
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export function getLayer(
|
||||
});
|
||||
}
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
let points: Point[] = [];
|
||||
data.forEach(d => {
|
||||
points = points.concat(d.path);
|
||||
|
||||
@@ -173,6 +173,10 @@ export type DeckGLPolygonProps = {
|
||||
height: number;
|
||||
};
|
||||
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.flatMap(getPointsFromPolygon);
|
||||
}
|
||||
|
||||
const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
|
||||
@@ -183,7 +187,7 @@ const DeckGLPolygon = (props: DeckGLPolygonProps) => {
|
||||
viewport = fitViewport(viewport, {
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
points: features.flatMap(getPointsFromPolygon),
|
||||
points: getPoints(features),
|
||||
});
|
||||
}
|
||||
if (viewport.zoom < 0) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import TooltipRow from '../../TooltipRow';
|
||||
import { unitToRadius } from '../../utils/geo';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from '../../DeckGLContainer';
|
||||
import { TooltipProps } from '../../components/Tooltip';
|
||||
|
||||
function getPoints(data: JsonObject[]) {
|
||||
export function getPoints(data: JsonObject[]) {
|
||||
return data.map(d => d.position);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-tip": "^0.9.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"dayjs": "^1.11.13",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.2.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 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 { QueryFormData } from '@superset-ui/core';
|
||||
import buildQuery from './buildQuery';
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
getXAxisColumn: jest.fn(() => 'order_date'),
|
||||
isXAxisSet: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
jest.mock('@superset-ui/chart-controls', () => ({
|
||||
pivotOperator: jest.fn(() => ({ operation: 'pivot' })),
|
||||
aggregationOperator: jest.fn(formData => {
|
||||
if (formData.aggregation === 'LAST_VALUE' || !formData.aggregation) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
operation: 'aggregation',
|
||||
options: { operator: formData.aggregation },
|
||||
};
|
||||
}),
|
||||
flattenOperator: jest.fn(() => ({ operation: 'flatten' })),
|
||||
resampleOperator: jest.fn(() => ({ operation: 'resample' })),
|
||||
rollingWindowOperator: jest.fn(() => ({ operation: 'rolling' })),
|
||||
}));
|
||||
|
||||
describe('BigNumberWithTrendline buildQuery', () => {
|
||||
const baseFormData: QueryFormData = {
|
||||
datasource: '1__table',
|
||||
viz_type: 'big_number',
|
||||
metric: 'custom_metric',
|
||||
aggregation: null,
|
||||
};
|
||||
|
||||
it('creates raw metric query when aggregation is null', () => {
|
||||
const queryContext = buildQuery({ ...baseFormData });
|
||||
const bigNumberQuery = queryContext.queries[1];
|
||||
|
||||
expect(bigNumberQuery.post_processing).toEqual([{ operation: 'pivot' }]);
|
||||
expect(bigNumberQuery.is_timeseries).toBe(true);
|
||||
});
|
||||
|
||||
it('adds aggregation operator when aggregation is "sum"', () => {
|
||||
const queryContext = buildQuery({ ...baseFormData, aggregation: 'sum' });
|
||||
const bigNumberQuery = queryContext.queries[1];
|
||||
|
||||
expect(bigNumberQuery.post_processing).toEqual([
|
||||
{ operation: 'pivot' },
|
||||
{ operation: 'aggregation', options: { operator: 'sum' } },
|
||||
]);
|
||||
expect(bigNumberQuery.is_timeseries).toBe(true);
|
||||
});
|
||||
|
||||
it('skips aggregation when aggregation is LAST_VALUE', () => {
|
||||
const queryContext = buildQuery({
|
||||
...baseFormData,
|
||||
aggregation: 'LAST_VALUE',
|
||||
});
|
||||
const bigNumberQuery = queryContext.queries[1];
|
||||
|
||||
expect(bigNumberQuery.post_processing).toEqual([{ operation: 'pivot' }]);
|
||||
expect(bigNumberQuery.is_timeseries).toBe(true);
|
||||
});
|
||||
|
||||
it('always returns two queries', () => {
|
||||
const queryContext = buildQuery({ ...baseFormData });
|
||||
expect(queryContext.queries.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
buildQueryContext,
|
||||
ensureIsArray,
|
||||
@@ -32,15 +33,17 @@ import {
|
||||
} from '@superset-ui/chart-controls';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const isRawMetric = formData.aggregation === 'raw';
|
||||
|
||||
const timeColumn = isXAxisSet(formData)
|
||||
? ensureIsArray(getXAxisColumn(formData))
|
||||
: [];
|
||||
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
columns: [
|
||||
...(isXAxisSet(formData)
|
||||
? ensureIsArray(getXAxisColumn(formData))
|
||||
: []),
|
||||
],
|
||||
...(isXAxisSet(formData) ? {} : { is_timeseries: true }),
|
||||
columns: [...timeColumn],
|
||||
...(timeColumn.length ? {} : { is_timeseries: true }),
|
||||
post_processing: [
|
||||
pivotOperator(formData, baseQueryObject),
|
||||
rollingWindowOperator(formData, baseQueryObject),
|
||||
@@ -48,19 +51,16 @@ export default function buildQuery(formData: QueryFormData) {
|
||||
flattenOperator(formData, baseQueryObject),
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
...baseQueryObject,
|
||||
columns: [
|
||||
...(isXAxisSet(formData)
|
||||
? ensureIsArray(getXAxisColumn(formData))
|
||||
: []),
|
||||
],
|
||||
...(isXAxisSet(formData) ? {} : { is_timeseries: true }),
|
||||
post_processing: [
|
||||
pivotOperator(formData, baseQueryObject),
|
||||
aggregationOperator(formData, baseQueryObject),
|
||||
],
|
||||
columns: [...(isRawMetric ? [] : timeColumn)],
|
||||
is_timeseries: !isRawMetric,
|
||||
post_processing: isRawMetric
|
||||
? []
|
||||
: [
|
||||
pivotOperator(formData, baseQueryObject),
|
||||
aggregationOperator(formData, baseQueryObject),
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
getXAxisFormatter,
|
||||
getYAxisFormatter,
|
||||
} from '../utils/formatters';
|
||||
import { getMetricDisplayName } from '../utils/metricDisplayName';
|
||||
|
||||
const getFormatter = (
|
||||
customFormatters: Record<string, ValueFormatter>,
|
||||
@@ -222,6 +223,10 @@ export default function transformProps(
|
||||
}
|
||||
|
||||
const rebasedDataA = rebaseForecastDatum(data1, verboseMap);
|
||||
|
||||
const MetricDisplayNameA = getMetricDisplayName(metrics[0], verboseMap);
|
||||
const MetricDisplayNameB = getMetricDisplayName(metricsB[0], verboseMap);
|
||||
|
||||
const [rawSeriesA] = extractSeries(rebasedDataA, {
|
||||
fillNeighborValue: stack ? 0 : undefined,
|
||||
xAxis: xAxisLabel,
|
||||
@@ -373,6 +378,12 @@ export default function transformProps(
|
||||
const seriesName = inverted[entryName] || entryName;
|
||||
const colorScaleKey = getOriginalSeries(seriesName, array);
|
||||
|
||||
let displayName = `${entryName} (Query A)`;
|
||||
|
||||
if (groupby.length > 0) {
|
||||
displayName = `${MetricDisplayNameA} (Query A), ${entryName}`;
|
||||
}
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
customFormatters,
|
||||
formatter,
|
||||
@@ -382,7 +393,10 @@ export default function transformProps(
|
||||
);
|
||||
|
||||
const transformedSeries = transformSeries(
|
||||
entry,
|
||||
{
|
||||
...entry,
|
||||
id: `${displayName || ''}`,
|
||||
},
|
||||
colorScale,
|
||||
colorScaleKey,
|
||||
{
|
||||
@@ -421,6 +435,12 @@ export default function transformProps(
|
||||
const seriesName = `${seriesEntry} (1)`;
|
||||
const colorScaleKey = getOriginalSeries(seriesEntry, array);
|
||||
|
||||
let displayName = `${entryName} (Query B)`;
|
||||
|
||||
if (groupbyB.length > 0) {
|
||||
displayName = `${MetricDisplayNameB} (Query B), ${entryName}`;
|
||||
}
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
customFormattersSecondary,
|
||||
formatterSecondary,
|
||||
@@ -430,7 +450,11 @@ export default function transformProps(
|
||||
);
|
||||
|
||||
const transformedSeries = transformSeries(
|
||||
entry,
|
||||
{
|
||||
...entry,
|
||||
id: `${displayName || ''}`,
|
||||
},
|
||||
|
||||
colorScale,
|
||||
colorScaleKey,
|
||||
{
|
||||
@@ -444,9 +468,7 @@ export default function transformProps(
|
||||
stackIdSuffix: '\nb',
|
||||
yAxisIndex: yAxisIndexB,
|
||||
filterState,
|
||||
seriesKey: primarySeries.has(entry.name as string)
|
||||
? `${entry.name} (1)`
|
||||
: entry.name,
|
||||
seriesKey: entry.name,
|
||||
sliceId,
|
||||
queryIndex: 1,
|
||||
formatter:
|
||||
|
||||
@@ -16,14 +16,30 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
|
||||
import {
|
||||
buildQueryContext,
|
||||
getMetricLabel,
|
||||
QueryFormData,
|
||||
} from '@superset-ui/core';
|
||||
import { getContributionLabel } from './utils';
|
||||
|
||||
export default function buildQuery(formData: QueryFormData) {
|
||||
const { metric, sort_by_metric } = formData;
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
|
||||
return buildQueryContext(formData, baseQueryObject => [
|
||||
{
|
||||
...baseQueryObject,
|
||||
...(sort_by_metric && { orderby: [[metric, false]] }),
|
||||
post_processing: [
|
||||
{
|
||||
operation: 'contribution',
|
||||
options: {
|
||||
columns: [metricLabel],
|
||||
rename_columns: [getContributionLabel(metricLabel)],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 const CONTRIBUTION_SUFFIX = '__contribution' as const;
|
||||
@@ -84,6 +84,23 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'threshold_for_other',
|
||||
config: {
|
||||
type: 'NumberControl',
|
||||
label: t('Threshold for Other'),
|
||||
min: 0,
|
||||
step: 0.5,
|
||||
max: 100,
|
||||
default: 0,
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Values less than this percentage will be grouped into the Other category.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'roseType',
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ValueFormatter,
|
||||
getValueFormatter,
|
||||
tooltipHtml,
|
||||
DataRecord,
|
||||
} from '@superset-ui/core';
|
||||
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import type { EChartsCoreOption } from 'echarts/core';
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
EchartsPieChartProps,
|
||||
EchartsPieFormData,
|
||||
EchartsPieLabelType,
|
||||
PieChartDataItem,
|
||||
PieChartTransformedProps,
|
||||
} from './types';
|
||||
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
|
||||
@@ -50,6 +52,7 @@ import { defaultGrid } from '../defaults';
|
||||
import { convertInteger } from '../utils/convertInteger';
|
||||
import { getDefaultTooltip } from '../utils/tooltip';
|
||||
import { Refs } from '../types';
|
||||
import { getContributionLabel } from './utils';
|
||||
|
||||
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
|
||||
|
||||
@@ -133,7 +136,7 @@ export default function transformProps(
|
||||
datasource,
|
||||
} = chartProps;
|
||||
const { columnFormats = {}, currencyFormats = {} } = datasource;
|
||||
const { data = [] } = queriesData[0];
|
||||
const { data: rawData = [] } = queriesData[0];
|
||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||
|
||||
const {
|
||||
@@ -159,6 +162,7 @@ export default function transformProps(
|
||||
sliceId,
|
||||
showTotal,
|
||||
roseType,
|
||||
thresholdForOther,
|
||||
}: EchartsPieFormData = {
|
||||
...DEFAULT_LEGEND_FORM_DATA,
|
||||
...DEFAULT_PIE_FORM_DATA,
|
||||
@@ -166,17 +170,68 @@ export default function transformProps(
|
||||
};
|
||||
const refs: Refs = {};
|
||||
const metricLabel = getMetricLabel(metric);
|
||||
const contributionLabel = getContributionLabel(metricLabel);
|
||||
const groupbyLabels = groupby.map(getColumnLabel);
|
||||
const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6;
|
||||
|
||||
const keys = data.map(datum =>
|
||||
extractGroupbyLabel({
|
||||
datum,
|
||||
groupby: groupbyLabels,
|
||||
coltypeMapping,
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
}),
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
currencyFormats,
|
||||
columnFormats,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
);
|
||||
|
||||
let data = rawData;
|
||||
const otherRows: DataRecord[] = [];
|
||||
const otherTooltipData: string[][] = [];
|
||||
let otherDatum: PieChartDataItem | null = null;
|
||||
let otherSum = 0;
|
||||
if (thresholdForOther) {
|
||||
let contributionSum = 0;
|
||||
data = data.filter(datum => {
|
||||
const contribution = datum[contributionLabel] as number;
|
||||
if (!contribution || contribution * 100 >= thresholdForOther) {
|
||||
return true;
|
||||
}
|
||||
otherSum += datum[metricLabel] as number;
|
||||
contributionSum += contribution;
|
||||
otherRows.push(datum);
|
||||
otherTooltipData.push([
|
||||
extractGroupbyLabel({
|
||||
datum,
|
||||
groupby: groupbyLabels,
|
||||
coltypeMapping,
|
||||
timeFormatter: getTimeFormatter(dateFormat),
|
||||
}),
|
||||
numberFormatter(datum[metricLabel] as number),
|
||||
percentFormatter(contribution),
|
||||
]);
|
||||
return false;
|
||||
});
|
||||
const otherName = t('Other');
|
||||
otherTooltipData.push([
|
||||
t('Total'),
|
||||
numberFormatter(otherSum),
|
||||
percentFormatter(contributionSum),
|
||||
]);
|
||||
if (otherSum) {
|
||||
otherDatum = {
|
||||
name: otherName,
|
||||
value: otherSum,
|
||||
itemStyle: {
|
||||
color: theme.colors.grayscale.dark1,
|
||||
opacity:
|
||||
filterState.selectedValues &&
|
||||
!filterState.selectedValues.includes(otherName)
|
||||
? OpacityEnum.SemiTransparent
|
||||
: OpacityEnum.NonTransparent,
|
||||
},
|
||||
isOther: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const labelMap = data.reduce((acc: Record<string, string[]>, datum) => {
|
||||
const label = extractGroupbyLabel({
|
||||
datum,
|
||||
@@ -192,13 +247,6 @@ export default function transformProps(
|
||||
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getValueFormatter(
|
||||
metric,
|
||||
currencyFormats,
|
||||
columnFormats,
|
||||
numberFormat,
|
||||
currencyFormat,
|
||||
);
|
||||
|
||||
let totalValue = 0;
|
||||
|
||||
@@ -229,6 +277,10 @@ export default function transformProps(
|
||||
},
|
||||
};
|
||||
});
|
||||
if (otherDatum) {
|
||||
transformedData.push(otherDatum);
|
||||
totalValue += otherSum;
|
||||
}
|
||||
|
||||
const selectedValues = (filterState.selectedValues || []).reduce(
|
||||
(acc: Record<string, number>, selectedValue: string) => {
|
||||
@@ -372,6 +424,9 @@ export default function transformProps(
|
||||
numberFormatter,
|
||||
sanitizeName: true,
|
||||
});
|
||||
if (params?.data?.isOther) {
|
||||
return tooltipHtml(otherTooltipData, name);
|
||||
}
|
||||
return tooltipHtml(
|
||||
[[metricLabel, formattedValue, formattedPercent]],
|
||||
name,
|
||||
@@ -380,7 +435,7 @@ export default function transformProps(
|
||||
},
|
||||
legend: {
|
||||
...getLegendProps(legendType, legendOrientation, showLegend, theme),
|
||||
data: keys,
|
||||
data: transformedData.map(datum => datum.name),
|
||||
},
|
||||
graphic: showTotal
|
||||
? {
|
||||
|
||||
@@ -47,6 +47,7 @@ export type EchartsPieFormData = QueryFormData &
|
||||
dateFormat: string;
|
||||
showLabelsThreshold: number;
|
||||
roseType: 'radius' | 'area' | null;
|
||||
thresholdForOther: number;
|
||||
};
|
||||
|
||||
export enum EchartsPieLabelType {
|
||||
@@ -82,9 +83,20 @@ export const DEFAULT_FORM_DATA: EchartsPieFormData = {
|
||||
showLabelsThreshold: 5,
|
||||
dateFormat: 'smart_date',
|
||||
roseType: null,
|
||||
thresholdForOther: 0,
|
||||
};
|
||||
|
||||
export type PieChartTransformedProps =
|
||||
BaseTransformedProps<EchartsPieFormData> &
|
||||
ContextMenuTransformedProps &
|
||||
CrossFilterTransformedProps;
|
||||
|
||||
export interface PieChartDataItem {
|
||||
name: string;
|
||||
value: number;
|
||||
itemStyle: {
|
||||
color: string;
|
||||
opacity: number;
|
||||
};
|
||||
isOther?: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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 { CONTRIBUTION_SUFFIX } from './constants';
|
||||
|
||||
export const getContributionLabel = (metricLabel: string) =>
|
||||
`${metricLabel}${CONTRIBUTION_SUFFIX}`;
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
NumberFormatter,
|
||||
isDefined,
|
||||
} from '@superset-ui/core';
|
||||
import type { CallbackDataParams } from 'echarts/types/src/util/types';
|
||||
import type { RadarSeriesDataItemOption } from 'echarts/types/src/chart/radar/RadarSeries';
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
EchartsRadarFormData,
|
||||
EchartsRadarLabelType,
|
||||
RadarChartTransformedProps,
|
||||
SeriesNormalizedMap,
|
||||
} from './types';
|
||||
import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants';
|
||||
import {
|
||||
@@ -46,18 +48,31 @@ import {
|
||||
import { defaultGrid } from '../defaults';
|
||||
import { Refs } from '../types';
|
||||
import { getDefaultTooltip } from '../utils/tooltip';
|
||||
import { findGlobalMax, renderNormalizedTooltip } from './utils';
|
||||
|
||||
export function formatLabel({
|
||||
params,
|
||||
labelType,
|
||||
numberFormatter,
|
||||
getDenormalizedSeriesValue,
|
||||
metricsWithCustomBounds,
|
||||
metricLabels,
|
||||
}: {
|
||||
params: CallbackDataParams;
|
||||
labelType: EchartsRadarLabelType;
|
||||
numberFormatter: NumberFormatter;
|
||||
getDenormalizedSeriesValue: (seriesName: string, value: string) => number;
|
||||
metricsWithCustomBounds: Set<string>;
|
||||
metricLabels: string[];
|
||||
}): string {
|
||||
const { name = '', value } = params;
|
||||
const formattedValue = numberFormatter(value as number);
|
||||
const { name = '', value, dimensionIndex = 0 } = params;
|
||||
const metricLabel = metricLabels[dimensionIndex];
|
||||
|
||||
const formattedValue = numberFormatter(
|
||||
metricsWithCustomBounds.has(metricLabel)
|
||||
? (value as number)
|
||||
: (getDenormalizedSeriesValue(name, String(value)) as number),
|
||||
);
|
||||
|
||||
switch (labelType) {
|
||||
case EchartsRadarLabelType.Value:
|
||||
@@ -85,6 +100,7 @@ export default function transformProps(
|
||||
} = chartProps;
|
||||
const refs: Refs = {};
|
||||
const { data = [] } = queriesData[0];
|
||||
const globalMax = findGlobalMax(data, Object.keys(data[0] || {}));
|
||||
const coltypeMapping = getColtypesMapping(queriesData[0]);
|
||||
|
||||
const {
|
||||
@@ -111,14 +127,38 @@ export default function transformProps(
|
||||
const { setDataMask = () => {}, onContextMenu } = hooks;
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
const numberFormatter = getNumberFormatter(numberFormat);
|
||||
const denormalizedSeriesValues: SeriesNormalizedMap = {};
|
||||
|
||||
const getDenormalizedSeriesValue = (
|
||||
seriesName: string,
|
||||
normalizedValue: string,
|
||||
): number =>
|
||||
denormalizedSeriesValues?.[seriesName]?.[normalizedValue] ??
|
||||
Number(normalizedValue);
|
||||
|
||||
const metricLabels = metrics.map(getMetricLabel);
|
||||
|
||||
const metricsWithCustomBounds = new Set(
|
||||
metricLabels.filter(metricLabel => {
|
||||
const config = columnConfig?.[metricLabel];
|
||||
const hasMax = !!isDefined(config?.radarMetricMaxValue);
|
||||
const hasMin =
|
||||
isDefined(config?.radarMetricMinValue) &&
|
||||
config?.radarMetricMinValue !== 0;
|
||||
return hasMax || hasMin;
|
||||
}),
|
||||
);
|
||||
|
||||
const formatter = (params: CallbackDataParams) =>
|
||||
formatLabel({
|
||||
params,
|
||||
numberFormatter,
|
||||
labelType,
|
||||
getDenormalizedSeriesValue,
|
||||
metricsWithCustomBounds,
|
||||
metricLabels,
|
||||
});
|
||||
|
||||
const metricLabels = metrics.map(getMetricLabel);
|
||||
const groupbyLabels = groupby.map(getColumnLabel);
|
||||
|
||||
const metricLabelAndMaxValueMap = new Map<string, number>();
|
||||
@@ -212,28 +252,58 @@ export default function transformProps(
|
||||
{},
|
||||
);
|
||||
|
||||
const normalizeArray = (arr: number[], decimals = 10, seriesName: string) =>
|
||||
arr.map((value, index) => {
|
||||
const metricLabel = metricLabels[index];
|
||||
if (metricsWithCustomBounds.has(metricLabel)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const max = Math.max(...arr);
|
||||
const normalizedValue = Number((value / max).toFixed(decimals));
|
||||
|
||||
denormalizedSeriesValues[seriesName][String(normalizedValue)] = value;
|
||||
return normalizedValue;
|
||||
});
|
||||
|
||||
// Normalize the transformed data
|
||||
const normalizedTransformedData = transformedData.map(series => {
|
||||
if (Array.isArray(series.value)) {
|
||||
const seriesName = String(series?.name || '');
|
||||
denormalizedSeriesValues[seriesName] = {};
|
||||
|
||||
return {
|
||||
...series,
|
||||
value: normalizeArray(series.value as number[], 10, seriesName),
|
||||
};
|
||||
}
|
||||
return series;
|
||||
});
|
||||
|
||||
const indicator = metricLabels.map(metricLabel => {
|
||||
const isMetricWithCustomBounds = metricsWithCustomBounds.has(metricLabel);
|
||||
if (!isMetricWithCustomBounds) {
|
||||
return {
|
||||
name: metricLabel,
|
||||
max: 1,
|
||||
min: 0,
|
||||
};
|
||||
}
|
||||
const maxValueInControl = columnConfig?.[metricLabel]?.radarMetricMaxValue;
|
||||
const minValueInControl = columnConfig?.[metricLabel]?.radarMetricMinValue;
|
||||
|
||||
// Ensure that 0 is at the center of the polar coordinates
|
||||
const metricValueAsMax =
|
||||
const maxValue =
|
||||
metricLabelAndMaxValueMap.get(metricLabel) === 0
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: metricLabelAndMaxValueMap.get(metricLabel);
|
||||
const max =
|
||||
maxValueInControl === null ? metricValueAsMax : maxValueInControl;
|
||||
: globalMax;
|
||||
const max = isDefined(maxValueInControl) ? maxValueInControl : maxValue;
|
||||
|
||||
let min: number;
|
||||
// If the min value doesn't exist, set it to 0 (default),
|
||||
// if it is null, set it to the min value of the data,
|
||||
// otherwise, use the value from the control
|
||||
if (minValueInControl === undefined) {
|
||||
min = 0;
|
||||
} else if (minValueInControl === null) {
|
||||
min = metricLabelAndMinValueMap.get(metricLabel) || 0;
|
||||
} else {
|
||||
if (isDefined(minValueInControl)) {
|
||||
min = minValueInControl;
|
||||
} else {
|
||||
min = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -255,10 +325,24 @@ export default function transformProps(
|
||||
backgroundColor: theme.colors.grayscale.light5,
|
||||
},
|
||||
},
|
||||
data: transformedData,
|
||||
data: normalizedTransformedData,
|
||||
},
|
||||
];
|
||||
|
||||
const NormalizedTooltipFormater = (
|
||||
params: CallbackDataParams & {
|
||||
color: string;
|
||||
name: string;
|
||||
value: number[];
|
||||
},
|
||||
) =>
|
||||
renderNormalizedTooltip(
|
||||
params,
|
||||
metricLabels,
|
||||
getDenormalizedSeriesValue,
|
||||
metricsWithCustomBounds,
|
||||
);
|
||||
|
||||
const echartOptions: EChartsCoreOption = {
|
||||
grid: {
|
||||
...defaultGrid,
|
||||
@@ -267,6 +351,7 @@ export default function transformProps(
|
||||
...getDefaultTooltip(refs),
|
||||
show: !inContextMenu,
|
||||
trigger: 'item',
|
||||
formatter: NormalizedTooltipFormater,
|
||||
},
|
||||
legend: {
|
||||
...getLegendProps(legendType, legendOrientation, showLegend, theme),
|
||||
|
||||
@@ -35,7 +35,7 @@ import { DEFAULT_LEGEND_FORM_DATA } from '../constants';
|
||||
|
||||
type RadarColumnConfig = Record<
|
||||
string,
|
||||
{ radarMetricMaxValue?: number; radarMetricMinValue?: number }
|
||||
{ radarMetricMaxValue?: number | null; radarMetricMinValue?: number }
|
||||
>;
|
||||
|
||||
export type EchartsRadarFormData = QueryFormData &
|
||||
@@ -53,6 +53,7 @@ export type EchartsRadarFormData = QueryFormData &
|
||||
isCircle: boolean;
|
||||
numberFormat: string;
|
||||
dateFormat: string;
|
||||
isNormalized: boolean;
|
||||
};
|
||||
|
||||
export enum EchartsRadarLabelType {
|
||||
@@ -83,3 +84,17 @@ export type RadarChartTransformedProps =
|
||||
BaseTransformedProps<EchartsRadarFormData> &
|
||||
ContextMenuTransformedProps &
|
||||
CrossFilterTransformedProps;
|
||||
|
||||
/**
|
||||
* Represents a mapping from a normalized value (as string) to an original numeric value.
|
||||
*/
|
||||
interface NormalizedValueMap {
|
||||
[normalized: string]: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a collection of series, each containing its own NormalizedValueMap.
|
||||
*/
|
||||
export interface SeriesNormalizedMap {
|
||||
[seriesName: string]: NormalizedValueMap;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
function for finding the max metric values among all series data for Radar Chart
|
||||
*/
|
||||
export const findGlobalMax = (
|
||||
data: Record<string, unknown>[],
|
||||
metrics: string[],
|
||||
): number => {
|
||||
if (!data?.length || !metrics?.length) return 0;
|
||||
|
||||
return data.reduce((globalMax, row) => {
|
||||
const rowMax = metrics.reduce((max, metric) => {
|
||||
const value = row[metric];
|
||||
return typeof value === 'number' &&
|
||||
Number.isFinite(value) &&
|
||||
!Number.isNaN(value)
|
||||
? Math.max(max, value)
|
||||
: max;
|
||||
}, 0);
|
||||
|
||||
return Math.max(globalMax, rowMax);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
interface TooltipParams {
|
||||
color: string;
|
||||
name?: string;
|
||||
value: number[];
|
||||
}
|
||||
|
||||
interface TooltipMetricValue {
|
||||
metric: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export const renderNormalizedTooltip = (
|
||||
params: TooltipParams,
|
||||
metrics: string[],
|
||||
getDenormalizedValue: (seriesName: string, value: string) => number,
|
||||
metricsWithCustomBounds: Set<string>,
|
||||
): string => {
|
||||
const { color, name = '', value: values } = params;
|
||||
const seriesName = name || 'series0';
|
||||
|
||||
const colorDot = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:5px;height:5px;background-color:${color}"></span>`;
|
||||
|
||||
// Get metric values with denormalization if needed
|
||||
const metricValues: TooltipMetricValue[] = metrics.map((metric, index) => {
|
||||
const value = values[index];
|
||||
const originalValue = metricsWithCustomBounds.has(metric)
|
||||
? value
|
||||
: getDenormalizedValue(name, String(value));
|
||||
|
||||
return {
|
||||
metric,
|
||||
value: originalValue,
|
||||
};
|
||||
});
|
||||
|
||||
const tooltipRows = metricValues
|
||||
.map(
|
||||
({ metric, value }) => `
|
||||
<div style="display:flex;">
|
||||
<div>${colorDot}${metric}:</div>
|
||||
<div style="font-weight:bold;margin-left:auto;">${value}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div style="font-weight:bold;margin-bottom:5px;">${seriesName}</div>
|
||||
${tooltipRows}
|
||||
`;
|
||||
};
|
||||
@@ -73,13 +73,25 @@ export default function transformProps(
|
||||
}));
|
||||
|
||||
// stores a map with the total values for each node considering the links
|
||||
const nodeValues = new Map<string, number>();
|
||||
const incomingFlows = new Map<string, number>();
|
||||
const outgoingFlows = new Map<string, number>();
|
||||
const allNodeNames = new Set<string>();
|
||||
|
||||
links.forEach(link => {
|
||||
const { source, target, value } = link;
|
||||
const sourceValue = nodeValues.get(source) || 0;
|
||||
const targetValue = nodeValues.get(target) || 0;
|
||||
nodeValues.set(source, sourceValue + value);
|
||||
nodeValues.set(target, targetValue + value);
|
||||
allNodeNames.add(source);
|
||||
allNodeNames.add(target);
|
||||
incomingFlows.set(target, (incomingFlows.get(target) || 0) + value);
|
||||
outgoingFlows.set(source, (outgoingFlows.get(source) || 0) + value);
|
||||
});
|
||||
|
||||
const nodeValues = new Map<string, number>();
|
||||
|
||||
allNodeNames.forEach(nodeName => {
|
||||
const totalIncoming = incomingFlows.get(nodeName) || 0;
|
||||
const totalOutgoing = outgoingFlows.get(nodeName) || 0;
|
||||
|
||||
nodeValues.set(nodeName, Math.max(totalIncoming, totalOutgoing));
|
||||
});
|
||||
|
||||
const tooltipFormatter = (params: CallbackDataParams) => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
import { JsonArray, t } from '@superset-ui/core';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
ControlPanelsContainerProps,
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
DEFAULT_FORM_DATA,
|
||||
TIME_SERIES_DESCRIPTION_TEXT,
|
||||
} from '../../constants';
|
||||
import { StackControlsValue } from '../../../constants';
|
||||
|
||||
const {
|
||||
logAxis,
|
||||
@@ -321,6 +322,38 @@ const config: ControlPanelConfig = {
|
||||
['color_scheme'],
|
||||
['time_shift_color'],
|
||||
...showValueSection,
|
||||
[
|
||||
{
|
||||
name: 'stackDimension',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
label: t('Split stack by'),
|
||||
visibility: ({ controls }) =>
|
||||
controls?.stack?.value === StackControlsValue.Stack,
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Stack in groups, where each group corresponds to a dimension',
|
||||
),
|
||||
shouldMapStateToProps: (
|
||||
prevState,
|
||||
state,
|
||||
controlState,
|
||||
chartState,
|
||||
) => true,
|
||||
mapStateToProps: (state, controlState, chartState) => {
|
||||
const value: JsonArray = state.controls.groupby
|
||||
.value as JsonArray;
|
||||
const valueAsStringArr: string[][] = value.map(v => {
|
||||
if (v) return [v.toString(), v.toString()];
|
||||
return ['', ''];
|
||||
});
|
||||
return {
|
||||
choices: valueAsStringArr,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
[minorTicks],
|
||||
[
|
||||
{
|
||||
|
||||
@@ -191,6 +191,7 @@ export default function transformProps(
|
||||
yAxisTitleMargin,
|
||||
yAxisTitlePosition,
|
||||
zoomable,
|
||||
stackDimension,
|
||||
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||
const refs: Refs = {};
|
||||
const groupBy = ensureIsArray(groupby);
|
||||
@@ -418,6 +419,23 @@ export default function transformProps(
|
||||
}
|
||||
});
|
||||
|
||||
if (
|
||||
stack === StackControlsValue.Stack &&
|
||||
stackDimension &&
|
||||
chartProps.rawFormData.groupby
|
||||
) {
|
||||
const idxSelectedDimension =
|
||||
formData.metrics.length > 1
|
||||
? 1
|
||||
: 0 + chartProps.rawFormData.groupby.indexOf(stackDimension);
|
||||
for (const s of series) {
|
||||
if (s.id) {
|
||||
const columnsArr = labelMap[s.id];
|
||||
(s as any).stack = columnsArr[idxSelectedDimension];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// axis bounds need to be parsed to replace incompatible values with undefined
|
||||
const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound);
|
||||
let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound);
|
||||
|
||||
@@ -74,6 +74,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
||||
rowLimit: number;
|
||||
seriesType: EchartsTimeseriesSeriesType;
|
||||
stack: StackType;
|
||||
stackDimension: string;
|
||||
timeCompare?: string[];
|
||||
tooltipTimeFormat?: string;
|
||||
showTooltipTotal?: boolean;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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 { QueryFormMetric } from '@superset-ui/core';
|
||||
|
||||
export const getMetricDisplayName = (
|
||||
metric: QueryFormMetric,
|
||||
verboseMap: Record<string, string> = {},
|
||||
): string => {
|
||||
// Case 1: Simple string metric - use verboseMap or the string itself
|
||||
if (typeof metric === 'string') {
|
||||
return verboseMap[metric] || metric;
|
||||
}
|
||||
|
||||
// Case 2: Metric with explicit label - always prefer this if available
|
||||
if (metric.label) {
|
||||
return metric.label;
|
||||
}
|
||||
|
||||
// Case 3: SIMPLE expression type (column with aggregate)
|
||||
if (metric.expressionType === 'SIMPLE') {
|
||||
const column = metric.column || {};
|
||||
const columnName = column.column_name || '';
|
||||
// Use verbose name from column if available
|
||||
const displayName = column.verbose_name || columnName;
|
||||
const aggregate = metric.aggregate || '';
|
||||
|
||||
// If the verbose map has this column, use that
|
||||
if (verboseMap[columnName]) {
|
||||
return `${aggregate}(${verboseMap[columnName]})`;
|
||||
}
|
||||
|
||||
return `${aggregate}(${displayName})`;
|
||||
}
|
||||
|
||||
// Case 4: SQL expression
|
||||
if (metric.expressionType === 'SQL') {
|
||||
return metric.sqlExpression || 'Custom SQL Metric';
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return 'Unknown Metric';
|
||||
};
|
||||
@@ -118,7 +118,9 @@ const chartPropsConfig = {
|
||||
|
||||
it('should transform chart props for viz', () => {
|
||||
const chartProps = new ChartProps(chartPropsConfig);
|
||||
expect(transformProps(chartProps as EchartsMixedTimeseriesProps)).toEqual(
|
||||
const transformed = transformProps(chartProps as EchartsMixedTimeseriesProps);
|
||||
|
||||
expect(transformed).toEqual(
|
||||
expect.objectContaining({
|
||||
echartOptions: expect.objectContaining({
|
||||
series: expect.arrayContaining([
|
||||
@@ -127,7 +129,7 @@ it('should transform chart props for viz', () => {
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'boy',
|
||||
id: 'sum__num (Query A), boy',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -135,15 +137,16 @@ it('should transform chart props for viz', () => {
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'girl',
|
||||
id: 'sum__num (Query A), girl',
|
||||
stack: 'obs\na',
|
||||
}),
|
||||
// Query B — Bar series
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[599616000000, 1],
|
||||
[599916000000, 3],
|
||||
],
|
||||
id: 'boy (1)',
|
||||
id: 'sum__num (Query B), boy',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -151,7 +154,7 @@ it('should transform chart props for viz', () => {
|
||||
[599616000000, 2],
|
||||
[599916000000, 4],
|
||||
],
|
||||
id: 'girl (1)',
|
||||
id: 'sum__num (Query B), girl',
|
||||
stack: 'obs\nb',
|
||||
}),
|
||||
]),
|
||||
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
CallbackDataParams,
|
||||
} from 'echarts/types/src/util/types';
|
||||
import transformProps, { parseParams } from '../../src/Pie/transformProps';
|
||||
import { EchartsPieChartProps } from '../../src/Pie/types';
|
||||
import { EchartsPieChartProps, PieChartDataItem } from '../../src/Pie/types';
|
||||
|
||||
describe('Pie transformProps', () => {
|
||||
const formData: SqlaFormData = {
|
||||
@@ -46,8 +46,13 @@ describe('Pie transformProps', () => {
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{ foo: 'Sylvester', bar: 1, sum__num: 10 },
|
||||
{ foo: 'Arnold', bar: 2, sum__num: 2.5 },
|
||||
{
|
||||
foo: 'Sylvester',
|
||||
bar: 1,
|
||||
sum__num: 10,
|
||||
sum__num__contribution: 0.8,
|
||||
},
|
||||
{ foo: 'Arnold', bar: 2, sum__num: 2.5, sum__num__contribution: 0.2 },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -215,3 +220,77 @@ describe('Pie label string template', () => {
|
||||
).toEqual('Tablet:123456\n55.5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other category', () => {
|
||||
const defaultFormData: SqlaFormData = {
|
||||
colorScheme: 'bnbColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
metric: 'metric',
|
||||
groupby: ['foo', 'bar'],
|
||||
viz_type: 'my_viz',
|
||||
};
|
||||
|
||||
const getChartProps = (formData: Partial<SqlaFormData>) =>
|
||||
new ChartProps({
|
||||
formData: {
|
||||
...defaultFormData,
|
||||
...formData,
|
||||
},
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
foo: 'foo 1',
|
||||
bar: 'bar 1',
|
||||
metric: 1,
|
||||
metric__contribution: 1 / 15, // 6.7%
|
||||
},
|
||||
{
|
||||
foo: 'foo 2',
|
||||
bar: 'bar 2',
|
||||
metric: 2,
|
||||
metric__contribution: 2 / 15, // 13.3%
|
||||
},
|
||||
{
|
||||
foo: 'foo 3',
|
||||
bar: 'bar 3',
|
||||
metric: 3,
|
||||
metric__contribution: 3 / 15, // 20%
|
||||
},
|
||||
{
|
||||
foo: 'foo 4',
|
||||
bar: 'bar 4',
|
||||
metric: 4,
|
||||
metric__contribution: 4 / 15, // 26.7%
|
||||
},
|
||||
{
|
||||
foo: 'foo 5',
|
||||
bar: 'bar 5',
|
||||
metric: 5,
|
||||
metric__contribution: 5 / 15, // 33.3%
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
theme: supersetTheme,
|
||||
});
|
||||
|
||||
it('generates Other category', () => {
|
||||
const chartProps = getChartProps({
|
||||
threshold_for_other: 20,
|
||||
});
|
||||
const transformed = transformProps(chartProps as EchartsPieChartProps);
|
||||
const series = transformed.echartOptions.series as PieSeriesOption[];
|
||||
const data = series[0].data as PieChartDataItem[];
|
||||
expect(data).toHaveLength(4);
|
||||
expect(data[0].value).toBe(3);
|
||||
expect(data[1].value).toBe(4);
|
||||
expect(data[2].value).toBe(5);
|
||||
expect(data[3].value).toBe(1 + 2);
|
||||
expect(data[3].name).toBe('Other');
|
||||
expect(data[3].isOther).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* 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 { ChartProps, supersetTheme } from '@superset-ui/core';
|
||||
import { RadarSeriesOption } from 'echarts/charts';
|
||||
import transformProps from '../../src/Radar/transformProps';
|
||||
import {
|
||||
EchartsRadarChartProps,
|
||||
EchartsRadarFormData,
|
||||
} from '../../src/Radar/types';
|
||||
|
||||
interface RadarIndicator {
|
||||
name: string;
|
||||
max: number;
|
||||
min: number;
|
||||
}
|
||||
|
||||
type RadarShape = 'circle' | 'polygon';
|
||||
|
||||
interface RadarChartConfig {
|
||||
shape: RadarShape;
|
||||
indicator: RadarIndicator[];
|
||||
}
|
||||
|
||||
interface RadarSeriesData {
|
||||
value: number[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
describe('Radar transformProps', () => {
|
||||
const formData: Partial<EchartsRadarFormData> = {
|
||||
colorScheme: 'supersetColors',
|
||||
datasource: '3__table',
|
||||
granularity_sqla: 'ds',
|
||||
columnConfig: {
|
||||
'MAX(na_sales)': {
|
||||
radarMetricMaxValue: null,
|
||||
radarMetricMinValue: 0,
|
||||
},
|
||||
'SUM(eu_sales)': {
|
||||
radarMetricMaxValue: 5000,
|
||||
},
|
||||
},
|
||||
groupby: [],
|
||||
metrics: [
|
||||
'MAX(na_sales)',
|
||||
'SUM(jp_sales)',
|
||||
'SUM(other_sales)',
|
||||
'SUM(eu_sales)',
|
||||
],
|
||||
viz_type: 'radar',
|
||||
numberFormat: 'SMART_NUMBER',
|
||||
dateFormat: 'smart_date',
|
||||
showLegend: true,
|
||||
showLabels: true,
|
||||
isCircle: false,
|
||||
};
|
||||
|
||||
const chartProps = new ChartProps({
|
||||
formData,
|
||||
width: 800,
|
||||
height: 600,
|
||||
queriesData: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
'MAX(na_sales)': 41.49,
|
||||
'SUM(jp_sales)': 1290.99,
|
||||
'SUM(other_sales)': 797.73,
|
||||
'SUM(eu_sales)': 2434.13,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
theme: supersetTheme,
|
||||
});
|
||||
|
||||
it('should transform chart props for normalized radar chart & normalize all metrics except the ones with custom min & max', () => {
|
||||
const transformedProps = transformProps(
|
||||
chartProps as EchartsRadarChartProps,
|
||||
);
|
||||
const series = transformedProps.echartOptions.series as RadarSeriesOption[];
|
||||
const radar = transformedProps.echartOptions.radar as RadarChartConfig;
|
||||
|
||||
expect((series[0].data as RadarSeriesData[])[0].value).toEqual([
|
||||
0.0170451044, 0.5303701939, 0.3277269497, 2434.13,
|
||||
]);
|
||||
|
||||
expect(radar.indicator).toEqual([
|
||||
{
|
||||
name: 'MAX(na_sales)',
|
||||
max: 1,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: 'SUM(jp_sales)',
|
||||
max: 1,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: 'SUM(other_sales)',
|
||||
max: 1,
|
||||
min: 0,
|
||||
},
|
||||
{
|
||||
name: 'SUM(eu_sales)',
|
||||
max: 5000,
|
||||
min: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -113,6 +113,10 @@ const StyledSpace = styled(Space)`
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
// Be sure to pass our updateMyData and the skipReset option
|
||||
export default typedMemo(function DataTable<D extends object>({
|
||||
tableClassName,
|
||||
@@ -447,9 +451,9 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
>
|
||||
{hasGlobalControl ? (
|
||||
<div ref={globalControlRef} className="form-inline dt-controls">
|
||||
<div className="row">
|
||||
<StyledRow className="row">
|
||||
<div
|
||||
className={renderTimeComparisonDropdown ? 'col-sm-5' : 'col-sm-6'}
|
||||
className={renderTimeComparisonDropdown ? 'col-sm-4' : 'col-sm-5'}
|
||||
>
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
@@ -466,7 +470,11 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
) : null}
|
||||
</div>
|
||||
{searchInput ? (
|
||||
<StyledSpace className="col-sm-6">
|
||||
<StyledSpace
|
||||
className={
|
||||
renderTimeComparisonDropdown ? 'col-sm-7' : 'col-sm-8'
|
||||
}
|
||||
>
|
||||
{serverPagination && (
|
||||
<div className="search-select-container">
|
||||
<span className="search-by-label">Search by: </span>
|
||||
@@ -500,7 +508,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
{renderTimeComparisonDropdown()}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</StyledRow>
|
||||
</div>
|
||||
) : null}
|
||||
{wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()}
|
||||
|
||||
@@ -79,7 +79,7 @@ import DataTable, {
|
||||
|
||||
import Styles from './Styles';
|
||||
import { formatColumnValue } from './utils/formatValue';
|
||||
import { PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
||||
import getScrollBarSize from './DataTable/utils/getScrollBarSize';
|
||||
|
||||
@@ -306,7 +306,9 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
// only take relevant page size options
|
||||
const pageSizeOptions = useMemo(() => {
|
||||
const getServerPagination = (n: number) => n <= rowCount;
|
||||
return PAGE_SIZE_OPTIONS.filter(([n]) =>
|
||||
return (
|
||||
serverPagination ? SERVER_PAGE_SIZE_OPTIONS : PAGE_SIZE_OPTIONS
|
||||
).filter(([n]) =>
|
||||
serverPagination ? getServerPagination(n) : n <= 2 * data.length,
|
||||
) as SizeOption[];
|
||||
}, [data.length, rowCount, serverPagination]);
|
||||
|
||||
@@ -30,3 +30,7 @@ export const PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
||||
100,
|
||||
200,
|
||||
]);
|
||||
|
||||
export const SERVER_PAGE_SIZE_OPTIONS = formatSelectOptions<number>([
|
||||
10, 20, 50, 100, 200,
|
||||
]);
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
|
||||
import { isEmpty, last } from 'lodash';
|
||||
import { PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { PAGE_SIZE_OPTIONS, SERVER_PAGE_SIZE_OPTIONS } from './consts';
|
||||
import { ColorSchemeEnum } from './types';
|
||||
|
||||
function getQueryMode(controls: ControlStateMapping): QueryMode {
|
||||
@@ -343,6 +343,26 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'order_desc',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Sort descending'),
|
||||
default: true,
|
||||
description: t(
|
||||
'If enabled, this control sorts the results/values descending, otherwise it sorts the results ascending.',
|
||||
),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) => {
|
||||
const hasSortMetric = Boolean(
|
||||
controls?.timeseries_limit_metric?.value,
|
||||
);
|
||||
return hasSortMetric && isAggMode({ controls });
|
||||
},
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'server_pagination',
|
||||
@@ -364,7 +384,7 @@ const config: ControlPanelConfig = {
|
||||
freeForm: true,
|
||||
label: t('Server Page Length'),
|
||||
default: 10,
|
||||
choices: PAGE_SIZE_OPTIONS,
|
||||
choices: SERVER_PAGE_SIZE_OPTIONS,
|
||||
description: t('Rows per page, 0 means no pagination'),
|
||||
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||
Boolean(controls?.server_pagination?.value),
|
||||
@@ -397,6 +417,7 @@ const config: ControlPanelConfig = {
|
||||
v,
|
||||
state?.server_pagination,
|
||||
state?.maxValueWithoutServerPagination || DEFAULT_MAX_ROW,
|
||||
state?.maxValue || DEFAULT_MAX_ROW_TABLE_SERVER,
|
||||
),
|
||||
],
|
||||
// Re run the validations when this control value
|
||||
@@ -412,21 +433,6 @@ const config: ControlPanelConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'order_desc',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('Sort descending'),
|
||||
default: true,
|
||||
description: t(
|
||||
'If enabled, this control sorts the results/values descending, otherwise it sorts the results ascending.',
|
||||
),
|
||||
visibility: isAggMode,
|
||||
resetOnHide: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'show_totals',
|
||||
|
||||
@@ -325,6 +325,8 @@ const SqlEditor: FC<Props> = ({
|
||||
|
||||
const SqlFormExtension = extensionsRegistry.get('sqleditor.extension.form');
|
||||
|
||||
const isTempId = (value: unknown): boolean => Number.isNaN(Number(value));
|
||||
|
||||
const startQuery = useCallback(
|
||||
(ctasArg = false, ctas_method = CtasEnum.Table) => {
|
||||
if (!database) {
|
||||
@@ -915,7 +917,7 @@ const SqlEditor: FC<Props> = ({
|
||||
)}
|
||||
{isActive && (
|
||||
<AceEditorWrapper
|
||||
autocomplete={autocompleteEnabled}
|
||||
autocomplete={autocompleteEnabled && !isTempId(queryEditor.id)}
|
||||
onBlur={onSqlChanged}
|
||||
onChange={onSqlChanged}
|
||||
queryEditorId={queryEditor.id}
|
||||
|
||||
@@ -77,21 +77,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
border: none;
|
||||
color: @gray;
|
||||
|
||||
&:hover {
|
||||
color: @gray-darker;
|
||||
}
|
||||
|
||||
&:before {
|
||||
font-family: 'FontAwesome';
|
||||
font-size: @font-size-xs;
|
||||
content: '\f078';
|
||||
}
|
||||
}
|
||||
|
||||
// Typography =================================================================
|
||||
|
||||
body {
|
||||
|
||||
@@ -11,37 +11,23 @@
|
||||
*
|
||||
* 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
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||
* OF ANY KIND, either express or implied. See the License for
|
||||
* the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import Tooltip, { getTooltipHTML } from './Tooltip';
|
||||
import { getTooltipHTML } from './Tooltip';
|
||||
|
||||
test('should render a tooltip', () => {
|
||||
const expected = {
|
||||
title: 'tooltip title',
|
||||
icon: <div>icon</div>,
|
||||
body: <div>body</div>,
|
||||
meta: 'meta',
|
||||
footer: <div>footer</div>,
|
||||
};
|
||||
render(<Tooltip {...expected} />);
|
||||
expect(screen.getByText(expected.title)).toBeInTheDocument();
|
||||
expect(screen.getByText(expected.meta)).toBeInTheDocument();
|
||||
expect(screen.getByText('icon')).toBeInTheDocument();
|
||||
expect(screen.getByText('body')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('returns the tooltip HTML', () => {
|
||||
test('getTooltipHTML returns the expected HTML (string inputs)', () => {
|
||||
const html = getTooltipHTML({
|
||||
title: 'tooltip title',
|
||||
icon: <div>icon</div>,
|
||||
body: <div>body</div>,
|
||||
meta: 'meta',
|
||||
footer: <div>footer</div>,
|
||||
body: 'body text',
|
||||
footer: 'footer note',
|
||||
});
|
||||
|
||||
expect(html).toContain('tooltip-detail');
|
||||
expect(html).toContain('tooltip title');
|
||||
expect(html).toContain('body text');
|
||||
expect(html).toContain('footer note');
|
||||
});
|
||||
|
||||
@@ -16,42 +16,22 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { Tag } from 'src/components';
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
body?: React.ReactNode;
|
||||
meta?: string;
|
||||
footer?: React.ReactNode;
|
||||
title?: string;
|
||||
body?: string;
|
||||
footer?: string;
|
||||
};
|
||||
|
||||
export const Tooltip: React.FC<Props> = ({
|
||||
title,
|
||||
icon,
|
||||
body,
|
||||
meta,
|
||||
footer,
|
||||
}) => (
|
||||
<div className="tooltip-detail">
|
||||
<div className="tooltip-detail-head">
|
||||
<div className="tooltip-detail-title">
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
{meta && (
|
||||
<span className="tooltip-detail-meta">
|
||||
<Tag color="default">{meta}</Tag>
|
||||
</span>
|
||||
)}
|
||||
export function getTooltipHTML({ title, body, footer }: Props): string {
|
||||
const html = `
|
||||
<div class="tooltip-detail">
|
||||
${title ? `<div class="tooltip-detail-title">${title}</div>` : ''}
|
||||
${body ? `<div class="tooltip-detail-body">${body}</div>` : ''}
|
||||
${footer ? `<div class="tooltip-detail-footer">${footer}</div>` : ''}
|
||||
</div>
|
||||
{body && <div className="tooltip-detail-body">{body ?? title}</div>}
|
||||
{footer && <div className="tooltip-detail-footer">{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const getTooltipHTML = (props: Props) =>
|
||||
`${renderToStaticMarkup(<Tooltip {...props} />)}`;
|
||||
|
||||
export default Tooltip;
|
||||
`;
|
||||
return DOMPurify.sanitize(html);
|
||||
}
|
||||
|
||||
@@ -190,50 +190,37 @@ export default function AsyncAceEditor(
|
||||
return (
|
||||
<>
|
||||
<Global
|
||||
key="ace-tooltip-global"
|
||||
styles={css`
|
||||
.ace_tooltip {
|
||||
margin-left: ${supersetTheme.gridUnit * 2}px;
|
||||
padding: 0px;
|
||||
all: unset;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
background: ${supersetTheme.colors.grayscale.light5};
|
||||
border: 1px solid ${supersetTheme.colors.grayscale.light1};
|
||||
padding: ${supersetTheme.gridUnit}px
|
||||
${supersetTheme.gridUnit * 2}px;
|
||||
line-height: 1.4;
|
||||
max-width: 400px;
|
||||
min-width: 200px;
|
||||
pointer-events: auto;
|
||||
font-size: ${supersetTheme.typography.sizes.m}px;
|
||||
}
|
||||
|
||||
& .tooltip-detail {
|
||||
background-color: ${supersetTheme.colors.grayscale.light5};
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-width: ${supersetTheme.gridUnit * 50}px;
|
||||
max-width: ${supersetTheme.gridUnit * 100}px;
|
||||
& .tooltip-detail-head {
|
||||
background-color: ${supersetTheme.colors.grayscale.light4};
|
||||
color: ${supersetTheme.colors.grayscale.dark1};
|
||||
display: flex;
|
||||
column-gap: ${supersetTheme.gridUnit}px;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
& .tooltip-detail-title {
|
||||
display: flex;
|
||||
column-gap: ${supersetTheme.gridUnit}px;
|
||||
font-weight: bold;
|
||||
font-size: ${supersetTheme.typography.sizes.m}px;
|
||||
}
|
||||
& .tooltip-detail-body {
|
||||
word-break: break-word;
|
||||
font-size: ${supersetTheme.typography.sizes.s}px;
|
||||
padding: ${supersetTheme.gridUnit}px;
|
||||
}
|
||||
& .tooltip-detail-head,
|
||||
& .tooltip-detail-body {
|
||||
padding: ${supersetTheme.gridUnit}px
|
||||
${supersetTheme.gridUnit * 2}px;
|
||||
}
|
||||
& .tooltip-detail-footer {
|
||||
border-top: 1px ${supersetTheme.colors.grayscale.light2}
|
||||
solid;
|
||||
padding: 0 ${supersetTheme.gridUnit * 2}px;
|
||||
color: ${supersetTheme.colors.grayscale.dark1};
|
||||
font-size: ${supersetTheme.typography.sizes.xs}px;
|
||||
}
|
||||
& .tooltip-detail-meta {
|
||||
& > .ant-tag {
|
||||
margin-right: 0px;
|
||||
}
|
||||
font-size: ${supersetTheme.typography.sizes.s}px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -39,19 +39,21 @@ const PLACEMENTS = [
|
||||
'topRight',
|
||||
];
|
||||
|
||||
const theme = useTheme();
|
||||
export const InteractiveIconTooltip = (args: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
export const InteractiveIconTooltip = (args: Props) => (
|
||||
<div
|
||||
css={css`
|
||||
margin: ${theme.gridUnit * 10}px ${theme.gridUnit * 17.5}px;
|
||||
`}
|
||||
>
|
||||
<IconTooltip {...args}>
|
||||
<Icons.InfoCircleOutlined />
|
||||
</IconTooltip>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
margin: ${theme.gridUnit * 10}px ${theme.gridUnit * 17.5}px;
|
||||
`}
|
||||
>
|
||||
<IconTooltip {...args}>
|
||||
<Icons.InfoCircleOutlined />
|
||||
</IconTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InteractiveIconTooltip.args = {
|
||||
tooltip: 'Tooltip',
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
BarChartOutlined,
|
||||
BellOutlined,
|
||||
BookOutlined,
|
||||
BulbOutlined,
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined,
|
||||
CaretLeftOutlined,
|
||||
@@ -61,6 +62,7 @@ import {
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
FallOutlined,
|
||||
FieldNumberOutlined,
|
||||
FieldTimeOutlined,
|
||||
FileImageOutlined,
|
||||
FileOutlined,
|
||||
@@ -90,6 +92,7 @@ import {
|
||||
SaveOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ShareAltOutlined,
|
||||
StarOutlined,
|
||||
StarFilled,
|
||||
StopOutlined,
|
||||
@@ -131,6 +134,7 @@ const AntdIcons = {
|
||||
BarChartOutlined,
|
||||
BellOutlined,
|
||||
BookOutlined,
|
||||
BulbOutlined,
|
||||
CaretUpOutlined,
|
||||
CaretDownOutlined,
|
||||
CaretLeftOutlined,
|
||||
@@ -162,6 +166,7 @@ const AntdIcons = {
|
||||
EyeOutlined,
|
||||
EyeInvisibleOutlined,
|
||||
FallOutlined,
|
||||
FieldNumberOutlined,
|
||||
FieldTimeOutlined,
|
||||
FileImageOutlined,
|
||||
FileOutlined,
|
||||
@@ -191,6 +196,7 @@ const AntdIcons = {
|
||||
SaveOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ShareAltOutlined,
|
||||
StarOutlined,
|
||||
StarFilled,
|
||||
StopOutlined,
|
||||
|
||||
@@ -443,6 +443,7 @@ const Select = forwardRef(
|
||||
<StyledBulkActionsContainer className="select-bulk-actions" size={0}>
|
||||
<Button
|
||||
type="link"
|
||||
buttonStyle="link"
|
||||
buttonSize="xsmall"
|
||||
disabled={bulkSelectCounts.selectable === 0}
|
||||
onMouseDown={e => {
|
||||
@@ -455,6 +456,7 @@ const Select = forwardRef(
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
buttonStyle="link"
|
||||
buttonSize="xsmall"
|
||||
disabled={bulkSelectCounts.deselectable === 0}
|
||||
onMouseDown={e => {
|
||||
|
||||
@@ -57,6 +57,7 @@ export const useResultsPane = ({
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [responseError, setResponseError] = useState<string>('');
|
||||
const queryCount = metadata?.queryObjectCount ?? 1;
|
||||
const isQueryCountDynamic = metadata?.dynamicQueryObjectCount;
|
||||
|
||||
useEffect(() => {
|
||||
// it's an invalid formData when gets a errorMessage
|
||||
@@ -139,19 +140,21 @@ export const useResultsPane = ({
|
||||
<EmptyState image="document.svg" title={title} />,
|
||||
);
|
||||
}
|
||||
return resultResp
|
||||
.slice(0, queryCount)
|
||||
.map((result, idx) => (
|
||||
<SingleQueryResultPane
|
||||
data={result.data}
|
||||
colnames={result.colnames}
|
||||
coltypes={result.coltypes}
|
||||
rowcount={result.rowcount}
|
||||
dataSize={dataSize}
|
||||
datasourceId={queryFormData.datasource}
|
||||
key={idx}
|
||||
isVisible={isVisible}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
));
|
||||
const resultRespToDisplay = isQueryCountDynamic
|
||||
? resultResp
|
||||
: resultResp.slice(0, queryCount);
|
||||
|
||||
return resultRespToDisplay.map((result, idx) => (
|
||||
<SingleQueryResultPane
|
||||
data={result.data}
|
||||
colnames={result.colnames}
|
||||
coltypes={result.coltypes}
|
||||
rowcount={result.rowcount}
|
||||
dataSize={dataSize}
|
||||
datasourceId={queryFormData.datasource}
|
||||
key={idx}
|
||||
isVisible={isVisible}
|
||||
canDownload={canDownload}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -174,4 +174,33 @@ describe('ResultsPaneOnDashboard', () => {
|
||||
expect(await findByText('Results')).toBeVisible();
|
||||
expect(await findByText('Results 2')).toBeVisible();
|
||||
});
|
||||
|
||||
test('dynamic number of results pane', async () => {
|
||||
const FakeChart = () => <span>test</span>;
|
||||
const metadata = new ChartMetadata({
|
||||
name: 'test-chart',
|
||||
thumbnail: '',
|
||||
dynamicQueryObjectCount: true,
|
||||
});
|
||||
|
||||
const plugin = new ChartPlugin({
|
||||
metadata,
|
||||
Chart: FakeChart,
|
||||
});
|
||||
plugin.configure({ key: VizType.MixedTimeseries }).register();
|
||||
|
||||
const props = createResultsPaneOnDashboardProps({
|
||||
sliceId: 196,
|
||||
vizType: VizType.MixedTimeseries,
|
||||
});
|
||||
const { findByText, queryByText } = render(
|
||||
<ResultsPaneOnDashboard {...props} />,
|
||||
{
|
||||
useRedux: true,
|
||||
},
|
||||
);
|
||||
expect(await findByText('Results')).toBeVisible();
|
||||
expect(await findByText('Results 2')).toBeVisible();
|
||||
expect(queryByText('Results 3')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* 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 'spec/helpers/testing-library';
|
||||
import NumberControl from '.';
|
||||
|
||||
const mockedProps = {
|
||||
min: -5,
|
||||
max: 10,
|
||||
step: 1,
|
||||
default: 0,
|
||||
};
|
||||
|
||||
test('render', () => {
|
||||
const { container } = render(<NumberControl {...mockedProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('type number', async () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(<NumberControl {...props} />);
|
||||
const input = screen.getByRole('spinbutton');
|
||||
await userEvent.type(input, '9');
|
||||
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.onChange).toHaveBeenLastCalledWith(9);
|
||||
});
|
||||
|
||||
test('type >max', async () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(<NumberControl {...props} />);
|
||||
const input = screen.getByRole('spinbutton');
|
||||
await userEvent.type(input, '20');
|
||||
expect(props.onChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.onChange).toHaveBeenLastCalledWith(2);
|
||||
});
|
||||
|
||||
test('type NaN', async () => {
|
||||
const props = {
|
||||
...mockedProps,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
render(<NumberControl {...props} />);
|
||||
const input = screen.getByRole('spinbutton');
|
||||
await userEvent.type(input, 'not a number');
|
||||
expect(props.onChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 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 { styled } from '@superset-ui/core';
|
||||
import { InputNumber } from 'src/components/Input';
|
||||
import ControlHeader, { ControlHeaderProps } from '../../ControlHeader';
|
||||
|
||||
type NumberValueType = number | undefined;
|
||||
|
||||
export interface NumberControlProps extends ControlHeaderProps {
|
||||
onChange?: (value: NumberValueType) => void;
|
||||
value?: NumberValueType;
|
||||
label?: string;
|
||||
description?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const FullWidthDiv = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const FullWidthInputNumber = styled(InputNumber)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
function parseValue(value: string | number | null | undefined) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return undefined;
|
||||
}
|
||||
const num = Number(value);
|
||||
return Number.isNaN(num) ? undefined : num;
|
||||
}
|
||||
|
||||
export default function NumberControl({
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
...rest
|
||||
}: NumberControlProps) {
|
||||
return (
|
||||
<FullWidthDiv>
|
||||
<ControlHeader {...rest} />
|
||||
<FullWidthInputNumber
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={value => onChange?.(parseValue(value))}
|
||||
disabled={disabled}
|
||||
aria-label={rest.label}
|
||||
/>
|
||||
</FullWidthDiv>
|
||||
);
|
||||
}
|
||||
@@ -19,10 +19,7 @@
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextArea } from 'src/components/Input';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipProps as TooltipOptions,
|
||||
} from 'src/components/Tooltip';
|
||||
import { Tooltip, TooltipProps } from 'src/components/Tooltip';
|
||||
import { t, withTheme } from '@superset-ui/core';
|
||||
|
||||
import Button from 'src/components/Button';
|
||||
@@ -59,7 +56,7 @@ const propTypes = {
|
||||
'vertical',
|
||||
]),
|
||||
textAreaStyles: PropTypes.object,
|
||||
tooltipOptions: PropTypes.oneOf([null, TooltipOptions]),
|
||||
tooltipOptions: PropTypes.oneOf([null, TooltipProps]),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
|
||||
@@ -53,6 +53,7 @@ import { ComparisonRangeLabel } from './ComparisonRangeLabel';
|
||||
import LayerConfigsControl from './LayerConfigsControl/LayerConfigsControl';
|
||||
import MapViewControl from './MapViewControl/MapViewControl';
|
||||
import ZoomConfigControl from './ZoomConfigControl/ZoomConfigControl';
|
||||
import NumberControl from './NumberControl';
|
||||
|
||||
const controlMap = {
|
||||
AnnotationLayerControl,
|
||||
@@ -90,6 +91,7 @@ const controlMap = {
|
||||
ComparisonRangeLabel,
|
||||
TimeOffsetControl,
|
||||
ZoomConfigControl,
|
||||
NumberControl,
|
||||
...sharedControlComponents,
|
||||
};
|
||||
export default controlMap;
|
||||
|
||||
@@ -36,11 +36,8 @@ export function getColumnKeywords(columns: ColumnMeta[]) {
|
||||
value: column_name,
|
||||
docHTML: getTooltipHTML({
|
||||
title: column_name,
|
||||
meta: type ? `column: ${type}` : 'column',
|
||||
body: `${description ?? ''}`,
|
||||
footer: is_certified ? (
|
||||
<>{t('Certified by %s', certified_by)}</>
|
||||
) : undefined,
|
||||
body: `type: ${type || 'unknown'}<br />${description ? `description: ${description}` : ''}`,
|
||||
footer: is_certified ? t('Certified by %s', certified_by) : undefined,
|
||||
}),
|
||||
score: COLUMN_AUTOCOMPLETE_SCORE,
|
||||
meta: 'column',
|
||||
|
||||
@@ -91,12 +91,13 @@ describe('AllEntitiesTable', () => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders when empty', () => {
|
||||
it('renders when empty with button to tag if user has perm', () => {
|
||||
render(
|
||||
<AllEntitiesTable
|
||||
search=""
|
||||
setShowTagModal={mockSetShowTagModal}
|
||||
objects={mockObjects}
|
||||
canEditTag
|
||||
/>,
|
||||
{ useRouter: true },
|
||||
);
|
||||
@@ -108,25 +109,70 @@ describe('AllEntitiesTable', () => {
|
||||
expect(screen.getByText('Add tag to entities')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders when empty without button to tag if user does not have perm', () => {
|
||||
render(
|
||||
<AllEntitiesTable
|
||||
search=""
|
||||
setShowTagModal={mockSetShowTagModal}
|
||||
objects={mockObjects}
|
||||
canEditTag={false}
|
||||
/>,
|
||||
{ useRouter: true },
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText('No entities have this tag currently assigned'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Add tag to entities')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the correct tags for each object type, excluding the current tag', () => {
|
||||
render(
|
||||
<AllEntitiesTable
|
||||
search=""
|
||||
setShowTagModal={mockSetShowTagModal}
|
||||
objects={mockObjectsWithTags}
|
||||
canEditTag
|
||||
/>,
|
||||
{ useRouter: true },
|
||||
);
|
||||
|
||||
expect(screen.getByText('Dashboards')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sales Dashboard')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sales')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Charts')).toBeInTheDocument();
|
||||
expect(screen.getByText('Monthly Revenue')).toBeInTheDocument();
|
||||
expect(screen.getByText('Revenue')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Queries')).toBeInTheDocument();
|
||||
expect(screen.getByText('User Engagement')).toBeInTheDocument();
|
||||
expect(screen.getByText('Engagement')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Current Tag')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Only list asset types that have entities', () => {
|
||||
const mockObjects = {
|
||||
dashboard: [],
|
||||
chart: [mockObjectsWithTags.chart[0]],
|
||||
query: [],
|
||||
};
|
||||
|
||||
render(
|
||||
<AllEntitiesTable
|
||||
search=""
|
||||
setShowTagModal={mockSetShowTagModal}
|
||||
objects={mockObjects}
|
||||
canEditTag
|
||||
/>,
|
||||
{ useRouter: true },
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Dashboards')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Charts')).toBeInTheDocument();
|
||||
expect(screen.getByText('Monthly Revenue')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Queries')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,20 +53,22 @@ interface AllEntitiesTableProps {
|
||||
search?: string;
|
||||
setShowTagModal: (show: boolean) => void;
|
||||
objects: TaggedObjects;
|
||||
canEditTag: boolean;
|
||||
}
|
||||
|
||||
export default function AllEntitiesTable({
|
||||
search = '',
|
||||
setShowTagModal,
|
||||
objects,
|
||||
canEditTag,
|
||||
}: AllEntitiesTableProps) {
|
||||
type objectType = 'dashboard' | 'chart' | 'query';
|
||||
|
||||
const [tagId] = useQueryParam('id', NumberParam);
|
||||
const showListViewObjs =
|
||||
objects.dashboard.length > 0 ||
|
||||
objects.chart.length > 0 ||
|
||||
objects.query.length > 0;
|
||||
const showDashboardList = objects.dashboard.length > 0;
|
||||
const showChartList = objects.chart.length > 0;
|
||||
const showQueryList = objects.query.length > 0;
|
||||
const showListViewObjs = showDashboardList || showChartList || showQueryList;
|
||||
|
||||
const renderTable = (type: objectType) => {
|
||||
const data = objects[type].map((o: TaggedObject) => ({
|
||||
@@ -134,20 +136,34 @@ export default function AllEntitiesTable({
|
||||
<AllEntitiesTableContainer>
|
||||
{showListViewObjs ? (
|
||||
<>
|
||||
<div className="entity-title">{t('Dashboards')}</div>
|
||||
{renderTable('dashboard')}
|
||||
<div className="entity-title">{t('Charts')}</div>
|
||||
{renderTable('chart')}
|
||||
<div className="entity-title">{t('Queries')}</div>
|
||||
{renderTable('query')}
|
||||
{showDashboardList && (
|
||||
<>
|
||||
<div className="entity-title">{t('Dashboards')}</div>
|
||||
{renderTable('dashboard')}
|
||||
</>
|
||||
)}
|
||||
{showChartList && (
|
||||
<>
|
||||
<div className="entity-title">{t('Charts')}</div>
|
||||
{renderTable('chart')}
|
||||
</>
|
||||
)}
|
||||
{showQueryList && (
|
||||
<>
|
||||
<div className="entity-title">{t('Queries')}</div>
|
||||
{renderTable('query')}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
image="dashboard.svg"
|
||||
size="large"
|
||||
title={t('No entities have this tag currently assigned')}
|
||||
buttonAction={() => setShowTagModal(true)}
|
||||
buttonText={t('Add tag to entities')}
|
||||
{...(canEditTag && {
|
||||
buttonAction: () => setShowTagModal(true),
|
||||
buttonText: t('Add tag to entities'),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</AllEntitiesTableContainer>
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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 {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from 'spec/helpers/testing-library';
|
||||
import { t } from '@superset-ui/core';
|
||||
import * as ace from 'ace-builds';
|
||||
|
||||
import ExtraOptions from './ExtraOptions';
|
||||
import { DatabaseObject } from '../types';
|
||||
|
||||
const defaultDb = {
|
||||
expose_in_sqllab: true,
|
||||
allow_ctas: false,
|
||||
allow_cvas: false,
|
||||
allow_dml: false,
|
||||
allow_run_async: false,
|
||||
cache_timeout: 300,
|
||||
force_ctas_schema: 'public',
|
||||
masked_encrypted_extra: '',
|
||||
server_cert: '',
|
||||
impersonate_user: false,
|
||||
extra: JSON.stringify({
|
||||
cost_estimate_enabled: false,
|
||||
allows_virtual_table_explore: true,
|
||||
disable_data_preview: false,
|
||||
schema_options: { expand_rows: false },
|
||||
metadata_cache_timeout: {
|
||||
schema_cache_timeout: 600,
|
||||
table_cache_timeout: 1200,
|
||||
},
|
||||
disable_drill_to_detail: false,
|
||||
metadata_params: {},
|
||||
engine_params: {},
|
||||
version: '',
|
||||
cancel_query_on_windows_unload: false,
|
||||
}),
|
||||
engine_information: {
|
||||
supports_file_upload: true,
|
||||
supports_dynamic_catalog: true,
|
||||
},
|
||||
configuration_method: '', // added dummy value for configuration_method
|
||||
database_name: 'Test Database', // added dummy value for database_name
|
||||
driver: 'sqlite', // added dummy value for driver
|
||||
id: 1, // added dummy value for id
|
||||
sqlalchemy_uri: 'sqlite:///:memory:', // added dummy value for sqlalchemy_uri
|
||||
parameters: {}, // added dummy value for parameters
|
||||
};
|
||||
|
||||
describe('ExtraOptions Component', () => {
|
||||
const onInputChange = jest.fn();
|
||||
const onTextChange = jest.fn();
|
||||
const onEditorChange = jest.fn();
|
||||
const onExtraInputChange = jest.fn();
|
||||
const onExtraEditorChange = jest.fn();
|
||||
|
||||
const renderComponent = (dbProps = defaultDb, extension = undefined) =>
|
||||
render(
|
||||
<ExtraOptions
|
||||
db={dbProps as unknown as DatabaseObject}
|
||||
onInputChange={onInputChange}
|
||||
onTextChange={onTextChange}
|
||||
onEditorChange={onEditorChange}
|
||||
onExtraInputChange={onExtraInputChange}
|
||||
onExtraEditorChange={onExtraEditorChange}
|
||||
extraExtension={extension}
|
||||
/>,
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders all main panels', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText(t('SQL Lab'))).toBeInTheDocument();
|
||||
expect(screen.getByText(t('Performance'))).toBeInTheDocument();
|
||||
expect(screen.getByText(t('Security'))).toBeInTheDocument();
|
||||
expect(screen.getByText(t('Other'))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onInputChange when "Expose database in SQL Lab" checkbox is clicked', () => {
|
||||
renderComponent();
|
||||
const sqlLabText = screen.getByText(t('SQL Lab'));
|
||||
fireEvent.click(sqlLabText);
|
||||
|
||||
const checkbox = screen.getByLabelText(t('Expose database in SQL Lab'));
|
||||
fireEvent.click(checkbox);
|
||||
expect(onInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onExtraInputChange when "Enable query cost estimation" checkbox is clicked', () => {
|
||||
renderComponent();
|
||||
const sqlLabText = screen.getByText(t('SQL Lab'));
|
||||
fireEvent.click(sqlLabText);
|
||||
const checkbox = screen.getByLabelText(t('Enable query cost estimation'));
|
||||
fireEvent.click(checkbox);
|
||||
expect(onExtraInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onExtraEditorChange when metadata_params json editor changes', async () => {
|
||||
renderComponent();
|
||||
|
||||
// Click to open the editor tab/section
|
||||
const otherHeader = screen.getByText(t('Other'));
|
||||
fireEvent.click(otherHeader);
|
||||
|
||||
// Wait for Ace to initialize (in case it's async)
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('#metadata_params')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Grab editor instance by ID or name
|
||||
const editorInstance = ace.edit('metadata_params');
|
||||
|
||||
act(() => {
|
||||
editorInstance.setValue('{"key":"value"}');
|
||||
});
|
||||
|
||||
expect(onExtraEditorChange).toHaveBeenCalledWith({
|
||||
json: '{"key":"value"}',
|
||||
name: 'metadata_params',
|
||||
});
|
||||
|
||||
act(() => {
|
||||
editorInstance.setValue('foo');
|
||||
});
|
||||
|
||||
expect(onExtraEditorChange).toHaveBeenCalledWith({
|
||||
json: 'foo',
|
||||
name: 'metadata_params',
|
||||
});
|
||||
|
||||
// it accepts invalid json strings
|
||||
act(() => {
|
||||
editorInstance.setValue('{"key":"value');
|
||||
});
|
||||
|
||||
expect(onExtraEditorChange).toHaveBeenCalledWith({
|
||||
json: '{"key":"value',
|
||||
name: 'metadata_params',
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onTextChange when server certificate textarea is changed', () => {
|
||||
renderComponent();
|
||||
// Click to open the security tab/section
|
||||
const securityHeader = screen.getByText(t('Security'));
|
||||
fireEvent.click(securityHeader);
|
||||
|
||||
const textarea = screen.getByPlaceholderText(t('Enter CA_BUNDLE'));
|
||||
fireEvent.change(textarea, { target: { value: 'new cert' } });
|
||||
expect(onTextChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles input change for schema cache timeout', () => {
|
||||
renderComponent();
|
||||
const performanceHeader = screen.getByText(t('Performance'));
|
||||
fireEvent.click(performanceHeader);
|
||||
const input = screen.getByTestId('schema-cache-timeout-test');
|
||||
fireEvent.change(input, { target: { value: '500' } });
|
||||
expect(onExtraInputChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles input change for table cache timeout', () => {
|
||||
renderComponent();
|
||||
const performanceHeader = screen.getByText(t('Performance'));
|
||||
fireEvent.click(performanceHeader);
|
||||
const input = screen.getByTestId('table-cache-timeout-test');
|
||||
fireEvent.change(input, { target: { value: '1000' } });
|
||||
expect(onExtraInputChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -271,6 +271,7 @@ const ExtraOptions = ({
|
||||
value={db?.cache_timeout || ''}
|
||||
placeholder={t('Enter duration in seconds')}
|
||||
onChange={onInputChange}
|
||||
data-test="cache-timeout-test"
|
||||
/>
|
||||
</div>
|
||||
<div className="helper">
|
||||
@@ -533,7 +534,9 @@ const ExtraOptions = ({
|
||||
value={
|
||||
!Object.keys(extraJson?.metadata_params || {}).length
|
||||
? ''
|
||||
: extraJson?.metadata_params
|
||||
: typeof extraJson?.metadata_params === 'string'
|
||||
? extraJson?.metadata_params
|
||||
: JSON.stringify(extraJson?.metadata_params)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
31
superset-frontend/src/features/groups/types.ts
Normal file
31
superset-frontend/src/features/groups/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 interface BaseGroupListModalProps {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export interface FormValues {
|
||||
name: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
roles: number[];
|
||||
users: { value: number; label: string }[];
|
||||
}
|
||||
74
superset-frontend/src/features/groups/utils.ts
Normal file
74
superset-frontend/src/features/groups/utils.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 { SupersetClient, t } from '@superset-ui/core';
|
||||
import rison from 'rison';
|
||||
import { FormValues } from './types';
|
||||
|
||||
export const createGroup = async (values: FormValues) => {
|
||||
await SupersetClient.post({
|
||||
endpoint: '/api/v1/security/groups/',
|
||||
jsonPayload: { ...values, users: values.users.map(user => user.value) },
|
||||
});
|
||||
};
|
||||
|
||||
export const updateGroup = async (groupId: number, values: FormValues) => {
|
||||
await SupersetClient.put({
|
||||
endpoint: `/api/v1/security/groups/${groupId}`,
|
||||
jsonPayload: { ...values, users: values.users.map(user => user.value) },
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteGroup = async (groupId: number) =>
|
||||
SupersetClient.delete({
|
||||
endpoint: `/api/v1/security/groups/${groupId}`,
|
||||
});
|
||||
|
||||
export const fetchUserOptions = async (
|
||||
filterValue: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
addDangerToast: (msg: string) => void,
|
||||
) => {
|
||||
const query = rison.encode({
|
||||
filter: filterValue,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
order_column: 'username',
|
||||
order_direction: 'asc',
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: `/api/v1/security/users/?q=${query}`,
|
||||
});
|
||||
|
||||
const results = response.json?.result || [];
|
||||
|
||||
return {
|
||||
data: results.map((user: any) => ({
|
||||
value: user.id,
|
||||
label: user.username,
|
||||
})),
|
||||
totalCount: response.json?.count ?? 0,
|
||||
};
|
||||
} catch (error) {
|
||||
addDangerToast(t('There was an error while fetching users'));
|
||||
return { data: [], totalCount: 0 };
|
||||
}
|
||||
};
|
||||
@@ -335,7 +335,13 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Handle the default to first Value case
|
||||
if (filterState.value !== undefined) {
|
||||
// Set the filter state value if it is defined
|
||||
updateDataMask(filterState.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the default to first Value case
|
||||
if (defaultToFirstItem) {
|
||||
// Set to first item if defaultToFirstItem is true
|
||||
const firstItem: SelectValue = data[0]
|
||||
@@ -345,7 +351,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
updateDataMask(firstItem);
|
||||
}
|
||||
} else if (formData?.defaultValue) {
|
||||
// Case 3 : Handle defalut value case
|
||||
// Handle defalut value case
|
||||
updateDataMask(formData.defaultValue);
|
||||
}
|
||||
}, [
|
||||
|
||||
282
superset-frontend/src/pages/ActionLog/index.tsx
Normal file
282
superset-frontend/src/pages/ActionLog/index.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 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 { useMemo } from 'react';
|
||||
import { t, css } from '@superset-ui/core';
|
||||
import SubMenu, { SubMenuProps } from 'src/features/home/SubMenu';
|
||||
import { useListViewResource } from 'src/views/CRUD/hooks';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import ListView, { Filters, FilterOperator } from 'src/components/ListView';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Typography } from 'antd-v5';
|
||||
import { fetchUserOptions } from 'src/features/groups/utils';
|
||||
|
||||
export type ActionLogObject = {
|
||||
user: {
|
||||
username: string;
|
||||
};
|
||||
action: string;
|
||||
dttm: string | null;
|
||||
dashboard_id?: number;
|
||||
slice_id?: number;
|
||||
json?: string;
|
||||
duration_ms?: number;
|
||||
referrer?: string;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
function ActionLogList() {
|
||||
const { addDangerToast, addSuccessToast } = useToasts();
|
||||
const initialSort = [{ id: 'dttm', desc: true }];
|
||||
const subMenuButtons: SubMenuProps['buttons'] = [];
|
||||
|
||||
const {
|
||||
state: {
|
||||
loading,
|
||||
resourceCount: LogsCount,
|
||||
resourceCollection: Logs,
|
||||
bulkSelectEnabled,
|
||||
},
|
||||
fetchData,
|
||||
refreshData,
|
||||
toggleBulkSelect,
|
||||
} = useListViewResource<ActionLogObject>(
|
||||
'log',
|
||||
t('Log'),
|
||||
addDangerToast,
|
||||
false,
|
||||
);
|
||||
const filters: Filters = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: t('Users'),
|
||||
key: 'user',
|
||||
id: 'user',
|
||||
input: 'select',
|
||||
operator: FilterOperator.RelationOneMany,
|
||||
unfilteredLabel: t('All'),
|
||||
fetchSelects: async (filterValue, page, pageSize) =>
|
||||
fetchUserOptions(filterValue, page, pageSize, addDangerToast),
|
||||
},
|
||||
{
|
||||
Header: t('Dashboard Id'),
|
||||
key: 'dashboard_id',
|
||||
id: 'dashboard_id',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Equals,
|
||||
},
|
||||
{
|
||||
Header: t('Slice Id'),
|
||||
key: 'slice_id',
|
||||
id: 'slice_id',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Equals,
|
||||
},
|
||||
{
|
||||
Header: t('Action'),
|
||||
key: 'action',
|
||||
id: 'action',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Contains,
|
||||
},
|
||||
{
|
||||
Header: t('JSON'),
|
||||
key: 'json',
|
||||
id: 'json',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Contains,
|
||||
},
|
||||
{
|
||||
Header: t('dttm'),
|
||||
key: 'dttm',
|
||||
id: 'dttm',
|
||||
input: 'datetime_range',
|
||||
operator: FilterOperator.Between,
|
||||
dateFilterValueType: 'iso',
|
||||
},
|
||||
{
|
||||
Header: t('Referrer'),
|
||||
key: 'referrer',
|
||||
id: 'referrer',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Equals,
|
||||
},
|
||||
{
|
||||
Header: t('Duration Ms'),
|
||||
key: 'duration_ms',
|
||||
id: 'duration_ms',
|
||||
input: 'search',
|
||||
operator: FilterOperator.Equals,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessor: 'action',
|
||||
Header: t('Action'),
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { action },
|
||||
},
|
||||
}: any) => <span>{action}</span>,
|
||||
},
|
||||
{
|
||||
accessor: 'user',
|
||||
Header: t('User'),
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { user },
|
||||
},
|
||||
}: any) => <span>{user?.username}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
accessor: 'duration_ms',
|
||||
Header: t('Duration Ms'),
|
||||
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { duration_ms },
|
||||
},
|
||||
}: any) => <span>{duration_ms}</span>,
|
||||
},
|
||||
{
|
||||
accessor: 'dashboard_id',
|
||||
Header: t('Dashboard Id'),
|
||||
hidden: false,
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { dashboard_id },
|
||||
},
|
||||
}: any) => <span>{dashboard_id}</span>,
|
||||
},
|
||||
{
|
||||
accessor: 'slice_id',
|
||||
Header: t('Slice Id'),
|
||||
hidden: false,
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { slice_id },
|
||||
},
|
||||
}: any) => <span>{slice_id}</span>,
|
||||
},
|
||||
{
|
||||
accessor: 'json',
|
||||
Header: t('JSON'),
|
||||
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { json },
|
||||
},
|
||||
}: any) => (
|
||||
<Typography.Text
|
||||
css={css`
|
||||
.antd5-typography-copy {
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover .antd5-typography-copy {
|
||||
visibility: visible;
|
||||
}
|
||||
`}
|
||||
copyable={!!json}
|
||||
ellipsis={{
|
||||
tooltip: { styles: { root: { maxWidth: '900px' } }, title: json },
|
||||
}}
|
||||
>
|
||||
{json}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
accessor: 'referrer',
|
||||
Header: t('Referrer'),
|
||||
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { referrer },
|
||||
},
|
||||
}: any) => (
|
||||
<Typography.Text
|
||||
css={css`
|
||||
.antd5-typography-copy {
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover .antd5-typography-copy {
|
||||
visibility: visible;
|
||||
}
|
||||
`}
|
||||
copyable={!!referrer}
|
||||
ellipsis={{
|
||||
tooltip: {
|
||||
styles: { root: { maxWidth: '580px' } },
|
||||
title: referrer,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{referrer}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'dttm',
|
||||
Header: t('Dttm'),
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { dttm },
|
||||
},
|
||||
}: any) => <span>{dttm}</span>,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const emptyState = {
|
||||
title: t('No Logs yet'),
|
||||
image: 'filter-results.svg',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu name={t('Action Logs')} buttons={subMenuButtons} />
|
||||
<ListView<ActionLogObject>
|
||||
className="action-log-view"
|
||||
columns={columns}
|
||||
count={LogsCount}
|
||||
data={Logs}
|
||||
fetchData={fetchData}
|
||||
filters={filters}
|
||||
initialSort={initialSort}
|
||||
loading={loading}
|
||||
pageSize={PAGE_SIZE}
|
||||
bulkSelectEnabled={bulkSelectEnabled}
|
||||
disableBulkSelect={toggleBulkSelect}
|
||||
addDangerToast={addDangerToast}
|
||||
addSuccessToast={addSuccessToast}
|
||||
emptyState={emptyState}
|
||||
refreshData={refreshData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionLogList;
|
||||
@@ -35,6 +35,9 @@ import { fetchObjectsByTagIds, fetchSingleTag } from 'src/features/tags/tags';
|
||||
import Loading from 'src/components/Loading';
|
||||
import getOwnerName from 'src/utils/getOwnerName';
|
||||
import { TaggedObject, TaggedObjects } from 'src/types/TaggedObject';
|
||||
import { findPermission } from 'src/utils/findPermission';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from 'src/dashboard/types';
|
||||
|
||||
const additionalItemsStyles = (theme: SupersetTheme) => css`
|
||||
display: flex;
|
||||
@@ -100,6 +103,10 @@ function AllEntities() {
|
||||
query: [],
|
||||
});
|
||||
|
||||
const canEditTag = useSelector((state: RootState) =>
|
||||
findPermission('can_write', 'Tag', state.user?.roles),
|
||||
);
|
||||
|
||||
const editableTitleProps = {
|
||||
title: tag?.name || '',
|
||||
placeholder: 'testing',
|
||||
@@ -211,14 +218,16 @@ function AllEntities() {
|
||||
}
|
||||
rightPanelAdditionalItems={
|
||||
<>
|
||||
<Button
|
||||
data-test="bulk-select-action"
|
||||
buttonStyle="secondary"
|
||||
onClick={() => setShowTagModal(true)}
|
||||
showMarginRight={false}
|
||||
>
|
||||
{t('Edit Tag')}{' '}
|
||||
</Button>
|
||||
{canEditTag && (
|
||||
<Button
|
||||
data-test="bulk-select-action"
|
||||
buttonStyle="secondary"
|
||||
onClick={() => setShowTagModal(true)}
|
||||
showMarginRight={false}
|
||||
>
|
||||
{t('Edit tag')}{' '}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
menuDropdownProps={{
|
||||
@@ -232,6 +241,7 @@ function AllEntities() {
|
||||
search={tag?.name || ''}
|
||||
setShowTagModal={setShowTagModal}
|
||||
objects={objects}
|
||||
canEditTag={canEditTag}
|
||||
/>
|
||||
</div>
|
||||
</AllEntitiesContainer>
|
||||
|
||||
@@ -137,6 +137,9 @@ const RolesList = lazy(
|
||||
const UsersList: LazyExoticComponent<any> = lazy(
|
||||
() => import(/* webpackChunkName: "UsersList" */ 'src/pages/UsersList'),
|
||||
);
|
||||
const ActionLogList: LazyExoticComponent<any> = lazy(
|
||||
() => import(/* webpackChunkName: "ActionLogList" */ 'src/pages/ActionLog'),
|
||||
);
|
||||
|
||||
type Routes = {
|
||||
path: string;
|
||||
@@ -240,6 +243,10 @@ export const routes: Routes = [
|
||||
path: '/sqllab/',
|
||||
Component: SqlLab,
|
||||
},
|
||||
{
|
||||
path: '/actionlog/list',
|
||||
Component: ActionLogList,
|
||||
},
|
||||
];
|
||||
|
||||
if (isFeatureEnabled(FeatureFlag.TaggingSystem)) {
|
||||
|
||||
242
superset-websocket/package-lock.json
generated
242
superset-websocket/package-lock.json
generated
@@ -31,8 +31,8 @@
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.0",
|
||||
"@typescript-eslint/parser": "^8.29.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
@@ -747,12 +747,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
|
||||
"integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
||||
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.5",
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
@@ -760,11 +761,22 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
|
||||
"integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
|
||||
"integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
@@ -773,10 +785,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
|
||||
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
||||
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
@@ -799,13 +812,15 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -818,6 +833,7 @@
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
@@ -826,30 +842,36 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
|
||||
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
|
||||
"version": "9.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
|
||||
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
|
||||
"integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
|
||||
"integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.14.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -905,10 +927,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/retry": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
|
||||
"integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
@@ -2527,6 +2550,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
@@ -2536,6 +2560,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -3166,21 +3191,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
|
||||
"integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
|
||||
"version": "9.27.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
|
||||
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.9.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.17.0",
|
||||
"@eslint/plugin-kit": "^0.2.3",
|
||||
"@eslint/config-array": "^0.20.0",
|
||||
"@eslint/config-helpers": "^0.2.1",
|
||||
"@eslint/core": "^0.14.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.27.0",
|
||||
"@eslint/plugin-kit": "^0.3.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
@@ -3188,7 +3215,7 @@
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^8.2.0",
|
||||
"eslint-scope": "^8.3.0",
|
||||
"eslint-visitor-keys": "^4.2.0",
|
||||
"espree": "^10.3.0",
|
||||
"esquery": "^1.5.0",
|
||||
@@ -3225,13 +3252,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"version": "10.1.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
|
||||
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-config-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
@@ -3253,10 +3284,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
|
||||
"integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
||||
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
@@ -3280,16 +3312,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/@eslint/js": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -3409,6 +3431,7 @@
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
@@ -3426,6 +3449,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
||||
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
@@ -3464,6 +3488,7 @@
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
@@ -3552,7 +3577,8 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
@@ -3885,10 +3911,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parent-module": "^1.0.0",
|
||||
"resolve-from": "^4.0.0"
|
||||
@@ -3905,6 +3932,7 @@
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -5407,7 +5435,8 @@
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
@@ -5826,6 +5855,7 @@
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"callsites": "^3.0.0"
|
||||
},
|
||||
@@ -5993,6 +6023,7 @@
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -6812,6 +6843,7 @@
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
@@ -7567,29 +7599,35 @@
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/config-array": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz",
|
||||
"integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==",
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz",
|
||||
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint/object-schema": "^2.1.5",
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"@eslint/config-helpers": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz",
|
||||
"integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==",
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/core": {
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz",
|
||||
"integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==",
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
}
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
|
||||
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
|
||||
"integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.4",
|
||||
@@ -7627,23 +7665,24 @@
|
||||
}
|
||||
},
|
||||
"@eslint/js": {
|
||||
"version": "9.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
|
||||
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
|
||||
"version": "9.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
|
||||
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/object-schema": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz",
|
||||
"integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==",
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"dev": true
|
||||
},
|
||||
"@eslint/plugin-kit": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz",
|
||||
"integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint/core": "^0.14.0",
|
||||
"levn": "^0.4.1"
|
||||
}
|
||||
},
|
||||
@@ -7678,9 +7717,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@humanwhocodes/retry": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
|
||||
"integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
@@ -9359,21 +9398,22 @@
|
||||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz",
|
||||
"integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==",
|
||||
"version": "9.27.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
|
||||
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.19.0",
|
||||
"@eslint/core": "^0.9.0",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "9.17.0",
|
||||
"@eslint/plugin-kit": "^0.2.3",
|
||||
"@eslint/config-array": "^0.20.0",
|
||||
"@eslint/config-helpers": "^0.2.1",
|
||||
"@eslint/core": "^0.14.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.27.0",
|
||||
"@eslint/plugin-kit": "^0.3.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
@@ -9381,7 +9421,7 @@
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^8.2.0",
|
||||
"eslint-scope": "^8.3.0",
|
||||
"eslint-visitor-keys": "^4.2.0",
|
||||
"espree": "^10.3.0",
|
||||
"esquery": "^1.5.0",
|
||||
@@ -9400,12 +9440,6 @@
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
||||
"dev": true
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -9481,9 +9515,9 @@
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
|
||||
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
|
||||
"version": "10.1.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
|
||||
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -9497,9 +9531,9 @@
|
||||
}
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
|
||||
"integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
|
||||
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esrecurse": "^4.3.0",
|
||||
@@ -9875,9 +9909,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"parent-module": "^1.0.0",
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.26.0",
|
||||
"@typescript-eslint/parser": "^8.29.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-lodash": "^8.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
|
||||
@@ -102,7 +102,7 @@ if (startServer && opts.jwtSecret.length < 32) {
|
||||
|
||||
if (startServer && opts.jwtSecret.startsWith('CHANGE-ME')) {
|
||||
console.warn(
|
||||
'WARNING: it appears you secret in your config.json is insecure',
|
||||
'WARNING: it appears your secret in your config.json is insecure',
|
||||
);
|
||||
console.warn('DO NOT USE IN PRODUCTION');
|
||||
}
|
||||
|
||||
1276
superset-websocket/utils/client-ws-app/package-lock.json
generated
1276
superset-websocket/utils/client-ws-app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.7",
|
||||
"debug": "~4.4.0",
|
||||
"express": "~4.21.2",
|
||||
"debug": "~4.4.1",
|
||||
"express": "~5.1.0",
|
||||
"http-errors": "~2.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "~1.10.0",
|
||||
|
||||
@@ -92,7 +92,7 @@ class ExecuteSqlCommand(BaseCommand):
|
||||
pass
|
||||
|
||||
@transaction()
|
||||
def run( # pylint: disable=too-many-statements,useless-suppression
|
||||
def run(
|
||||
self,
|
||||
) -> CommandResult:
|
||||
"""Runs arbitrary sql and returns data as json"""
|
||||
|
||||
@@ -27,7 +27,7 @@ from superset.commands.base import BaseCommand
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import SupersetErrorException, SupersetSecurityException
|
||||
from superset.models.sql_lab import Query
|
||||
from superset.sql_parse import ParsedQuery
|
||||
from superset.sql.parse import SQLScript
|
||||
from superset.sqllab.limiting_factor import LimitingFactor
|
||||
from superset.utils import core as utils, csv
|
||||
from superset.views.utils import _deserialize_results_payload
|
||||
@@ -115,10 +115,9 @@ class SqlResultExportCommand(BaseCommand):
|
||||
limit = None
|
||||
else:
|
||||
sql = self._query.executed_sql
|
||||
limit = ParsedQuery(
|
||||
sql,
|
||||
engine=self._query.database.db_engine_spec.engine,
|
||||
).limit
|
||||
script = SQLScript(sql, self._query.database.db_engine_spec.engine)
|
||||
# when a query has multiple statements only the last one returns data
|
||||
limit = script.statements[-1].get_limit_value()
|
||||
if limit is not None and self._query.limiting_factor in {
|
||||
LimitingFactor.QUERY,
|
||||
LimitingFactor.DROPDOWN,
|
||||
|
||||
@@ -765,7 +765,7 @@ SCREENSHOT_WAIT_FOR_ERROR_MODAL_INVISIBLE = 5
|
||||
SCREENSHOT_PLAYWRIGHT_WAIT_EVENT = "load"
|
||||
# Default timeout for Playwright browser context for all operations
|
||||
SCREENSHOT_PLAYWRIGHT_DEFAULT_TIMEOUT = int(
|
||||
timedelta(seconds=30).total_seconds() * 1000
|
||||
timedelta(seconds=60).total_seconds() * 1000
|
||||
)
|
||||
|
||||
# ---------------------------------------------------
|
||||
@@ -1299,7 +1299,7 @@ TRACKING_URL_TRANSFORMER = lambda url: url # noqa: E731
|
||||
DB_POLL_INTERVAL_SECONDS: dict[str, int] = {}
|
||||
|
||||
# Interval between consecutive polls when using Presto Engine
|
||||
# See here: https://github.com/dropbox/PyHive/blob/8eb0aeab8ca300f3024655419b93dad926c1a351/pyhive/presto.py#L93 # pylint: disable=line-too-long,useless-suppression # noqa: E501
|
||||
# See here: https://github.com/dropbox/PyHive/blob/8eb0aeab8ca300f3024655419b93dad926c1a351/pyhive/presto.py#L93 # noqa: E501
|
||||
PRESTO_POLL_INTERVAL = int(timedelta(seconds=1).total_seconds())
|
||||
|
||||
# Allow list of custom authentications for each DB engine.
|
||||
|
||||
@@ -39,7 +39,6 @@ from uuid import uuid4
|
||||
|
||||
import pandas as pd
|
||||
import requests
|
||||
import sqlparse
|
||||
from apispec import APISpec
|
||||
from apispec.ext.marshmallow import MarshmallowPlugin
|
||||
from deprecation import deprecated
|
||||
@@ -55,17 +54,21 @@ from sqlalchemy.engine.reflection import Inspector
|
||||
from sqlalchemy.engine.url import URL
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.sql import literal_column, quoted_name, text
|
||||
from sqlalchemy.sql.expression import ColumnClause, Select, TextAsFrom, TextClause
|
||||
from sqlalchemy.sql.expression import ColumnClause, Select, TextClause
|
||||
from sqlalchemy.types import TypeEngine
|
||||
from sqlparse.tokens import CTE
|
||||
|
||||
from superset import db, sql_parse
|
||||
from superset.constants import QUERY_CANCEL_KEY, TimeGrain as TimeGrainConstants
|
||||
from superset.databases.utils import get_table_metadata, make_url_safe
|
||||
from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
|
||||
from superset.exceptions import DisallowedSQLFunction, OAuth2Error, OAuth2RedirectError
|
||||
from superset.sql.parse import BaseSQLStatement, SQLScript, Table
|
||||
from superset.sql_parse import ParsedQuery
|
||||
from superset.sql.parse import (
|
||||
BaseSQLStatement,
|
||||
LimitMethod,
|
||||
SQLScript,
|
||||
SQLStatement,
|
||||
Table,
|
||||
)
|
||||
from superset.superset_typing import (
|
||||
OAuth2ClientConfig,
|
||||
OAuth2State,
|
||||
@@ -166,14 +169,6 @@ def compile_timegrain_expression(
|
||||
return element.name.replace("{col}", compiler.process(element.col, **kwargs))
|
||||
|
||||
|
||||
class LimitMethod: # pylint: disable=too-few-public-methods
|
||||
"""Enum the ways that limits can be applied"""
|
||||
|
||||
FETCH_MANY = "fetch_many"
|
||||
WRAP_SQL = "wrap_sql"
|
||||
FORCE_LIMIT = "force_limit"
|
||||
|
||||
|
||||
class MetricType(TypedDict, total=False):
|
||||
"""
|
||||
Type for metrics return by `get_metrics`.
|
||||
@@ -377,16 +372,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
allows_cte_in_subquery = True
|
||||
# Define alias for CTE
|
||||
cte_alias = "__cte"
|
||||
# Whether allow LIMIT clause in the SQL
|
||||
# If True, then the database engine is allowed for LIMIT clause
|
||||
# If False, then the database engine is allowed for TOP clause
|
||||
allow_limit_clause = True
|
||||
# This set will give keywords for select statements
|
||||
# to consider for the engines with TOP SQL parsing
|
||||
select_keywords: set[str] = {"SELECT"}
|
||||
# This set will give the keywords for data limit statements
|
||||
# to consider for the engines with TOP SQL parsing
|
||||
top_keywords: set[str] = {"TOP"}
|
||||
# A set of disallowed connection query parameters by driver name
|
||||
disallow_uri_query_params: dict[str, set[str]] = {}
|
||||
# A Dict of query parameters that will always be used on every connection
|
||||
@@ -1119,100 +1107,6 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def apply_limit_to_sql(
|
||||
cls, sql: str, limit: int, database: Database, force: bool = False
|
||||
) -> str:
|
||||
"""
|
||||
Alters the SQL statement to apply a LIMIT clause
|
||||
|
||||
:param sql: SQL query
|
||||
:param limit: Maximum number of rows to be returned by the query
|
||||
:param database: Database instance
|
||||
:return: SQL query with limit clause
|
||||
"""
|
||||
if cls.limit_method == LimitMethod.WRAP_SQL:
|
||||
sql = sql.strip("\t\n ;")
|
||||
qry = (
|
||||
select("*")
|
||||
.select_from(TextAsFrom(text(sql), ["*"]).alias("inner_qry"))
|
||||
.limit(limit)
|
||||
)
|
||||
return database.compile_sqla_query(qry)
|
||||
|
||||
if cls.limit_method == LimitMethod.FORCE_LIMIT:
|
||||
parsed_query = sql_parse.ParsedQuery(sql, engine=cls.engine)
|
||||
sql = parsed_query.set_or_update_query_limit(limit, force=force)
|
||||
|
||||
return sql
|
||||
|
||||
@classmethod
|
||||
def apply_top_to_sql(cls, sql: str, limit: int) -> str: # noqa: C901
|
||||
"""
|
||||
Alters the SQL statement to apply a TOP clause
|
||||
:param limit: Maximum number of rows to be returned by the query
|
||||
:param sql: SQL query
|
||||
:return: SQL query with top clause
|
||||
"""
|
||||
|
||||
cte = None
|
||||
sql_remainder = None
|
||||
sql = sql.strip(" \t\n;")
|
||||
query_limit: int | None = sql_parse.extract_top_from_query(
|
||||
sql, cls.top_keywords
|
||||
)
|
||||
if not limit:
|
||||
final_limit = query_limit
|
||||
elif int(query_limit or 0) < limit and query_limit is not None:
|
||||
final_limit = query_limit
|
||||
else:
|
||||
final_limit = limit
|
||||
if not cls.allows_cte_in_subquery:
|
||||
cte, sql_remainder = sql_parse.get_cte_remainder_query(sql)
|
||||
if cte:
|
||||
str_statement = str(sql_remainder)
|
||||
cte = cte + "\n"
|
||||
else:
|
||||
cte = ""
|
||||
str_statement = str(sql)
|
||||
str_statement = str_statement.replace("\n", " ").replace("\r", "")
|
||||
|
||||
tokens = str_statement.rstrip().split(" ")
|
||||
tokens = [token for token in tokens if token]
|
||||
if cls.top_not_in_sql(str_statement):
|
||||
selects = [
|
||||
i
|
||||
for i, word in enumerate(tokens)
|
||||
if word.upper() in cls.select_keywords
|
||||
]
|
||||
first_select = selects[0]
|
||||
if tokens[first_select + 1].upper() == "DISTINCT":
|
||||
first_select += 1
|
||||
|
||||
tokens.insert(first_select + 1, "TOP")
|
||||
tokens.insert(first_select + 2, str(final_limit))
|
||||
|
||||
next_is_limit_token = False
|
||||
new_tokens = []
|
||||
|
||||
for token in tokens:
|
||||
if token in cls.top_keywords:
|
||||
next_is_limit_token = True
|
||||
elif next_is_limit_token:
|
||||
if token.isdigit():
|
||||
token = str(final_limit)
|
||||
next_is_limit_token = False
|
||||
new_tokens.append(token)
|
||||
sql = " ".join(new_tokens)
|
||||
return cte + sql
|
||||
|
||||
@classmethod
|
||||
def top_not_in_sql(cls, sql: str) -> bool:
|
||||
for top_word in cls.top_keywords:
|
||||
if top_word.upper() in sql.upper():
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_limit_from_sql(cls, sql: str) -> int | None:
|
||||
"""
|
||||
@@ -1221,20 +1115,8 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
:param sql: SQL query
|
||||
:return: Value of limit clause in query
|
||||
"""
|
||||
parsed_query = sql_parse.ParsedQuery(sql, engine=cls.engine)
|
||||
return parsed_query.limit
|
||||
|
||||
@classmethod
|
||||
def set_or_update_query_limit(cls, sql: str, limit: int) -> str:
|
||||
"""
|
||||
Create a query based on original query but with new limit clause
|
||||
|
||||
:param sql: SQL query
|
||||
:param limit: New limit to insert/replace into query
|
||||
:return: Query with new limit
|
||||
"""
|
||||
parsed_query = sql_parse.ParsedQuery(sql, engine=cls.engine)
|
||||
return parsed_query.set_or_update_query_limit(limit)
|
||||
script = SQLScript(sql, engine=cls.engine)
|
||||
return script.statements[-1].get_limit_value()
|
||||
|
||||
@classmethod
|
||||
def get_cte_query(cls, sql: str) -> str | None:
|
||||
@@ -1246,18 +1128,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
|
||||
"""
|
||||
if not cls.allows_cte_in_subquery:
|
||||
stmt = sqlparse.parse(sql)[0]
|
||||
|
||||
# The first meaningful token for CTE will be with WITH
|
||||
idx, token = stmt.token_next(-1, skip_ws=True, skip_cm=True)
|
||||
if not (token and token.ttype == CTE):
|
||||
return None
|
||||
idx, token = stmt.token_next(idx)
|
||||
idx = stmt.token_index(token) + 1
|
||||
|
||||
# extract rest of the SQLs after CTE
|
||||
remainder = "".join(str(token) for token in stmt.tokens[idx:]).strip()
|
||||
return f"WITH {token.value},\n{cls.cte_alias} AS (\n{remainder}\n)"
|
||||
statement = SQLStatement(sql, engine=cls.engine)
|
||||
if statement.has_cte():
|
||||
return statement.as_cte(cls.cte_alias).format()
|
||||
|
||||
return None
|
||||
|
||||
@@ -1686,8 +1559,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
full_table_name = cls.quote_table(table, engine.dialect)
|
||||
qry = select(fields).select_from(text(full_table_name))
|
||||
|
||||
if limit and cls.allow_limit_clause:
|
||||
qry = qry.limit(limit)
|
||||
qry = qry.limit(limit)
|
||||
if latest_partition:
|
||||
partition_query = cls.where_latest_partition(
|
||||
database,
|
||||
@@ -2088,14 +1960,6 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
logger.error(ex, exc_info=True)
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def is_select_query(cls, parsed_query: ParsedQuery) -> bool:
|
||||
"""
|
||||
Determine if the statement should be considered as SELECT statement.
|
||||
Some query dialects do not contain "SELECT" word in queries (eg. Kusto)
|
||||
"""
|
||||
return parsed_query.is_select()
|
||||
|
||||
@classmethod
|
||||
def get_column_spec( # pylint: disable=unused-argument
|
||||
cls,
|
||||
@@ -2201,10 +2065,6 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def parse_sql(cls, sql: str) -> list[str]:
|
||||
return [str(s).strip(" ;") for s in sqlparse.parse(sql)]
|
||||
|
||||
@classmethod
|
||||
def get_impersonation_key(cls, user: User | None) -> Any:
|
||||
"""
|
||||
|
||||
@@ -20,9 +20,9 @@ from typing import Optional, Union
|
||||
from sqlalchemy.engine.reflection import Inspector
|
||||
|
||||
from superset.constants import TimeGrain
|
||||
from superset.db_engine_specs.base import BaseEngineSpec, LimitMethod
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
from superset.models.core import Database
|
||||
from superset.sql_parse import Table
|
||||
from superset.sql.parse import LimitMethod, Table
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ from typing import Any, Optional
|
||||
from sqlalchemy import types
|
||||
|
||||
from superset.constants import TimeGrain
|
||||
from superset.db_engine_specs.base import BaseEngineSpec, LimitMethod
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
from superset.sql.parse import LimitMethod
|
||||
|
||||
|
||||
class FirebirdEngineSpec(BaseEngineSpec):
|
||||
|
||||
@@ -20,8 +20,8 @@ from typing import Any, Optional
|
||||
from sqlalchemy import types
|
||||
|
||||
from superset.constants import TimeGrain
|
||||
from superset.db_engine_specs.base import LimitMethod
|
||||
from superset.db_engine_specs.postgres import PostgresBaseEngineSpec
|
||||
from superset.sql.parse import LimitMethod
|
||||
|
||||
|
||||
class HanaEngineSpec(PostgresBaseEngineSpec):
|
||||
|
||||
@@ -22,13 +22,13 @@ from sqlalchemy import types
|
||||
from sqlalchemy.dialects.mssql.base import SMALLDATETIME
|
||||
|
||||
from superset.constants import TimeGrain
|
||||
from superset.db_engine_specs.base import BaseEngineSpec, LimitMethod
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
from superset.db_engine_specs.exceptions import (
|
||||
SupersetDBAPIDatabaseError,
|
||||
SupersetDBAPIOperationalError,
|
||||
SupersetDBAPIProgrammingError,
|
||||
)
|
||||
from superset.sql_parse import ParsedQuery
|
||||
from superset.sql.parse import LimitMethod
|
||||
from superset.utils.core import GenericDataType
|
||||
|
||||
|
||||
@@ -106,7 +106,6 @@ class KustoSqlEngineSpec(BaseEngineSpec): # pylint: disable=abstract-method
|
||||
|
||||
|
||||
class KustoKqlEngineSpec(BaseEngineSpec): # pylint: disable=abstract-method
|
||||
limit_method = LimitMethod.WRAP_SQL
|
||||
engine = "kustokql"
|
||||
engine_name = "KustoKQL"
|
||||
time_groupby_inline = True
|
||||
@@ -154,15 +153,3 @@ class KustoKqlEngineSpec(BaseEngineSpec): # pylint: disable=abstract-method
|
||||
return f"""datetime({dttm.isoformat(timespec="microseconds")})"""
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def is_select_query(cls, parsed_query: ParsedQuery) -> bool:
|
||||
return not parsed_query.sql.startswith(".")
|
||||
|
||||
@classmethod
|
||||
def parse_sql(cls, sql: str) -> list[str]:
|
||||
"""
|
||||
Kusto supports a single query statement, but it could include sub queries
|
||||
and variables declared via let keyword.
|
||||
"""
|
||||
return [sql]
|
||||
|
||||
@@ -119,7 +119,7 @@ def diagnose(spec: type[BaseEngineSpec]) -> dict[str, Any]:
|
||||
output.update(
|
||||
{
|
||||
"module": spec.__module__,
|
||||
"limit_method": spec.limit_method.upper(),
|
||||
"limit_method": spec.limit_method.value,
|
||||
"joins": spec.allows_joins,
|
||||
"subqueries": spec.allows_subqueries,
|
||||
"alias_in_select": spec.allows_alias_in_select,
|
||||
@@ -129,7 +129,6 @@ def diagnose(spec: type[BaseEngineSpec]) -> dict[str, Any]:
|
||||
"order_by_not_in_select": spec.allows_hidden_orderby_agg,
|
||||
"expressions_in_orderby": spec.allows_hidden_cc_in_orderby,
|
||||
"cte_in_subquery": spec.allows_cte_in_subquery,
|
||||
"limit_clause": spec.allow_limit_clause,
|
||||
"max_column_name": spec.max_column_name_length,
|
||||
"sql_comments": spec.allows_sql_comments,
|
||||
"escaped_colons": spec.allows_escaped_colons,
|
||||
@@ -223,7 +222,7 @@ def generate_table() -> list[list[Any]]:
|
||||
|
||||
rows = [] # pylint: disable=redefined-outer-name
|
||||
rows.append(["Feature"] + list(info)) # header row
|
||||
rows.append(["Module"] + list(db_info["module"] for db_info in info.values())) # noqa: C400
|
||||
rows.append(["Module"] + [db_info["module"] for db_info in info.values()])
|
||||
|
||||
# descriptive
|
||||
keys = [
|
||||
@@ -244,14 +243,14 @@ def generate_table() -> list[list[Any]]:
|
||||
]
|
||||
for key in keys:
|
||||
rows.append(
|
||||
[DATABASE_DETAILS[key]] + list(db_info[key] for db_info in info.values()) # noqa: C400
|
||||
[DATABASE_DETAILS[key]] + [db_info[key] for db_info in info.values()]
|
||||
)
|
||||
|
||||
# basic
|
||||
for time_grain in TimeGrain:
|
||||
rows.append(
|
||||
[f"Has time grain {time_grain.name}"]
|
||||
+ list(db_info["time_grains"][time_grain.name] for db_info in info.values()) # noqa: C400
|
||||
+ [db_info["time_grains"][time_grain.name] for db_info in info.values()]
|
||||
)
|
||||
keys = [
|
||||
"masked_encrypted_extra",
|
||||
@@ -259,9 +258,7 @@ def generate_table() -> list[list[Any]]:
|
||||
"function_names",
|
||||
]
|
||||
for key in keys:
|
||||
rows.append(
|
||||
[BASIC_FEATURES[key]] + list(db_info[key] for db_info in info.values()) # noqa: C400
|
||||
)
|
||||
rows.append([BASIC_FEATURES[key]] + [db_info[key] for db_info in info.values()])
|
||||
|
||||
# nice to have
|
||||
keys = [
|
||||
@@ -280,8 +277,7 @@ def generate_table() -> list[list[Any]]:
|
||||
]
|
||||
for key in keys:
|
||||
rows.append(
|
||||
[NICE_TO_HAVE_FEATURES[key]]
|
||||
+ list(db_info[key] for db_info in info.values()) # noqa: C400
|
||||
[NICE_TO_HAVE_FEATURES[key]] + [db_info[key] for db_info in info.values()]
|
||||
)
|
||||
|
||||
# advanced
|
||||
@@ -292,10 +288,10 @@ def generate_table() -> list[list[Any]]:
|
||||
]
|
||||
for key in keys:
|
||||
rows.append(
|
||||
[ADVANCED_FEATURES[key]] + list(db_info[key] for db_info in info.values()) # noqa: C400
|
||||
[ADVANCED_FEATURES[key]] + [db_info[key] for db_info in info.values()]
|
||||
)
|
||||
|
||||
rows.append(["Score"] + list(db_info["score"] for db_info in info.values())) # noqa: C400
|
||||
rows.append(["Score"] + [db_info["score"] for db_info in info.values()])
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from sqlalchemy import types
|
||||
from sqlalchemy.dialects.mssql.base import SMALLDATETIME
|
||||
|
||||
from superset.constants import TimeGrain
|
||||
from superset.db_engine_specs.base import BaseEngineSpec, LimitMethod
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
from superset.errors import SupersetErrorType
|
||||
from superset.models.sql_types.mssql_sql_types import GUID
|
||||
from superset.utils.core import GenericDataType
|
||||
@@ -52,10 +52,8 @@ CONNECTION_HOST_DOWN_REGEX = re.compile(
|
||||
class MssqlEngineSpec(BaseEngineSpec):
|
||||
engine = "mssql"
|
||||
engine_name = "Microsoft SQL Server"
|
||||
limit_method = LimitMethod.WRAP_SQL
|
||||
max_column_name_length = 128
|
||||
allows_cte_in_subquery = False
|
||||
allow_limit_clause = False
|
||||
supports_multivalues_insert = True
|
||||
|
||||
_time_grain_expressions = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user