Compare commits
138 Commits
chart-samp
...
misc-chart
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f866f25546 | ||
|
|
3392576923 | ||
|
|
e95b37450e | ||
|
|
ff73f2c070 | ||
|
|
612fd0ac5d | ||
|
|
83dbdfa355 | ||
|
|
fc3872fe9e | ||
|
|
0946df50a1 | ||
|
|
d9c3a2cf4c | ||
|
|
f84d7c664e | ||
|
|
45fd4117a9 | ||
|
|
4ee500e47b | ||
|
|
79acc3a712 | ||
|
|
3ff5dbfe81 | ||
|
|
cfb04a6e79 | ||
|
|
62d0cfb6d3 | ||
|
|
49f4e84b48 | ||
|
|
921f75d544 | ||
|
|
bcf0361a91 | ||
|
|
5776aff50a | ||
|
|
14c96761e5 | ||
|
|
d41f0febae | ||
|
|
c43effa4a3 | ||
|
|
b885657ad0 | ||
|
|
a191f72698 | ||
|
|
498400e974 | ||
|
|
b9a8321802 | ||
|
|
902ebcdf72 | ||
|
|
f76471f30f | ||
|
|
b671356748 | ||
|
|
206fe7ab12 | ||
|
|
1049826bc1 | ||
|
|
6317113ee8 | ||
|
|
fdb9c152f0 | ||
|
|
2317d9cf91 | ||
|
|
df8bbb548a | ||
|
|
6b02a423ef | ||
|
|
ff273478ff | ||
|
|
f1cc9367de | ||
|
|
f2848d951c | ||
|
|
02e4b4be7d | ||
|
|
cb319c9b81 | ||
|
|
cc8edd2231 | ||
|
|
c5f6d33d21 | ||
|
|
2e72080a16 | ||
|
|
6654318491 | ||
|
|
ab553c37ab | ||
|
|
873da606dd | ||
|
|
1dd3103c24 | ||
|
|
2926d6f6a9 | ||
|
|
ecb4aa93f6 | ||
|
|
4e1913d436 | ||
|
|
b6ae8fb25d | ||
|
|
10f2461b5f | ||
|
|
67924ad35e | ||
|
|
4e715f66dc | ||
|
|
6dfae09cc5 | ||
|
|
a4f74b09bb | ||
|
|
9f39fe3e10 | ||
|
|
c6f8706aa1 | ||
|
|
07a6406d97 | ||
|
|
a585a0901e | ||
|
|
fff5351a33 | ||
|
|
b4f8d87a49 | ||
|
|
3256ee4fbb | ||
|
|
e5b3ba9efc | ||
|
|
ae93dc2041 | ||
|
|
792d0fd2b6 | ||
|
|
8dcb83fd3c | ||
|
|
8f07ab4840 | ||
|
|
89d64b89b8 | ||
|
|
4ce589dcda | ||
|
|
a31210df08 | ||
|
|
00e95dcb87 | ||
|
|
c9960002f1 | ||
|
|
dbac244857 | ||
|
|
4483cb09f0 | ||
|
|
28d977bc0c | ||
|
|
b248c4d7c0 | ||
|
|
3e9fdc4cdf | ||
|
|
387b742303 | ||
|
|
533f90e4c6 | ||
|
|
b887c30573 | ||
|
|
2cd369d02a | ||
|
|
4b3bbe19f6 | ||
|
|
28eae5956e | ||
|
|
239e3a4323 | ||
|
|
573054346f | ||
|
|
a42c7e3be0 | ||
|
|
935b4acdbd | ||
|
|
bba2d5f812 | ||
|
|
7d45b8ae7e | ||
|
|
5942d570c3 | ||
|
|
45adb8429d | ||
|
|
3a03f0a820 | ||
|
|
105f6d0bc7 | ||
|
|
c8d6faf7e8 | ||
|
|
ae15dd883d | ||
|
|
2afa775db5 | ||
|
|
e17d49bec0 | ||
|
|
68324050cd | ||
|
|
2a55ad77f4 | ||
|
|
cd535f89e2 | ||
|
|
8b2c07f5f3 | ||
|
|
a9045296d9 | ||
|
|
32cbc4a7a6 | ||
|
|
4f47eda5de | ||
|
|
ba2b0d6ea3 | ||
|
|
df176cfa57 | ||
|
|
66e911eeea | ||
|
|
759457264d | ||
|
|
5722b96280 | ||
|
|
deb6cacea7 | ||
|
|
0f8e2a274d | ||
|
|
7e0de356d4 | ||
|
|
4e93b58a61 | ||
|
|
45b232cba0 | ||
|
|
bcc58d27f8 | ||
|
|
b5df673d21 | ||
|
|
ef459c21e8 | ||
|
|
3ed8889a01 | ||
|
|
17bd286ae9 | ||
|
|
32937f24d2 | ||
|
|
e075133a83 | ||
|
|
8d034ef65c | ||
|
|
641713f623 | ||
|
|
40d9e7d048 | ||
|
|
19159d58c8 | ||
|
|
894c0191d6 | ||
|
|
49b4243129 | ||
|
|
ac41f61d4a | ||
|
|
f50b7b7154 | ||
|
|
be5744c431 | ||
|
|
8c5d465a65 | ||
|
|
c5935b6904 | ||
|
|
10ff470702 | ||
|
|
1c0f259e38 | ||
|
|
b8f3b768d4 |
2
.github/CODEOWNERS
vendored
@@ -34,7 +34,7 @@
|
||||
|
||||
**/*.geojson @villebro @rusackas
|
||||
**/*.ipynb @villebro @rusackas
|
||||
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
|
||||
/superset-frontend/plugins/plugin-chart-country-map/ @villebro @rusackas
|
||||
|
||||
# Notify translation maintainers of changes to translations
|
||||
|
||||
|
||||
4
.github/dependabot.yml
vendored
@@ -70,6 +70,10 @@ updates:
|
||||
- "@storybook/*"
|
||||
- "storybook"
|
||||
- "eslint-plugin-storybook"
|
||||
ag-grid:
|
||||
patterns:
|
||||
- "ag-grid-react"
|
||||
- "ag-grid-community"
|
||||
open-pull-requests-limit: 30
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/labeler.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
|
||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||
with:
|
||||
sync-labels: true
|
||||
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
|
||||
- name: Install helm-docs
|
||||
run: go install github.com/norwoodj/helm-docs/cmd/helm-docs@v1.14.2
|
||||
|
||||
1
.gitignore
vendored
@@ -137,6 +137,7 @@ PROJECT.md
|
||||
.aider*
|
||||
.claude_rc*
|
||||
.claude/settings.local.json
|
||||
.claude/worktrees/
|
||||
.env.local
|
||||
oxc-custom-build/
|
||||
*.code-workspace
|
||||
|
||||
@@ -168,6 +168,11 @@ categories:
|
||||
url: https://www.now.vn/
|
||||
contributors: ["@davidkohcw"]
|
||||
|
||||
- name: Pattern
|
||||
url: https://www.pattern.com
|
||||
contributors: ["@alephys26", "@hladush", "@sanketjadhavSF"]
|
||||
logo: pattern_logo.png
|
||||
|
||||
- name: Qunar
|
||||
url: https://www.qunar.com/
|
||||
contributors: ["@flametest"]
|
||||
|
||||
106
UPDATING.md
@@ -193,6 +193,10 @@ are the intended model going forward; deprecating and removing implicit viewersh
|
||||
in a later major version.
|
||||
|
||||
- [41044](https://github.com/apache/superset/issues/41044): Removes the deprecated `AVOID_COLORS_COLLISION` feature flag (it defaulted to `True`). Color-collision avoidance is now permanently enabled; any config override setting it to `False` is ignored.
|
||||
- [41714](https://github.com/apache/superset/pull/41714): **Breaking — the legacy `explore_json` chart-data pipeline is removed** at its long-declared `5.0.0` EOL. The `/superset/explore_json/` and `/superset/explore_json/data/<cache_key>` endpoints, `superset/viz.py`, the `Slice.viz` property, the `get_viz` factory, the `load_explore_json_into_cache` celery task and the `viz=` overload of `security_manager.raise_for_access` are gone. Anything importing `superset.viz` must migrate to the QueryContext / `pandas_postprocessing` pipeline behind `/api/v1/chart/data`. All 15 remaining legacy charts were migrated first: most keep their `viz_type` and renderer (no action needed for saved charts), while saved nvd3 Bubble charts are auto-migrated to the ECharts Bubble Chart (`bubble_v2`) and saved "Time-series Percent Change" (`compare`) charts to the ECharts Line Chart — the nvd3 renderer's interactive percent re-basing is not preserved. The deck.gl Multiple Layers chart now fetches its layers entirely client-side; its initial autozoom falls back to the saved viewport, and dashboard filter badges no longer aggregate child-layer filter metadata.
|
||||
|
||||
- [41714](https://github.com/apache/superset/pull/41714): Charts migrated in place keep a `NULL` saved query context until they are next opened in Explore (which regenerates it automatically) or re-saved. Until then, cache warm-up and annotation layers referencing such a chart report an actionable error rather than warming/rendering; opening the chart once resolves it.
|
||||
- [41714](https://github.com/apache/superset/pull/41714): **Breaking for third-party viz plugins** — the `useLegacyApi` field of `ChartMetadata` in `@superset-ui/core` is removed. Plugins that set it must provide a `buildQuery` and consume `/api/v1/chart/data`. The migrated first-party packages also drop their `legacy-` prefix: `@superset-ui/legacy-plugin-chart-{calendar,chord,country-map,horizon,paired-t-test,parallel-coordinates,partition,rose,world-map}` → `@superset-ui/plugin-chart-*`, and `@superset-ui/legacy-preset-chart-nvd3` → `@superset-ui/preset-chart-nvd3`. The `can_explore_json` permission is no longer created or granted; custom roles referencing it should switch to the `can_read` permissions on `Chart`.
|
||||
|
||||
- [41813](https://github.com/apache/superset/pull/41813): `redis` (the Python client, `redis-py`) is bumped from 5.3.1 to 8.0.1. redis-py 8 changes several connection defaults; Superset's own Redis-backed features (`GLOBAL_ASYNC_QUERIES_CACHE_BACKEND`, `DISTRIBUTED_COORDINATION_CONFIG`, and the MCP Redis store) explicitly pin the pre-upgrade behavior so this bump is a no-op for them: the wire protocol stays RESP2 (not the new RESP3 default, which requires Redis/Sentinel 6+ to speak `HELLO`) and there is still no socket timeout by default (redis-py 8 defaults to 5s, which could otherwise newly time out large cached payloads or slow networks). The no-timeout default can now be overridden via two new config keys, `CACHE_REDIS_SOCKET_TIMEOUT` / `CACHE_REDIS_SOCKET_CONNECT_TIMEOUT`, on any `CacheConfig` dict using `CACHE_TYPE: RedisCache` or `RedisSentinelCache`. Separately, redis-py 6+ changed the default for `ssl_check_hostname` from `False` to `True` for SSL connections using `ssl_cert_reqs="required"` (the default) — this is a security improvement, so it has **not** been reverted; deployments with `CACHE_REDIS_SSL=True` whose certificates lack a hostname matching the connection address should set `CACHE_REDIS_SSL_CERT_REQS="none"` (disables cert verification entirely, matching hostname-check bypass) or replace the certificate. General-purpose cache/results backends configured via `CACHE_CONFIG` / `DATA_CACHE_CONFIG` / `RESULTS_BACKEND` with `CACHE_TYPE: RedisCache` go through `flask-caching`'s own Redis backend (outside Superset's code) and are subject to the same new defaults; pass `socket_timeout` / `protocol` via `CACHE_OPTIONS` there if needed. Celery broker and result-backend connections (built by `kombu`, also outside Superset's code) keep their no-socket-timeout behavior (`kombu` passes `socket_timeout=None` explicitly) but do **not** pin the wire protocol, so they follow redis-py's RESP3 default — which requires a Redis server new enough to speak `HELLO` (Redis 6+). Deployments using a pre-6.0 Redis server (EOL) as a Celery broker should upgrade the server before taking this bump.
|
||||
|
||||
@@ -212,7 +216,7 @@ in a later major version.
|
||||
|
||||
- **`SqlaTable.sql_url` query-string format.** `SqlaTable.sql_url` now URL-encodes `table_name` and joins it as a query parameter rather than concatenating a second `?`. Previously, with `Database.sql_url` returning `/sqllab/?dbid=<id>`, the concatenation produced `/sqllab/?dbid=<id>?table_name=<raw>` — a malformed second `?` that broke the query parser. External code that parsed the legacy `<base>?table_name=<raw>` shape now sees properly percent-encoded values (e.g. `/` → `%2F`, ` ` → `+` or `%20`); decode with `urllib.parse.parse_qsl`.
|
||||
|
||||
- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` *and* who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host.
|
||||
- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` _and_ who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host.
|
||||
|
||||
- [41651](https://github.com/apache/superset/pull/41651): **New do-not-translate standard for translation catalogs.** Strings that must stay identical to the source — icon names (e.g. `bolt`), enum/option values (`step-after`), SQL keywords, API field names (`error_message`), code constants, and example placeholders — are now marked with a `#. do-not-translate` extracted comment. The list lives in the `superset/translations/do-not-translate.txt` registry; `scripts/translations/apply_do_not_translate.py` stamps the marker onto `messages.pot` during `babel_update.sh`, and `pybabel update` propagates it to every `.po`, so the status is consistent across all languages. The AI backfill (`backfill_po.py`) and translators leave these entries untranslated (source fallback). The legacy per-catalog convention (a `# Не переводить` translator comment in the `ru` catalog) is still honored for back-compat but is superseded by this standard; contributors adding new machine-read strings should add the msgid to the registry rather than annotating individual catalogs.
|
||||
|
||||
@@ -249,7 +253,7 @@ Theme tokens are unaffected — antd 6 removed none of the tokens Superset expos
|
||||
|
||||
### Guest-token RLS rules reject unknown fields
|
||||
|
||||
The `rls` rules passed to `POST /api/v1/security/guest_token/` are now validated strictly: a rule may only contain `dataset` and `clause`. Previously unknown fields were silently dropped, so a mistyped or legacy scope key (most commonly `datasource` instead of `dataset`) produced a rule with no `dataset`, which is treated as a *global* rule applied to every dataset the embedded resource can reach. Such a request now returns HTTP 400 identifying the offending field instead of issuing a token with an unintended global rule. Integrators that were sending extra fields in RLS rules must remove them; valid dataset-scoped (`{"dataset": 41, "clause": "..."}`) and global (`{"clause": "..."}`) rules are unaffected.
|
||||
The `rls` rules passed to `POST /api/v1/security/guest_token/` are now validated strictly: a rule may only contain `dataset` and `clause`. Previously unknown fields were silently dropped, so a mistyped or legacy scope key (most commonly `datasource` instead of `dataset`) produced a rule with no `dataset`, which is treated as a _global_ rule applied to every dataset the embedded resource can reach. Such a request now returns HTTP 400 identifying the offending field instead of issuing a token with an unintended global rule. Integrators that were sending extra fields in RLS rules must remove them; valid dataset-scoped (`{"dataset": 41, "clause": "..."}`) and global (`{"clause": "..."}`) rules are unaffected.
|
||||
|
||||
### MCP service requires `MCP_JWT_AUDIENCE` when JWT auth is enabled
|
||||
|
||||
@@ -288,6 +292,22 @@ The pivot table chart's `First` and `Last` aggregations now return the first and
|
||||
|
||||
The `error` and `response` parameters of the `retryDelay` and `retryOn` callbacks in `FetchRetryOptions` (exported from `@superset-ui/core`) are now typed `Error | null` and `Response | null` to match the actual call-site signature provided by `fetch-retry`. Because these parameter types are contravariant, consumers who typed their callbacks with the non-nullable `(attempt: number, error: Error, response: Response) => number` will get a TypeScript compile error. Widen your callback signatures to accept `Error | null` / `Response | null`.
|
||||
|
||||
### Pivot Table totals are now computed by the database (per-metric "Aggregation function" control removed)
|
||||
|
||||
Pivot Table subtotals and grand totals are now computed by the database at each
|
||||
rollup level instead of re-aggregating the already-aggregated cell values on the
|
||||
client. This fixes long-standing incorrect totals for non-additive metrics
|
||||
(ratios such as `SUM(a)/SUM(b)`, `COUNT_DISTINCT`, `AVG`, percentiles, etc.),
|
||||
which previously summed the displayed cell values.
|
||||
|
||||
As a result the per-table **"Aggregation function"** control (which let you pick
|
||||
how totals were aggregated client-side, e.g. Sum/Average/Count) has been
|
||||
removed: totals now always reflect the metric's own definition evaluated at the
|
||||
total's granularity. For additive metrics (`SUM`/`COUNT`/`MIN`/`MAX`) the result
|
||||
is unchanged. Saved charts that set `aggregateFunction` will ignore it; no
|
||||
migration is required. If you previously relied on a plain sum-of-cells total
|
||||
for a non-additive metric, that specific behavior is no longer available.
|
||||
|
||||
### `thumbnail_url` removed from dashboard list API response
|
||||
|
||||
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
|
||||
@@ -317,6 +337,7 @@ ALTER TABLE tagged_object DROP CONSTRAINT <constraint_name>;
|
||||
-- MySQL: find names via `SHOW CREATE TABLE tagged_object;`
|
||||
ALTER TABLE tagged_object DROP FOREIGN KEY <constraint_name>;
|
||||
```
|
||||
|
||||
### Entity version-history infrastructure (gated off by default)
|
||||
|
||||
Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}/<uuid>/versions/` endpoints. This ships **inert**: a new config flag `ENABLE_VERSIONING_CAPTURE` defaults to `False`, so no save writes any version rows and the endpoints return empty. It is an operational kill-switch (a release toggle that becomes a permanent ops switch), not a feature flag — set it to `True` to enable capture once validated. The migration is additive; existing entity `PUT` responses gain `old_version_uuid` / `new_version_uuid` body fields and an `ETag` header (both null/absent when capture is off).
|
||||
@@ -333,12 +354,12 @@ These are behavior changes that take effect on upgrade regardless of `ENABLE_VER
|
||||
|
||||
A read-only companion to the version-history endpoints: each entity type gains a `GET /api/v1/{chart,dashboard,dataset}/<uuid>/activity/` endpoint returning a chronological, access-filtered stream of edits — the entity's own edits plus, for charts and dashboards, transitive edits to related entities during their association windows. Datasets have no related layer in V2, so `include=related` returns an empty stream for a dataset and `include=all` reduces to the dataset's own edits.
|
||||
|
||||
| Param | Type | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `since` / `until` | ISO 8601 | — | Bound `issued_at` |
|
||||
| `include` | `self` \| `related` \| `all` | `all` | Own edits, related edits, or both |
|
||||
| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) |
|
||||
| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) |
|
||||
| Param | Type | Default | Purpose |
|
||||
| -------------------- | ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `since` / `until` | ISO 8601 | — | Bound `issued_at` |
|
||||
| `include` | `self` \| `related` \| `all` | `all` | Own edits, related edits, or both |
|
||||
| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) |
|
||||
| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) |
|
||||
|
||||
Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream is empty unless version capture is on (`ENABLE_VERSIONING_CAPTURE`).
|
||||
|
||||
@@ -389,6 +410,7 @@ Operators can tune or disable the policy via config:
|
||||
### Data uploads bounded by UPLOAD_MAX_FILE_SIZE_BYTES
|
||||
|
||||
Single data-file uploads (CSV, Excel, columnar) are now bounded by the `UPLOAD_MAX_FILE_SIZE_BYTES` config option, which defaults to `100 * 1024 * 1024` (100 MB). Files larger than this are rejected with a `413` before their contents are buffered into memory. Set `UPLOAD_MAX_FILE_SIZE_BYTES = None` to disable the check and restore unbounded uploads.
|
||||
|
||||
### Currency symbol position follows the locale when unset
|
||||
|
||||
When a chart's currency control leaves the **Prefix or suffix** field empty, the currency symbol position is now derived from the deployment locale's own convention via `Intl.NumberFormat` instead of always defaulting to a suffix. For example, under the default `en-US` locale `USD`, `GBP`, and `EUR` render as a prefix (`$ 1,000`), while eurozone locales such as `fr-FR` render `EUR` as a suffix (`1 000 €`). An explicit Prefix/Suffix selection is always honored and is unaffected.
|
||||
@@ -514,7 +536,7 @@ SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes"
|
||||
```bash
|
||||
superset re-encrypt-secrets --engine aes-gcm
|
||||
```
|
||||
A live instance keeps writing *new* secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped).
|
||||
A live instance keeps writing _new_ secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped).
|
||||
|
||||
Schedule the cutover in a quiet window. Runtime reads use only the single configured engine, so in a multi-worker deployment there is an unavoidable brief decrypt-outage between the migration commit and the last worker restarting with the new config — each migrator run is transactional, but the fleet-wide cutover is not zero-downtime.
|
||||
|
||||
@@ -542,11 +564,11 @@ With the flag enabled: `DELETE /api/v1/dataset/<id>` no longer hard-deletes the
|
||||
|
||||
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_tables_deleted_at`) to the `tables` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `tables` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking). Production deployments with many thousands of datasets should run this migration during a maintenance window.
|
||||
|
||||
**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
|
||||
**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) _before_ downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
|
||||
|
||||
**SQL Lab / dataset-creation flows:** creating a dataset over a table whose dataset sits in the trash is refused. The SQL Lab "save as dataset" flow (`get_or_create_dataset`) and file uploads return a **422 naming the hidden twin and the restore endpoint**; the plain create, update, and duplicate paths currently fail with the generic "already exists" 422. In all cases the remediation is the same: restore the hidden dataset (or use a different table name). Perm-string maintenance also covers hidden rows: renaming a database rewrites `perm`/`schema_perm`/`catalog_perm` on soft-deleted datasets and their charts, so a later restore does not resurrect stale permission strings.
|
||||
|
||||
**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
**Uniqueness-validation changes that apply regardless of the feature flag:** two dataset uniqueness checks were tightened alongside this work and are active even with `SOFT_DELETE` off. (1) Create/update uniqueness treats a dataset whose `catalog` is `NULL` as belonging to the database's default catalog, so a legacy twin pair (`catalog=NULL` vs. `catalog=<default>`, same database/schema/name) that older versions allowed now fails validation with "already exists" when either row is edited — resolve by renaming or removing one of the twins. (2) Duplicating a dataset now checks name collisions scoped to the target (database, catalog, schema) instead of globally by name alone: duplicates into other databases that were previously blocked are now allowed.
|
||||
|
||||
@@ -566,9 +588,9 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
|
||||
|
||||
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_slices_deleted_at`) to the `slices` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `slices` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking).
|
||||
|
||||
**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
|
||||
**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) _before_ downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
|
||||
|
||||
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path.
|
||||
|
||||
@@ -592,7 +614,7 @@ The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WH
|
||||
|
||||
**Slug semantics:** on PostgreSQL and MySQL 8.0.13+, the slug of a soft-deleted dashboard is **free for reuse**. A new active dashboard can claim it immediately. Restoring a soft-deleted dashboard whose slug has since been claimed returns **422 with a clean error** (`DashboardSlugConflictError`) — rename one of the dashboards and retry; the restore is not silently rejected by a database-level constraint violation.
|
||||
|
||||
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
### Engine spec capability flag: `supports_offset`
|
||||
|
||||
@@ -604,10 +626,10 @@ A new `BaseEngineSpec.supports_offset` attribute (default `True`) indicates whet
|
||||
|
||||
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
|
||||
|
||||
| Permission | Controls |
|
||||
|---|---|
|
||||
| `can_export_data` | CSV, Excel, JSON exports |
|
||||
| `can_export_image` | Screenshot/PDF exports |
|
||||
| Permission | Controls |
|
||||
| -------------------- | ---------------------------- |
|
||||
| `can_export_data` | CSV, Excel, JSON exports |
|
||||
| `can_export_image` | Screenshot/PDF exports |
|
||||
| `can_copy_clipboard` | Copy-to-clipboard operations |
|
||||
|
||||
When the feature flag is enabled, these permissions are enforced on both the frontend (disabled buttons with tooltips) and backend (403 responses from API endpoints). When disabled, legacy `can_csv` behavior is preserved.
|
||||
@@ -636,19 +658,26 @@ Added a new combined datasource list endpoint at `GET /api/v1/datasource/` to se
|
||||
|
||||
The minimum required version of `clickhouse-connect` has been raised to `>=0.13.0`. If you are using the ClickHouse connector, please upgrade your `clickhouse-connect` package. The `_mutate_label` workaround that appended hash suffixes to column aliases has also been removed, as it is no longer needed with modern versions of the driver.
|
||||
|
||||
### Kenya Country Map: Updated Administrative Divisions
|
||||
|
||||
The Kenya country map has been updated to reflect the 47 counties established under Kenya's 2010 constitution, replacing the outdated 8-province boundaries from the Natural Earth dataset. County keys now use ISO 3166-2:KE codes (`KE-01` through `KE-47`), replacing the former province codes (`KE-110`, `KE-200`, ..., `KE-800`). Dashboards that join on the old province codes will need to re-key their datasets to use the new county codes.
|
||||
|
||||
### MCP Tool Observability
|
||||
|
||||
MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging:
|
||||
|
||||
**Two-layer instrumentation:**
|
||||
|
||||
1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table)
|
||||
2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution
|
||||
|
||||
**Action naming convention:**
|
||||
|
||||
- Tool-level logs: `mcp_tool_call` (via middleware)
|
||||
- Sub-operation logs: `mcp.{tool_name}.{operation}` (e.g., `mcp.generate_chart.validation`, `mcp.execute_sql.query_execution`)
|
||||
|
||||
**Querying MCP logs:**
|
||||
|
||||
```sql
|
||||
-- Top slowest MCP operations
|
||||
SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms
|
||||
@@ -683,6 +712,7 @@ A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-b
|
||||
The distributed coordination is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments.
|
||||
|
||||
Example configuration in `superset_config.py`:
|
||||
|
||||
```python
|
||||
DISTRIBUTED_COORDINATION_CONFIG = {
|
||||
"CACHE_TYPE": "RedisCache",
|
||||
@@ -697,9 +727,11 @@ See `superset/config.py` for complete configuration options.
|
||||
### WebSocket config for GAQ with Docker
|
||||
|
||||
[35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to:
|
||||
|
||||
- Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward)
|
||||
- Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example:
|
||||
``` yaml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
superset-websocket:
|
||||
volumes:
|
||||
@@ -712,7 +744,9 @@ services:
|
||||
### Example Data Loading Improvements
|
||||
|
||||
#### New Directory Structure
|
||||
|
||||
Examples are now organized by name with data and configs co-located:
|
||||
|
||||
```
|
||||
superset/examples/
|
||||
├── _shared/ # Shared database & metadata configs
|
||||
@@ -725,12 +759,14 @@ superset/examples/
|
||||
```
|
||||
|
||||
#### Simplified Parquet-based Loading
|
||||
|
||||
- Auto-discovery: create `superset/examples/my_dataset/data.parquet` to add a new example
|
||||
- Parquet is an Apache project format: compressed (~27% smaller), self-describing schema
|
||||
- YAML configs define datasets, charts, and dashboards declaratively
|
||||
- Removed Python-based data generation from individual example files
|
||||
|
||||
#### Test Data Reorganization
|
||||
|
||||
- Moved `big_data.py` to `superset/cli/test_loaders.py` - better reflects its purpose as a test utility
|
||||
- Fixed inverted logic for `--load-test-data` flag (now correctly includes .test.yaml files when flag is set)
|
||||
- Clarified CLI flags:
|
||||
@@ -740,6 +776,7 @@ superset/examples/
|
||||
- `--load-big-data` / `-b`: Generate synthetic stress-test data
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fixed numpy array serialization for PostgreSQL (converts complex types to JSON strings)
|
||||
- Fixed KeyError for `allow_csv_upload` field in database configs (now optional with default)
|
||||
- Fixed test data loading logic that was incorrectly filtering files
|
||||
@@ -749,6 +786,7 @@ superset/examples/
|
||||
The MCP (Model Context Protocol) service enables AI assistants and automation tools to interact programmatically with Superset.
|
||||
|
||||
#### New Features
|
||||
|
||||
- MCP service infrastructure with FastMCP framework
|
||||
- Tools for dashboards, charts, datasets, SQL Lab, and instance metadata
|
||||
- Optional dependency: install with `pip install apache-superset[fastmcp]`
|
||||
@@ -758,6 +796,7 @@ The MCP (Model Context Protocol) service enables AI assistants and automation to
|
||||
#### New Configuration Options
|
||||
|
||||
**Development** (single-user, local testing):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_DEV_USERNAME = "admin" # User for MCP authentication
|
||||
@@ -766,6 +805,7 @@ MCP_SERVICE_PORT = 5008
|
||||
```
|
||||
|
||||
**Production** (JWT-based, multi-user):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
@@ -811,12 +851,14 @@ superset mcp run --port 5008 --use-factory-config
|
||||
The MCP service runs as a **separate process** from the Superset web server.
|
||||
|
||||
**Important**:
|
||||
|
||||
- Requires same Python environment and configuration as Superset
|
||||
- Shares database connections with main Superset app
|
||||
- Can be scaled independently from web server
|
||||
- Requires `fastmcp` package (optional dependency)
|
||||
|
||||
**Installation**:
|
||||
|
||||
```bash
|
||||
# Install with MCP support
|
||||
pip install apache-superset[fastmcp]
|
||||
@@ -830,6 +872,7 @@ Use systemd, supervisord, or Kubernetes to manage the MCP service process.
|
||||
See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
|
||||
**Security**:
|
||||
|
||||
- Development: Uses `MCP_DEV_USERNAME` for single-user access
|
||||
- Production: **MUST** configure JWT authentication
|
||||
- See `superset/mcp_service/SECURITY.md` for details
|
||||
@@ -849,8 +892,10 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
- [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [37370](https://github.com/apache/superset/pull/37370): The `APP_NAME` configuration variable no longer controls the browser window/tab title or other frontend branding. Application names should now be configured using the theme system with the `brandAppName` token. The `APP_NAME` config is still used for backend contexts (MCP service, logs, etc.) and serves as a fallback if `brandAppName` is not set.
|
||||
- **Migration:**
|
||||
|
||||
```python
|
||||
# Before (Superset 5.x)
|
||||
APP_NAME = "My Custom App"
|
||||
@@ -894,16 +939,16 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
|
||||
Eight M:N association tables move from a synthetic `id INTEGER PRIMARY KEY` to a composite `PRIMARY KEY (fk1, fk2)` on their two foreign-key columns. The surrogate `id` is dropped, and the redundant `UNIQUE (fk1, fk2)` on the two tables that carried one is removed (now subsumed by the PK).
|
||||
|
||||
| Table | Composite PK |
|
||||
|---|---|
|
||||
| `dashboard_roles` | `(dashboard_id, role_id)` |
|
||||
| `dashboard_slices` | `(dashboard_id, slice_id)` |
|
||||
| `dashboard_user` | `(user_id, dashboard_id)` |
|
||||
| Table | Composite PK |
|
||||
| ---------------------- | ------------------------------- |
|
||||
| `dashboard_roles` | `(dashboard_id, role_id)` |
|
||||
| `dashboard_slices` | `(dashboard_id, slice_id)` |
|
||||
| `dashboard_user` | `(user_id, dashboard_id)` |
|
||||
| `report_schedule_user` | `(user_id, report_schedule_id)` |
|
||||
| `rls_filter_roles` | `(role_id, rls_filter_id)` |
|
||||
| `rls_filter_tables` | `(table_id, rls_filter_id)` |
|
||||
| `slice_user` | `(user_id, slice_id)` |
|
||||
| `sqlatable_user` | `(user_id, table_id)` |
|
||||
| `rls_filter_roles` | `(role_id, rls_filter_id)` |
|
||||
| `rls_filter_tables` | `(table_id, rls_filter_id)` |
|
||||
| `slice_user` | `(user_id, slice_id)` |
|
||||
| `sqlatable_user` | `(user_id, table_id)` |
|
||||
|
||||
**Before upgrading:**
|
||||
|
||||
@@ -914,6 +959,7 @@ Eight M:N association tables move from a synthetic `id INTEGER PRIMARY KEY` to a
|
||||
For large `dashboard_slices` / `report_schedule_user` tables, see the operator runbook in [#39859](https://github.com/apache/superset/pull/39859) — pre-flight inventory queries, per-dialect lock-window sizing, and the duplicate / NULL-FK roll-up — to plan the maintenance window.
|
||||
|
||||
## 6.0.0
|
||||
|
||||
- [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading.
|
||||
- [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact.
|
||||
- [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files).
|
||||
@@ -922,8 +968,8 @@ For large `dashboard_slices` / `report_schedule_user` tables, see the operator r
|
||||
- Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
|
||||
- Custom colors are no longer supported to maintain consistency with Ant Design components
|
||||
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
|
||||
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
|
||||
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
|
||||
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
|
||||
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
|
||||
- [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading
|
||||
- [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel.
|
||||
- [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default.
|
||||
|
||||
@@ -149,10 +149,10 @@ database engine on a separate host or container.
|
||||
|
||||
Superset supports the following database engines/versions:
|
||||
|
||||
| Database Engine | Supported Versions |
|
||||
| ----------------------------------------- | ---------------------------------------- |
|
||||
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X |
|
||||
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
|
||||
| Database Engine | Supported Versions |
|
||||
| ----------------------------------------- | ---------------------------------------------- |
|
||||
| [PostgreSQL](https://www.postgresql.org/) | 10.X, 11.X, 12.X, 13.X, 14.X, 15.X, 16.X, 17.X |
|
||||
| [MySQL](https://www.mysql.com/) | 5.7, 8.X |
|
||||
|
||||
Use the following database drivers and connection strings:
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ code is less ambiguous and is unique to all regions in the world.
|
||||
## Included Maps
|
||||
|
||||
The current list of countries can be found in the src
|
||||
[legacy-plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts)
|
||||
[plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts)
|
||||
|
||||
The Country Maps visualization already ships with the maps for the following countries:
|
||||
|
||||
@@ -31,10 +31,10 @@ The Country Maps visualization already ships with the maps for the following cou
|
||||
## Adding a New Country
|
||||
|
||||
To add a new country to the list, you'd have to edit files in
|
||||
[@superset-ui/legacy-plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-country-map).
|
||||
[@superset-ui/plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-country-map).
|
||||
|
||||
1. Generate a new GeoJSON file for your country following the guide in [this Jupyter notebook](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country%20Map%20GeoJSON%20Generator.ipynb).
|
||||
2. Edit the countries list in [legacy-plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts).
|
||||
1. Generate a new GeoJSON file for your country following the guide in [this Jupyter notebook](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/scripts/Country%20Map%20GeoJSON%20Generator.ipynb).
|
||||
2. Edit the countries list in [plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts).
|
||||
3. Install superset-frontend dependencies: `cd superset-frontend && npm install`
|
||||
4. Verify your countries in Superset plugins storybook: `npm run plugins:storybook`.
|
||||
5. Build and install Superset from source code.
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"@swc/core": "^1.15.43",
|
||||
"antd": "^6.5.1",
|
||||
"baseline-browser-mapping": "^2.10.43",
|
||||
"caniuse-lite": "^1.0.30001805",
|
||||
"caniuse-lite": "^1.0.30001806",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.2",
|
||||
"js-yaml": "^5.2.0",
|
||||
|
||||
BIN
docs/static/img/logos/pattern_logo.png
vendored
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
@@ -6602,10 +6602,10 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001805:
|
||||
version "1.0.30001805"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz#78d5d5968a69b7ff81af87a96d7ddc7ea6670b1e"
|
||||
integrity sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001806:
|
||||
version "1.0.30001806"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz#1bc8e502b723fa393455dfbedd5ccec0c29bb74e"
|
||||
integrity sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==
|
||||
|
||||
ccount@^2.0.0:
|
||||
version "2.0.1"
|
||||
|
||||
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.22.1 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.22.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
@@ -131,7 +131,7 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|
||||
| httproute.hostnames | list | `[]` | Hostnames that match against the HTTP Host header (templated) |
|
||||
| httproute.labels | object | `{}` | Additional labels to add to the HTTPRoute |
|
||||
| httproute.parentRefs | list | `[]` | Gateways this HTTPRoute attaches to |
|
||||
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. |
|
||||
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. When both `supersetMcp.enabled` and `supersetMcp.httproute.enabled` are true, an extra rule routing `supersetMcp.httproute.path` to the `-mcp` service is also appended. |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.repository | string | `"apachesuperset.docker.scarf.sh/apache/superset"` | |
|
||||
| image.tag | string | `nil` | |
|
||||
@@ -266,6 +266,8 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|
||||
| supersetMcp.extraContainers | list | `[]` | Launch additional containers into supersetMcp pods |
|
||||
| supersetMcp.extraInitContainers | list | `[]` | Extra init containers appended after supersetMcp initContainers |
|
||||
| supersetMcp.forceReload | bool | `false` | If true, forces deployment to reload on each upgrade |
|
||||
| supersetMcp.httproute.enabled | bool | `false` | If true, the MCP server will be exposed via the HTTPRoute /mcp subpath |
|
||||
| supersetMcp.httproute.path | string | `"/mcp"` | |
|
||||
| supersetMcp.ingress.enabled | bool | `false` | If true, the MCP server will be exposed via the ingress /mcp subpath |
|
||||
| supersetMcp.ingress.path | string | `"/mcp"` | |
|
||||
| supersetMcp.ingress.pathType | string | `"Prefix"` | |
|
||||
|
||||
@@ -80,4 +80,16 @@ spec:
|
||||
type: PathPrefix
|
||||
value: {{ .Values.supersetWebsockets.ingress.path }}
|
||||
{{- end }}
|
||||
{{- if and .Values.supersetMcp.enabled .Values.supersetMcp.httproute.enabled }}
|
||||
- backendRefs:
|
||||
- group: ''
|
||||
kind: Service
|
||||
name: {{ $fullName }}-mcp
|
||||
port: {{ .Values.supersetMcp.service.port }}
|
||||
weight: 1
|
||||
matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: {{ .Values.supersetMcp.httproute.path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
92
helm/superset/tests/httproute_test.yaml
Normal file
@@ -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.
|
||||
#
|
||||
suite: HTTPRoute
|
||||
templates:
|
||||
- httproute.yaml
|
||||
tests:
|
||||
- it: routes the general Superset path
|
||||
set:
|
||||
httproute.enabled: true
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 1
|
||||
- equal:
|
||||
path: spec.rules[0].backendRefs[0].name
|
||||
value: RELEASE-NAME-superset
|
||||
- equal:
|
||||
path: spec.rules[0].backendRefs[0].port
|
||||
value: 8088
|
||||
- equal:
|
||||
path: spec.rules[0].matches[0].path.value
|
||||
value: /
|
||||
|
||||
- it: routes the WebSocket path when WebSockets are enabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetWebsockets.enabled: true
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 2
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].name
|
||||
value: RELEASE-NAME-superset-ws
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].port
|
||||
value: 8080
|
||||
- equal:
|
||||
path: spec.rules[1].matches[0].path.value
|
||||
value: /ws
|
||||
|
||||
- it: does not route the WebSocket path when WebSockets are disabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetWebsockets.enabled: false
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 1
|
||||
|
||||
- it: routes the MCP path when MCP HTTPRoute is enabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetMcp.enabled: true
|
||||
supersetMcp.httproute.enabled: true
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 2
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].name
|
||||
value: RELEASE-NAME-superset-mcp
|
||||
- equal:
|
||||
path: spec.rules[1].backendRefs[0].port
|
||||
value: 5008
|
||||
- equal:
|
||||
path: spec.rules[1].matches[0].path.value
|
||||
value: /mcp
|
||||
|
||||
- it: does not route MCP when MCP HTTPRoute is disabled
|
||||
set:
|
||||
httproute.enabled: true
|
||||
supersetMcp.enabled: true
|
||||
supersetMcp.httproute.enabled: false
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.rules
|
||||
count: 1
|
||||
@@ -277,7 +277,9 @@ httproute:
|
||||
# per rule to leave room for traffic splitting (defaults to 1). When
|
||||
# `supersetWebsockets.enabled` is true, an extra rule routing
|
||||
# `supersetWebsockets.ingress.path` to the `-ws` service is appended
|
||||
# automatically, mirroring the ingress behavior.
|
||||
# automatically, mirroring the ingress behavior. When both `supersetMcp.enabled` and
|
||||
# `supersetMcp.httproute.enabled` are true, an extra rule routing
|
||||
# `supersetMcp.httproute.path` to the `-mcp` service is also appended.
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
@@ -987,7 +989,6 @@ supersetWebsockets:
|
||||
# -- Set priorityClassName for supersetWebsockets pods
|
||||
priorityClassName: ~
|
||||
|
||||
|
||||
supersetMcp:
|
||||
# -- Enables the Superset MCP Server. To expose it via the shared ingress at /mcp,
|
||||
# also set supersetMcp.ingress.enabled=true.
|
||||
@@ -1015,6 +1016,10 @@ supersetMcp:
|
||||
enabled: false
|
||||
path: /mcp
|
||||
pathType: Prefix
|
||||
httproute:
|
||||
# -- If true, the MCP server will be exposed via the HTTPRoute /mcp subpath
|
||||
enabled: false
|
||||
path: /mcp
|
||||
service:
|
||||
type: ClusterIP
|
||||
annotations: {}
|
||||
|
||||
@@ -139,7 +139,7 @@ d1 = [
|
||||
]
|
||||
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
|
||||
databricks = [
|
||||
"databricks-sql-connector==4.2.6",
|
||||
"databricks-sql-connector>=4.2.6, <4.4.0",
|
||||
"databricks-sqlalchemy==1.0.5",
|
||||
]
|
||||
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
|
||||
@@ -209,7 +209,7 @@ spark = [
|
||||
]
|
||||
tdengine = [
|
||||
"taospy>=2.8.9",
|
||||
"taos-ws-py>=0.6.9"
|
||||
"taos-ws-py>=0.7.0"
|
||||
]
|
||||
teradata = ["teradatasql>=20.0.0.62"]
|
||||
thumbnails = [] # deprecated, will be removed in 7.0
|
||||
|
||||
@@ -36,11 +36,9 @@ filterwarnings =
|
||||
error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The autoload parameter is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The connection.execute\(\) method:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The current statement is being autocommitted using implicit autocommit:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The ``declarative_base\(\)`` function is now available:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The Engine.execute\(\) method is considered legacy:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The legacy calling style of select\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:The "whens" argument to case:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"User" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
|
||||
@@ -28,6 +28,8 @@ babel==2.17.0
|
||||
# via flask-babel
|
||||
backoff==2.2.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
backports-zstd==1.6.0
|
||||
# via flask-compress
|
||||
bcrypt==4.3.0
|
||||
# via paramiko
|
||||
billiard==4.2.1
|
||||
@@ -130,7 +132,7 @@ flask-babel==3.1.0
|
||||
# via flask-appbuilder
|
||||
flask-caching==2.3.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-compress==1.17
|
||||
flask-compress==1.24
|
||||
# via apache-superset (pyproject.toml)
|
||||
flask-cors==6.0.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -244,7 +246,7 @@ msgpack==1.2.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
msgspec==0.19.0
|
||||
# via flask-session
|
||||
nh3==0.3.5
|
||||
nh3==0.3.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
numexpr==2.10.2
|
||||
# via -r requirements/base.in
|
||||
@@ -285,7 +287,7 @@ parsedatetime==2.6
|
||||
# via apache-superset (pyproject.toml)
|
||||
pgsanity==0.2.9
|
||||
# via apache-superset (pyproject.toml)
|
||||
pillow==12.2.0
|
||||
pillow==12.3.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
platformdirs==4.3.8
|
||||
# via requests-cache
|
||||
@@ -306,11 +308,11 @@ pyasn1-modules==0.4.2
|
||||
# via google-auth
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.11.7
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
pydantic-core==2.33.2
|
||||
pydantic-core==2.46.4
|
||||
# via pydantic
|
||||
pygeohash==3.2.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
@@ -408,7 +410,7 @@ sqlalchemy==1.4.54
|
||||
# shillelagh
|
||||
# sqlalchemy-continuum
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-continuum==1.6.0
|
||||
sqlalchemy-continuum==1.7.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
sqlalchemy-utils==0.42.1
|
||||
# via
|
||||
@@ -443,7 +445,7 @@ typing-extensions==4.16.0
|
||||
# selenium
|
||||
# shillelagh
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
typing-inspection==0.4.2
|
||||
# via pydantic
|
||||
tzdata==2025.2
|
||||
# via
|
||||
@@ -496,5 +498,3 @@ xlsxwriter==3.2.9
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# pandas
|
||||
zstandard==0.23.0
|
||||
# via flask-compress
|
||||
|
||||
@@ -64,6 +64,10 @@ backoff==2.2.1
|
||||
# apache-superset
|
||||
backports-tarfile==1.2.0
|
||||
# via jaraco-context
|
||||
backports-zstd==1.6.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-compress
|
||||
bcrypt==4.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
@@ -276,7 +280,7 @@ flask-caching==2.3.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
flask-compress==1.17
|
||||
flask-compress==1.24
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -570,7 +574,7 @@ msgspec==0.19.0
|
||||
# flask-session
|
||||
mysqlclient==2.2.8
|
||||
# via apache-superset
|
||||
nh3==0.3.5
|
||||
nh3==0.3.6
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -665,7 +669,7 @@ pgsanity==0.2.9
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
pillow==12.2.0
|
||||
pillow==12.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -738,7 +742,7 @@ pycparser==2.22
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# cffi
|
||||
pydantic==2.11.7
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -747,7 +751,7 @@ pydantic==2.11.7
|
||||
# mcp
|
||||
# openapi-pydantic
|
||||
# pydantic-settings
|
||||
pydantic-core==2.33.2
|
||||
pydantic-core==2.46.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pydantic
|
||||
@@ -987,7 +991,7 @@ sqlalchemy==1.4.54
|
||||
# sqlalchemy-utils
|
||||
sqlalchemy-bigquery==1.17.0
|
||||
# via apache-superset
|
||||
sqlalchemy-continuum==1.6.0
|
||||
sqlalchemy-continuum==1.7.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -1066,7 +1070,7 @@ typing-extensions==4.16.0
|
||||
# shillelagh
|
||||
# starlette
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.1
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# mcp
|
||||
@@ -1168,7 +1172,4 @@ zope-event==5.0
|
||||
zope-interface==5.4.0
|
||||
# via gevent
|
||||
zstandard==0.23.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-compress
|
||||
# trino
|
||||
# via trino
|
||||
|
||||
@@ -93,7 +93,7 @@ def find_models(module: ModuleType) -> list[type[Model]]: # noqa: C901
|
||||
# where the current model is out-of-sync with the existing table after a
|
||||
# downgrade
|
||||
sqlalchemy_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
|
||||
engine = create_engine(sqlalchemy_uri)
|
||||
engine = create_engine(sqlalchemy_uri, future=True)
|
||||
Base = automap_base() # noqa: N806
|
||||
Base.prepare(engine, reflect=True)
|
||||
seen = set()
|
||||
|
||||
17
superset-frontend/cypress-base/package-lock.json
generated
@@ -15,7 +15,7 @@
|
||||
"chai": "^4.5.0",
|
||||
"cy-verify-downloads": "^0.2.5",
|
||||
"cypress-fail-on-console-error": "^4.0.3",
|
||||
"nanoid": "^5.0.9",
|
||||
"nanoid": "^6.0.0",
|
||||
"querystringify": "^2.2.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"rison": "^0.1.1"
|
||||
@@ -6656,20 +6656,21 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||
"integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-6.0.0.tgz",
|
||||
"integrity": "sha512-mkUH+rPkwU2qPadJ0oJZOjeZ5Mxn8Q1UhevwkTRWNuUZzyia3h4rhzK39hxaHTk0o2OxB8W2SQ6A8k23ZDi1pQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
"node": "^22 || ^24 || >=26"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
@@ -13769,9 +13770,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||
"integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q=="
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-6.0.0.tgz",
|
||||
"integrity": "sha512-mkUH+rPkwU2qPadJ0oJZOjeZ5Mxn8Q1UhevwkTRWNuUZzyia3h4rhzK39hxaHTk0o2OxB8W2SQ6A8k23ZDi1pQ=="
|
||||
},
|
||||
"natural-compare": {
|
||||
"version": "1.4.0",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"chai": "^4.5.0",
|
||||
"cy-verify-downloads": "^0.2.5",
|
||||
"cypress-fail-on-console-error": "^4.0.3",
|
||||
"nanoid": "^5.0.9",
|
||||
"nanoid": "^6.0.0",
|
||||
"querystringify": "^2.2.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"rison": "^0.1.1"
|
||||
|
||||
1094
superset-frontend/package-lock.json
generated
@@ -78,6 +78,7 @@
|
||||
"playwright:debug": "playwright test --debug",
|
||||
"playwright:report": "playwright show-report",
|
||||
"docs:screenshots": "playwright test --config=playwright/generators/playwright.config.ts docs/",
|
||||
"playwright:thumbnails": "CAPTURE_THUMBNAILS=1 playwright test tests/tools/capture-viz-thumbnails.spec.ts --project chromium",
|
||||
"prettier": "npm run _prettier -- --write",
|
||||
"prettier-check": "npm run _prettier -- --check",
|
||||
"prod": "npm run build",
|
||||
@@ -89,7 +90,7 @@
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent",
|
||||
"test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%",
|
||||
"type": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit",
|
||||
"update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
|
||||
"update-maps": "cd plugins/plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'",
|
||||
"validate-release": "../RELEASING/validate_this_release.sh"
|
||||
},
|
||||
"browserslist": [
|
||||
@@ -136,16 +137,15 @@
|
||||
"@scarf/scarf": "^1.4.0",
|
||||
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
|
||||
"@superset-ui/core": "file:./packages/superset-ui-core",
|
||||
"@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar",
|
||||
"@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord",
|
||||
"@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map",
|
||||
"@superset-ui/legacy-plugin-chart-horizon": "file:./plugins/legacy-plugin-chart-horizon",
|
||||
"@superset-ui/legacy-plugin-chart-paired-t-test": "file:./plugins/legacy-plugin-chart-paired-t-test",
|
||||
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "file:./plugins/legacy-plugin-chart-parallel-coordinates",
|
||||
"@superset-ui/legacy-plugin-chart-partition": "file:./plugins/legacy-plugin-chart-partition",
|
||||
"@superset-ui/legacy-plugin-chart-rose": "file:./plugins/legacy-plugin-chart-rose",
|
||||
"@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map",
|
||||
"@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
|
||||
"@superset-ui/plugin-chart-calendar": "file:./plugins/plugin-chart-calendar",
|
||||
"@superset-ui/plugin-chart-chord": "file:./plugins/plugin-chart-chord",
|
||||
"@superset-ui/plugin-chart-country-map": "file:./plugins/plugin-chart-country-map",
|
||||
"@superset-ui/plugin-chart-horizon": "file:./plugins/plugin-chart-horizon",
|
||||
"@superset-ui/plugin-chart-paired-t-test": "file:./plugins/plugin-chart-paired-t-test",
|
||||
"@superset-ui/plugin-chart-parallel-coordinates": "file:./plugins/plugin-chart-parallel-coordinates",
|
||||
"@superset-ui/plugin-chart-partition": "file:./plugins/plugin-chart-partition",
|
||||
"@superset-ui/plugin-chart-rose": "file:./plugins/plugin-chart-rose",
|
||||
"@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map",
|
||||
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram",
|
||||
"@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
|
||||
@@ -166,8 +166,8 @@
|
||||
"@visx/scale": "^4.0.0",
|
||||
"@visx/tooltip": "^4.0.0",
|
||||
"@visx/xychart": "^4.0.0",
|
||||
"ag-grid-community": "36.0.0",
|
||||
"ag-grid-react": "36.0.0",
|
||||
"ag-grid-community": "36.0.1",
|
||||
"ag-grid-react": "36.0.1",
|
||||
"antd": "^6.5.1",
|
||||
"chrono-node": "^2.10.0",
|
||||
"classnames": "^2.2.5",
|
||||
@@ -201,7 +201,7 @@
|
||||
"memoize-one": "^6.0.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.16",
|
||||
"nanoid": "^6.0.0",
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
@@ -263,13 +263,13 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.17",
|
||||
"@formatjs/intl-durationformat": "^0.10.18",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.5.0",
|
||||
"@storybook/addon-links": "10.5.0",
|
||||
"@storybook/react-webpack5": "10.5.0",
|
||||
"@storybook/addon-docs": "10.5.2",
|
||||
"@storybook/addon-links": "10.5.2",
|
||||
"@storybook/react-webpack5": "10.5.2",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.43",
|
||||
@@ -325,7 +325,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.5.0",
|
||||
"eslint-plugin-storybook": "10.5.2",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -357,7 +357,7 @@
|
||||
"source-map": "^0.7.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"speed-measure-webpack-plugin": "^1.6.0",
|
||||
"storybook": "10.5.0",
|
||||
"storybook": "10.5.2",
|
||||
"style-loader": "^4.0.0",
|
||||
"stylelint": "^17.14.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"nanoid": "^5.0.9",
|
||||
"nanoid": "*",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
|
||||
@@ -16,9 +16,21 @@
|
||||
* specific language governing permissions and limitationsxw
|
||||
* under the License.
|
||||
*/
|
||||
import { PostProcessingProphet, getXAxisLabel } from '@superset-ui/core';
|
||||
import {
|
||||
PostProcessingProphet,
|
||||
TimeGranularity,
|
||||
getXAxisColumn,
|
||||
getXAxisLabel,
|
||||
isAdhocColumn,
|
||||
} from '@superset-ui/core';
|
||||
import { PostProcessingFactory } from './types';
|
||||
|
||||
// Fallback grain used only when no time grain can be resolved from the form
|
||||
// data, query object, or x-axis column. Matches the `time_grain_sqla` control
|
||||
// default in sharedControls so forecasting stays functional rather than failing
|
||||
// with an opaque backend error.
|
||||
const DEFAULT_TIME_GRAIN = TimeGranularity.DAY;
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export const prophetOperator: PostProcessingFactory<PostProcessingProphet> = (
|
||||
formData,
|
||||
@@ -26,10 +38,25 @@ export const prophetOperator: PostProcessingFactory<PostProcessingProphet> = (
|
||||
) => {
|
||||
const xAxisLabel = getXAxisLabel(formData);
|
||||
if (formData.forecastEnabled && xAxisLabel) {
|
||||
// The effective time grain can live in several places depending on how the
|
||||
// chart was configured. Prefer, in order:
|
||||
// 1. the grain popover on an adhoc x-axis column (generic x-axis),
|
||||
// 2. the grain resolved onto the query object's extras (picks up
|
||||
// dashboard-applied grains and the panel control),
|
||||
// 3. the `time_grain_sqla` panel control on the form data directly.
|
||||
// Fall back to a daily grain so a saved/dashboard chart with the grain
|
||||
// cleared to "None" still forecasts instead of raising a backend error.
|
||||
const xAxisColumn = getXAxisColumn(formData);
|
||||
const timeGrain =
|
||||
(isAdhocColumn(xAxisColumn) &&
|
||||
(xAxisColumn.timeGrain as TimeGranularity)) ||
|
||||
queryObject.extras?.time_grain_sqla ||
|
||||
formData.time_grain_sqla ||
|
||||
DEFAULT_TIME_GRAIN;
|
||||
return {
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: formData.time_grain_sqla,
|
||||
time_grain: timeGrain,
|
||||
periods: parseInt(formData.forecastPeriods, 10),
|
||||
confidence_interval: parseFloat(formData.forecastInterval),
|
||||
yearly_seasonality: formData.forecastSeasonalityYearly,
|
||||
|
||||
@@ -43,6 +43,11 @@ const queryObject: QueryObject = {
|
||||
granularity: 'P1Y',
|
||||
};
|
||||
|
||||
// A chart whose Time Grain control was cleared to "None": form_data has no
|
||||
// `time_grain_sqla` key at all (SC-113749).
|
||||
const formDataWithoutGrain: SqlaFormData = { ...formData };
|
||||
delete formDataWithoutGrain.time_grain_sqla;
|
||||
|
||||
test('should skip prophetOperator', () => {
|
||||
expect(prophetOperator(formData, queryObject)).toEqual(undefined);
|
||||
});
|
||||
@@ -137,3 +142,103 @@ test('should do prophetOperator over adhoc column', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should fall back to a daily grain when no time grain is resolvable', () => {
|
||||
// Regression for SC-113749: a saved/dashboard chart with the Time Grain
|
||||
// control cleared to "None" has no `time_grain_sqla` in form_data. Prior to
|
||||
// the fix this emitted `time_grain: undefined`, which `JSON.stringify` drops,
|
||||
// causing the backend `prophet()` call to raise a raw `TypeError`.
|
||||
expect(
|
||||
prophetOperator(
|
||||
{
|
||||
...formDataWithoutGrain,
|
||||
granularity_sqla: 'time_column',
|
||||
forecastEnabled: true,
|
||||
forecastPeriods: '3',
|
||||
forecastInterval: '5',
|
||||
forecastSeasonalityYearly: true,
|
||||
forecastSeasonalityWeekly: false,
|
||||
forecastSeasonalityDaily: false,
|
||||
},
|
||||
{ ...queryObject, extras: {} },
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: 'P1D',
|
||||
periods: 3.0,
|
||||
confidence_interval: 5.0,
|
||||
yearly_seasonality: true,
|
||||
weekly_seasonality: false,
|
||||
daily_seasonality: false,
|
||||
index: DTTM_ALIAS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should resolve the time grain from the adhoc x-axis column', () => {
|
||||
// With the generic x-axis, the grain lives on the column's popover
|
||||
// (`timeGrain`) rather than the `time_grain_sqla` panel control.
|
||||
expect(
|
||||
prophetOperator(
|
||||
{
|
||||
...formDataWithoutGrain,
|
||||
x_axis: {
|
||||
label: 'ds',
|
||||
expressionType: 'SQL',
|
||||
sqlExpression: 'ds',
|
||||
timeGrain: 'P1M',
|
||||
},
|
||||
forecastEnabled: true,
|
||||
forecastPeriods: '3',
|
||||
forecastInterval: '5',
|
||||
forecastSeasonalityYearly: true,
|
||||
forecastSeasonalityWeekly: false,
|
||||
forecastSeasonalityDaily: false,
|
||||
},
|
||||
{ ...queryObject, extras: {} },
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: 'P1M',
|
||||
periods: 3.0,
|
||||
confidence_interval: 5.0,
|
||||
yearly_seasonality: true,
|
||||
weekly_seasonality: false,
|
||||
daily_seasonality: false,
|
||||
index: 'ds',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should resolve the time grain from the query object extras', () => {
|
||||
// Dashboard-applied grains (e.g. via native filters) land in
|
||||
// `queryObject.extras.time_grain_sqla` even when form_data has none.
|
||||
expect(
|
||||
prophetOperator(
|
||||
{
|
||||
...formDataWithoutGrain,
|
||||
granularity_sqla: 'time_column',
|
||||
forecastEnabled: true,
|
||||
forecastPeriods: '3',
|
||||
forecastInterval: '5',
|
||||
forecastSeasonalityYearly: true,
|
||||
forecastSeasonalityWeekly: false,
|
||||
forecastSeasonalityDaily: false,
|
||||
},
|
||||
{ ...queryObject, extras: { time_grain_sqla: 'P1W' } },
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'prophet',
|
||||
options: {
|
||||
time_grain: 'P1W',
|
||||
periods: 3.0,
|
||||
confidence_interval: 5.0,
|
||||
yearly_seasonality: true,
|
||||
weekly_seasonality: false,
|
||||
daily_seasonality: false,
|
||||
index: DTTM_ALIAS,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@visx/responsive": "^4.0.0",
|
||||
"ace-builds": "^1.44.0",
|
||||
"ag-grid-community": "36.0.0",
|
||||
"ag-grid-react": "36.0.0",
|
||||
"ag-grid-community": "36.0.1",
|
||||
"ag-grid-react": "36.0.1",
|
||||
"brace": "^0.11.1",
|
||||
"classnames": "^2.5.1",
|
||||
"core-js": "^3.49.0",
|
||||
@@ -101,7 +101,7 @@
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^6.0.0",
|
||||
"nanoid": "^5.0.9",
|
||||
"nanoid": "*",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
|
||||
@@ -104,24 +104,15 @@ export default class ChartClient {
|
||||
const buildQueryRegistry = getChartBuildQueryRegistry();
|
||||
|
||||
if (metaDataRegistry.has(visType)) {
|
||||
const { useLegacyApi } = metaDataRegistry.get(visType)!;
|
||||
const buildQuery =
|
||||
(await buildQueryRegistry.get(visType)) ?? (() => formData);
|
||||
const requestConfig: RequestConfig = useLegacyApi
|
||||
? {
|
||||
endpoint: '/explore_json/',
|
||||
postPayload: {
|
||||
form_data: buildQuery(formData),
|
||||
},
|
||||
...options,
|
||||
}
|
||||
: {
|
||||
endpoint: '/api/v1/chart/data',
|
||||
jsonPayload: {
|
||||
query_context: buildQuery(formData),
|
||||
},
|
||||
...options,
|
||||
};
|
||||
const requestConfig: RequestConfig = {
|
||||
endpoint: '/api/v1/chart/data',
|
||||
jsonPayload: {
|
||||
query_context: buildQuery(formData),
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.client
|
||||
.post(requestConfig)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { render, waitFor, configure } from '@testing-library/react';
|
||||
import { render, waitFor, configure, act } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import StatefulChart from './StatefulChart';
|
||||
import getChartControlPanelRegistry from '../registries/ChartControlPanelRegistrySingleton';
|
||||
@@ -67,19 +67,17 @@ beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Setup default registry mocks
|
||||
(getChartMetadataRegistry as any).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue({
|
||||
useLegacyApi: false,
|
||||
}),
|
||||
});
|
||||
jest.mocked(getChartMetadataRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue({}),
|
||||
} as unknown as ReturnType<typeof getChartMetadataRegistry>);
|
||||
|
||||
(getChartBuildQueryRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartBuildQueryRegistry).mockReturnValue({
|
||||
get: jest.fn().mockResolvedValue(null),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartBuildQueryRegistry>);
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(null),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
// Mock ChartClient constructor
|
||||
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
||||
@@ -113,9 +111,9 @@ test('should refetch data when non-renderTrigger control changes', async () => {
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -165,9 +163,9 @@ test('should NOT refetch data when only renderTrigger controls change', async ()
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender, getByTestId } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -201,9 +199,9 @@ test('should NOT refetch data when only renderTrigger controls change', async ()
|
||||
|
||||
test('should refetch when control panel config is not available', async () => {
|
||||
// No control panel config available
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(null),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -245,9 +243,9 @@ test('should refetch when viz_type changes', async () => {
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -299,9 +297,9 @@ test('should handle mixed renderTrigger and non-renderTrigger changes', async ()
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -352,9 +350,9 @@ test('should handle controls with complex structure', async () => {
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender, getByTestId } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -404,11 +402,11 @@ test('should not refetch when formData has not changed', async () => {
|
||||
|
||||
test('should handle errors gracefully when accessing registry', async () => {
|
||||
// Mock registry to throw an error
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockImplementation(() => {
|
||||
throw new Error('Registry error');
|
||||
}),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -492,9 +490,9 @@ test('should NOT refetch data when string-based renderTrigger control (zoomable)
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const formDataWithZoom = {
|
||||
...mockFormData,
|
||||
@@ -542,9 +540,9 @@ test('should NOT refetch data when other string-based renderTrigger controls cha
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender, getByTestId } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -585,9 +583,9 @@ test('should refetch when string control is NOT in RENDER_TRIGGER_SHARED_CONTROL
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
@@ -631,9 +629,9 @@ test('should handle mixed string and object controls correctly', async () => {
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const formDataWithControls = {
|
||||
...mockFormData,
|
||||
@@ -687,9 +685,9 @@ test('should refetch when mixing renderTrigger string control with non-renderTri
|
||||
],
|
||||
};
|
||||
|
||||
(getChartControlPanelRegistry as any).mockReturnValue({
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue(controlPanelConfig),
|
||||
});
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
|
||||
const formDataWithZoom = {
|
||||
...mockFormData,
|
||||
@@ -720,6 +718,397 @@ test('should refetch when mixing renderTrigger string control with non-renderTri
|
||||
});
|
||||
});
|
||||
|
||||
test('resolves async (202) responses via the injected handleAsyncChartData hook', async () => {
|
||||
const asyncJob = {
|
||||
channel_id: 'c1',
|
||||
job_id: 'j1',
|
||||
status: 'running',
|
||||
result_url: '/api/v1/chart/data/abc',
|
||||
};
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: asyncJob,
|
||||
});
|
||||
const handleAsyncChartData = jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ data: 'async result' }]);
|
||||
|
||||
const { getByTestId } = render(
|
||||
<StatefulChart
|
||||
formData={mockFormData}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
// Delegates the raw response + job metadata (and the abort signal)
|
||||
expect(handleAsyncChartData).toHaveBeenCalledWith(
|
||||
{ status: 202 },
|
||||
asyncJob,
|
||||
expect.any(AbortSignal),
|
||||
);
|
||||
// Chart renders once the async data resolves
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('super-chart')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('errors on async (202) response when no async handler is provided', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: { job_id: 'j1', channel_id: 'c1', status: 'running' },
|
||||
});
|
||||
const onError = jest.fn();
|
||||
|
||||
const { findByText } = render(
|
||||
<StatefulChart
|
||||
formData={mockFormData}
|
||||
chartType="test_chart"
|
||||
onError={onError}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Fails loudly instead of rendering the job metadata as empty data
|
||||
expect(await findByText(/async handler/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('renders synchronous (200) responses that include a response object', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 200 } as Response,
|
||||
json: [{ result: [{ data: 'sync result' }] }],
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<StatefulChart formData={mockFormData} chartType="test_chart" />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('super-chart')).toBeInTheDocument();
|
||||
});
|
||||
// Synchronous path: no async handler needed, single request
|
||||
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('does not apply a superseded async response over a newer one', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: { job_id: 'j', channel_id: 'c' },
|
||||
});
|
||||
let resolveFirst: (data: unknown) => void = () => {};
|
||||
let resolveSecond: (data: unknown) => void = () => {};
|
||||
const handleAsyncChartData = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
resolveFirst = resolve;
|
||||
}),
|
||||
)
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
resolveSecond = resolve;
|
||||
}),
|
||||
);
|
||||
const onLoad = jest.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart
|
||||
formData={mockFormData}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
onLoad={onLoad}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// A newer request supersedes the first (viz_type change forces a refetch)
|
||||
const newFormData = { ...mockFormData, viz_type: 'different_chart' };
|
||||
rerender(
|
||||
<StatefulChart
|
||||
formData={newFormData}
|
||||
chartType="different_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
onLoad={onLoad}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// Resolve the newer request first, then the stale one
|
||||
await act(async () => {
|
||||
resolveSecond([{ data: 'B' }]);
|
||||
});
|
||||
await act(async () => {
|
||||
resolveFirst([{ data: 'A' }]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onLoad).toHaveBeenCalledWith([{ data: 'B' }]);
|
||||
});
|
||||
// The stale (superseded) response must not overwrite the newer one
|
||||
expect(onLoad).not.toHaveBeenCalledWith([{ data: 'A' }]);
|
||||
expect(onLoad).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('preserves the detailed message from an async (array) rejection', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: { job_id: 'j', channel_id: 'c' },
|
||||
});
|
||||
const handleAsyncChartData = jest
|
||||
.fn()
|
||||
.mockRejectedValue([{ error: 'Async query failed: table not found' }]);
|
||||
const onError = jest.fn();
|
||||
|
||||
const { findByText } = render(
|
||||
<StatefulChart
|
||||
formData={mockFormData}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
onError={onError}
|
||||
/>,
|
||||
);
|
||||
|
||||
// The detailed message survives instead of collapsing to the generic one
|
||||
expect(await findByText(/table not found/i)).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError.mock.calls[0][0].message).toContain('table not found');
|
||||
});
|
||||
});
|
||||
|
||||
test('refetches with the latest formData rather than the initial props', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 200 } as Response,
|
||||
json: [{ result: [{ data: 'x' }] }],
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart
|
||||
formData={{ ...mockFormData, metrics: ['metric_v1'] }}
|
||||
chartType="test_chart"
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Change a data-affecting control -> triggers a refetch
|
||||
rerender(
|
||||
<StatefulChart
|
||||
formData={{ ...mockFormData, metrics: ['metric_v2'] }}
|
||||
chartType="test_chart"
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(mockChartClient.client.post).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// The second request must carry the updated formData, not the initial props
|
||||
const secondRequestConfig = mockChartClient.client.post.mock.calls[1][0];
|
||||
expect(JSON.stringify(secondRequestConfig)).toContain('metric_v2');
|
||||
expect(JSON.stringify(secondRequestConfig)).not.toContain('metric_v1');
|
||||
});
|
||||
|
||||
test('does not revert a render-only change when a slow async request resolves', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: { job_id: 'j', channel_id: 'c' },
|
||||
});
|
||||
// color_scheme is a renderTrigger control -> its change does not refetch
|
||||
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue({
|
||||
controlPanelSections: [
|
||||
{
|
||||
controlSetRows: [
|
||||
[{ name: 'color_scheme', config: { renderTrigger: true } }],
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
|
||||
let resolveAsync: (data: unknown) => void = () => {};
|
||||
const handleAsyncChartData = jest.fn(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
resolveAsync = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
const { rerender, getByTestId } = render(
|
||||
<StatefulChart
|
||||
formData={{ ...mockFormData, color_scheme: 'scheme_one' }}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Render-only change while the async request is still pending (no refetch)
|
||||
rerender(
|
||||
<StatefulChart
|
||||
formData={{ ...mockFormData, color_scheme: 'scheme_two' }}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
/>,
|
||||
);
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
|
||||
|
||||
// The stale request resolves; it must not revert color_scheme back
|
||||
await act(async () => {
|
||||
resolveAsync([{ data: 'd' }]);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('super-chart')).toHaveTextContent('scheme_two');
|
||||
});
|
||||
expect(getByTestId('super-chart')).not.toHaveTextContent('scheme_one');
|
||||
});
|
||||
|
||||
test('passes an abort signal to the async handler and aborts it on unmount', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: { job_id: 'j', channel_id: 'c' },
|
||||
});
|
||||
// Typed with a rest param so mock.calls is indexable (the 4th arg is the signal)
|
||||
const handleAsyncChartData = jest.fn(
|
||||
(..._args: unknown[]) => new Promise<never>(() => {}), // never resolves
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
<StatefulChart
|
||||
formData={mockFormData}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const signal = handleAsyncChartData.mock.calls[0][2] as AbortSignal;
|
||||
expect(signal).toBeInstanceOf(AbortSignal);
|
||||
expect(signal.aborted).toBe(false);
|
||||
|
||||
// Unmounting aborts the signal so a signal-aware handler can stop polling
|
||||
unmount();
|
||||
expect(signal.aborted).toBe(true);
|
||||
});
|
||||
|
||||
test('suppresses stale error state from a superseded request', async () => {
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: { job_id: 'j', channel_id: 'c' },
|
||||
});
|
||||
let rejectFirst: (err: unknown) => void = () => {};
|
||||
const handleAsyncChartData = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectFirst = reject;
|
||||
}),
|
||||
)
|
||||
.mockImplementationOnce(() => new Promise(() => {})); // newer request stays pending
|
||||
const onError = jest.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
<StatefulChart
|
||||
formData={mockFormData}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
onError={onError}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Supersede the first request (aborts its controller)
|
||||
rerender(
|
||||
<StatefulChart
|
||||
formData={{ ...mockFormData, viz_type: 'different_chart' }}
|
||||
chartType="different_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
onError={onError}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// The stale request now fails; its error must not surface
|
||||
await act(async () => {
|
||||
rejectFirst(new Error('stale failure'));
|
||||
});
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not publish stale data when switching from chartId to formData mode', async () => {
|
||||
mockChartClient.loadFormData.mockResolvedValue({ ...mockFormData });
|
||||
mockChartClient.client.post.mockResolvedValue({
|
||||
response: { status: 202 } as Response,
|
||||
json: { job_id: 'j', channel_id: 'c' },
|
||||
});
|
||||
let resolveFirst: (data: unknown) => void = () => {};
|
||||
const handleAsyncChartData = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise(resolve => {
|
||||
resolveFirst = resolve;
|
||||
}),
|
||||
)
|
||||
.mockImplementationOnce(() => new Promise(() => {}));
|
||||
const onLoad = jest.fn();
|
||||
|
||||
// Start in chartId mode
|
||||
const { rerender } = render(
|
||||
<StatefulChart
|
||||
chartId={1}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
onLoad={onLoad}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Switch to direct-formData mode
|
||||
rerender(
|
||||
<StatefulChart
|
||||
formData={{ ...mockFormData, metrics: ['m'] }}
|
||||
chartType="test_chart"
|
||||
hooks={{ handleAsyncChartData }}
|
||||
onLoad={onLoad}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(handleAsyncChartData).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// The stale chartId-mode request resolves; its data must not be published
|
||||
await act(async () => {
|
||||
resolveFirst([{ data: 'stale' }]);
|
||||
});
|
||||
expect(onLoad).not.toHaveBeenCalledWith([{ data: 'stale' }]);
|
||||
});
|
||||
|
||||
test('should display error message when HTTP request fails with Response object', async () => {
|
||||
const errorBody = JSON.stringify({ message: 'Error: division by zero' });
|
||||
const mockResponse = new Response(errorBody, {
|
||||
|
||||
@@ -18,21 +18,22 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ParentSize } from '@visx/responsive';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
QueryFormData,
|
||||
QueryData,
|
||||
JsonObject,
|
||||
SupersetClientInterface,
|
||||
buildQueryContext,
|
||||
RequestConfig,
|
||||
getClientErrorObject,
|
||||
ensureIsArray,
|
||||
} from '../..';
|
||||
import type { HandlerFunction } from '../types/Base';
|
||||
import { Loading } from '../../components/Loading';
|
||||
import ChartClient from '../clients/ChartClient';
|
||||
import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton';
|
||||
import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton';
|
||||
import getChartControlPanelRegistry from '../registries/ChartControlPanelRegistrySingleton';
|
||||
import SuperChart from './SuperChart';
|
||||
|
||||
@@ -189,6 +190,12 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
const chartClientRef = useRef<ChartClient>();
|
||||
const abortControllerRef = useRef<AbortController>();
|
||||
|
||||
// fetchData is memoized with an empty dep list, so it would otherwise close
|
||||
// over the first render's props. Keep the latest props in a ref so refetches
|
||||
// (triggered by updated filters/formData/overrides) use current values.
|
||||
const propsRef = useRef(props);
|
||||
propsRef.current = props;
|
||||
|
||||
// Initialize chart client
|
||||
if (!chartClientRef.current) {
|
||||
chartClientRef.current = new ChartClient({ client: props.client });
|
||||
@@ -199,20 +206,48 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
chartId,
|
||||
formData: propsFormData,
|
||||
formDataOverrides,
|
||||
onError,
|
||||
onLoad,
|
||||
chartType,
|
||||
force,
|
||||
timeout,
|
||||
} = props;
|
||||
hooks,
|
||||
} = propsRef.current;
|
||||
|
||||
// Cancel any in-flight requests
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// Create new abort controller
|
||||
abortControllerRef.current = new AbortController();
|
||||
// Create new abort controller (kept in a local so we can detect when this
|
||||
// request has been superseded by a newer one, even across async awaits).
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
// A request is superseded if it was aborted, or if the props changed in a
|
||||
// data-affecting way since it began - including switching between chartId
|
||||
// and direct-formData modes. Props are captured during render but the abort
|
||||
// happens in a passive effect, so the abort signal alone can let a stale
|
||||
// success or error slip through in the render->effect gap. This mirrors the
|
||||
// effect's own refetch decision; render-only changes are intentionally not
|
||||
// treated as superseding.
|
||||
const isSuperseded = () => {
|
||||
if (controller.signal.aborted) {
|
||||
return true;
|
||||
}
|
||||
const latest = propsRef.current;
|
||||
const vizTypeForCompare = latest.formData?.viz_type || latest.chartType;
|
||||
return (
|
||||
latest.chartId !== chartId ||
|
||||
// Deep compare overrides: callers commonly pass a fresh object with the
|
||||
// same contents each render, which should not count as superseding.
|
||||
!isEqual(latest.formDataOverrides, formDataOverrides) ||
|
||||
latest.force !== force ||
|
||||
Boolean(propsFormData) !== Boolean(latest.formData) ||
|
||||
(!!propsFormData &&
|
||||
!!latest.formData &&
|
||||
latest.formData !== propsFormData &&
|
||||
shouldRefetchData(propsFormData, latest.formData, vizTypeForCompare))
|
||||
);
|
||||
};
|
||||
|
||||
setStatus('loading');
|
||||
setError(undefined);
|
||||
@@ -224,7 +259,7 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
// Load formData from chartId
|
||||
finalFormData = await chartClientRef.current!.loadFormData(
|
||||
{ sliceId: chartId },
|
||||
{ signal: abortControllerRef.current.signal } as RequestConfig,
|
||||
{ signal: controller.signal } as RequestConfig,
|
||||
);
|
||||
} else if (propsFormData) {
|
||||
// Use provided formData
|
||||
@@ -245,9 +280,6 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
}
|
||||
finalFormData.viz_type = vizType;
|
||||
|
||||
// Get chart metadata
|
||||
const { useLegacyApi } = getChartMetadataRegistry().get(vizType) || {};
|
||||
|
||||
// Build query using the chart's buildQuery function
|
||||
const buildQuery = await getChartBuildQueryRegistry().get(vizType);
|
||||
let queryContext;
|
||||
@@ -259,65 +291,141 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
queryContext = buildQueryContext(finalFormData);
|
||||
}
|
||||
|
||||
// Ensure query_context is properly formatted for new API
|
||||
if (!useLegacyApi && !queryContext.queries) {
|
||||
// Ensure query_context is properly formatted for the API
|
||||
if (!queryContext.queries) {
|
||||
queryContext = { queries: [queryContext] };
|
||||
}
|
||||
const endpoint = useLegacyApi ? '/explore_json/' : '/api/v1/chart/data';
|
||||
|
||||
const requestConfig: RequestConfig = {
|
||||
endpoint,
|
||||
signal: abortControllerRef.current.signal,
|
||||
endpoint: '/api/v1/chart/data',
|
||||
signal: controller.signal,
|
||||
...(timeout && { timeout: timeout * 1000 }),
|
||||
};
|
||||
|
||||
if (useLegacyApi) {
|
||||
requestConfig.postPayload = {
|
||||
form_data: {
|
||||
...finalFormData,
|
||||
...(force && { force: true }),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
requestConfig.jsonPayload = {
|
||||
jsonPayload: {
|
||||
...queryContext,
|
||||
...(force && { force: true }),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const response = await chartClientRef.current!.client.post(requestConfig);
|
||||
let responseData = Array.isArray(response.json)
|
||||
? response.json
|
||||
: [response.json];
|
||||
const clientResponse =
|
||||
await chartClientRef.current!.client.post(requestConfig);
|
||||
|
||||
// Handle the nested result structure from the new API
|
||||
if (!useLegacyApi && responseData[0]?.result) {
|
||||
responseData = responseData[0].result;
|
||||
}
|
||||
|
||||
setStatus('loaded');
|
||||
setData(responseData);
|
||||
setFormData(finalFormData);
|
||||
|
||||
if (onLoad) {
|
||||
onLoad(responseData);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore abort errors
|
||||
if ((err as Error).name === 'AbortError') {
|
||||
// A newer request may have started while the POST was in flight; discard
|
||||
// this stale response so it can't overwrite the newer chart data.
|
||||
if (isSuperseded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedError = await getClientErrorObject(
|
||||
err as Parameters<typeof getClientErrorObject>[0],
|
||||
);
|
||||
const errorMessage =
|
||||
parsedError.error || parsedError.message || 'An error occurred';
|
||||
const rawResponse = clientResponse.response as Response | undefined;
|
||||
|
||||
let responseData: QueryData[];
|
||||
if (rawResponse?.status === 202) {
|
||||
// With GLOBAL_ASYNC_QUERIES the query is dispatched to a Celery worker
|
||||
// and the 202 body is job metadata (channel_id, job_id, result_url),
|
||||
// not chart data. Delegate to the injected handler, which polls the
|
||||
// async event channel and resolves the cached results. Without a
|
||||
// handler we fail loudly rather than rendering the job metadata as if
|
||||
// it were an (empty) result set.
|
||||
if (!hooks?.handleAsyncChartData) {
|
||||
throw new Error(
|
||||
'Received an async chart data response (HTTP 202) but no async ' +
|
||||
'handler was provided, so results cannot be retrieved. Wire up ' +
|
||||
'the async handler or disable GLOBAL_ASYNC_QUERIES for this chart.',
|
||||
);
|
||||
}
|
||||
responseData = ensureIsArray(
|
||||
await hooks.handleAsyncChartData(
|
||||
rawResponse,
|
||||
clientResponse.json as JsonObject,
|
||||
controller.signal,
|
||||
),
|
||||
);
|
||||
|
||||
// Async results can resolve well after a newer request began polling.
|
||||
if (isSuperseded()) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const rows = (
|
||||
Array.isArray(clientResponse.json)
|
||||
? clientResponse.json
|
||||
: [clientResponse.json]
|
||||
) as JsonObject[];
|
||||
|
||||
// Handle the nested result structure from the API
|
||||
responseData = (rows[0]?.result ? rows[0].result : rows) as QueryData[];
|
||||
}
|
||||
|
||||
// Don't pair this request's data with newer props or fire a stale onLoad
|
||||
// if it has been superseded (see isSuperseded).
|
||||
if (isSuperseded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestProps = propsRef.current;
|
||||
setStatus('loaded');
|
||||
setData(responseData);
|
||||
// Render the resolved data with the latest formData so a render-only
|
||||
// change made while the request was in flight isn't reverted.
|
||||
setFormData(
|
||||
latestProps.formData
|
||||
? {
|
||||
...latestProps.formData,
|
||||
...latestProps.formDataOverrides,
|
||||
viz_type: finalFormData.viz_type,
|
||||
}
|
||||
: finalFormData,
|
||||
);
|
||||
|
||||
// Read onLoad from the latest props (like setFormData above) so a stale
|
||||
// callback captured at request start isn't invoked.
|
||||
if (latestProps.onLoad) {
|
||||
latestProps.onLoad(responseData);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore aborted requests, whether they threw AbortError or were
|
||||
// superseded by a newer request (including the render->effect gap).
|
||||
if ((err as Error)?.name === 'AbortError' || isSuperseded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// waitForAsyncData rejects with an array of already-parsed client-error
|
||||
// objects; unwrap the first element so its detailed message survives.
|
||||
const rawError = Array.isArray(err) ? err[0] : err;
|
||||
|
||||
let errorMessage: string | undefined;
|
||||
if (
|
||||
rawError &&
|
||||
typeof rawError === 'object' &&
|
||||
!(rawError instanceof Error) &&
|
||||
!(rawError instanceof Response) &&
|
||||
typeof (rawError as { error?: unknown }).error === 'string'
|
||||
) {
|
||||
// Already a parsed client-error object (e.g. from the async handler);
|
||||
// getClientErrorObject would discard its `error` field, so read it here.
|
||||
const parsed = rawError as { error?: string; message?: string };
|
||||
errorMessage = parsed.error || parsed.message;
|
||||
} else {
|
||||
const parsedError = await getClientErrorObject(
|
||||
rawError as Parameters<typeof getClientErrorObject>[0],
|
||||
);
|
||||
errorMessage = parsedError.error || parsedError.message;
|
||||
}
|
||||
|
||||
const errorObj = new Error(errorMessage || 'An error occurred');
|
||||
|
||||
// The request may have been superseded while its error response was being
|
||||
// parsed above (or in the render->effect gap before its abort ran); don't
|
||||
// set stale error state or call onError in that case.
|
||||
if (isSuperseded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorObj = new Error(errorMessage);
|
||||
setStatus('error');
|
||||
setError(errorObj);
|
||||
|
||||
// Read onError from the latest props so a stale callback captured at
|
||||
// request start isn't invoked.
|
||||
const { onError } = propsRef.current;
|
||||
if (onError) {
|
||||
onError(errorObj);
|
||||
}
|
||||
@@ -481,7 +589,7 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
enableNoResults={enableNoResults}
|
||||
noResults={NoDataComponent && <NoDataComponent />}
|
||||
onRenderSuccess={onRenderSuccess}
|
||||
onRenderFailure={onRenderFailure as HandlerFunction | undefined}
|
||||
onRenderFailure={onRenderFailure}
|
||||
hooks={hooks}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -40,7 +40,6 @@ export interface ChartMetadataConfig {
|
||||
supportedAnnotationTypes?: string[];
|
||||
thumbnail: string;
|
||||
thumbnailDark?: string;
|
||||
useLegacyApi?: boolean;
|
||||
behaviors?: Behavior[];
|
||||
exampleGallery?: ExampleImage[];
|
||||
tags?: string[];
|
||||
@@ -75,8 +74,6 @@ export default class ChartMetadata {
|
||||
|
||||
thumbnailDark?: string;
|
||||
|
||||
useLegacyApi: boolean;
|
||||
|
||||
behaviors: Behavior[];
|
||||
|
||||
datasourceCount: number;
|
||||
@@ -112,7 +109,6 @@ export default class ChartMetadata {
|
||||
supportedAnnotationTypes = [],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi = false,
|
||||
behaviors = [],
|
||||
datasourceCount = 1,
|
||||
enableNoResults = true,
|
||||
@@ -144,7 +140,6 @@ export default class ChartMetadata {
|
||||
this.supportedAnnotationTypes = supportedAnnotationTypes;
|
||||
this.thumbnail = thumbnail;
|
||||
this.thumbnailDark = thumbnailDark;
|
||||
this.useLegacyApi = useLegacyApi;
|
||||
this.behaviors = behaviors;
|
||||
this.datasourceCount = datasourceCount;
|
||||
this.enableNoResults = enableNoResults;
|
||||
|
||||
@@ -66,6 +66,17 @@ type Hooks = {
|
||||
setTooltip?: HandlerFunction;
|
||||
/* handle legend scroll changes */
|
||||
onLegendScroll?: HandlerFunction;
|
||||
/**
|
||||
* Resolve an async chart-data response (HTTP 202 from GLOBAL_ASYNC_QUERIES).
|
||||
* Injected by the app so components in this package (e.g. Matrixify's
|
||||
* StatefulChart) can await async results without importing app-level
|
||||
* async-event middleware. Returns the resolved query results.
|
||||
*/
|
||||
handleAsyncChartData?: (
|
||||
response: Response,
|
||||
json: JsonObject,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<QueryData[]> | QueryData[];
|
||||
} & PlainObject;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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, waitFor } from '@superset-ui/core/spec';
|
||||
import { RlsBadge, type RlsBadgeProps } from '.';
|
||||
|
||||
const mockFilters = [
|
||||
{ id: 1, name: 'Finance filter', filter_type: 'Regular', group_key: 'dept' },
|
||||
{ id: 2, name: 'Base restriction', filter_type: 'Base', group_key: null },
|
||||
];
|
||||
|
||||
const asyncRender = (props: RlsBadgeProps) =>
|
||||
waitFor(() => render(<RlsBadge {...props} />));
|
||||
|
||||
test('renders nothing when rlsFilters is empty', async () => {
|
||||
const { container } = await asyncRender({ rlsFilters: [] });
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
test('renders a lock icon when filters exist', async () => {
|
||||
await asyncRender({ rlsFilters: mockFilters });
|
||||
expect(
|
||||
screen.getByRole('img', { name: /row-level security/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders a tooltip with filter details when hovered', async () => {
|
||||
await asyncRender({ rlsFilters: mockFilters });
|
||||
await userEvent.hover(screen.getByRole('img'));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('Row-Level Security');
|
||||
expect(tooltip).toHaveTextContent('Finance filter');
|
||||
expect(tooltip).toHaveTextContent('Base restriction');
|
||||
});
|
||||
|
||||
test('shows filter type in tooltip', async () => {
|
||||
await asyncRender({ rlsFilters: mockFilters });
|
||||
await userEvent.hover(screen.getByRole('img'));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('Regular');
|
||||
expect(tooltip).toHaveTextContent('Base');
|
||||
});
|
||||
|
||||
test('shows group key in tooltip when present', async () => {
|
||||
await asyncRender({ rlsFilters: mockFilters });
|
||||
await userEvent.hover(screen.getByRole('img'));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('[dept]');
|
||||
});
|
||||
|
||||
test('shows inherited suffix and summary note for inherited filters', async () => {
|
||||
const inheritedFilters = [
|
||||
{
|
||||
id: 10,
|
||||
name: 'Inherited filter',
|
||||
filter_type: 'Regular',
|
||||
group_key: null,
|
||||
inherited: true,
|
||||
},
|
||||
];
|
||||
await asyncRender({ rlsFilters: inheritedFilters });
|
||||
await userEvent.hover(screen.getByRole('img'));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('from underlying table');
|
||||
expect(tooltip).toHaveTextContent(
|
||||
'Some filters are inherited from physical tables',
|
||||
);
|
||||
});
|
||||
|
||||
test('shows roles when present', async () => {
|
||||
const filtersWithRoles = [
|
||||
{
|
||||
id: 20,
|
||||
name: 'Role filter',
|
||||
filter_type: 'Regular',
|
||||
group_key: null,
|
||||
roles: [
|
||||
{ id: 1, name: 'Admin' },
|
||||
{ id: 2, name: 'Analyst' },
|
||||
],
|
||||
},
|
||||
];
|
||||
await asyncRender({ rlsFilters: filtersWithRoles });
|
||||
await userEvent.hover(screen.getByRole('img'));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('Roles');
|
||||
expect(tooltip).toHaveTextContent('Admin');
|
||||
expect(tooltip).toHaveTextContent('Analyst');
|
||||
});
|
||||
|
||||
test('shows clause when present', async () => {
|
||||
const filtersWithClause = [
|
||||
{
|
||||
id: 30,
|
||||
name: 'Clause filter',
|
||||
filter_type: 'Base',
|
||||
group_key: null,
|
||||
clause: "dept = 'Finance'",
|
||||
},
|
||||
];
|
||||
await asyncRender({ rlsFilters: filtersWithClause });
|
||||
await userEvent.hover(screen.getByRole('img'));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toHaveTextContent('Clause');
|
||||
expect(tooltip).toHaveTextContent("dept = 'Finance'");
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { useTheme, css } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
import type { IconType } from '@superset-ui/core/components/Icons/types';
|
||||
|
||||
export interface RlsFilterSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
filter_type?: string;
|
||||
group_key?: string | null;
|
||||
inherited?: boolean;
|
||||
clause?: string;
|
||||
roles?: Array<{ id: number; name: string }>;
|
||||
}
|
||||
|
||||
export interface RlsBadgeProps {
|
||||
rlsFilters: RlsFilterSummary[];
|
||||
size?: IconType['iconSize'];
|
||||
}
|
||||
|
||||
export function RlsBadge({ rlsFilters, size = 'l' }: RlsBadgeProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!rlsFilters?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasInherited = rlsFilters.some(f => f.inherited);
|
||||
|
||||
const tooltipContent = (
|
||||
<div>
|
||||
<strong>
|
||||
{t(
|
||||
'Row-Level Security: %d filter(s) may restrict data based on your role.',
|
||||
rlsFilters.length,
|
||||
)}
|
||||
</strong>
|
||||
<ul
|
||||
css={css`
|
||||
padding-left: ${theme.sizeUnit * 4}px;
|
||||
margin: ${theme.sizeUnit}px 0 0;
|
||||
`}
|
||||
>
|
||||
{rlsFilters.map(filter => (
|
||||
<li key={filter.id}>
|
||||
<div>
|
||||
{filter.name}
|
||||
{filter.filter_type ? ` (${filter.filter_type})` : ''}
|
||||
{filter.group_key ? ` [${filter.group_key}]` : ''}
|
||||
{filter.inherited ? ` — ${t('from underlying table')}` : ''}
|
||||
</div>
|
||||
{filter.roles && filter.roles.length > 0 && (
|
||||
<div>
|
||||
{t('Roles')}: {filter.roles.map(role => role.name).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{filter.clause && (
|
||||
<div>
|
||||
{t('Clause')}: {filter.clause}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{hasInherited && (
|
||||
<div
|
||||
css={css`
|
||||
margin-top: ${theme.sizeUnit}px;
|
||||
font-style: italic;
|
||||
`}
|
||||
>
|
||||
{t(
|
||||
'Some filters are inherited from physical tables referenced in this virtual dataset.',
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip id="rls-badge-tooltip" title={tooltipContent}>
|
||||
<Icons.LockOutlined
|
||||
aria-label={t('Row-Level Security')}
|
||||
iconColor={theme.colorWarning}
|
||||
iconSize={size}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,11 @@ export {
|
||||
type ConfirmStatusChangeProps,
|
||||
} from './ConfirmStatusChange';
|
||||
export { CertifiedBadge } from './CertifiedBadge';
|
||||
export {
|
||||
RlsBadge,
|
||||
type RlsFilterSummary,
|
||||
type RlsBadgeProps,
|
||||
} from './RlsBadge';
|
||||
export * from './Icons';
|
||||
export * from './Timer';
|
||||
export {
|
||||
|
||||
@@ -160,42 +160,6 @@ describe('ChartClient', () => {
|
||||
datasource: '1__table',
|
||||
}),
|
||||
).rejects.toEqual(new Error('Unknown chart type: rainbow_3d_pie')));
|
||||
|
||||
test('fetches data from the legacy API if ChartMetadata has useLegacyApi=true,', () => {
|
||||
// note legacy charts do not register a buildQuery function in the registry
|
||||
getChartMetadataRegistry().registerValue(
|
||||
'word_cloud_legacy',
|
||||
new ChartMetadata({
|
||||
name: 'Legacy Word Cloud',
|
||||
thumbnail: '.png',
|
||||
useLegacyApi: true,
|
||||
}),
|
||||
);
|
||||
|
||||
fetchMock.post('glob:*/api/v1/chart/data', () =>
|
||||
Promise.reject(new Error('Unexpected all to v1 API')),
|
||||
);
|
||||
|
||||
// post `Superset.route_base = ""`, the legacy endpoint
|
||||
// collapsed from `/superset/explore_json/` to `/explore_json/`.
|
||||
fetchMock.post('glob:*/explore_json/', {
|
||||
field1: 'abc',
|
||||
field2: 'def',
|
||||
});
|
||||
|
||||
return expect(
|
||||
chartClient.loadQueryData({
|
||||
granularity: 'minute',
|
||||
viz_type: 'word_cloud_legacy',
|
||||
datasource: '1__table',
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
field1: 'abc',
|
||||
field2: 'def',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.loadDatasource(datasourceKey, options)', () => {
|
||||
|
||||
@@ -19,6 +19,19 @@
|
||||
|
||||
import { Page, APIResponse } from '@playwright/test';
|
||||
import rison from 'rison';
|
||||
import getEmptyLayout from '../../../src/dashboard/util/getEmptyLayout';
|
||||
import {
|
||||
BACKGROUND_TRANSPARENT,
|
||||
DASHBOARD_GRID_ID,
|
||||
DASHBOARD_ROOT_ID,
|
||||
GRID_COLUMN_COUNT,
|
||||
GRID_DEFAULT_CHART_WIDTH,
|
||||
} from '../../../src/dashboard/util/constants';
|
||||
import {
|
||||
CHART_TYPE,
|
||||
ROW_TYPE,
|
||||
} from '../../../src/dashboard/util/componentTypes';
|
||||
import type { LayoutItem } from '../../../src/dashboard/types';
|
||||
import {
|
||||
apiGet,
|
||||
apiPost,
|
||||
@@ -47,6 +60,78 @@ export interface DashboardCreatePayload {
|
||||
theme_id?: number;
|
||||
}
|
||||
|
||||
export interface DashboardLayoutChart {
|
||||
id: number;
|
||||
sliceName: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
type DashboardPositionItem = Omit<LayoutItem, 'meta'> & {
|
||||
meta?: LayoutItem['meta'];
|
||||
};
|
||||
|
||||
export type DashboardPositionJson = Record<
|
||||
string,
|
||||
DashboardPositionItem | string
|
||||
>;
|
||||
|
||||
const DASHBOARD_ROW_ID = 'ROW-1';
|
||||
const DEFAULT_CHART_HEIGHT = 50;
|
||||
|
||||
/**
|
||||
* Build a v2 dashboard layout with every chart in one row.
|
||||
*/
|
||||
export function buildSingleRowDashboardLayout(
|
||||
charts: readonly DashboardLayoutChart[],
|
||||
): DashboardPositionJson {
|
||||
const totalWidth = charts.reduce(
|
||||
(sum, chart) => sum + (chart.width ?? GRID_DEFAULT_CHART_WIDTH),
|
||||
0,
|
||||
);
|
||||
if (totalWidth > GRID_COLUMN_COUNT) {
|
||||
throw new Error(
|
||||
`Chart widths total ${totalWidth} columns, exceeding the ` +
|
||||
`${GRID_COLUMN_COUNT}-column dashboard grid`,
|
||||
);
|
||||
}
|
||||
|
||||
const emptyLayout = getEmptyLayout();
|
||||
const chartKeys = charts.map(chart => `CHART-${chart.id}`);
|
||||
const positionJson: DashboardPositionJson = {
|
||||
...emptyLayout,
|
||||
[DASHBOARD_GRID_ID]: {
|
||||
...emptyLayout[DASHBOARD_GRID_ID],
|
||||
children: [DASHBOARD_ROW_ID],
|
||||
},
|
||||
[DASHBOARD_ROW_ID]: {
|
||||
type: ROW_TYPE,
|
||||
id: DASHBOARD_ROW_ID,
|
||||
children: chartKeys,
|
||||
parents: [DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID],
|
||||
meta: { background: BACKGROUND_TRANSPARENT },
|
||||
},
|
||||
};
|
||||
|
||||
charts.forEach(chart => {
|
||||
const chartKey = `CHART-${chart.id}`;
|
||||
positionJson[chartKey] = {
|
||||
type: CHART_TYPE,
|
||||
id: chartKey,
|
||||
children: [],
|
||||
parents: [DASHBOARD_ROOT_ID, DASHBOARD_GRID_ID, DASHBOARD_ROW_ID],
|
||||
meta: {
|
||||
chartId: chart.id,
|
||||
width: chart.width ?? GRID_DEFAULT_CHART_WIDTH,
|
||||
height: chart.height ?? DEFAULT_CHART_HEIGHT,
|
||||
sliceName: chart.sliceName,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return positionJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request to create a dashboard
|
||||
* @param page - Playwright page instance (provides authentication context)
|
||||
|
||||
@@ -69,6 +69,15 @@ export class DashboardPage {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a chart grid component by its chart ID.
|
||||
*/
|
||||
getChart(chartId: number): Locator {
|
||||
return this.page.locator(
|
||||
`[data-test="chart-grid-component"][data-test-chart-id="${chartId}"]`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all charts on the dashboard to finish loading.
|
||||
* Waits until no loading indicators are visible on the page.
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import { apiPost, apiPut } from '../../helpers/api/requests';
|
||||
import { apiPostDashboard } from '../../helpers/api/dashboard';
|
||||
import {
|
||||
apiPostDashboard,
|
||||
buildSingleRowDashboardLayout,
|
||||
} from '../../helpers/api/dashboard';
|
||||
import { getDatasetByName } from '../../helpers/api/dataset';
|
||||
import { DashboardPage } from '../../pages/DashboardPage';
|
||||
import { TIMEOUT } from '../../utils/constants';
|
||||
@@ -61,36 +64,14 @@ testWithAssets(
|
||||
|
||||
// Create dashboard with chart in position_json and a native filter in json_metadata
|
||||
const filterId = `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const chartLayoutKey = `CHART-${chartId}`;
|
||||
const positionJson = {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-1'],
|
||||
parents: ['ROOT_ID'],
|
||||
const positionJson = buildSingleRowDashboardLayout([
|
||||
{
|
||||
id: chartId,
|
||||
sliceName: 'clear_all_repro',
|
||||
width: 6,
|
||||
height: 50,
|
||||
},
|
||||
'ROW-1': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-1',
|
||||
children: [chartLayoutKey],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
[chartLayoutKey]: {
|
||||
type: 'CHART',
|
||||
id: chartLayoutKey,
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
||||
meta: {
|
||||
chartId,
|
||||
width: 6,
|
||||
height: 50,
|
||||
sliceName: 'clear_all_repro',
|
||||
},
|
||||
},
|
||||
};
|
||||
]);
|
||||
|
||||
const jsonMetadata = {
|
||||
native_filter_configuration: [
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* E2E migration of the Cypress "Dashboard load" suite (dashboard/load.test.ts).
|
||||
*
|
||||
* Only the "should load dashboard" case is a genuine end-to-end test: it loads a
|
||||
* multi-chart dashboard and proves every chart renders by issuing real backend
|
||||
* queries. The remaining legacy cases (edit/standalone URL-param rendering,
|
||||
* send-log-data) only assert DOM/URL state with no backend round-trip and belong
|
||||
* in component/RTL coverage instead.
|
||||
*
|
||||
* The dashboard and charts are built via the API and cleaned up by the fixture;
|
||||
* the chart queries use the repository's read-only birth_names example dataset.
|
||||
*
|
||||
* CI green => the dashboard route mounts, every chart POSTs /api/v1/chart/data
|
||||
* successfully, and each chart paints its expected output.
|
||||
* CI red => the dashboard failed to load or a chart never rendered.
|
||||
*/
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import { apiPostChart, apiPutChart } from '../../helpers/api/chart';
|
||||
import {
|
||||
apiPostDashboard,
|
||||
buildSingleRowDashboardLayout,
|
||||
type DashboardLayoutChart,
|
||||
} from '../../helpers/api/dashboard';
|
||||
import { getDatasetByName } from '../../helpers/api/dataset';
|
||||
import { extractIdFromResponse } from '../../helpers/api/assertions';
|
||||
import { TIMEOUT } from '../../utils/constants';
|
||||
import { DashboardPage } from '../../pages/DashboardPage';
|
||||
|
||||
const DATASET_NAME = 'birth_names';
|
||||
const ECHARTS_SERIES_COLOR: [number, number, number] = [31, 168, 201];
|
||||
|
||||
type ChartOutput = 'big-number' | 'table' | 'echarts';
|
||||
type CreatedChart = DashboardLayoutChart & { output: ChartOutput };
|
||||
|
||||
async function canvasColorPixelCount(
|
||||
canvas: Locator,
|
||||
target: [number, number, number],
|
||||
): Promise<number> {
|
||||
return canvas.evaluate((element: HTMLCanvasElement, [red, green, blue]) => {
|
||||
const context = element.getContext('2d');
|
||||
if (!context || element.width === 0 || element.height === 0) {
|
||||
return 0;
|
||||
}
|
||||
const pixels = context.getImageData(0, 0, element.width, element.height);
|
||||
let count = 0;
|
||||
for (let index = 0; index < pixels.data.length; index += 4) {
|
||||
if (
|
||||
pixels.data[index + 3] >= 200 &&
|
||||
Math.abs(pixels.data[index] - red) < 12 &&
|
||||
Math.abs(pixels.data[index + 1] - green) < 12 &&
|
||||
Math.abs(pixels.data[index + 2] - blue) < 12
|
||||
) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}, target);
|
||||
}
|
||||
|
||||
async function expectChartOutput(
|
||||
chart: Locator,
|
||||
output: ChartOutput,
|
||||
): Promise<void> {
|
||||
if (output === 'big-number') {
|
||||
const value = chart.locator(
|
||||
'.superset-legacy-chart-big-number .header-line',
|
||||
);
|
||||
await expect(value).toBeVisible();
|
||||
await expect(value).toHaveText(/\d/);
|
||||
return;
|
||||
}
|
||||
if (output === 'table') {
|
||||
await expect(
|
||||
chart.locator('table tbody tr:not(:has(.dt-no-results))').first(),
|
||||
).toBeVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = chart.locator('canvas').first();
|
||||
await expect(canvas).toBeVisible();
|
||||
await expect
|
||||
.poll(() => canvasColorPixelCount(canvas, ECHARTS_SERIES_COLOR), {
|
||||
timeout: TIMEOUT.API_RESPONSE * 2,
|
||||
message: 'ECharts canvas should paint the configured data series',
|
||||
})
|
||||
.toBeGreaterThan(20);
|
||||
}
|
||||
|
||||
testWithAssets(
|
||||
'dashboard loads and every chart renders via real queries',
|
||||
async ({ page, testAssets }) => {
|
||||
// Building + loading a multi-chart dashboard chains several slow queries.
|
||||
testWithAssets.setTimeout(TIMEOUT.SLOW_TEST);
|
||||
|
||||
const dataset = await getDatasetByName(page, DATASET_NAME);
|
||||
if (!dataset) {
|
||||
throw new Error(`Dataset ${DATASET_NAME} not found`);
|
||||
}
|
||||
const datasetId = dataset.id;
|
||||
const datasource = `${datasetId}__table`;
|
||||
|
||||
// A spread of viz types that all render cleanly from the birth_names dataset.
|
||||
const chartSpecs: {
|
||||
viz_type: string;
|
||||
output: ChartOutput;
|
||||
params: Record<string, unknown>;
|
||||
}[] = [
|
||||
{
|
||||
viz_type: 'big_number_total',
|
||||
output: 'big-number',
|
||||
params: { datasource, viz_type: 'big_number_total', metric: 'count' },
|
||||
},
|
||||
{
|
||||
viz_type: 'table',
|
||||
output: 'table',
|
||||
params: {
|
||||
datasource,
|
||||
viz_type: 'table',
|
||||
query_mode: 'aggregate',
|
||||
groupby: ['name'],
|
||||
metrics: ['count'],
|
||||
row_limit: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
viz_type: 'echarts_timeseries_line',
|
||||
output: 'echarts',
|
||||
params: {
|
||||
datasource,
|
||||
viz_type: 'echarts_timeseries_line',
|
||||
x_axis: 'ds',
|
||||
xAxisForceCategorical: true,
|
||||
time_grain_sqla: 'P1Y',
|
||||
metrics: ['count'],
|
||||
groupby: [],
|
||||
row_limit: 100,
|
||||
color_scheme: 'supersetColors',
|
||||
show_legend: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Parallel-safe suffix so chart/dashboard names never collide across workers.
|
||||
const uniqueSuffix = `${Date.now()}_${testWithAssets.info().parallelIndex}`;
|
||||
|
||||
// Create each chart via the API.
|
||||
const charts: CreatedChart[] = [];
|
||||
for (const spec of chartSpecs) {
|
||||
const sliceName = `load_smoke_${spec.viz_type}_${uniqueSuffix}`;
|
||||
const resp = await apiPostChart(page, {
|
||||
slice_name: sliceName,
|
||||
viz_type: spec.viz_type,
|
||||
datasource_id: datasetId,
|
||||
datasource_type: 'table',
|
||||
params: JSON.stringify(spec.params),
|
||||
});
|
||||
expect(resp.ok()).toBe(true);
|
||||
const chartId = await extractIdFromResponse(resp);
|
||||
testAssets.trackChart(chartId);
|
||||
charts.push({ id: chartId, sliceName, output: spec.output });
|
||||
}
|
||||
const chartIds = charts.map(chart => chart.id);
|
||||
|
||||
// Lay all charts out in a single row.
|
||||
const positionJson = buildSingleRowDashboardLayout(charts);
|
||||
|
||||
const dashResp = await apiPostDashboard(page, {
|
||||
dashboard_title: `load_smoke_${uniqueSuffix}`,
|
||||
published: true,
|
||||
position_json: JSON.stringify(positionJson),
|
||||
});
|
||||
expect(dashResp.ok()).toBe(true);
|
||||
const dashboardId = await extractIdFromResponse(dashResp);
|
||||
testAssets.trackDashboard(dashboardId);
|
||||
|
||||
// Associate every chart with the dashboard so they actually render.
|
||||
for (const chartId of chartIds) {
|
||||
await apiPutChart(page, chartId, { dashboards: [dashboardId] });
|
||||
}
|
||||
|
||||
// Record the real chart-data round-trips the dashboard makes on load,
|
||||
// keyed by the chart each one queried for. The chart-data POST carries its
|
||||
// slice id in the encoded `form_data={"slice_id":<id>}` query param (see
|
||||
// chartAction.ts), so parsing it lets us prove every chart queried — not
|
||||
// just that some chart did.
|
||||
const chartDataStatusBySliceId = new Map<number, number>();
|
||||
page.on('response', response => {
|
||||
const request = response.request();
|
||||
if (
|
||||
request.method() !== 'POST' ||
|
||||
!response.url().includes('/api/v1/chart/data')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const formData = new URL(response.url()).searchParams.get('form_data');
|
||||
if (!formData) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const sliceId = JSON.parse(formData).slice_id;
|
||||
if (typeof sliceId === 'number') {
|
||||
chartDataStatusBySliceId.set(sliceId, response.status());
|
||||
}
|
||||
} catch {
|
||||
// Not a slice-id form_data payload; ignore.
|
||||
}
|
||||
});
|
||||
|
||||
const dashboard = new DashboardPage(page);
|
||||
await dashboard.gotoById(dashboardId);
|
||||
await dashboard.waitForLoad();
|
||||
|
||||
// Assert the real terminal output for each known visualization rather than
|
||||
// inferring completion from a generic wrapper shared by loading and errors.
|
||||
for (const chart of charts) {
|
||||
await expectChartOutput(dashboard.getChart(chart.id), chart.output);
|
||||
}
|
||||
|
||||
// The render came from real backend queries: every chart issued its own
|
||||
// chart-data POST and each one was accepted. 202 counts as accepted — with
|
||||
// GLOBAL_ASYNC_QUERIES enabled a cold-cache query legitimately returns 202
|
||||
// and delivers its result out of band. The render assertion above is what
|
||||
// proves the data actually arrived, so this only needs to rule out a chart
|
||||
// that never queried or was rejected outright.
|
||||
for (const chartId of chartIds) {
|
||||
const status = chartDataStatusBySliceId.get(chartId);
|
||||
expect(
|
||||
status,
|
||||
`chart ${chartId} should have issued a /api/v1/chart/data POST`,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
[200, 202],
|
||||
`chart ${chartId}'s /api/v1/chart/data response should be 200 or 202, got ${status}`,
|
||||
).toContain(status);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -25,7 +25,10 @@
|
||||
*/
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import { apiPost, apiPut } from '../../helpers/api/requests';
|
||||
import { apiPostDashboard } from '../../helpers/api/dashboard';
|
||||
import {
|
||||
apiPostDashboard,
|
||||
buildSingleRowDashboardLayout,
|
||||
} from '../../helpers/api/dashboard';
|
||||
import { getDatasetByName } from '../../helpers/api/dataset';
|
||||
import { DashboardPage } from '../../pages/DashboardPage';
|
||||
|
||||
@@ -73,36 +76,14 @@ testWithAssets(
|
||||
const chartId: number = chart.id ?? chart.result?.id;
|
||||
testAssets.trackChart(chartId);
|
||||
|
||||
const chartLayoutKey = `CHART-${chartId}`;
|
||||
const positionJson = {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-1'],
|
||||
parents: ['ROOT_ID'],
|
||||
const positionJson = buildSingleRowDashboardLayout([
|
||||
{
|
||||
id: chartId,
|
||||
sliceName: 'display_control_repro',
|
||||
width: 6,
|
||||
height: 50,
|
||||
},
|
||||
'ROW-1': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-1',
|
||||
children: [chartLayoutKey],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
[chartLayoutKey]: {
|
||||
type: 'CHART',
|
||||
id: chartLayoutKey,
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
||||
meta: {
|
||||
chartId,
|
||||
width: 6,
|
||||
height: 50,
|
||||
sliceName: 'display_control_repro',
|
||||
},
|
||||
},
|
||||
};
|
||||
]);
|
||||
|
||||
// 2. json_metadata: one dashboard filter + one Display Control.
|
||||
const filterId = `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`;
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
*/
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import { apiPostChart, apiPutChart } from '../../helpers/api/chart';
|
||||
import { apiPostDashboard } from '../../helpers/api/dashboard';
|
||||
import {
|
||||
apiPostDashboard,
|
||||
buildSingleRowDashboardLayout,
|
||||
} from '../../helpers/api/dashboard';
|
||||
import { getDatasetByName } from '../../helpers/api/dataset';
|
||||
import { DashboardPage } from '../../pages/DashboardPage';
|
||||
|
||||
@@ -93,36 +96,9 @@ testWithAssets(
|
||||
}
|
||||
testAssets.trackChart(chartId);
|
||||
|
||||
const chartLayoutKey = `CHART-${chartId}`;
|
||||
const positionJson = {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-1'],
|
||||
parents: ['ROOT_ID'],
|
||||
},
|
||||
'ROW-1': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-1',
|
||||
children: [chartLayoutKey],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
[chartLayoutKey]: {
|
||||
type: 'CHART',
|
||||
id: chartLayoutKey,
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
||||
meta: {
|
||||
chartId,
|
||||
width: 6,
|
||||
height: 60,
|
||||
sliceName,
|
||||
},
|
||||
},
|
||||
};
|
||||
const positionJson = buildSingleRowDashboardLayout([
|
||||
{ id: chartId, sliceName, width: 6, height: 60 },
|
||||
]);
|
||||
const dashResp = await apiPostDashboard(page, {
|
||||
dashboard_title: `gauge_interval_colors_${Date.now()}`,
|
||||
published: true,
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
*/
|
||||
import { testWithAssets, expect } from '../../helpers/fixtures';
|
||||
import { apiPost, apiPut } from '../../helpers/api/requests';
|
||||
import { apiPostDashboard } from '../../helpers/api/dashboard';
|
||||
import {
|
||||
apiPostDashboard,
|
||||
buildSingleRowDashboardLayout,
|
||||
} from '../../helpers/api/dashboard';
|
||||
import { DashboardPage } from '../../pages/DashboardPage';
|
||||
|
||||
const DATASET_NAME = 'birth_names';
|
||||
@@ -86,37 +89,15 @@ testWithAssets(
|
||||
const chartId: number = (await chartResp.json()).id;
|
||||
testAssets.trackChart(chartId);
|
||||
|
||||
const chartLayoutKey = `CHART-${chartId}`;
|
||||
const filterId = `NATIVE_FILTER-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const positionJson = {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-1'],
|
||||
parents: ['ROOT_ID'],
|
||||
const positionJson = buildSingleRowDashboardLayout([
|
||||
{
|
||||
id: chartId,
|
||||
sliceName: 'mixed_filter_repro',
|
||||
width: 8,
|
||||
height: 60,
|
||||
},
|
||||
'ROW-1': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-1',
|
||||
children: [chartLayoutKey],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
[chartLayoutKey]: {
|
||||
type: 'CHART',
|
||||
id: chartLayoutKey,
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
||||
meta: {
|
||||
chartId,
|
||||
width: 8,
|
||||
height: 60,
|
||||
sliceName: 'mixed_filter_repro',
|
||||
},
|
||||
},
|
||||
};
|
||||
]);
|
||||
const jsonMetadata = {
|
||||
native_filter_configuration: [
|
||||
{
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
*
|
||||
* NOTE: the embedded suite only runs when the embedded SDK bundle is built and
|
||||
* INCLUDE_EMBEDDED=true (CI sets both). It is skipped otherwise.
|
||||
*
|
||||
* NOTE: the embedded project runs with admin storageState, and the session
|
||||
* cookie the iframe ends up with is racy: the /embedded/<uuid> response
|
||||
* rotates it to an anonymous session while the SDK's parallel csrf and
|
||||
* guest-token fetches rewrite the admin one, so chart data requests are
|
||||
* evaluated as either admin or guest depending on which response lands last.
|
||||
* Fixture charts must therefore hold up under GUEST evaluation — see the
|
||||
* query_context note below — or the suite only passes when admin wins.
|
||||
*/
|
||||
import { test, expect, Browser, BrowserContext, Page } from '@playwright/test';
|
||||
import { createServer, IncomingMessage, ServerResponse, Server } from 'http';
|
||||
@@ -53,6 +61,7 @@ import { apiPost, apiPut } from '../../helpers/api/requests';
|
||||
import {
|
||||
apiPostDashboard,
|
||||
apiDeleteDashboard,
|
||||
buildSingleRowDashboardLayout,
|
||||
} from '../../helpers/api/dashboard';
|
||||
import { apiDeleteChart } from '../../helpers/api/chart';
|
||||
import { EmbeddedPage } from '../../pages/EmbeddedPage';
|
||||
@@ -181,45 +190,54 @@ test.describe('Embedded Pivot Table collapse state (#33406)', () => {
|
||||
row_limit: 1000,
|
||||
order_desc: true,
|
||||
};
|
||||
// Charts saved through Explore always persist a query_context alongside
|
||||
// params. Store one here too: guest (embedded) requests are validated
|
||||
// against the stored chart, and a params-only chart makes the guest
|
||||
// payload look tampered (its query `columns` aren't found on the chart),
|
||||
// failing every chart data request with a 403.
|
||||
const queryContext = {
|
||||
datasource: { id: datasetId, type: 'table' },
|
||||
force: false,
|
||||
queries: [
|
||||
{
|
||||
filters: [],
|
||||
extras: { having: '', where: '' },
|
||||
applied_time_extras: {},
|
||||
columns: ['state', 'name'],
|
||||
metrics: ['count'],
|
||||
orderby: [['count', false]],
|
||||
annotation_layers: [],
|
||||
row_limit: 1000,
|
||||
series_limit: 0,
|
||||
order_desc: true,
|
||||
url_params: {},
|
||||
custom_params: {},
|
||||
custom_form_data: {},
|
||||
},
|
||||
],
|
||||
form_data: params,
|
||||
result_format: 'json',
|
||||
result_type: 'full',
|
||||
};
|
||||
const chartResp = await apiPost(setupPage, 'api/v1/chart/', {
|
||||
slice_name: `pivot_collapse_repro_${Date.now()}`,
|
||||
viz_type: 'pivot_table_v2',
|
||||
datasource_id: datasetId,
|
||||
datasource_type: 'table',
|
||||
params: JSON.stringify(params),
|
||||
query_context: JSON.stringify(queryContext),
|
||||
query_context_generation: true,
|
||||
});
|
||||
chartId = (await chartResp.json()).id;
|
||||
|
||||
const chartLayoutKey = `CHART-${chartId}`;
|
||||
const positionJson = {
|
||||
DASHBOARD_VERSION_KEY: 'v2',
|
||||
ROOT_ID: { type: 'ROOT', id: 'ROOT_ID', children: ['GRID_ID'] },
|
||||
GRID_ID: {
|
||||
type: 'GRID',
|
||||
id: 'GRID_ID',
|
||||
children: ['ROW-1'],
|
||||
parents: ['ROOT_ID'],
|
||||
const positionJson = buildSingleRowDashboardLayout([
|
||||
{
|
||||
id: chartId,
|
||||
sliceName: 'pivot_collapse_repro',
|
||||
width: 6,
|
||||
height: 80,
|
||||
},
|
||||
'ROW-1': {
|
||||
type: 'ROW',
|
||||
id: 'ROW-1',
|
||||
children: [chartLayoutKey],
|
||||
parents: ['ROOT_ID', 'GRID_ID'],
|
||||
meta: { background: 'BACKGROUND_TRANSPARENT' },
|
||||
},
|
||||
[chartLayoutKey]: {
|
||||
type: 'CHART',
|
||||
id: chartLayoutKey,
|
||||
children: [],
|
||||
parents: ['ROOT_ID', 'GRID_ID', 'ROW-1'],
|
||||
meta: {
|
||||
chartId,
|
||||
width: 6,
|
||||
height: 80,
|
||||
sliceName: 'pivot_collapse_repro',
|
||||
},
|
||||
},
|
||||
};
|
||||
]);
|
||||
const dashResp = await apiPostDashboard(setupPage, {
|
||||
dashboard_title: `pivot_collapse_repro_${Date.now()}`,
|
||||
published: true,
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Crawls every dashboard on the target instance and refreshes viz-picker
|
||||
* gallery thumbnails from live example charts.
|
||||
*
|
||||
* This is a maintenance tool, not a test: charts are DISCOVERED via the
|
||||
* API (dashboards -> charts), so it keeps working as example dashboards
|
||||
* evolve. For each viz type found, one representative chart is rendered
|
||||
* standalone at 512x512 and the plugin's `thumbnail.png` is overwritten.
|
||||
* The only static piece is the viz type -> image path map below, which
|
||||
* changes when plugins are added or moved — never when examples change.
|
||||
* Viz types found on dashboards but missing from the map are reported at
|
||||
* the end without failing the run.
|
||||
*
|
||||
* It only runs when CAPTURE_THUMBNAILS=1 is set, so the regular
|
||||
* Playwright suites never execute it.
|
||||
*
|
||||
* Usage (requires a running Superset with examples loaded):
|
||||
* npm run playwright:thumbnails
|
||||
* VIZ_TYPES=bullet,rose npm run playwright:thumbnails # subset
|
||||
*
|
||||
* Notes:
|
||||
* - Dark variants (`thumbnail-dark.png`, `example-dark.jpg`) are captured
|
||||
* via prefers-color-scheme emulation whenever the sibling file exists;
|
||||
* if the app ignores the emulation (dark theming disabled) the dark
|
||||
* file is left untouched.
|
||||
* - New gallery images (e.g. the Line percent-change example) still need
|
||||
* to be registered in the plugin's metadata before they render in the
|
||||
* gallery; the capture logs a reminder for any new image.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const THUMBNAIL_SIZE = 512;
|
||||
const RENDERED_CHART_SELECTOR =
|
||||
'[data-test="chart-container"]:has(svg, canvas, table):not(:has([data-test="loading-indicator"]))';
|
||||
/**
|
||||
* Charts that render plain markup — no svg/canvas/table for the rendered
|
||||
* selector to key on — get a looser signal plus a longer settle.
|
||||
*/
|
||||
const TEXT_ONLY_VIZ_TYPES = new Set([
|
||||
'ag-grid-table',
|
||||
'big_number_total',
|
||||
'handlebars',
|
||||
'pop_kpi',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Hover this element just before the screenshot, so charts whose identity
|
||||
* benefits from an interaction (a visible tooltip) capture mid-hover.
|
||||
*/
|
||||
const HOVER_BEFORE_CAPTURE: Record<string, string> = {
|
||||
cal_heatmap: '[data-test="chart-container"] svg rect[class*=" r"]',
|
||||
};
|
||||
const TEXT_RENDERED_CHART_SELECTOR =
|
||||
'[data-test="chart-container"]:not(:has([data-test="loading-indicator"]))';
|
||||
|
||||
/** superset-frontend root, resolved from this spec's location. */
|
||||
const FRONTEND_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
const ECHARTS = 'plugins/plugin-chart-echarts/src';
|
||||
const DECKGL = 'plugins/preset-chart-deckgl/src/layers';
|
||||
|
||||
/**
|
||||
* Where each viz type's gallery thumbnail lives, relative to
|
||||
* superset-frontend. One entry per registered viz type with a thumbnail;
|
||||
* add a line when a new plugin ships.
|
||||
*/
|
||||
const VIZ_TYPE_THUMBNAILS: Record<string, string> = {
|
||||
'ag-grid-table':
|
||||
'plugins/plugin-chart-ag-grid-table/src/images/thumbnail.png',
|
||||
big_number: `${ECHARTS}/BigNumber/BigNumberWithTrendline/images/thumbnail.png`,
|
||||
big_number_total: `${ECHARTS}/BigNumber/BigNumberTotal/images/thumbnail.png`,
|
||||
box_plot: `${ECHARTS}/BoxPlot/images/thumbnail.png`,
|
||||
bubble_v2: `${ECHARTS}/Bubble/images/thumbnail.png`,
|
||||
bullet: `${ECHARTS}/Bullet/images/thumbnail.png`,
|
||||
cal_heatmap: 'plugins/plugin-chart-calendar/src/images/thumbnail.png',
|
||||
cartodiagram: 'plugins/plugin-chart-cartodiagram/src/images/thumbnail.png',
|
||||
chord: 'plugins/plugin-chart-chord/src/images/thumbnail.png',
|
||||
country_map: 'plugins/plugin-chart-country-map/src/images/thumbnail.png',
|
||||
deck_arc: `${DECKGL}/Arc/images/thumbnail.png`,
|
||||
deck_contour: `${DECKGL}/Contour/images/thumbnail.png`,
|
||||
deck_geojson: `${DECKGL}/Geojson/images/thumbnail.png`,
|
||||
deck_grid: `${DECKGL}/Grid/images/thumbnail.png`,
|
||||
deck_heatmap: `${DECKGL}/Heatmap/images/thumbnail.png`,
|
||||
deck_hex: `${DECKGL}/Hex/images/thumbnail.png`,
|
||||
deck_multi: 'plugins/preset-chart-deckgl/src/Multi/images/thumbnail.png',
|
||||
deck_path: `${DECKGL}/Path/images/thumbnail.png`,
|
||||
deck_polygon: `${DECKGL}/Polygon/images/thumbnail.png`,
|
||||
deck_scatter: `${DECKGL}/Scatter/images/thumbnail.png`,
|
||||
deck_screengrid: `${DECKGL}/Screengrid/images/thumbnail.png`,
|
||||
echarts_area: `${ECHARTS}/Timeseries/Area/images/thumbnail.png`,
|
||||
echarts_timeseries: `${ECHARTS}/Timeseries/images/thumbnail.png`,
|
||||
echarts_timeseries_bar: `${ECHARTS}/Timeseries/Regular/Bar/images/thumbnail.png`,
|
||||
echarts_timeseries_line: `${ECHARTS}/Timeseries/Regular/Line/images/thumbnail.png`,
|
||||
echarts_timeseries_scatter: `${ECHARTS}/Timeseries/Regular/Scatter/images/thumbnail.png`,
|
||||
echarts_timeseries_smooth: `${ECHARTS}/Timeseries/Regular/SmoothLine/images/thumbnail.png`,
|
||||
echarts_timeseries_step: `${ECHARTS}/Timeseries/Step/images/thumbnail.png`,
|
||||
funnel: `${ECHARTS}/Funnel/images/thumbnail.png`,
|
||||
gantt_chart: `${ECHARTS}/Gantt/images/thumbnail.png`,
|
||||
gauge_chart: `${ECHARTS}/Gauge/images/thumbnail.png`,
|
||||
graph_chart: `${ECHARTS}/Graph/images/thumbnail.png`,
|
||||
// handlebars intentionally unmapped: its generic logo represents the
|
||||
// template-anything nature of the chart better than any one example.
|
||||
heatmap_v2: `${ECHARTS}/Heatmap/images/thumbnail.png`,
|
||||
histogram_v2: `${ECHARTS}/Histogram/images/thumbnail.png`,
|
||||
horizon: 'plugins/plugin-chart-horizon/src/images/thumbnail.png',
|
||||
mixed_timeseries: `${ECHARTS}/MixedTimeseries/images/thumbnail.png`,
|
||||
paired_ttest: 'plugins/plugin-chart-paired-t-test/src/images/thumbnail.png',
|
||||
para: 'plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png',
|
||||
partition: 'plugins/plugin-chart-partition/src/images/thumbnail.png',
|
||||
pie: `${ECHARTS}/Pie/images/thumbnail.png`,
|
||||
pivot_table_v2: 'plugins/plugin-chart-pivot-table/src/images/thumbnail.png',
|
||||
point_cluster_map:
|
||||
'plugins/plugin-chart-point-cluster-map/src/images/thumbnail.png',
|
||||
pop_kpi: `${ECHARTS}/BigNumber/BigNumberPeriodOverPeriod/images/thumbnail.png`,
|
||||
radar: `${ECHARTS}/Radar/images/thumbnail.png`,
|
||||
rose: 'plugins/plugin-chart-rose/src/images/thumbnail.png',
|
||||
sankey_v2: `${ECHARTS}/Sankey/images/thumbnail.png`,
|
||||
sunburst_v2: `${ECHARTS}/Sunburst/images/thumbnail.png`,
|
||||
table: 'plugins/plugin-chart-table/src/images/thumbnail.png',
|
||||
time_pivot: `${ECHARTS}/TimePivot/images/thumbnail.png`,
|
||||
time_table: 'src/visualizations/TimeTable/images/thumbnail.png',
|
||||
tree_chart: `${ECHARTS}/Tree/images/thumbnail.png`,
|
||||
treemap_v2: `${ECHARTS}/Treemap/images/thumbnail.png`,
|
||||
waterfall: `${ECHARTS}/Waterfall/images/thumbnail.png`,
|
||||
word_cloud: 'plugins/plugin-chart-word-cloud/src/images/thumbnail.png',
|
||||
world_map: 'plugins/plugin-chart-world-map/src/images/thumbnail.png',
|
||||
};
|
||||
|
||||
/**
|
||||
* When several example charts share a viz type, prefer these slices over
|
||||
* the default alphabetically-first pick. Missing slices fall back to the
|
||||
* default, so stale entries degrade gracefully.
|
||||
*/
|
||||
const PREFERRED_SLICES: Record<string, string> = {
|
||||
big_number: 'Sales Year over Year',
|
||||
bubble_v2: 'Life Expectancy VS Rural %',
|
||||
bullet: 'Total Sales Bullet',
|
||||
cal_heatmap: 'Sales Calendar Heatmap',
|
||||
chord: 'Product Line Chord',
|
||||
echarts_area: 'Sales Stacked Area',
|
||||
echarts_timeseries_bar: 'Sales Stacked Bars',
|
||||
echarts_timeseries_line: 'Monthly Sales Line',
|
||||
echarts_timeseries_smooth: 'Monthly Sales Smooth',
|
||||
echarts_timeseries_step: 'Quarterly Sales Steps',
|
||||
funnel: 'Population Funnel',
|
||||
gauge_chart: 'Rural Population Gauge',
|
||||
heatmap_v2: 'Sales Grid Heatmap',
|
||||
histogram_v2: 'Life Expectancy Histogram',
|
||||
mixed_timeseries: 'Sales Mixed Chart',
|
||||
paired_ttest: 'Population Paired t-Test',
|
||||
partition: 'Population Partition',
|
||||
pie: 'Product Line Donut',
|
||||
pivot_table_v2: 'Sales Pivot Highlights',
|
||||
radar: 'Game Sales Radar',
|
||||
rose: 'Population Nightingale Rose',
|
||||
sunburst_v2: 'Population Sunburst',
|
||||
table: 'Sales Summary Table',
|
||||
time_pivot: 'Sales Period Pivot',
|
||||
time_table: 'Product Line Time Table',
|
||||
treemap_v2: 'Population Treemap',
|
||||
waterfall: 'Monthly Sales Waterfall',
|
||||
};
|
||||
|
||||
/** Gallery example images use a wide aspect, matching the existing art. */
|
||||
const EXAMPLE_WIDTH = 800;
|
||||
const EXAMPLE_HEIGHT = 460;
|
||||
|
||||
interface ExtraCapture {
|
||||
sliceName: string;
|
||||
output: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra captures beyond the per-viz-type thumbnail: gallery example
|
||||
* images that are obsolete (the Bullet/TimePivot/TimeTable examples
|
||||
* still show the removed nvd3 renderers) and brand-new gallery images.
|
||||
* Register new images in the plugin metadata after capturing.
|
||||
*/
|
||||
const EXTRA_CAPTURES: ExtraCapture[] = [
|
||||
{
|
||||
sliceName: 'Population Percent Change',
|
||||
output: `${ECHARTS}/Timeseries/Regular/Line/images/Line3.png`,
|
||||
width: EXAMPLE_WIDTH,
|
||||
height: EXAMPLE_HEIGHT,
|
||||
},
|
||||
{
|
||||
sliceName: 'Total Sales Bullet',
|
||||
output: `${ECHARTS}/Bullet/images/example.jpg`,
|
||||
width: EXAMPLE_WIDTH,
|
||||
height: EXAMPLE_HEIGHT,
|
||||
},
|
||||
{
|
||||
sliceName: 'Sales Period Pivot',
|
||||
output: `${ECHARTS}/TimePivot/images/example.jpg`,
|
||||
width: EXAMPLE_WIDTH,
|
||||
height: EXAMPLE_HEIGHT,
|
||||
},
|
||||
{
|
||||
sliceName: 'Product Line Time Table',
|
||||
output: 'src/visualizations/TimeTable/images/example.jpg',
|
||||
width: EXAMPLE_WIDTH,
|
||||
height: EXAMPLE_HEIGHT,
|
||||
},
|
||||
];
|
||||
|
||||
interface ExampleChart {
|
||||
id: number;
|
||||
sliceName: string;
|
||||
vizType: string;
|
||||
}
|
||||
|
||||
interface DashboardRow {
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface DashboardChartRow {
|
||||
id?: number;
|
||||
slice_name?: string;
|
||||
form_data?: { viz_type?: string };
|
||||
}
|
||||
|
||||
/** Pages through a list endpoint, returning every result row. */
|
||||
async function fetchAllPages<T>(page: Page, endpoint: string): Promise<T[]> {
|
||||
const rows: T[] = [];
|
||||
const pageSize = 100;
|
||||
for (let pageNum = 0; ; pageNum += 1) {
|
||||
const q = encodeURIComponent(
|
||||
`(page_size:${pageSize},page:${pageNum},order_direction:asc)`,
|
||||
);
|
||||
const response = await page.request.get(`${endpoint}?q=${q}`);
|
||||
expect(response.ok(), `GET ${endpoint} page ${pageNum}`).toBeTruthy();
|
||||
const { result }: { result: T[] } = await response.json();
|
||||
rows.push(...result);
|
||||
if (result.length < pageSize) return rows;
|
||||
}
|
||||
}
|
||||
|
||||
/** Discovers every chart placed on any dashboard. */
|
||||
async function discoverDashboardCharts(page: Page): Promise<ExampleChart[]> {
|
||||
const dashboards = await fetchAllPages<DashboardRow>(
|
||||
page,
|
||||
'/api/v1/dashboard/',
|
||||
);
|
||||
const chartsById = new Map<number, ExampleChart>();
|
||||
for (const dashboard of dashboards) {
|
||||
const response = await page.request.get(
|
||||
`/api/v1/dashboard/${dashboard.id}/charts`,
|
||||
);
|
||||
if (!response.ok()) continue;
|
||||
const { result }: { result: DashboardChartRow[] } = await response.json();
|
||||
for (const chart of result) {
|
||||
const vizType = chart.form_data?.viz_type;
|
||||
if (chart.id && chart.slice_name && vizType) {
|
||||
chartsById.set(chart.id, {
|
||||
id: chart.id,
|
||||
sliceName: chart.slice_name,
|
||||
vizType,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...chartsById.values()];
|
||||
}
|
||||
|
||||
/** thumbnail.png -> thumbnail-dark.png, example.jpg -> example-dark.jpg */
|
||||
function darkSibling(output: string): string {
|
||||
return output.replace(/\.(png|jpg)$/, '-dark.$1');
|
||||
}
|
||||
|
||||
async function renderAndShoot(
|
||||
page: Page,
|
||||
chart: ExampleChart,
|
||||
colorScheme: 'light' | 'dark',
|
||||
): Promise<Buffer> {
|
||||
// An explicit navigation timeout keeps one hung load from stalling the
|
||||
// whole crawl until the test timeout.
|
||||
await page.goto(`/explore/?slice_id=${chart.id}&standalone=1`, {
|
||||
timeout: 60_000,
|
||||
});
|
||||
// Dashboards render charts on colorBgContainer cards, but the standalone
|
||||
// explore page paints the gray colorBgLayout (via the antd Layout
|
||||
// wrapper); match the dashboard context so thumbnails look like charts
|
||||
// do where users see them.
|
||||
await page
|
||||
.addStyleTag({
|
||||
content: `body, .ant-layout { background: ${
|
||||
colorScheme === 'dark' ? '#141414' : '#ffffff'
|
||||
} !important; }`,
|
||||
})
|
||||
.catch(() => {});
|
||||
const textOnly = TEXT_ONLY_VIZ_TYPES.has(chart.vizType);
|
||||
await page
|
||||
.locator(textOnly ? TEXT_RENDERED_CHART_SELECTOR : RENDERED_CHART_SELECTOR)
|
||||
.first()
|
||||
.waitFor({ state: 'visible', timeout: 60_000 });
|
||||
// Give animations/map tiles (or text-only chart data) time to settle
|
||||
await page.waitForTimeout(textOnly ? 4_000 : 2_000);
|
||||
// Some thumbnails read better mid-interaction (e.g. the calendar heatmap
|
||||
// showing its tooltip); hover the configured element before the still.
|
||||
const hoverSelector = HOVER_BEFORE_CAPTURE[chart.vizType];
|
||||
if (hoverSelector) {
|
||||
// Hover a mid-chart element rather than the first (often an empty
|
||||
// corner cell), falling back to the first when there are few.
|
||||
const cells = page.locator(hoverSelector);
|
||||
const count = await cells.count().catch(() => 0);
|
||||
await cells
|
||||
.nth(Math.floor(count / 2))
|
||||
.hover({ timeout: 5_000 })
|
||||
.catch(() => {});
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
return page.screenshot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a chart light and (when a dark variant is wanted) dark. Dark
|
||||
* rendering relies on the app following prefers-color-scheme (theme mode
|
||||
* SYSTEM); if the dark render is byte-identical to the light one the app
|
||||
* ignored the emulation, and the dark file is left untouched rather than
|
||||
* overwritten with light-theme art.
|
||||
*/
|
||||
async function captureChart(
|
||||
page: Page,
|
||||
chart: ExampleChart,
|
||||
output: string,
|
||||
size: { width: number; height: number },
|
||||
): Promise<void> {
|
||||
const outputPath = path.join(FRONTEND_ROOT, output);
|
||||
const darkPath = path.join(FRONTEND_ROOT, darkSibling(output));
|
||||
const isNewImage = !fs.existsSync(outputPath);
|
||||
const wantDark = fs.existsSync(darkPath) || isNewImage;
|
||||
|
||||
await page.setViewportSize(size);
|
||||
await page.emulateMedia({ colorScheme: 'light' });
|
||||
const lightShot = await renderAndShoot(page, chart, 'light');
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, lightShot);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`captured ${chart.sliceName} (${chart.vizType}) -> ${output}`);
|
||||
|
||||
if (wantDark) {
|
||||
await page.emulateMedia({ colorScheme: 'dark' });
|
||||
const darkShot = await renderAndShoot(page, chart, 'dark');
|
||||
if (darkShot.equals(lightShot)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`SKIPPED dark variant for ${chart.sliceName}: the app ignored the dark color-scheme emulation (is dark theming enabled?)`,
|
||||
);
|
||||
} else {
|
||||
fs.writeFileSync(darkPath, darkShot);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`captured ${chart.sliceName} (dark) -> ${darkSibling(output)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewImage) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`NOTE: ${output} is a new gallery image — register it in the plugin metadata (exampleGallery) to surface it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('capture viz thumbnails', () => {
|
||||
test.skip(
|
||||
!process.env.CAPTURE_THUMBNAILS,
|
||||
'Thumbnail capture only runs with CAPTURE_THUMBNAILS=1',
|
||||
);
|
||||
|
||||
test('crawls example dashboards and refreshes gallery thumbnails', async ({
|
||||
page,
|
||||
}) => {
|
||||
test.setTimeout(90 * 60_000);
|
||||
|
||||
const vizTypeFilter = process.env.VIZ_TYPES
|
||||
? new Set(process.env.VIZ_TYPES.split(',').map(v => v.trim()))
|
||||
: null;
|
||||
|
||||
const charts = await discoverDashboardCharts(page);
|
||||
expect(
|
||||
charts.length,
|
||||
'no dashboard charts found — are examples loaded?',
|
||||
).toBeGreaterThan(0);
|
||||
|
||||
// One representative chart per viz type: the preferred slice when
|
||||
// present, else the alphabetically-first slice for determinism.
|
||||
const byVizType = new Map<string, ExampleChart[]>();
|
||||
for (const chart of charts) {
|
||||
const group = byVizType.get(chart.vizType) ?? [];
|
||||
group.push(chart);
|
||||
byVizType.set(chart.vizType, group);
|
||||
}
|
||||
|
||||
const thumbnailSize = { width: THUMBNAIL_SIZE, height: THUMBNAIL_SIZE };
|
||||
const unmapped: string[] = [];
|
||||
const failures: string[] = [];
|
||||
for (const [vizType, group] of [...byVizType.entries()].sort()) {
|
||||
if (vizTypeFilter && !vizTypeFilter.has(vizType)) continue;
|
||||
const output = VIZ_TYPE_THUMBNAILS[vizType];
|
||||
if (!output) {
|
||||
unmapped.push(vizType);
|
||||
continue;
|
||||
}
|
||||
group.sort((a, b) => a.sliceName.localeCompare(b.sliceName));
|
||||
const chart =
|
||||
group.find(c => c.sliceName === PREFERRED_SLICES[vizType]) ?? group[0];
|
||||
try {
|
||||
await captureChart(page, chart, output, thumbnailSize);
|
||||
} catch (error) {
|
||||
failures.push(`${vizType} (${chart.sliceName}): ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const extra of EXTRA_CAPTURES) {
|
||||
const chart = charts.find(c => c.sliceName === extra.sliceName);
|
||||
if (!chart || (vizTypeFilter && !vizTypeFilter.has(chart.vizType))) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await captureChart(page, chart, extra.output, {
|
||||
width: extra.width,
|
||||
height: extra.height,
|
||||
});
|
||||
} catch (error) {
|
||||
failures.push(`${extra.sliceName}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (unmapped.length) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`viz types on dashboards with no thumbnail mapping (add to VIZ_TYPE_THUMBNAILS if wanted): ${unmapped.join(', ')}`,
|
||||
);
|
||||
}
|
||||
expect(failures, failures.join('\n')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 44 KiB |
@@ -1,143 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import { memo } from 'react';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import TTestTable, { DataEntry } from './TTestTable';
|
||||
|
||||
interface PairedTTestProps {
|
||||
alpha?: number;
|
||||
className?: string;
|
||||
data: Record<string, DataEntry[]>;
|
||||
groups: string[];
|
||||
liftValPrec?: number;
|
||||
metrics: string[];
|
||||
pValPrec?: number;
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
${({ theme }) => `
|
||||
.superset-legacy-chart-paired-t-test .scrollbar-container {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.paired-ttest-table .scrollbar-content {
|
||||
padding-left: ${theme.sizeUnit}px;
|
||||
padding-right: ${theme.sizeUnit}px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.paired-ttest-table table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.paired-ttest-table h1 {
|
||||
margin-left: ${theme.sizeUnit}px;
|
||||
}
|
||||
|
||||
.reactable-data tr {
|
||||
font-feature-settings: 'tnum' 1;
|
||||
}
|
||||
|
||||
.reactable-data tr,
|
||||
.reactable-header-sortable {
|
||||
-webkit-transition: ease-in-out 0.1s;
|
||||
transition: ease-in-out 0.1s;
|
||||
}
|
||||
|
||||
.reactable-data tr:hover {
|
||||
background-color: ${theme.colorFillTertiary};
|
||||
}
|
||||
|
||||
.reactable-data tr .false {
|
||||
color: ${theme.colorError};
|
||||
}
|
||||
|
||||
.reactable-data tr .true {
|
||||
color: ${theme.colorSuccess};
|
||||
}
|
||||
|
||||
.reactable-data tr .control {
|
||||
color: ${theme.colorPrimary};
|
||||
}
|
||||
|
||||
.reactable-data tr .invalid {
|
||||
color: ${theme.colorWarning};
|
||||
}
|
||||
|
||||
.reactable-data .control td {
|
||||
background-color: ${theme.colorFillTertiary};
|
||||
}
|
||||
|
||||
.reactable-header-sortable:hover,
|
||||
.reactable-header-sortable:focus,
|
||||
.reactable-header-sort-asc,
|
||||
.reactable-header-sort-desc {
|
||||
background-color: ${theme.colorFillTertiary};
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.reactable-header-sort-asc:after {
|
||||
content: '\\25bc';
|
||||
position: absolute;
|
||||
right: ${theme.sizeUnit * 3}px;
|
||||
}
|
||||
|
||||
.reactable-header-sort-desc:after {
|
||||
content: '\\25b2';
|
||||
position: absolute;
|
||||
right: ${theme.sizeUnit * 3}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function PairedTTest({
|
||||
alpha = 0.05,
|
||||
className = '',
|
||||
data,
|
||||
groups,
|
||||
liftValPrec = 4,
|
||||
metrics,
|
||||
pValPrec = 6,
|
||||
}: PairedTTestProps) {
|
||||
return (
|
||||
<StyledDiv>
|
||||
<div className={`superset-legacy-chart-paired-t-test ${className}`}>
|
||||
<div className="paired-ttest-table">
|
||||
<div className="scrollbar-content">
|
||||
{metrics.map((metric, i) => (
|
||||
<TTestTable
|
||||
key={i}
|
||||
metric={metric}
|
||||
groups={groups}
|
||||
data={data[metric]}
|
||||
alpha={alpha}
|
||||
pValPrec={Math.min(pValPrec, 32)}
|
||||
liftValPrec={Math.min(liftValPrec, 32)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
// memo preserves the shallow-prop render bailout of the PureComponent original
|
||||
export default memo(PairedTTest);
|
||||
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 60 KiB |
@@ -1,113 +0,0 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.20.0](https://github.com/apache/superset/compare/v2021.41.0...v0.20.0) (2024-09-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- adding missing examples for bubble chart, bullet chart, calendar heatmap chart and country map chart in the gallery ([#22523](https://github.com/apache/superset/issues/22523)) ([839ec7c](https://github.com/apache/superset/commit/839ec7ceacc66c65928fd0ddead2b014db3d5563))
|
||||
- Adds the Deprecated label to Time-series Percent Change chart ([#30148](https://github.com/apache/superset/issues/30148)) ([5e42d7a](https://github.com/apache/superset/commit/5e42d7aed0d11c7aac91ab19088d2632e49da614))
|
||||
- **area chart legacy:** tool tip shows actual value rather than y axi… ([#23469](https://github.com/apache/superset/issues/23469)) ([db9ca20](https://github.com/apache/superset/commit/db9ca20737fecda8eee342b34d62d3b700ef3687))
|
||||
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
|
||||
- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d))
|
||||
- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220))
|
||||
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
|
||||
- **legacy-chart:** corrupted raw chart data ([#24850](https://github.com/apache/superset/issues/24850)) ([1c5971d](https://github.com/apache/superset/commit/1c5971d3afb70a338444c41943ff90c3a9c03ec3))
|
||||
- Rename legacy line and area charts ([#28113](https://github.com/apache/superset/issues/28113)) ([b4c4ab7](https://github.com/apache/superset/commit/b4c4ab7790cbeb8d65ec7c1084482c21932e755b))
|
||||
- Reset sorting bar issue in Barchart ([#19371](https://github.com/apache/superset/issues/19371)) ([94e06c2](https://github.com/apache/superset/commit/94e06c2b6a1f782133bb9ef85a1d46ce7eacf9ba))
|
||||
- **storybook:** fix broken Storybook stories during development ([#29587](https://github.com/apache/superset/issues/29587)) ([462cda4](https://github.com/apache/superset/commit/462cda400baa00b3bcc4a7f8aded362ca55e18a5))
|
||||
- Tooltip of area chart shows undefined total ([#24916](https://github.com/apache/superset/issues/24916)) ([ec9e9a4](https://github.com/apache/superset/commit/ec9e9a46f2f092ce56d3ed5a8a9a3ea0214db88a))
|
||||
- warning of nth-child ([#23638](https://github.com/apache/superset/issues/23638)) ([16cc089](https://github.com/apache/superset/commit/16cc089b198dcdebc2422845aa08d18233c6b3a4))
|
||||
- Zero values on Dual Line axis bounds ([#23649](https://github.com/apache/superset/issues/23649)) ([d66e6e6](https://github.com/apache/superset/commit/d66e6e6d400db0fee35d73cd43e610cd1c491f4b))
|
||||
|
||||
### Features
|
||||
|
||||
- Adds the ECharts Bubble chart ([#22107](https://github.com/apache/superset/issues/22107)) ([c81c60c](https://github.com/apache/superset/commit/c81c60c91fbcb09dd63c05f050e18ee09ceebfd6))
|
||||
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
|
||||
- **chart & legend:** make to enable show legend by default ([#19927](https://github.com/apache/superset/issues/19927)) ([7b3d0f0](https://github.com/apache/superset/commit/7b3d0f040b050905f7d0901d0227f1cd6b761b56))
|
||||
- **explore:** Apply denormalization to tier 2 charts form data ([#20524](https://github.com/apache/superset/issues/20524)) ([e12ee59](https://github.com/apache/superset/commit/e12ee59b13822241dca8d8015f1222c477edd4f3))
|
||||
- **explore:** Denormalize form data in echarts, world map and nvd3 bar and line charts ([#20313](https://github.com/apache/superset/issues/20313)) ([354a899](https://github.com/apache/superset/commit/354a89950c4d001da3e107f60788cea873bd6bf6))
|
||||
- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97))
|
||||
- **explore:** standardized controls for time pivot chart ([#21321](https://github.com/apache/superset/issues/21321)) ([79525df](https://github.com/apache/superset/commit/79525dfaf29b810af668e3b6c5a56cd866370d92))
|
||||
- **formatters:** Add custom d3-time-format locale ([#24263](https://github.com/apache/superset/issues/24263)) ([024cfd8](https://github.com/apache/superset/commit/024cfd86e408ec5f7ddf49a9e90908e2fb2e6b70))
|
||||
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
|
||||
- **legacy-preset-chart-nvd3:** add richtooltip in nvd3 bar chart ([#17615](https://github.com/apache/superset/issues/17615)) ([72f3215](https://github.com/apache/superset/commit/72f3215ffc74ead33dba57196aeaf4e1db63fd6c))
|
||||
- Migrates Dual Line Chart to Mixed Chart ([#23910](https://github.com/apache/superset/issues/23910)) ([f5148ef](https://github.com/apache/superset/commit/f5148ef728ce649697c10fb7aa65982d7dd05638))
|
||||
- Removes the Multiple Line Charts ([#23933](https://github.com/apache/superset/issues/23933)) ([6ce8592](https://github.com/apache/superset/commit/6ce85921fc103ba0e93b437d473003e6f1b4a42b))
|
||||
- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9))
|
||||
- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719))
|
||||
- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082))
|
||||
|
||||
# [0.19.0](https://github.com/apache/superset/compare/v2021.41.0...v0.19.0) (2024-09-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- adding missing examples for bubble chart, bullet chart, calendar heatmap chart and country map chart in the gallery ([#22523](https://github.com/apache/superset/issues/22523)) ([839ec7c](https://github.com/apache/superset/commit/839ec7ceacc66c65928fd0ddead2b014db3d5563))
|
||||
- Adds the Deprecated label to Time-series Percent Change chart ([#30148](https://github.com/apache/superset/issues/30148)) ([5e42d7a](https://github.com/apache/superset/commit/5e42d7aed0d11c7aac91ab19088d2632e49da614))
|
||||
- **area chart legacy:** tool tip shows actual value rather than y axi… ([#23469](https://github.com/apache/superset/issues/23469)) ([db9ca20](https://github.com/apache/superset/commit/db9ca20737fecda8eee342b34d62d3b700ef3687))
|
||||
- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964))
|
||||
- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d))
|
||||
- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220))
|
||||
- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b))
|
||||
- **legacy-chart:** corrupted raw chart data ([#24850](https://github.com/apache/superset/issues/24850)) ([1c5971d](https://github.com/apache/superset/commit/1c5971d3afb70a338444c41943ff90c3a9c03ec3))
|
||||
- Rename legacy line and area charts ([#28113](https://github.com/apache/superset/issues/28113)) ([b4c4ab7](https://github.com/apache/superset/commit/b4c4ab7790cbeb8d65ec7c1084482c21932e755b))
|
||||
- Reset sorting bar issue in Barchart ([#19371](https://github.com/apache/superset/issues/19371)) ([94e06c2](https://github.com/apache/superset/commit/94e06c2b6a1f782133bb9ef85a1d46ce7eacf9ba))
|
||||
- **storybook:** fix broken Storybook stories during development ([#29587](https://github.com/apache/superset/issues/29587)) ([462cda4](https://github.com/apache/superset/commit/462cda400baa00b3bcc4a7f8aded362ca55e18a5))
|
||||
- Tooltip of area chart shows undefined total ([#24916](https://github.com/apache/superset/issues/24916)) ([ec9e9a4](https://github.com/apache/superset/commit/ec9e9a46f2f092ce56d3ed5a8a9a3ea0214db88a))
|
||||
- warning of nth-child ([#23638](https://github.com/apache/superset/issues/23638)) ([16cc089](https://github.com/apache/superset/commit/16cc089b198dcdebc2422845aa08d18233c6b3a4))
|
||||
- Zero values on Dual Line axis bounds ([#23649](https://github.com/apache/superset/issues/23649)) ([d66e6e6](https://github.com/apache/superset/commit/d66e6e6d400db0fee35d73cd43e610cd1c491f4b))
|
||||
|
||||
### Features
|
||||
|
||||
- Adds the ECharts Bubble chart ([#22107](https://github.com/apache/superset/issues/22107)) ([c81c60c](https://github.com/apache/superset/commit/c81c60c91fbcb09dd63c05f050e18ee09ceebfd6))
|
||||
- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442))
|
||||
- **chart & legend:** make to enable show legend by default ([#19927](https://github.com/apache/superset/issues/19927)) ([7b3d0f0](https://github.com/apache/superset/commit/7b3d0f040b050905f7d0901d0227f1cd6b761b56))
|
||||
- **explore:** Apply denormalization to tier 2 charts form data ([#20524](https://github.com/apache/superset/issues/20524)) ([e12ee59](https://github.com/apache/superset/commit/e12ee59b13822241dca8d8015f1222c477edd4f3))
|
||||
- **explore:** Denormalize form data in echarts, world map and nvd3 bar and line charts ([#20313](https://github.com/apache/superset/issues/20313)) ([354a899](https://github.com/apache/superset/commit/354a89950c4d001da3e107f60788cea873bd6bf6))
|
||||
- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97))
|
||||
- **explore:** standardized controls for time pivot chart ([#21321](https://github.com/apache/superset/issues/21321)) ([79525df](https://github.com/apache/superset/commit/79525dfaf29b810af668e3b6c5a56cd866370d92))
|
||||
- **formatters:** Add custom d3-time-format locale ([#24263](https://github.com/apache/superset/issues/24263)) ([024cfd8](https://github.com/apache/superset/commit/024cfd86e408ec5f7ddf49a9e90908e2fb2e6b70))
|
||||
- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11))
|
||||
- **legacy-preset-chart-nvd3:** add richtooltip in nvd3 bar chart ([#17615](https://github.com/apache/superset/issues/17615)) ([72f3215](https://github.com/apache/superset/commit/72f3215ffc74ead33dba57196aeaf4e1db63fd6c))
|
||||
- Migrates Dual Line Chart to Mixed Chart ([#23910](https://github.com/apache/superset/issues/23910)) ([f5148ef](https://github.com/apache/superset/commit/f5148ef728ce649697c10fb7aa65982d7dd05638))
|
||||
- Removes the Multiple Line Charts ([#23933](https://github.com/apache/superset/issues/23933)) ([6ce8592](https://github.com/apache/superset/commit/6ce85921fc103ba0e93b437d473003e6f1b4a42b))
|
||||
- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9))
|
||||
- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719))
|
||||
- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082))
|
||||
|
||||
# [0.18.0](https://github.com/apache-superset/superset-ui/compare/v0.17.87...v0.18.0) (2021-08-30)
|
||||
|
||||
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
|
||||
|
||||
## [0.17.63](https://github.com/apache-superset/superset-ui/compare/v0.17.62...v0.17.63) (2021-07-02)
|
||||
|
||||
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
|
||||
|
||||
## [0.17.62](https://github.com/apache-superset/superset-ui/compare/v0.17.61...v0.17.62) (2021-07-02)
|
||||
|
||||
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
|
||||
|
||||
## [0.17.61](https://github.com/apache-superset/superset-ui/compare/v0.17.60...v0.17.61) (2021-07-02)
|
||||
|
||||
**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3
|
||||
@@ -1,64 +0,0 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## @superset-ui/legacy-preset-chart-nvd3
|
||||
|
||||
[](https://www.npmjs.com/package/@superset-ui/legacy-preset-chart-nvd3)
|
||||
[](https://libraries.io/npm/@superset-ui%2Flegacy-preset-chart-nvd3)
|
||||
|
||||
This plugin provides Big Number for Superset.
|
||||
|
||||
### Usage
|
||||
|
||||
Import the preset and register. This will register all the chart plugins under nvd3.
|
||||
|
||||
```js
|
||||
import { NVD3ChartPreset } from '@superset-ui/legacy-preset-chart-nvd3';
|
||||
|
||||
new NVD3ChartPreset().register();
|
||||
```
|
||||
|
||||
or register charts one by one. Configure `key`, which can be any `string`, and register the plugin.
|
||||
This `key` will be used to lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import {
|
||||
AreaChartPlugin,
|
||||
LineChartPlugin,
|
||||
} from '@superset-ui/legacy-preset-chart-nvd3';
|
||||
|
||||
new AreaChartPlugin().configure({ key: 'area' }).register();
|
||||
new LineChartPlugin().configure({ key: 'line' }).register();
|
||||
```
|
||||
|
||||
Then use it via `SuperChart`. See
|
||||
[storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-nvd3)
|
||||
for more details.
|
||||
|
||||
```js
|
||||
<SuperChart
|
||||
chartType="line"
|
||||
width={600}
|
||||
height={600}
|
||||
formData={...}
|
||||
queriesData={[{
|
||||
data: {...},
|
||||
}]}
|
||||
/>
|
||||
```
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"name": "@superset-ui/legacy-preset-chart-nvd3",
|
||||
"version": "0.20.3",
|
||||
"description": "Superset Legacy Chart - NVD3",
|
||||
"sideEffects": [
|
||||
"*.css"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"files": [
|
||||
"esm",
|
||||
"lib"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/packages/legacy-preset-chart-nvd3"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"author": "Superset",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-preset-chart-nvd3#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-tip": "^0.9.1",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"dompurify": "^3.4.12",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11",
|
||||
"lodash-es": "^4.18.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SuperChart, VizType } from '@superset-ui/core';
|
||||
import { EchartsBoxPlotChartPlugin } from '@superset-ui/plugin-chart-echarts';
|
||||
import { dummyDatasource, withResizableChartDemo } from '@storybook-shared';
|
||||
import data from './data';
|
||||
|
||||
new EchartsBoxPlotChartPlugin().configure({ key: 'box-plot' }).register();
|
||||
|
||||
export default {
|
||||
title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/BoxPlot',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
colorScheme: 'd3Category10',
|
||||
whiskerOptions: 'Min/max (no outliers)',
|
||||
},
|
||||
argTypes: {
|
||||
colorScheme: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'supersetColors',
|
||||
'd3Category10',
|
||||
'bnbColors',
|
||||
'googleCategory20c',
|
||||
],
|
||||
},
|
||||
whiskerOptions: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'Tukey',
|
||||
'Min/max (no outliers)',
|
||||
'2/98 percentiles',
|
||||
'9/91 percentiles',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Basic = ({
|
||||
colorScheme,
|
||||
whiskerOptions,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
colorScheme: string;
|
||||
whiskerOptions: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType="box-plot"
|
||||
width={width}
|
||||
height={height}
|
||||
datasource={dummyDatasource}
|
||||
queriesData={[{ data }]}
|
||||
formData={{
|
||||
color_scheme: colorScheme,
|
||||
viz_type: VizType.BoxPlot,
|
||||
whisker_options: whiskerOptions,
|
||||
groupby: ['region'],
|
||||
metrics: ['sum__SP_POP_TOTL'],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
// Data format for ECharts BoxPlot - requires metric__min, metric__q1, metric__median, etc.
|
||||
export default [
|
||||
{
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL__min: 1031863394.0,
|
||||
sum__SP_POP_TOTL__q1: 1384725172.5,
|
||||
sum__SP_POP_TOTL__median: 1717904169.0,
|
||||
sum__SP_POP_TOTL__q3: 2032724922.5,
|
||||
sum__SP_POP_TOTL__max: 2240687901.0,
|
||||
sum__SP_POP_TOTL__mean: 1681000000.0,
|
||||
sum__SP_POP_TOTL__count: 50,
|
||||
sum__SP_POP_TOTL__outliers: [],
|
||||
},
|
||||
{
|
||||
region: 'Europe & Central Asia',
|
||||
sum__SP_POP_TOTL__min: 660881033.0,
|
||||
sum__SP_POP_TOTL__q1: 751386460.5,
|
||||
sum__SP_POP_TOTL__median: 820716895.0,
|
||||
sum__SP_POP_TOTL__q3: 862814192.5,
|
||||
sum__SP_POP_TOTL__max: 903095786.0,
|
||||
sum__SP_POP_TOTL__mean: 799778873.0,
|
||||
sum__SP_POP_TOTL__count: 50,
|
||||
sum__SP_POP_TOTL__outliers: [],
|
||||
},
|
||||
{
|
||||
region: 'Latin America & Caribbean',
|
||||
sum__SP_POP_TOTL__min: 220564224.0,
|
||||
sum__SP_POP_TOTL__q1: 313690832.5,
|
||||
sum__SP_POP_TOTL__median: 421490233.0,
|
||||
sum__SP_POP_TOTL__q3: 529668114.5,
|
||||
sum__SP_POP_TOTL__max: 626270167.0,
|
||||
sum__SP_POP_TOTL__mean: 422336714.0,
|
||||
sum__SP_POP_TOTL__count: 50,
|
||||
sum__SP_POP_TOTL__outliers: [],
|
||||
},
|
||||
{
|
||||
region: 'South Asia',
|
||||
sum__SP_POP_TOTL__min: 572036107.0,
|
||||
sum__SP_POP_TOTL__q1: 772373036.5,
|
||||
sum__SP_POP_TOTL__median: 1059570231.0,
|
||||
sum__SP_POP_TOTL__q3: 1398841234.0,
|
||||
sum__SP_POP_TOTL__max: 1720976995.0,
|
||||
sum__SP_POP_TOTL__mean: 1104759521.0,
|
||||
sum__SP_POP_TOTL__count: 50,
|
||||
sum__SP_POP_TOTL__outliers: [],
|
||||
},
|
||||
{
|
||||
region: 'Sub-Saharan Africa',
|
||||
sum__SP_POP_TOTL__min: 228268752.0,
|
||||
sum__SP_POP_TOTL__q1: 320037758.0,
|
||||
sum__SP_POP_TOTL__median: 467337821.0,
|
||||
sum__SP_POP_TOTL__q3: 676768689.0,
|
||||
sum__SP_POP_TOTL__max: 974315323.0,
|
||||
sum__SP_POP_TOTL__mean: 533345669.0,
|
||||
sum__SP_POP_TOTL__count: 50,
|
||||
sum__SP_POP_TOTL__outliers: [1100000000, 1200000000],
|
||||
},
|
||||
];
|
||||
@@ -1,140 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
formatSelectOptions,
|
||||
D3_FORMAT_OPTIONS,
|
||||
getStandardizedControls,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
showLegend,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
bottomMargin,
|
||||
xTicksLayout,
|
||||
xAxisFormat,
|
||||
yLogScale,
|
||||
xAxisShowMinmax,
|
||||
yAxisShowMinmax,
|
||||
leftMargin,
|
||||
yAxisBounds,
|
||||
} from '../NVD3Controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
{
|
||||
label: t('Query'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
['series'],
|
||||
['entity'],
|
||||
['x'],
|
||||
['y'],
|
||||
['adhoc_filters'],
|
||||
['size'],
|
||||
[
|
||||
{
|
||||
name: 'max_bubble_size',
|
||||
config: {
|
||||
type: 'SelectControl',
|
||||
freeForm: true,
|
||||
label: t('Max Bubble Size'),
|
||||
default: '25',
|
||||
choices: formatSelectOptions([
|
||||
'5',
|
||||
'10',
|
||||
'15',
|
||||
'25',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
['limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [['color_scheme'], [showLegend, null]],
|
||||
},
|
||||
{
|
||||
label: t('X Axis'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[xAxisLabel, leftMargin],
|
||||
[
|
||||
{
|
||||
name: xAxisFormat.name,
|
||||
config: {
|
||||
...xAxisFormat.config,
|
||||
default: 'SMART_NUMBER',
|
||||
choices: D3_FORMAT_OPTIONS,
|
||||
},
|
||||
},
|
||||
xTicksLayout,
|
||||
],
|
||||
[
|
||||
{
|
||||
name: 'x_log_scale',
|
||||
config: {
|
||||
type: 'CheckboxControl',
|
||||
label: t('X Log Scale'),
|
||||
default: false,
|
||||
renderTrigger: true,
|
||||
description: t('Use a log scale for the X-axis'),
|
||||
},
|
||||
},
|
||||
xAxisShowMinmax,
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Y Axis'),
|
||||
expanded: true,
|
||||
tabOverride: 'customize',
|
||||
controlSetRows: [
|
||||
[yAxisLabel, bottomMargin],
|
||||
['y_axis_format', null],
|
||||
[yLogScale, yAxisShowMinmax],
|
||||
[yAxisBounds],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
color_scheme: {
|
||||
renderTrigger: false,
|
||||
},
|
||||
},
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
series: getStandardizedControls().shiftColumn(),
|
||||
entity: getStandardizedControls().shiftColumn(),
|
||||
x: getStandardizedControls().shiftMetric(),
|
||||
y: getStandardizedControls().shiftMetric(),
|
||||
size: getStandardizedControls().shiftMetric(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 44 KiB |
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SuperChart, VizType } from '@superset-ui/core';
|
||||
import { BubbleChartPlugin } from '@superset-ui/legacy-preset-chart-nvd3';
|
||||
import { dummyDatasource, withResizableChartDemo } from '@storybook-shared';
|
||||
import data from './data';
|
||||
|
||||
new BubbleChartPlugin().configure({ key: VizType.LegacyBubble }).register();
|
||||
|
||||
export default {
|
||||
title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/Bubble',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
colorScheme: 'd3Category10',
|
||||
maxBubbleSize: 50,
|
||||
showLegend: true,
|
||||
xLogScale: false,
|
||||
yLogScale: false,
|
||||
},
|
||||
argTypes: {
|
||||
colorScheme: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'supersetColors',
|
||||
'd3Category10',
|
||||
'bnbColors',
|
||||
'googleCategory20c',
|
||||
],
|
||||
},
|
||||
maxBubbleSize: {
|
||||
control: { type: 'range', min: 10, max: 100, step: 5 },
|
||||
},
|
||||
showLegend: { control: 'boolean' },
|
||||
xLogScale: { control: 'boolean' },
|
||||
yLogScale: { control: 'boolean' },
|
||||
},
|
||||
};
|
||||
|
||||
export const Basic = ({
|
||||
colorScheme,
|
||||
maxBubbleSize,
|
||||
showLegend,
|
||||
xLogScale,
|
||||
yLogScale,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
colorScheme: string;
|
||||
maxBubbleSize: number;
|
||||
showLegend: boolean;
|
||||
xLogScale: boolean;
|
||||
yLogScale: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType={VizType.LegacyBubble}
|
||||
width={width}
|
||||
height={height}
|
||||
datasource={dummyDatasource}
|
||||
queriesData={[{ data }]}
|
||||
formData={{
|
||||
annotation_data: {},
|
||||
bottom_margin: 'auto',
|
||||
color_scheme: colorScheme,
|
||||
entity: 'country_name',
|
||||
left_margin: 'auto',
|
||||
max_bubble_size: String(maxBubbleSize),
|
||||
series: 'region',
|
||||
show_legend: showLegend,
|
||||
size: 'sum__SP_POP_TOTL',
|
||||
viz_type: VizType.LegacyBubble,
|
||||
x: 'sum__SP_RUR_TOTL_ZS',
|
||||
x_axis_format: '.3s',
|
||||
x_axis_label: 'Rural Population %',
|
||||
x_axis_showminmax: false,
|
||||
x_log_scale: xLogScale,
|
||||
x_ticks_layout: 'auto',
|
||||
y: 'sum__SP_DYN_LE00_IN',
|
||||
y_axis_format: '.3s',
|
||||
y_axis_label: 'Life Expectancy',
|
||||
y_axis_showminmax: false,
|
||||
y_log_scale: yLogScale,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1,357 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable sort-keys, no-magic-numbers */
|
||||
export default [
|
||||
{
|
||||
key: 'East Asia & Pacific',
|
||||
values: [
|
||||
{
|
||||
country_name: 'China',
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL: 1344130000.0,
|
||||
sum__SP_RUR_TOTL_ZS: 49.427,
|
||||
sum__SP_DYN_LE00_IN: 75.042,
|
||||
x: 49.427,
|
||||
y: 75.042,
|
||||
size: 1344130000.0,
|
||||
shape: 'circle',
|
||||
group: 'East Asia & Pacific',
|
||||
},
|
||||
{
|
||||
country_name: 'Indonesia',
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL: 244808254.0,
|
||||
sum__SP_RUR_TOTL_ZS: 49.288,
|
||||
sum__SP_DYN_LE00_IN: 70.3915609756,
|
||||
x: 49.288,
|
||||
y: 70.3915609756,
|
||||
size: 244808254.0,
|
||||
shape: 'circle',
|
||||
group: 'East Asia & Pacific',
|
||||
},
|
||||
{
|
||||
country_name: 'Japan',
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL: 127817277.0,
|
||||
sum__SP_RUR_TOTL_ZS: 8.752,
|
||||
sum__SP_DYN_LE00_IN: 82.5912195122,
|
||||
x: 8.752,
|
||||
y: 82.5912195122,
|
||||
size: 127817277.0,
|
||||
shape: 'circle',
|
||||
group: 'East Asia & Pacific',
|
||||
},
|
||||
{
|
||||
country_name: 'Philippines',
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL: 94501233.0,
|
||||
sum__SP_RUR_TOTL_ZS: 54.983,
|
||||
sum__SP_DYN_LE00_IN: 68.3914878049,
|
||||
x: 54.983,
|
||||
y: 68.3914878049,
|
||||
size: 94501233.0,
|
||||
shape: 'circle',
|
||||
group: 'East Asia & Pacific',
|
||||
},
|
||||
{
|
||||
country_name: 'Vietnam',
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL: 87840000.0,
|
||||
sum__SP_RUR_TOTL_ZS: 68.971,
|
||||
sum__SP_DYN_LE00_IN: 75.457902439,
|
||||
x: 68.971,
|
||||
y: 75.457902439,
|
||||
size: 87840000.0,
|
||||
shape: 'circle',
|
||||
group: 'East Asia & Pacific',
|
||||
},
|
||||
{
|
||||
country_name: 'Thailand',
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL: 66902958.0,
|
||||
sum__SP_RUR_TOTL_ZS: 54.606,
|
||||
sum__SP_DYN_LE00_IN: 74.008902439,
|
||||
x: 54.606,
|
||||
y: 74.008902439,
|
||||
size: 66902958.0,
|
||||
shape: 'circle',
|
||||
group: 'East Asia & Pacific',
|
||||
},
|
||||
{
|
||||
country_name: 'Myanmar',
|
||||
region: 'East Asia & Pacific',
|
||||
sum__SP_POP_TOTL: 52125411.0,
|
||||
sum__SP_RUR_TOTL_ZS: 68.065,
|
||||
sum__SP_DYN_LE00_IN: 64.7612439024,
|
||||
x: 68.065,
|
||||
y: 64.7612439024,
|
||||
size: 52125411.0,
|
||||
shape: 'circle',
|
||||
group: 'East Asia & Pacific',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'South Asia',
|
||||
values: [
|
||||
{
|
||||
country_name: 'India',
|
||||
region: 'South Asia',
|
||||
sum__SP_POP_TOTL: 1247446011.0,
|
||||
sum__SP_RUR_TOTL_ZS: 68.724,
|
||||
sum__SP_DYN_LE00_IN: 65.9584878049,
|
||||
x: 68.724,
|
||||
y: 65.9584878049,
|
||||
size: 1247446011.0,
|
||||
shape: 'circle',
|
||||
group: 'South Asia',
|
||||
},
|
||||
{
|
||||
country_name: 'Pakistan',
|
||||
region: 'South Asia',
|
||||
sum__SP_POP_TOTL: 173669648.0,
|
||||
sum__SP_RUR_TOTL_ZS: 62.993,
|
||||
sum__SP_DYN_LE00_IN: 66.2838780488,
|
||||
x: 62.993,
|
||||
y: 66.2838780488,
|
||||
size: 173669648.0,
|
||||
shape: 'circle',
|
||||
group: 'South Asia',
|
||||
},
|
||||
{
|
||||
country_name: 'Bangladesh',
|
||||
region: 'South Asia',
|
||||
sum__SP_POP_TOTL: 153405612.0,
|
||||
sum__SP_RUR_TOTL_ZS: 68.775,
|
||||
sum__SP_DYN_LE00_IN: 69.891804878,
|
||||
x: 68.775,
|
||||
y: 69.891804878,
|
||||
size: 153405612.0,
|
||||
shape: 'circle',
|
||||
group: 'South Asia',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'North America',
|
||||
values: [
|
||||
{
|
||||
country_name: 'United States',
|
||||
region: 'North America',
|
||||
sum__SP_POP_TOTL: 311721632.0,
|
||||
sum__SP_RUR_TOTL_ZS: 19.06,
|
||||
sum__SP_DYN_LE00_IN: 78.6414634146,
|
||||
x: 19.06,
|
||||
y: 78.6414634146,
|
||||
size: 311721632.0,
|
||||
shape: 'circle',
|
||||
group: 'North America',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Latin America & Caribbean',
|
||||
values: [
|
||||
{
|
||||
country_name: 'Brazil',
|
||||
region: 'Latin America & Caribbean',
|
||||
sum__SP_POP_TOTL: 200517584.0,
|
||||
sum__SP_RUR_TOTL_ZS: 15.377,
|
||||
sum__SP_DYN_LE00_IN: 73.3473658537,
|
||||
x: 15.377,
|
||||
y: 73.3473658537,
|
||||
size: 200517584.0,
|
||||
shape: 'circle',
|
||||
group: 'Latin America & Caribbean',
|
||||
},
|
||||
{
|
||||
country_name: 'Mexico',
|
||||
region: 'Latin America & Caribbean',
|
||||
sum__SP_POP_TOTL: 120365271.0,
|
||||
sum__SP_RUR_TOTL_ZS: 21.882,
|
||||
sum__SP_DYN_LE00_IN: 76.9141707317,
|
||||
x: 21.882,
|
||||
y: 76.9141707317,
|
||||
size: 120365271.0,
|
||||
shape: 'circle',
|
||||
group: 'Latin America & Caribbean',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Sub-Saharan Africa',
|
||||
values: [
|
||||
{
|
||||
country_name: 'Nigeria',
|
||||
region: 'Sub-Saharan Africa',
|
||||
sum__SP_POP_TOTL: 163770669.0,
|
||||
sum__SP_RUR_TOTL_ZS: 55.638,
|
||||
sum__SP_DYN_LE00_IN: 51.7102439024,
|
||||
x: 55.638,
|
||||
y: 51.7102439024,
|
||||
size: 163770669.0,
|
||||
shape: 'circle',
|
||||
group: 'Sub-Saharan Africa',
|
||||
},
|
||||
{
|
||||
country_name: 'Ethiopia',
|
||||
region: 'Sub-Saharan Africa',
|
||||
sum__SP_POP_TOTL: 89858696.0,
|
||||
sum__SP_RUR_TOTL_ZS: 82.265,
|
||||
sum__SP_DYN_LE00_IN: 62.2528536585,
|
||||
x: 82.265,
|
||||
y: 62.2528536585,
|
||||
size: 89858696.0,
|
||||
shape: 'circle',
|
||||
group: 'Sub-Saharan Africa',
|
||||
},
|
||||
{
|
||||
country_name: 'Congo, Dem. Rep.',
|
||||
region: 'Sub-Saharan Africa',
|
||||
sum__SP_POP_TOTL: 68087376.0,
|
||||
sum__SP_RUR_TOTL_ZS: 59.558,
|
||||
sum__SP_DYN_LE00_IN: 49.3007073171,
|
||||
x: 59.558,
|
||||
y: 49.3007073171,
|
||||
size: 68087376.0,
|
||||
shape: 'circle',
|
||||
group: 'Sub-Saharan Africa',
|
||||
},
|
||||
{
|
||||
country_name: 'South Africa',
|
||||
region: 'Sub-Saharan Africa',
|
||||
sum__SP_POP_TOTL: 51553479.0,
|
||||
sum__SP_RUR_TOTL_ZS: 37.254,
|
||||
sum__SP_DYN_LE00_IN: 55.2956585366,
|
||||
x: 37.254,
|
||||
y: 55.2956585366,
|
||||
size: 51553479.0,
|
||||
shape: 'circle',
|
||||
group: 'Sub-Saharan Africa',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Europe & Central Asia',
|
||||
values: [
|
||||
{
|
||||
country_name: 'Russian Federation',
|
||||
region: 'Europe & Central Asia',
|
||||
sum__SP_POP_TOTL: 142960868.0,
|
||||
sum__SP_RUR_TOTL_ZS: 26.268,
|
||||
sum__SP_DYN_LE00_IN: 69.6585365854,
|
||||
x: 26.268,
|
||||
y: 69.6585365854,
|
||||
size: 142960868.0,
|
||||
shape: 'circle',
|
||||
group: 'Europe & Central Asia',
|
||||
},
|
||||
{
|
||||
country_name: 'Germany',
|
||||
region: 'Europe & Central Asia',
|
||||
sum__SP_POP_TOTL: 81797673.0,
|
||||
sum__SP_RUR_TOTL_ZS: 25.512,
|
||||
sum__SP_DYN_LE00_IN: 80.7414634146,
|
||||
x: 25.512,
|
||||
y: 80.7414634146,
|
||||
size: 81797673.0,
|
||||
shape: 'circle',
|
||||
group: 'Europe & Central Asia',
|
||||
},
|
||||
{
|
||||
country_name: 'Turkey',
|
||||
region: 'Europe & Central Asia',
|
||||
sum__SP_POP_TOTL: 73199372.0,
|
||||
sum__SP_RUR_TOTL_ZS: 28.718,
|
||||
sum__SP_DYN_LE00_IN: 74.5404878049,
|
||||
x: 28.718,
|
||||
y: 74.5404878049,
|
||||
size: 73199372.0,
|
||||
shape: 'circle',
|
||||
group: 'Europe & Central Asia',
|
||||
},
|
||||
{
|
||||
country_name: 'France',
|
||||
region: 'Europe & Central Asia',
|
||||
sum__SP_POP_TOTL: 65342776.0,
|
||||
sum__SP_RUR_TOTL_ZS: 21.416,
|
||||
sum__SP_DYN_LE00_IN: 82.1146341463,
|
||||
x: 21.416,
|
||||
y: 82.1146341463,
|
||||
size: 65342776.0,
|
||||
shape: 'circle',
|
||||
group: 'Europe & Central Asia',
|
||||
},
|
||||
{
|
||||
country_name: 'United Kingdom',
|
||||
region: 'Europe & Central Asia',
|
||||
sum__SP_POP_TOTL: 63258918.0,
|
||||
sum__SP_RUR_TOTL_ZS: 18.43,
|
||||
sum__SP_DYN_LE00_IN: 80.9512195122,
|
||||
x: 18.43,
|
||||
y: 80.9512195122,
|
||||
size: 63258918.0,
|
||||
shape: 'circle',
|
||||
group: 'Europe & Central Asia',
|
||||
},
|
||||
{
|
||||
country_name: 'Italy',
|
||||
region: 'Europe & Central Asia',
|
||||
sum__SP_POP_TOTL: 59379449.0,
|
||||
sum__SP_RUR_TOTL_ZS: 31.556,
|
||||
sum__SP_DYN_LE00_IN: 82.187804878,
|
||||
x: 31.556,
|
||||
y: 82.187804878,
|
||||
size: 59379449.0,
|
||||
shape: 'circle',
|
||||
group: 'Europe & Central Asia',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'Middle East & North Africa',
|
||||
values: [
|
||||
{
|
||||
country_name: 'Egypt, Arab Rep.',
|
||||
region: 'Middle East & North Africa',
|
||||
sum__SP_POP_TOTL: 83787634.0,
|
||||
sum__SP_RUR_TOTL_ZS: 57.0,
|
||||
sum__SP_DYN_LE00_IN: 70.6785609756,
|
||||
x: 57.0,
|
||||
y: 70.6785609756,
|
||||
size: 83787634.0,
|
||||
shape: 'circle',
|
||||
group: 'Middle East & North Africa',
|
||||
},
|
||||
{
|
||||
country_name: 'Iran, Islamic Rep.',
|
||||
region: 'Middle East & North Africa',
|
||||
sum__SP_POP_TOTL: 75184322.0,
|
||||
sum__SP_RUR_TOTL_ZS: 28.8,
|
||||
sum__SP_DYN_LE00_IN: 73.4493170732,
|
||||
x: 28.8,
|
||||
y: 73.4493170732,
|
||||
size: 75184322.0,
|
||||
shape: 'circle',
|
||||
group: 'Middle East & North Africa',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 978 B |
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { ChartMetadata, ChartPlugin } from '@superset-ui/core';
|
||||
import transformProps from '../transformProps';
|
||||
import example from './images/example.jpg';
|
||||
import exampleDark from './images/example-dark.jpg';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
import thumbnailDark from './images/thumbnail-dark.png';
|
||||
import controlPanel from './controlPanel';
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('KPI'),
|
||||
credits: ['http://nvd3.org'],
|
||||
description: t(
|
||||
'Showcases the progress of a single metric against a given target. The higher the fill, the closer the metric is to the target.',
|
||||
),
|
||||
exampleGallery: [{ url: example, urlDark: exampleDark }],
|
||||
name: t('Bullet Chart'),
|
||||
tags: [t('Business'), t('Legacy'), t('Report'), t('nvd3')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class BulletChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('../ReactNVD3'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SuperChart, VizType } from '@superset-ui/core';
|
||||
import { BulletChartPlugin } from '@superset-ui/legacy-preset-chart-nvd3';
|
||||
import { dummyDatasource, withResizableChartDemo } from '@storybook-shared';
|
||||
import data from './data';
|
||||
|
||||
new BulletChartPlugin().configure({ key: VizType.Bullet }).register();
|
||||
|
||||
export default {
|
||||
title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/Bullet',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
ranges: '0, 50, 75, 100',
|
||||
rangeLabels: 'Low, Medium, High',
|
||||
markers: '65',
|
||||
markerLabels: 'Target',
|
||||
},
|
||||
argTypes: {
|
||||
ranges: {
|
||||
control: 'text',
|
||||
description: 'Comma-separated range values',
|
||||
},
|
||||
rangeLabels: {
|
||||
control: 'text',
|
||||
description: 'Comma-separated range labels',
|
||||
},
|
||||
markers: {
|
||||
control: 'text',
|
||||
description: 'Comma-separated marker values',
|
||||
},
|
||||
markerLabels: {
|
||||
control: 'text',
|
||||
description: 'Comma-separated marker labels',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Basic = ({
|
||||
ranges,
|
||||
rangeLabels,
|
||||
markers,
|
||||
markerLabels,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
ranges: string;
|
||||
rangeLabels: string;
|
||||
markers: string;
|
||||
markerLabels: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}) => (
|
||||
<SuperChart
|
||||
chartType={VizType.Bullet}
|
||||
width={width}
|
||||
height={height}
|
||||
datasource={dummyDatasource}
|
||||
queriesData={[{ data }]}
|
||||
formData={{
|
||||
marker_labels: markerLabels,
|
||||
marker_line_labels: '',
|
||||
marker_lines: '',
|
||||
markers,
|
||||
range_labels: rangeLabels,
|
||||
ranges,
|
||||
viz_type: VizType.Bullet,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
ControlPanelConfig,
|
||||
getStandardizedControls,
|
||||
sections,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import {
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
bottomMargin,
|
||||
xTicksLayout,
|
||||
xAxisFormat,
|
||||
yLogScale,
|
||||
yAxisBounds,
|
||||
xAxisShowMinmax,
|
||||
yAxisShowMinmax,
|
||||
leftMargin,
|
||||
timeSeriesSection,
|
||||
} from '../NVD3Controls';
|
||||
|
||||
const config: ControlPanelConfig = {
|
||||
controlPanelSections: [
|
||||
sections.legacyTimeseriesTime,
|
||||
timeSeriesSection[0],
|
||||
{
|
||||
label: t('Chart Options'),
|
||||
expanded: true,
|
||||
controlSetRows: [['color_scheme']],
|
||||
},
|
||||
{
|
||||
label: t('X Axis'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[xAxisLabel, bottomMargin],
|
||||
[xTicksLayout, xAxisFormat],
|
||||
[xAxisShowMinmax, null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Y Axis'),
|
||||
expanded: true,
|
||||
controlSetRows: [
|
||||
[yAxisLabel, leftMargin],
|
||||
[yAxisShowMinmax, yLogScale],
|
||||
['y_axis_format', yAxisBounds],
|
||||
],
|
||||
},
|
||||
timeSeriesSection[1],
|
||||
sections.annotations,
|
||||
],
|
||||
formDataOverrides: formData => ({
|
||||
...formData,
|
||||
groupby: getStandardizedControls().popAllColumns(),
|
||||
metrics: getStandardizedControls().popAllMetrics(),
|
||||
}),
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 65 KiB |