Compare commits
1 Commits
misc-chart
...
mobile-das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a256eb04cd |
2
.github/CODEOWNERS
vendored
@@ -34,7 +34,7 @@
|
||||
|
||||
**/*.geojson @villebro @rusackas
|
||||
**/*.ipynb @villebro @rusackas
|
||||
/superset-frontend/plugins/plugin-chart-country-map/ @villebro @rusackas
|
||||
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
|
||||
|
||||
# Notify translation maintainers of changes to translations
|
||||
|
||||
|
||||
11
.github/workflows/superset-playwright.yml
vendored
@@ -155,6 +155,17 @@ jobs:
|
||||
INCLUDE_EMBEDDED: "true"
|
||||
with:
|
||||
run: playwright-run "${{ matrix.app_root }}" embedded
|
||||
- name: Run Playwright (Mobile Tests)
|
||||
uses: ./.github/actions/cached-dependencies
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
# Scoped to this step for the same reason as the embedded flags
|
||||
# above: the mobile consumption mode should not alter Flask's
|
||||
# configuration for the required desktop test steps.
|
||||
SUPERSET_FEATURE_MOBILE_CONSUMPTION_MODE: "true"
|
||||
INCLUDE_MOBILE: "true"
|
||||
with:
|
||||
run: playwright-run "${{ matrix.app_root }}" mobile/
|
||||
- name: Set safe app root
|
||||
if: failure()
|
||||
id: set-safe-app-root
|
||||
|
||||
1
.gitignore
vendored
@@ -137,7 +137,6 @@ PROJECT.md
|
||||
.aider*
|
||||
.claude_rc*
|
||||
.claude/settings.local.json
|
||||
.claude/worktrees/
|
||||
.env.local
|
||||
oxc-custom-build/
|
||||
*.code-workspace
|
||||
|
||||
86
UPDATING.md
@@ -193,10 +193,6 @@ 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.
|
||||
|
||||
@@ -216,7 +212,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.
|
||||
|
||||
@@ -253,7 +249,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
|
||||
|
||||
@@ -337,7 +333,6 @@ 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).
|
||||
@@ -354,12 +349,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`).
|
||||
|
||||
@@ -410,7 +405,6 @@ 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.
|
||||
@@ -536,7 +530,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.
|
||||
|
||||
@@ -564,11 +558,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.
|
||||
|
||||
@@ -588,9 +582,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.
|
||||
|
||||
@@ -614,7 +608,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`
|
||||
|
||||
@@ -626,10 +620,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.
|
||||
@@ -667,17 +661,14 @@ The Kenya country map has been updated to reflect the 47 counties established un
|
||||
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
|
||||
@@ -712,7 +703,6 @@ 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",
|
||||
@@ -727,11 +717,9 @@ 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:
|
||||
@@ -744,9 +732,7 @@ 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
|
||||
@@ -759,14 +745,12 @@ 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:
|
||||
@@ -776,7 +760,6 @@ 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
|
||||
@@ -786,7 +769,6 @@ 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]`
|
||||
@@ -796,7 +778,6 @@ 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
|
||||
@@ -805,7 +786,6 @@ MCP_SERVICE_PORT = 5008
|
||||
```
|
||||
|
||||
**Production** (JWT-based, multi-user):
|
||||
|
||||
```python
|
||||
# superset_config.py
|
||||
MCP_AUTH_ENABLED = True
|
||||
@@ -851,14 +831,12 @@ 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]
|
||||
@@ -872,7 +850,6 @@ 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
|
||||
@@ -892,10 +869,8 @@ 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"
|
||||
@@ -939,16 +914,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:**
|
||||
|
||||
@@ -959,7 +934,6 @@ 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).
|
||||
@@ -968,8 +942,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.
|
||||
|
||||
@@ -110,6 +110,7 @@ FEATURE_FLAGS = {
|
||||
"ALERT_REPORTS": True,
|
||||
"DATASET_FOLDERS": True,
|
||||
"ENABLE_EXTENSIONS": True,
|
||||
"MOBILE_CONSUMPTION_MODE": True,
|
||||
"SEMANTIC_LAYERS": True,
|
||||
}
|
||||
EXTENSIONS_PATH = "/app/docker/extensions"
|
||||
|
||||
@@ -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
|
||||
[plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts)
|
||||
[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)
|
||||
|
||||
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/plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-country-map).
|
||||
[@superset-ui/legacy-plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-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/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).
|
||||
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).
|
||||
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.
|
||||
|
||||
89
docs/docs/using-superset/mobile-experience.mdx
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Mobile Experience
|
||||
sidebar_position: 7
|
||||
version: 1
|
||||
---
|
||||
|
||||
import useBaseUrl from "@docusaurus/useBaseUrl";
|
||||
|
||||
# Mobile Experience
|
||||
|
||||
Superset ships an optional, consumption-only mobile experience for viewing
|
||||
dashboards on phones and other small screens. When enabled, screens below
|
||||
768px wide get a layout built for touch: dashboards render their charts
|
||||
stacked full-width, navigation collapses into a drawer, and dashboard
|
||||
filters open in a slide-out panel.
|
||||
|
||||
The mobile experience is **read-only by design**. It is aimed at consumers
|
||||
of analytics — people checking a dashboard from a phone — not at dashboard
|
||||
authors. Authoring surfaces (chart builder, SQL Lab, dataset management,
|
||||
and administrative screens) remain desktop-only.
|
||||
|
||||
## Enabling the mobile experience
|
||||
|
||||
The mobile experience is gated behind the `MOBILE_CONSUMPTION_MODE` feature
|
||||
flag, which is off by default. Enable it in your `superset_config.py`:
|
||||
|
||||
```python
|
||||
FEATURE_FLAGS = {
|
||||
"MOBILE_CONSUMPTION_MODE": True,
|
||||
}
|
||||
```
|
||||
|
||||
With the flag disabled, Superset renders identically at every screen size,
|
||||
and phones display the desktop layout scaled down (the pre-existing
|
||||
behavior). The flag also controls whether Superset serves a viewport meta
|
||||
tag, which is required for mobile browsers to apply the responsive layout
|
||||
at their native width.
|
||||
|
||||
## What works on mobile
|
||||
|
||||
| Area | Mobile behavior |
|
||||
| --- | --- |
|
||||
| **Dashboards** | Charts stack vertically at full width, sized to the screen. Tab bars are sticky and swipeable. Native filters open in a drawer via the filter icon in the header. |
|
||||
| **Dashboard list** | Card view with full-width cards; search and filters open in a drawer. |
|
||||
| **Home** | Recents (dashboards only) and dashboard cards; desktop-only sections are hidden. |
|
||||
| **Navigation** | A hamburger menu opens a drawer with links to dashboards, theme and language selection, and user info/logout. |
|
||||
|
||||
<div style={{display: 'flex', gap: '1rem', flexWrap: 'wrap'}}>
|
||||
<img src={useBaseUrl("/img/screenshots/mobile/mobile_dashboard.jpg")} alt="A dashboard on mobile with charts stacked full width" width="260" />
|
||||
<img src={useBaseUrl("/img/screenshots/mobile/mobile_filter_drawer.jpg")} alt="The dashboard filter drawer on mobile" width="260" />
|
||||
<img src={useBaseUrl("/img/screenshots/mobile/mobile_dashboard_list.jpg")} alt="The dashboard list in card view on mobile" width="260" />
|
||||
</div>
|
||||
|
||||
<div style={{display: 'flex', gap: '1rem', flexWrap: 'wrap', marginTop: '1rem'}}>
|
||||
<img src={useBaseUrl("/img/screenshots/mobile/mobile_home.jpg")} alt="The Superset home page on mobile" width="260" />
|
||||
<img src={useBaseUrl("/img/screenshots/mobile/mobile_nav_drawer.jpg")} alt="The mobile navigation drawer" width="260" />
|
||||
<img src={useBaseUrl("/img/screenshots/mobile/mobile_unsupported.jpg")} alt="The screen shown for views that are not available on mobile" width="260" />
|
||||
</div>
|
||||
|
||||
## What doesn't work on mobile
|
||||
|
||||
Everything not listed above shows a friendly "This view isn't available on
|
||||
mobile" screen with shortcuts back to dashboards and the home page. That
|
||||
includes:
|
||||
|
||||
- Chart builder (Explore) and chart-level links — chart titles on
|
||||
dashboards are plain text on mobile, and chart entries are filtered out
|
||||
of the home page's Recents feed
|
||||
- SQL Lab and query history
|
||||
- Creating or editing dashboards, charts, datasets, and databases
|
||||
- List views other than dashboards (charts, datasets, saved queries, etc.)
|
||||
- Administrative and settings screens
|
||||
|
||||
Editing controls are also removed from the screens that *are* supported:
|
||||
the dashboard header hides the edit, publish, and favorite controls, and
|
||||
dashboard/chart kebab menus are reduced to view-oriented actions.
|
||||
|
||||
If a device crosses the 768px threshold — for example, rotating a tablet
|
||||
to landscape or resizing a window — the full desktop experience becomes
|
||||
available immediately.
|
||||
|
||||
## Notes for operators
|
||||
|
||||
- The flag is deployment-wide; there is no per-role or per-user targeting.
|
||||
- Dashboard permalinks and links shared from desktop resolve normally on
|
||||
mobile as long as they point at dashboards.
|
||||
- Embedded dashboards are unaffected: the embedded SDK controls its own
|
||||
layout, and the viewport meta tag is only interpreted by the top-level
|
||||
page.
|
||||
6
docs/static/feature-flags.json
vendored
@@ -69,6 +69,12 @@
|
||||
"lifecycle": "development",
|
||||
"description": "Enable Matrixify feature for matrix-style chart layouts"
|
||||
},
|
||||
{
|
||||
"name": "MOBILE_CONSUMPTION_MODE",
|
||||
"default": false,
|
||||
"lifecycle": "development",
|
||||
"description": "Serve a consumption-only mobile experience (dashboards, dashboard list, and home page) on small screens; other views show a \"not supported on mobile\" screen. Authoring features are hidden on mobile when enabled."
|
||||
},
|
||||
{
|
||||
"name": "OPTIMIZE_SQL",
|
||||
"default": false,
|
||||
|
||||
BIN
docs/static/img/screenshots/mobile/mobile_dashboard.jpg
vendored
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/static/img/screenshots/mobile/mobile_dashboard_list.jpg
vendored
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/static/img/screenshots/mobile/mobile_filter_drawer.jpg
vendored
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/static/img/screenshots/mobile/mobile_home.jpg
vendored
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
docs/static/img/screenshots/mobile/mobile_nav_drawer.jpg
vendored
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/static/img/screenshots/mobile/mobile_unsupported.jpg
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
597
superset-frontend/package-lock.json
generated
@@ -51,25 +51,25 @@
|
||||
"@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-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
|
||||
"@superset-ui/plugin-chart-calendar": "file:./plugins/plugin-chart-calendar",
|
||||
"@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram",
|
||||
"@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-echarts": "file:./plugins/plugin-chart-echarts",
|
||||
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
|
||||
"@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-pivot-table": "file:./plugins/plugin-chart-pivot-table",
|
||||
"@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map",
|
||||
"@superset-ui/plugin-chart-rose": "file:./plugins/plugin-chart-rose",
|
||||
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
|
||||
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
|
||||
"@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map",
|
||||
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
|
||||
"@superset-ui/preset-chart-nvd3": "file:./plugins/preset-chart-nvd3",
|
||||
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
|
||||
"@types/d3-format": "^3.0.1",
|
||||
"@types/d3-selection": "^3.0.11",
|
||||
@@ -10717,26 +10717,54 @@
|
||||
"resolved": "packages/generator-superset",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-calendar": {
|
||||
"resolved": "plugins/legacy-plugin-chart-calendar",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-chord": {
|
||||
"resolved": "plugins/legacy-plugin-chart-chord",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-country-map": {
|
||||
"resolved": "plugins/legacy-plugin-chart-country-map",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-horizon": {
|
||||
"resolved": "plugins/legacy-plugin-chart-horizon",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-paired-t-test": {
|
||||
"resolved": "plugins/legacy-plugin-chart-paired-t-test",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-parallel-coordinates": {
|
||||
"resolved": "plugins/legacy-plugin-chart-parallel-coordinates",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-partition": {
|
||||
"resolved": "plugins/legacy-plugin-chart-partition",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-rose": {
|
||||
"resolved": "plugins/legacy-plugin-chart-rose",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-plugin-chart-world-map": {
|
||||
"resolved": "plugins/legacy-plugin-chart-world-map",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/legacy-preset-chart-nvd3": {
|
||||
"resolved": "plugins/legacy-preset-chart-nvd3",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-ag-grid-table": {
|
||||
"resolved": "plugins/plugin-chart-ag-grid-table",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-calendar": {
|
||||
"resolved": "plugins/plugin-chart-calendar",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-cartodiagram": {
|
||||
"resolved": "plugins/plugin-chart-cartodiagram",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-chord": {
|
||||
"resolved": "plugins/plugin-chart-chord",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-country-map": {
|
||||
"resolved": "plugins/plugin-chart-country-map",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-echarts": {
|
||||
"resolved": "plugins/plugin-chart-echarts",
|
||||
"link": true
|
||||
@@ -10745,22 +10773,6 @@
|
||||
"resolved": "plugins/plugin-chart-handlebars",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-horizon": {
|
||||
"resolved": "plugins/plugin-chart-horizon",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-paired-t-test": {
|
||||
"resolved": "plugins/plugin-chart-paired-t-test",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-parallel-coordinates": {
|
||||
"resolved": "plugins/plugin-chart-parallel-coordinates",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-partition": {
|
||||
"resolved": "plugins/plugin-chart-partition",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-pivot-table": {
|
||||
"resolved": "plugins/plugin-chart-pivot-table",
|
||||
"link": true
|
||||
@@ -10769,10 +10781,6 @@
|
||||
"resolved": "plugins/plugin-chart-point-cluster-map",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-rose": {
|
||||
"resolved": "plugins/plugin-chart-rose",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-table": {
|
||||
"resolved": "plugins/plugin-chart-table",
|
||||
"link": true
|
||||
@@ -10781,18 +10789,10 @@
|
||||
"resolved": "plugins/plugin-chart-word-cloud",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/plugin-chart-world-map": {
|
||||
"resolved": "plugins/plugin-chart-world-map",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/preset-chart-deckgl": {
|
||||
"resolved": "plugins/preset-chart-deckgl",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/preset-chart-nvd3": {
|
||||
"resolved": "plugins/preset-chart-nvd3",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@superset-ui/switchboard": {
|
||||
"resolved": "packages/superset-ui-switchboard",
|
||||
"link": true
|
||||
@@ -16453,6 +16453,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/cephes": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cephes/-/cephes-2.0.0.tgz",
|
||||
"integrity": "sha512-4GMUzkcXHZ0HMZ3gZdBrv8pQs1/zkJh2Q9rQOF8NJZHanM359y3XOSdeqmDBPfxQKYQpJt58R3dUpofrIXJ2mg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
||||
@@ -18870,6 +18876,15 @@
|
||||
"readable-stream": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/distributions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/distributions/-/distributions-2.2.0.tgz",
|
||||
"integrity": "sha512-n7ybud+CRAOZlpg+ETuA0PTiSBfyVNt8Okns5gSK4NvHwj7RamQoufptOucvVcTn9CV4DZ38p1k6TgwMexUNkQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cephes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dnd-core": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz",
|
||||
@@ -35943,6 +35958,15 @@
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reactable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reactable/-/reactable-1.1.0.tgz",
|
||||
"integrity": "sha512-SnvZ3CXyFFxGotw9cqNiVUGb2oW16UlIypGQZRJGgPiJuFqW22jO7A+Y/Tvv8no8F/bZoLdZ+QJP7eZfcc9kCw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "* || ^0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read/-/read-4.1.0.tgz",
|
||||
@@ -44067,6 +44091,245 @@
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"plugins/legacy-plugin-chart-calendar": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-calendar",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-tip": "^0.9.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-calendar/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-calendar/node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-chord": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-chord",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-chord/node_modules/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-country-map": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-country-map",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-array": "^3.2.4",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-country-map/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-horizon": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-horizon",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-scale": "^4.0.2",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-horizon/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-paired-t-test": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-paired-t-test",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"distributions": "^2.2.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"reactable": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-parallel-coordinates": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-parallel-coordinates",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3v3": "npm:d3@3.5.17",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-partition": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-partition",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-hierarchy": "^3.1.2",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-rose": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-rose",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-world-map": {
|
||||
"name": "@superset-ui/legacy-plugin-chart-world-map",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-array": "^3.2.4",
|
||||
"datamaps": "^0.5.10",
|
||||
"prop-types": "^15.8.1",
|
||||
"tinycolor2": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-plugin-chart-world-map/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/legacy-preset-chart-nvd3": {
|
||||
"name": "@superset-ui/legacy-preset-chart-nvd3",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-tip": "^0.9.1",
|
||||
"dompurify": "^3.4.12",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-ag-grid-table": {
|
||||
"name": "@superset-ui/plugin-chart-ag-grid-table",
|
||||
"version": "0.20.3",
|
||||
@@ -44109,45 +44372,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-calendar": {
|
||||
"name": "@superset-ui/plugin-chart-calendar",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-tip": "^0.9.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-calendar/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-calendar/node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-cartodiagram": {
|
||||
"name": "@superset-ui/plugin-chart-cartodiagram",
|
||||
"version": "0.0.1",
|
||||
@@ -44176,58 +44400,6 @@
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-chord": {
|
||||
"name": "@superset-ui/plugin-chart-chord",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-chord/node_modules/react": {
|
||||
"version": "19.2.7",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-country-map": {
|
||||
"name": "@superset-ui/plugin-chart-country-map",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-array": "^3.2.4",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-country-map/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-echarts": {
|
||||
"name": "@superset-ui/plugin-chart-echarts",
|
||||
"version": "0.20.3",
|
||||
@@ -44315,79 +44487,6 @@
|
||||
"sprintf-js": ">= 1.1.1 < 2"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-horizon": {
|
||||
"name": "@superset-ui/plugin-chart-horizon",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-scale": "^4.0.2",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-horizon/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-paired-t-test": {
|
||||
"name": "@superset-ui/plugin-chart-paired-t-test",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-parallel-coordinates": {
|
||||
"name": "@superset-ui/plugin-chart-parallel-coordinates",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3v3": "npm:d3@3.5.17",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-partition": {
|
||||
"name": "@superset-ui/plugin-chart-partition",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-hierarchy": "^3.1.2",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-pivot-table": {
|
||||
"name": "@superset-ui/plugin-chart-pivot-table",
|
||||
"version": "0.20.3",
|
||||
@@ -44431,23 +44530,6 @@
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-rose": {
|
||||
"name": "@superset-ui/plugin-chart-rose",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-table": {
|
||||
"name": "@superset-ui/plugin-chart-table",
|
||||
"version": "0.20.3",
|
||||
@@ -44513,36 +44595,6 @@
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-world-map": {
|
||||
"name": "@superset-ui/plugin-chart-world-map",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-array": "^3.2.4",
|
||||
"datamaps": "^0.5.10",
|
||||
"prop-types": "^15.8.1",
|
||||
"tinycolor2": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
},
|
||||
"plugins/plugin-chart-world-map/node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/preset-chart-deckgl": {
|
||||
"name": "@superset-ui/preset-chart-deckgl",
|
||||
"version": "1.0.0",
|
||||
@@ -44624,29 +44676,6 @@
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"plugins/preset-chart-nvd3": {
|
||||
"name": "@superset-ui/preset-chart-nvd3",
|
||||
"version": "0.20.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3-tip": "^0.9.1",
|
||||
"dompurify": "^3.4.11",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"nvd3-fork": "^2.0.5",
|
||||
"prop-types": "^15.8.1",
|
||||
"urijs": "^1.19.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
"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",
|
||||
@@ -90,7 +89,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/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/legacy-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": [
|
||||
@@ -137,16 +136,16 @@
|
||||
"@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/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/preset-chart-nvd3": "file:./plugins/preset-chart-nvd3",
|
||||
"@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-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",
|
||||
|
||||
@@ -104,15 +104,24 @@ 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 = {
|
||||
endpoint: '/api/v1/chart/data',
|
||||
jsonPayload: {
|
||||
query_context: buildQuery(formData),
|
||||
},
|
||||
...options,
|
||||
};
|
||||
const requestConfig: RequestConfig = useLegacyApi
|
||||
? {
|
||||
endpoint: '/explore_json/',
|
||||
postPayload: {
|
||||
form_data: buildQuery(formData),
|
||||
},
|
||||
...options,
|
||||
}
|
||||
: {
|
||||
endpoint: '/api/v1/chart/data',
|
||||
jsonPayload: {
|
||||
query_context: buildQuery(formData),
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
return this.client
|
||||
.post(requestConfig)
|
||||
|
||||
@@ -68,7 +68,9 @@ beforeEach(() => {
|
||||
|
||||
// Setup default registry mocks
|
||||
(getChartMetadataRegistry as any).mockReturnValue({
|
||||
get: jest.fn().mockReturnValue({}),
|
||||
get: jest.fn().mockReturnValue({
|
||||
useLegacyApi: false,
|
||||
}),
|
||||
});
|
||||
|
||||
(getChartBuildQueryRegistry as any).mockReturnValue({
|
||||
|
||||
@@ -32,6 +32,7 @@ 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';
|
||||
|
||||
@@ -244,6 +245,9 @@ 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;
|
||||
@@ -255,28 +259,39 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
queryContext = buildQueryContext(finalFormData);
|
||||
}
|
||||
|
||||
// Ensure query_context is properly formatted for the API
|
||||
if (!queryContext.queries) {
|
||||
// Ensure query_context is properly formatted for new API
|
||||
if (!useLegacyApi && !queryContext.queries) {
|
||||
queryContext = { queries: [queryContext] };
|
||||
}
|
||||
const endpoint = useLegacyApi ? '/explore_json/' : '/api/v1/chart/data';
|
||||
|
||||
const requestConfig: RequestConfig = {
|
||||
endpoint: '/api/v1/chart/data',
|
||||
endpoint,
|
||||
signal: abortControllerRef.current.signal,
|
||||
...(timeout && { timeout: timeout * 1000 }),
|
||||
jsonPayload: {
|
||||
};
|
||||
|
||||
if (useLegacyApi) {
|
||||
requestConfig.postPayload = {
|
||||
form_data: {
|
||||
...finalFormData,
|
||||
...(force && { force: true }),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
requestConfig.jsonPayload = {
|
||||
...queryContext,
|
||||
...(force && { force: true }),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const response = await chartClientRef.current!.client.post(requestConfig);
|
||||
let responseData = Array.isArray(response.json)
|
||||
? response.json
|
||||
: [response.json];
|
||||
|
||||
// Handle the nested result structure from the API
|
||||
if (responseData[0]?.result) {
|
||||
// Handle the nested result structure from the new API
|
||||
if (!useLegacyApi && responseData[0]?.result) {
|
||||
responseData = responseData[0].result;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface ChartMetadataConfig {
|
||||
supportedAnnotationTypes?: string[];
|
||||
thumbnail: string;
|
||||
thumbnailDark?: string;
|
||||
useLegacyApi?: boolean;
|
||||
behaviors?: Behavior[];
|
||||
exampleGallery?: ExampleImage[];
|
||||
tags?: string[];
|
||||
@@ -74,6 +75,8 @@ export default class ChartMetadata {
|
||||
|
||||
thumbnailDark?: string;
|
||||
|
||||
useLegacyApi: boolean;
|
||||
|
||||
behaviors: Behavior[];
|
||||
|
||||
datasourceCount: number;
|
||||
@@ -109,6 +112,7 @@ export default class ChartMetadata {
|
||||
supportedAnnotationTypes = [],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi = false,
|
||||
behaviors = [],
|
||||
datasourceCount = 1,
|
||||
enableNoResults = true,
|
||||
@@ -140,6 +144,7 @@ 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;
|
||||
|
||||
@@ -49,6 +49,7 @@ const titleStyles = (theme: SupersetTheme) => css`
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
font-weight: inherit;
|
||||
color: ${theme.colorText};
|
||||
background-color: ${theme.colorBgContainer};
|
||||
|
||||
@@ -127,6 +128,21 @@ export const DynamicEditableTitle = memo(
|
||||
}
|
||||
}, [currentTitle, placeholder]);
|
||||
|
||||
// Webfont metrics differ from the fallback font's, so a measurement
|
||||
// taken before fonts finish loading under- or over-sizes the input.
|
||||
// Re-measure once all fonts are ready.
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
document.fonts?.ready?.then(() => {
|
||||
if (!cancelled && sizerRef.current) {
|
||||
setInputWidth(sizerRef.current.offsetWidth);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const inputElement = inputRef.current?.input;
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ReactNode, ReactElement, memo } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { css, SupersetTheme, useTheme } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
import { FeatureFlag, isFeatureEnabled } from '../../utils/featureFlags';
|
||||
import type { DropdownProps } from '../Dropdown/types';
|
||||
import type { TooltipPlacement } from '../Tooltip/types';
|
||||
import type { CertifiedBadgeProps } from '../CertifiedBadge/types';
|
||||
@@ -82,6 +83,20 @@ const headerStyles = (theme: SupersetTheme) => css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Mobile consumption mode: center the title between left/right panels */
|
||||
${
|
||||
isFeatureEnabled(FeatureFlag.MobileConsumptionMode) &&
|
||||
css`
|
||||
@media (max-width: ${theme.screenSMMax}px) {
|
||||
.title-panel {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
`;
|
||||
|
||||
const buttonsStyles = (theme: SupersetTheme) => css`
|
||||
@@ -109,6 +124,7 @@ export type PageHeaderWithActionsProps = {
|
||||
showFaveStar: boolean;
|
||||
showMenuDropdown?: boolean;
|
||||
faveStarProps: FaveStarProps;
|
||||
leftPanelItems?: ReactNode;
|
||||
titlePanelAdditionalItems: ReactNode;
|
||||
rightPanelAdditionalItems: ReactNode;
|
||||
additionalActionsMenu: ReactElement;
|
||||
@@ -126,6 +142,7 @@ export const PageHeaderWithActions = memo(
|
||||
certificatiedBadgeProps,
|
||||
showFaveStar,
|
||||
faveStarProps,
|
||||
leftPanelItems,
|
||||
titlePanelAdditionalItems,
|
||||
rightPanelAdditionalItems,
|
||||
additionalActionsMenu,
|
||||
@@ -136,6 +153,7 @@ export const PageHeaderWithActions = memo(
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div css={headerStyles} className="header-with-actions">
|
||||
{leftPanelItems}
|
||||
<div className="title-panel">
|
||||
<DynamicEditableTitle {...editableTitleProps} />
|
||||
{showTitlePanelItems && (
|
||||
|
||||
@@ -57,6 +57,7 @@ export enum FeatureFlag {
|
||||
GranularExportControls = 'GRANULAR_EXPORT_CONTROLS',
|
||||
ListviewsDefaultCardView = 'LISTVIEWS_DEFAULT_CARD_VIEW',
|
||||
Matrixify = 'MATRIXIFY',
|
||||
MobileConsumptionMode = 'MOBILE_CONSUMPTION_MODE',
|
||||
ScheduledQueries = 'SCHEDULED_QUERIES',
|
||||
SemanticLayers = 'SEMANTIC_LAYERS',
|
||||
SqllabBackendPersistence = 'SQLLAB_BACKEND_PERSISTENCE',
|
||||
|
||||
@@ -160,6 +160,42 @@ 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)', () => {
|
||||
|
||||
@@ -96,6 +96,7 @@ export default defineConfig({
|
||||
'**/tests/auth/**/*.spec.ts',
|
||||
'**/tests/sqllab/**/*.spec.ts',
|
||||
'**/tests/embedded/**/*.spec.ts',
|
||||
'**/tests/mobile/**/*.spec.ts',
|
||||
...(process.env.INCLUDE_EXPERIMENTAL ? [] : ['**/experimental/**']),
|
||||
],
|
||||
use: {
|
||||
@@ -156,6 +157,23 @@ export default defineConfig({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Mobile consumption-mode tests need the MOBILE_CONSUMPTION_MODE feature
|
||||
// flag enabled in the Flask backend (the workflow's mobile step sets
|
||||
// SUPERSET_FEATURE_MOBILE_CONSUMPTION_MODE), so they only run when the
|
||||
// environment opts in. Same strict 'true' check as INCLUDE_EMBEDDED.
|
||||
...(process.env.INCLUDE_MOBILE?.toLowerCase() === 'true'
|
||||
? [
|
||||
{
|
||||
name: 'chromium-mobile',
|
||||
testMatch: '**/tests/mobile/**/*.spec.ts',
|
||||
use: {
|
||||
browserName: 'chromium' as const,
|
||||
testIdAttribute: 'data-test',
|
||||
storageState: 'playwright/.auth/user.json',
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
|
||||
// Web server setup - disabled in CI (Flask started separately in workflow)
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mobile Experience Documentation Screenshot Generator
|
||||
*
|
||||
* Captures phone-sized screenshots for the mobile consumption mode docs
|
||||
* (docs/docs/using-superset/mobile-experience.mdx). Depends on example data
|
||||
* loaded via `superset load_examples` AND the MOBILE_CONSUMPTION_MODE
|
||||
* feature flag being enabled in the target environment:
|
||||
*
|
||||
* FEATURE_FLAGS = {"MOBILE_CONSUMPTION_MODE": True}
|
||||
*
|
||||
* Run locally:
|
||||
* cd superset-frontend
|
||||
* PLAYWRIGHT_BASE_URL=http://localhost:8088 PLAYWRIGHT_ADMIN_PASSWORD=admin npm run docs:screenshots
|
||||
*
|
||||
* Screenshots are saved under docs/static/img/screenshots/mobile/.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { Page, test, expect } from '@playwright/test';
|
||||
import { URL } from '../../utils/urls';
|
||||
|
||||
const MOBILE_SCREENSHOTS_DIR = path.resolve(
|
||||
__dirname,
|
||||
'../../../../docs/static/img/screenshots/mobile',
|
||||
);
|
||||
|
||||
// iPhone 12-class viewport; 2x scale factor for crisp docs images
|
||||
test.use({
|
||||
viewport: { width: 390, height: 844 },
|
||||
deviceScaleFactor: 2,
|
||||
hasTouch: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Waits for animations and async renders to settle before taking a
|
||||
* screenshot. ECharts entry animations, drawer transitions, and image
|
||||
* lazy-loading require a short pause that can't be expressed as a
|
||||
* deterministic wait condition.
|
||||
*/
|
||||
async function settle(page: Page, ms = 1000): Promise<void> {
|
||||
await page.waitForTimeout(ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the Sales Dashboard (from example data) at phone size and waits for
|
||||
* the stacked charts to finish rendering.
|
||||
*/
|
||||
async function openSalesDashboardMobile(page: Page): Promise<void> {
|
||||
await page.goto(URL.DASHBOARD_LIST);
|
||||
// Mobile list is card-only; cards navigate on tap (titles are plain
|
||||
// text, not links, in consumption mode)
|
||||
const dashboardCard = page.getByText('Sales Dashboard', { exact: true });
|
||||
await expect(dashboardCard.first()).toBeVisible({ timeout: 15000 });
|
||||
await dashboardCard.first().click();
|
||||
|
||||
await expect(
|
||||
page.locator('[data-test="dashboard-content-wrapper"]'),
|
||||
).toBeVisible({ timeout: 30000 });
|
||||
await expect(
|
||||
page.locator('.dashboard-component-chart-holder canvas').first(),
|
||||
).toBeVisible({ timeout: 30000 });
|
||||
}
|
||||
|
||||
test('mobile dashboard screenshot', async ({ page }) => {
|
||||
await openSalesDashboardMobile(page);
|
||||
await settle(page, 2000);
|
||||
await page.screenshot({
|
||||
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_dashboard.jpg'),
|
||||
type: 'jpeg',
|
||||
});
|
||||
});
|
||||
|
||||
test('mobile dashboard filter drawer screenshot', async ({ page }) => {
|
||||
await openSalesDashboardMobile(page);
|
||||
|
||||
const filterTrigger = page.locator('[data-test="mobile-filters-trigger"]');
|
||||
await expect(filterTrigger).toBeVisible({ timeout: 15000 });
|
||||
await filterTrigger.click();
|
||||
|
||||
// Wait for the drawer and its filter controls to render
|
||||
await expect(page.locator('.ant-drawer-body')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.locator('[data-test="filter-bar"]')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
// Park the pointer so no hover card is open in the capture
|
||||
await page.mouse.move(5, 830);
|
||||
await settle(page);
|
||||
await page.screenshot({
|
||||
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_filter_drawer.jpg'),
|
||||
type: 'jpeg',
|
||||
});
|
||||
});
|
||||
|
||||
test('mobile dashboard list screenshot', async ({ page }) => {
|
||||
await page.goto(URL.DASHBOARD_LIST);
|
||||
// Card view is forced on mobile; wait for cards to render
|
||||
await expect(page.locator('[data-test="styled-card"]').first()).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await settle(page);
|
||||
await page.screenshot({
|
||||
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_dashboard_list.jpg'),
|
||||
type: 'jpeg',
|
||||
});
|
||||
});
|
||||
|
||||
test('mobile home screenshot', async ({ page }) => {
|
||||
await page.goto(URL.WELCOME);
|
||||
await expect(page.getByText('Recents')).toBeVisible({ timeout: 15000 });
|
||||
await settle(page, 2000);
|
||||
await page.screenshot({
|
||||
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_home.jpg'),
|
||||
type: 'jpeg',
|
||||
});
|
||||
});
|
||||
|
||||
test('mobile navigation drawer screenshot', async ({ page }) => {
|
||||
await page.goto(URL.WELCOME);
|
||||
await expect(page.getByText('Recents')).toBeVisible({ timeout: 15000 });
|
||||
|
||||
const menuButton = page.getByRole('button', { name: 'Menu' });
|
||||
await expect(menuButton).toBeVisible({ timeout: 10000 });
|
||||
await menuButton.click();
|
||||
|
||||
await expect(page.locator('.ant-drawer-body')).toBeVisible({
|
||||
timeout: 10000,
|
||||
});
|
||||
await expect(page.getByText('Dashboards').first()).toBeVisible();
|
||||
await settle(page);
|
||||
await page.screenshot({
|
||||
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_nav_drawer.jpg'),
|
||||
type: 'jpeg',
|
||||
});
|
||||
});
|
||||
|
||||
test('mobile unsupported route screenshot', async ({ page }) => {
|
||||
await page.goto(URL.SQLLAB);
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).toBeVisible({ timeout: 15000 });
|
||||
await settle(page);
|
||||
await page.screenshot({
|
||||
path: path.join(MOBILE_SCREENSHOTS_DIR, 'mobile_unsupported.jpg'),
|
||||
type: 'jpeg',
|
||||
});
|
||||
});
|
||||
@@ -38,14 +38,6 @@
|
||||
*
|
||||
* 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';
|
||||
@@ -190,43 +182,12 @@ 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;
|
||||
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* 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 { test, expect, devices } from '@playwright/test';
|
||||
|
||||
// NOTE: These tests exercise the mobile consumption experience and require
|
||||
// the MOBILE_CONSUMPTION_MODE feature flag to be enabled in the target
|
||||
// environment (FEATURE_FLAGS = {"MOBILE_CONSUMPTION_MODE": True}).
|
||||
import { TIMEOUT } from '../../utils/constants';
|
||||
|
||||
/**
|
||||
* Mobile dashboard viewing tests verify that dashboards can be viewed
|
||||
* and interacted with on mobile devices.
|
||||
*
|
||||
* These tests assume the World Bank's Health sample dashboard exists.
|
||||
*/
|
||||
|
||||
// Use iPhone 12 viewport for mobile tests
|
||||
const mobileViewport = devices['iPhone 12'];
|
||||
|
||||
test.describe('Mobile Dashboard Viewing', () => {
|
||||
test.use({
|
||||
viewport: mobileViewport.viewport,
|
||||
userAgent: mobileViewport.userAgent,
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to dashboard list to find a dashboard
|
||||
await page.goto('dashboard/list/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('dashboard list renders in card view on mobile', async ({ page }) => {
|
||||
// On mobile, dashboard list should show cards, not table
|
||||
// Look for card elements
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
|
||||
// Should have at least one card if dashboards exist
|
||||
// (This test may need adjustment based on test data availability)
|
||||
const cardCount = await cards.count();
|
||||
|
||||
// Either cards are visible, or the empty state is shown; the table
|
||||
// view must never render on mobile
|
||||
if (cardCount > 0) {
|
||||
await expect(cards.first()).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
} else {
|
||||
await expect(page.locator('[data-test="empty-state"]')).toBeVisible({
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
}
|
||||
await expect(page.locator('[data-test="listview-table"]')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('mobile search button appears in dashboard list', async ({ page }) => {
|
||||
// On mobile, the search/filter button should appear in the header
|
||||
const searchButton = page
|
||||
.locator('[aria-label="Search"]')
|
||||
.or(page.locator('[data-test="mobile-search-button"]'));
|
||||
|
||||
// Search button should be visible on mobile
|
||||
await expect(searchButton.first()).toBeVisible({
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
});
|
||||
|
||||
test('tapping dashboard card opens the dashboard', async ({ page }) => {
|
||||
// Find a dashboard card
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
const cardCount = await cards.count();
|
||||
|
||||
if (cardCount > 0) {
|
||||
// Click the first card
|
||||
await cards.first().click();
|
||||
|
||||
// Should navigate to dashboard view
|
||||
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
|
||||
// Dashboard should load (look for dashboard content)
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-test="dashboard-content-wrapper"]')
|
||||
.or(page.locator('.dashboard')),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
} else {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mobile Dashboard Interaction', () => {
|
||||
test.use({
|
||||
viewport: mobileViewport.viewport,
|
||||
userAgent: mobileViewport.userAgent,
|
||||
});
|
||||
|
||||
// Skip this test suite if no dashboards exist
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const page = await browser.newPage({
|
||||
viewport: mobileViewport.viewport,
|
||||
userAgent: mobileViewport.userAgent,
|
||||
});
|
||||
|
||||
await page.goto('dashboard/list/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
const cardCount = await cards.count();
|
||||
|
||||
await page.close();
|
||||
|
||||
if (cardCount === 0) {
|
||||
test.skip();
|
||||
}
|
||||
});
|
||||
|
||||
test('dashboard loads and shows charts on mobile', async ({ page }) => {
|
||||
// Navigate to dashboard list
|
||||
await page.goto('dashboard/list/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click first dashboard
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
const cardCount = await cards.count();
|
||||
|
||||
if (cardCount > 0) {
|
||||
await cards.first().click();
|
||||
|
||||
// Wait for dashboard to load
|
||||
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
|
||||
// Dashboard content should be visible
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-test="dashboard-content-wrapper"]')
|
||||
.or(page.locator('.dashboard')),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
// Charts should start loading (look for chart containers)
|
||||
const chartContainers = page
|
||||
.locator('[data-test="chart-container"]')
|
||||
.or(page.locator('.dashboard-chart'));
|
||||
|
||||
// Wait for at least one chart to be visible (with timeout)
|
||||
await expect(chartContainers.first()).toBeVisible({
|
||||
timeout: TIMEOUT.PAGE_LOAD * 2,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('dashboard header shows hamburger menu on mobile', async ({ page }) => {
|
||||
// Navigate to dashboard list
|
||||
await page.goto('dashboard/list/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click first dashboard
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
const cardCount = await cards.count();
|
||||
|
||||
if (cardCount > 0) {
|
||||
await cards.first().click();
|
||||
|
||||
// Wait for dashboard
|
||||
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
|
||||
// Look for the hamburger menu / more actions button
|
||||
const menuButton = page
|
||||
.locator('[data-test="actions-trigger"]')
|
||||
.or(page.locator('[aria-label="Menu actions trigger"]'));
|
||||
|
||||
await expect(menuButton.first()).toBeVisible({
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('refresh dashboard works from mobile menu', async ({ page }) => {
|
||||
// Navigate to dashboard list
|
||||
await page.goto('dashboard/list/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click first dashboard
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
const cardCount = await cards.count();
|
||||
|
||||
if (cardCount > 0) {
|
||||
await cards.first().click();
|
||||
|
||||
// Wait for dashboard
|
||||
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
|
||||
// Open the actions menu
|
||||
const menuButton = page
|
||||
.locator('[data-test="actions-trigger"]')
|
||||
.or(page.locator('[aria-label="Menu actions trigger"]'));
|
||||
|
||||
if ((await menuButton.count()) > 0) {
|
||||
await menuButton.first().click();
|
||||
|
||||
// Look for refresh option
|
||||
const refreshOption = page.getByText('Refresh dashboard');
|
||||
|
||||
if ((await refreshOption.count()) > 0) {
|
||||
await refreshOption.click();
|
||||
|
||||
// Should show success toast or refresh the charts
|
||||
// This is hard to verify without checking network requests
|
||||
// Just verify the menu closes and we're still on the dashboard
|
||||
await page.waitForTimeout(1000);
|
||||
expect(page.url()).toMatch(/\/dashboard\/(?!list)/);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mobile Filter Drawer', () => {
|
||||
test.use({
|
||||
viewport: mobileViewport.viewport,
|
||||
userAgent: mobileViewport.userAgent,
|
||||
});
|
||||
|
||||
test('filter button appears on dashboards with filters', async ({ page }) => {
|
||||
// Navigate to dashboard list
|
||||
await page.goto('dashboard/list/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click first dashboard
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
const cardCount = await cards.count();
|
||||
|
||||
if (cardCount > 0) {
|
||||
await cards.first().click();
|
||||
|
||||
// Wait for dashboard
|
||||
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
|
||||
// Give filters time to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check for filter button (only visible if dashboard has filters)
|
||||
const filterButton = page
|
||||
.locator('[data-test="filter-icon"]')
|
||||
.or(
|
||||
page
|
||||
.locator('[aria-label="Filters"]')
|
||||
.or(page.locator('.mobile-filter-button')),
|
||||
);
|
||||
|
||||
const filterCount = await filterButton.count();
|
||||
|
||||
// The test passes whether filters exist or not
|
||||
// If filters exist, button should be visible
|
||||
// If no filters, that's also valid
|
||||
if (filterCount > 0) {
|
||||
await expect(filterButton.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('filter drawer opens when filter button is tapped', async ({ page }) => {
|
||||
// Navigate to dashboard list
|
||||
await page.goto('dashboard/list/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click first dashboard
|
||||
const cards = page.locator('[data-test="styled-card"]');
|
||||
const cardCount = await cards.count();
|
||||
|
||||
if (cardCount > 0) {
|
||||
await cards.first().click();
|
||||
|
||||
// Wait for dashboard
|
||||
await page.waitForURL(url => /\/dashboard\/(?!list)/.test(url.pathname), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
|
||||
// Give filters time to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check for filter button
|
||||
const filterButton = page
|
||||
.locator('[data-test="filter-icon"]')
|
||||
.or(page.locator('[aria-label="Filters"]'));
|
||||
|
||||
if ((await filterButton.count()) > 0) {
|
||||
await filterButton.first().click();
|
||||
|
||||
// Filter drawer should open
|
||||
const drawer = page
|
||||
.locator('.ant-drawer-open')
|
||||
.or(page.locator('[data-test="filter-bar"]'));
|
||||
|
||||
await expect(drawer.first()).toBeVisible({
|
||||
timeout: TIMEOUT.FORM_LOAD,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* 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 { test, expect, devices } from '@playwright/test';
|
||||
|
||||
// NOTE: These tests exercise the mobile consumption experience and require
|
||||
// the MOBILE_CONSUMPTION_MODE feature flag to be enabled in the target
|
||||
// environment (FEATURE_FLAGS = {"MOBILE_CONSUMPTION_MODE": True}).
|
||||
import { URL } from '../../utils/urls';
|
||||
import { TIMEOUT } from '../../utils/constants';
|
||||
|
||||
/**
|
||||
* Mobile navigation tests verify the MobileRouteGuard behavior
|
||||
* and mobile-specific navigation patterns.
|
||||
*
|
||||
* These tests run with a mobile viewport to trigger mobile-specific behavior.
|
||||
*/
|
||||
|
||||
// Use iPhone 12 viewport for mobile tests
|
||||
const mobileViewport = devices['iPhone 12'];
|
||||
|
||||
test.describe('Mobile Navigation', () => {
|
||||
test.use({
|
||||
viewport: mobileViewport.viewport,
|
||||
userAgent: mobileViewport.userAgent,
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('mobile viewport redirects from chart list to MobileUnsupported page', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Navigate to chart list (not mobile-supported)
|
||||
await page.goto(URL.CHART_LIST);
|
||||
|
||||
// Should show the MobileUnsupported page
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
// Primary action buttons should be visible
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'View Dashboards' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Go to Welcome Page' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('mobile viewport allows access to dashboard list', async ({ page }) => {
|
||||
// Navigate to dashboard list (mobile-supported)
|
||||
await page.goto(URL.DASHBOARD_LIST);
|
||||
|
||||
// Should NOT show MobileUnsupported page
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).not.toBeVisible({ timeout: TIMEOUT.FORM_LOAD });
|
||||
|
||||
// Should show dashboard list content (look for dashboard list elements)
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-test="listview-table"]')
|
||||
.or(page.locator('[data-test="styled-card"]'))
|
||||
.first(),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
});
|
||||
|
||||
test('mobile viewport allows access to welcome page', async ({ page }) => {
|
||||
// Navigate to welcome page (mobile-supported)
|
||||
await page.goto(URL.WELCOME);
|
||||
|
||||
// Should NOT show MobileUnsupported page
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).not.toBeVisible({ timeout: TIMEOUT.FORM_LOAD });
|
||||
|
||||
// Should show welcome page content
|
||||
await expect(
|
||||
page.getByText('Recents').or(page.getByText('Dashboards')).first(),
|
||||
).toBeVisible({
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
});
|
||||
|
||||
test('View Dashboards button navigates to dashboard list', async ({
|
||||
page,
|
||||
}) => {
|
||||
// Navigate to unsupported route
|
||||
await page.goto(URL.CHART_LIST);
|
||||
|
||||
// Wait for MobileUnsupported page
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
// Click View Dashboards button
|
||||
await page.getByRole('button', { name: 'View Dashboards' }).click();
|
||||
|
||||
// Should navigate to dashboard list
|
||||
await page.waitForURL(url => url.pathname.includes('dashboard/list'), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
|
||||
// Dashboard list should be accessible
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-test="listview-table"]')
|
||||
.or(page.locator('[data-test="styled-card"]'))
|
||||
.first(),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
});
|
||||
|
||||
test('Go to Welcome Page button navigates to welcome', async ({ page }) => {
|
||||
// Navigate to unsupported route
|
||||
await page.goto(URL.CHART_LIST);
|
||||
|
||||
// Wait for MobileUnsupported page
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
// Click Go to Welcome Page button
|
||||
await page.getByRole('button', { name: 'Go to Welcome Page' }).click();
|
||||
|
||||
// Should navigate to welcome page
|
||||
await page.waitForURL(url => url.pathname.includes('welcome'), {
|
||||
timeout: TIMEOUT.PAGE_LOAD,
|
||||
});
|
||||
});
|
||||
|
||||
test('unsupported screen offers no bypass', async ({ page }) => {
|
||||
// The "Continue anyway" bypass was removed: desktop views are unusable
|
||||
// at phone width, and growing the viewport unblocks routes automatically
|
||||
await page.goto(URL.CHART_LIST);
|
||||
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
|
||||
await expect(page.getByText('Continue anyway')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('SQL Lab is not accessible on mobile', async ({ page }) => {
|
||||
// Navigate to SQL Lab (not mobile-supported)
|
||||
await page.goto(URL.SQLLAB);
|
||||
|
||||
// Should show the MobileUnsupported page
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Desktop Navigation (control group)', () => {
|
||||
// Use default desktop viewport
|
||||
|
||||
test('desktop viewport allows access to all routes', async ({ page }) => {
|
||||
// Navigate to chart list
|
||||
await page.goto(URL.CHART_LIST);
|
||||
|
||||
// Should NOT show MobileUnsupported page
|
||||
await expect(
|
||||
page.getByText("This view isn't available on mobile"),
|
||||
).not.toBeVisible({ timeout: TIMEOUT.FORM_LOAD });
|
||||
|
||||
// Should show chart list content
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-test="listview-table"]')
|
||||
.or(page.locator('[data-test="styled-card"]'))
|
||||
.first(),
|
||||
).toBeVisible({ timeout: TIMEOUT.PAGE_LOAD });
|
||||
});
|
||||
});
|
||||
@@ -1,169 +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.
|
||||
*/
|
||||
/**
|
||||
* Captures viz-picker gallery thumbnails from live example charts.
|
||||
*
|
||||
* This is a maintenance tool, not a test: it renders each example chart
|
||||
* standalone and overwrites the target plugin image with a fresh 512x512
|
||||
* screenshot. 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):
|
||||
* CAPTURE_THUMBNAILS=1 npx playwright test tests/tools/capture-viz-thumbnails.spec.ts --project chromium
|
||||
*
|
||||
* Notes:
|
||||
* - Only light-theme images are captured; `thumbnail-dark.png` variants
|
||||
* are 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 non-thumbnail output.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { test, expect } 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"]))';
|
||||
|
||||
/** superset-frontend root, resolved from this spec's location. */
|
||||
const FRONTEND_ROOT = path.resolve(__dirname, '..', '..', '..');
|
||||
|
||||
interface CaptureTarget {
|
||||
/** slice_name of the example chart to render */
|
||||
sliceName: string;
|
||||
/** output path relative to superset-frontend */
|
||||
output: string;
|
||||
}
|
||||
|
||||
const TARGETS: CaptureTarget[] = [
|
||||
// Charts from the Misc Charts example dashboard
|
||||
{
|
||||
sliceName: 'Total Sales Bullet',
|
||||
output: 'plugins/plugin-chart-echarts/src/Bullet/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Sales Period Pivot',
|
||||
output: 'plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Population Nightingale Rose',
|
||||
output: 'plugins/plugin-chart-rose/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Sales Calendar Heatmap',
|
||||
output: 'plugins/plugin-chart-calendar/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Population Partition',
|
||||
output: 'plugins/plugin-chart-partition/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Population Paired t-Test',
|
||||
output: 'plugins/plugin-chart-paired-t-test/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Region Population Time Table',
|
||||
output: 'src/visualizations/TimeTable/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
// Gallery example for the Line chart's percent-change mode; register
|
||||
// it in the Line plugin metadata after capturing.
|
||||
sliceName: 'Population Percent Change',
|
||||
output:
|
||||
'plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/images/Line3.png',
|
||||
},
|
||||
// Migrated charts whose examples live on other dashboards
|
||||
{
|
||||
sliceName: 'Parallel Coordinates',
|
||||
output:
|
||||
'plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Birth in France by department in 2016',
|
||||
output: 'plugins/plugin-chart-country-map/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Cross Channel Relationship',
|
||||
output: 'plugins/plugin-chart-chord/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: 'Seasonality of Revenue (per Product Line)',
|
||||
output: 'plugins/plugin-chart-horizon/src/images/thumbnail.png',
|
||||
},
|
||||
{
|
||||
sliceName: '% Rural',
|
||||
output: 'plugins/plugin-chart-world-map/src/images/thumbnail.png',
|
||||
},
|
||||
];
|
||||
|
||||
test.describe('capture viz thumbnails', () => {
|
||||
test.skip(
|
||||
!process.env.CAPTURE_THUMBNAILS,
|
||||
'Thumbnail capture only runs with CAPTURE_THUMBNAILS=1',
|
||||
);
|
||||
|
||||
for (const target of TARGETS) {
|
||||
test(`captures ${target.sliceName}`, async ({ page }) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
// Look up the slice id by name via the chart API
|
||||
const query = encodeURIComponent(
|
||||
`(filters:!((col:slice_name,opr:eq,value:'${target.sliceName.replace(
|
||||
/'/g,
|
||||
"''",
|
||||
)}')))`,
|
||||
);
|
||||
const response = await page.request.get(`/api/v1/chart/?q=${query}`);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const { result } = await response.json();
|
||||
expect(
|
||||
result.length,
|
||||
`example chart "${target.sliceName}" not found — are examples loaded?`,
|
||||
).toBeGreaterThan(0);
|
||||
const sliceId = result[0].id;
|
||||
|
||||
// Render the chart alone at thumbnail size and wait for real content
|
||||
await page.setViewportSize({
|
||||
width: THUMBNAIL_SIZE,
|
||||
height: THUMBNAIL_SIZE,
|
||||
});
|
||||
await page.goto(`/explore/?slice_id=${sliceId}&standalone=1`);
|
||||
await page
|
||||
.locator(RENDERED_CHART_SELECTOR)
|
||||
.first()
|
||||
.waitFor({ state: 'visible', timeout: 60_000 });
|
||||
// Give animations/tiles a moment to settle before the still
|
||||
await page.waitForTimeout(2_000);
|
||||
|
||||
const outputPath = path.join(FRONTEND_ROOT, target.output);
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
await page.screenshot({ path: outputPath });
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`captured ${target.sliceName} -> ${target.output}`);
|
||||
if (!target.output.endsWith('thumbnail.png')) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`NOTE: ${target.output} is a new gallery image — register it in the plugin metadata (exampleGallery) to surface it.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -17,10 +17,10 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## @superset-ui/plugin-chart-calendar
|
||||
## @superset-ui/legacy-plugin-chart-calendar
|
||||
|
||||
[](https://www.npmjs.com/package/@superset-ui/plugin-chart-calendar)
|
||||
[](https://libraries.io/npm/@superset-ui%2Fplugin-chart-calendar)
|
||||
[](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-calendar)
|
||||
[](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-calendar)
|
||||
|
||||
This plugin provides Calendar Heatmap for Superset.
|
||||
|
||||
@@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key`
|
||||
lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import CalendarChartPlugin from '@superset-ui/plugin-chart-calendar';
|
||||
import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar';
|
||||
|
||||
new CalendarChartPlugin().configure({ key: 'calendar' }).register();
|
||||
```
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@superset-ui/plugin-chart-calendar",
|
||||
"name": "@superset-ui/legacy-plugin-chart-calendar",
|
||||
"version": "0.20.3",
|
||||
"description": "Superset Legacy Chart - Calendar Heatmap",
|
||||
"keywords": [
|
||||
"superset"
|
||||
],
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-calendar#readme",
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-calendar#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/plugins/plugin-chart-calendar"
|
||||
"directory": "superset-frontend/plugins/legacy-plugin-chart-calendar"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "Superset",
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -43,13 +43,13 @@ const metadata = new ChartMetadata({
|
||||
],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class CalendarChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactCalendar'),
|
||||
loadBuildQuery: () => import('./buildQuery'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
@@ -18,14 +18,14 @@
|
||||
*/
|
||||
|
||||
import { SuperChart } from '@superset-ui/core';
|
||||
import CalendarChartPlugin from '@superset-ui/plugin-chart-calendar';
|
||||
import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar';
|
||||
import data from './data';
|
||||
import { dummyDatasource, withResizableChartDemo } from '@storybook-shared';
|
||||
|
||||
new CalendarChartPlugin().configure({ key: 'calendar' }).register();
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins/plugin-chart-calendar',
|
||||
title: 'Legacy Chart Plugins/legacy-plugin-chart-calendar',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
cellSize: 10,
|
||||
@@ -17,15 +17,8 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ChartProps,
|
||||
ensureIsArray,
|
||||
getMetricLabel,
|
||||
getNumberFormatter,
|
||||
QueryFormMetric,
|
||||
} from '@superset-ui/core';
|
||||
import { ChartProps, getNumberFormatter } from '@superset-ui/core';
|
||||
import { getFormattedUTCTime } from './utils';
|
||||
import transformData from './transformData';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { height, formData, queriesData, datasource } = chartProps;
|
||||
@@ -51,29 +44,9 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
getFormattedUTCTime(ts, xAxisTimeFormat);
|
||||
const valueFormatter = getNumberFormatter(yAxisFormat);
|
||||
|
||||
// The legacy explore_json endpoint computed the per-metric value maps
|
||||
// and domain range server-side; v1 responses arrive as flat records.
|
||||
const {
|
||||
data: rawData,
|
||||
from_dttm: fromDttm,
|
||||
to_dttm: toDttm,
|
||||
} = queriesData[0];
|
||||
const data = Array.isArray(rawData)
|
||||
? transformData(
|
||||
rawData,
|
||||
ensureIsArray(formData.metrics as QueryFormMetric[]).map(
|
||||
getMetricLabel,
|
||||
),
|
||||
fromDttm,
|
||||
toDttm,
|
||||
domainGranularity,
|
||||
subdomainGranularity,
|
||||
)
|
||||
: rawData;
|
||||
|
||||
return {
|
||||
height,
|
||||
data,
|
||||
data: queriesData[0].data,
|
||||
cellPadding,
|
||||
cellRadius,
|
||||
cellSize,
|
||||
@@ -17,10 +17,10 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## @superset-ui/plugin-chart-chord
|
||||
## @superset-ui/legacy-plugin-chart-chord
|
||||
|
||||
[](https://www.npmjs.com/package/@superset-ui/plugin-chart-chord)
|
||||
[](https://libraries.io/npm/@superset-ui%2Fplugin-chart-chord)
|
||||
[](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-chord)
|
||||
[](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-chord)
|
||||
|
||||
This plugin provides Chord Diagram for Superset.
|
||||
|
||||
@@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key`
|
||||
lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import ChordChartPlugin from '@superset-ui/plugin-chart-chord';
|
||||
import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord';
|
||||
|
||||
new ChordChartPlugin().configure({ key: 'chord' }).register();
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@superset-ui/plugin-chart-chord",
|
||||
"name": "@superset-ui/legacy-plugin-chart-chord",
|
||||
"version": "0.20.3",
|
||||
"description": "Superset Legacy Chart - Chord Diagram",
|
||||
"sideEffects": [
|
||||
@@ -14,7 +14,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/plugins/plugin-chart-chord"
|
||||
"directory": "superset-frontend/plugins/legacy-plugin-chart-chord"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
@@ -24,7 +24,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-chord#readme",
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-chord#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
@@ -39,16 +39,16 @@ const metadata = new ChartMetadata({
|
||||
},
|
||||
],
|
||||
name: t('Chord Diagram'),
|
||||
tags: [t('Circular'), t('Proportional'), t('Relational')],
|
||||
tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')],
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
useLegacyApi: true,
|
||||
});
|
||||
|
||||
export default class ChordChartPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
super({
|
||||
loadChart: () => import('./ReactChord'),
|
||||
loadBuildQuery: () => import('./buildQuery'),
|
||||
metadata,
|
||||
transformProps,
|
||||
controlPanel,
|
||||
@@ -18,14 +18,14 @@
|
||||
*/
|
||||
|
||||
import { SuperChart, VizType } from '@superset-ui/core';
|
||||
import ChordChartPlugin from '@superset-ui/plugin-chart-chord';
|
||||
import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord';
|
||||
import data from './data';
|
||||
import { withResizableChartDemo } from '@storybook-shared';
|
||||
|
||||
new ChordChartPlugin().configure({ key: VizType.Chord }).register();
|
||||
|
||||
export default {
|
||||
title: 'Chart Plugins/plugin-chart-chord',
|
||||
title: 'Legacy Chart Plugins/legacy-plugin-chart-chord',
|
||||
decorators: [withResizableChartDemo],
|
||||
args: {
|
||||
colorScheme: 'd3Category10',
|
||||
@@ -16,10 +16,18 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import CalendarChartPlugin from '../src/index';
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
|
||||
test('registers the plugin with its v1 metadata and buildQuery loader', () => {
|
||||
const plugin = new CalendarChartPlugin();
|
||||
expect(plugin.metadata.name).toEqual('Calendar Heatmap');
|
||||
expect(plugin.loadBuildQuery).toBeDefined();
|
||||
});
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { width, height, formData, queriesData } = chartProps;
|
||||
const { yAxisFormat, colorScheme, sliceId } = formData;
|
||||
|
||||
return {
|
||||
colorScheme,
|
||||
data: queriesData[0].data,
|
||||
height,
|
||||
numberFormat: yAxisFormat,
|
||||
width,
|
||||
sliceId,
|
||||
};
|
||||
}
|
||||
@@ -17,10 +17,10 @@ specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
## @superset-ui/plugin-chart-country-map
|
||||
## @superset-ui/legacy-plugin-chart-country-map
|
||||
|
||||
[](https://www.npmjs.com/package/@superset-ui/plugin-chart-country-map)
|
||||
[](https://libraries.io/npm/@superset-ui%2Fplugin-chart-country-map)
|
||||
[](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-country-map)
|
||||
[](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-country-map)
|
||||
|
||||
This plugin provides Country Map for Superset.
|
||||
|
||||
@@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key`
|
||||
lookup this chart throughout the app.
|
||||
|
||||
```js
|
||||
import CountryMapChartPlugin from '@superset-ui/plugin-chart-country-map';
|
||||
import CountryMapChartPlugin from '@superset-ui/legacy-plugin-chart-country-map';
|
||||
|
||||
new CountryMapChartPlugin().configure({ key: 'country-map' }).register();
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@superset-ui/plugin-chart-country-map",
|
||||
"name": "@superset-ui/legacy-plugin-chart-country-map",
|
||||
"version": "0.20.3",
|
||||
"description": "Superset Legacy Chart - Country Map",
|
||||
"main": "lib/index.js",
|
||||
@@ -11,7 +11,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apache/superset.git",
|
||||
"directory": "superset-frontend/plugins/plugin-chart-country-map"
|
||||
"directory": "superset-frontend/plugins/legacy-plugin-chart-country-map"
|
||||
},
|
||||
"keywords": [
|
||||
"superset"
|
||||
@@ -21,7 +21,7 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/apache/superset/issues"
|
||||
},
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-country-map#readme",
|
||||
"homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-country-map#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||