mirror of
https://github.com/apache/superset.git
synced 2026-07-03 21:35:32 +00:00
Compare commits
70 Commits
react18-co
...
fix/105973
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
797153b74c | ||
|
|
4f37e955b5 | ||
|
|
46a153d17e | ||
|
|
6d22697cba | ||
|
|
7cbdc726e3 | ||
|
|
ceadce234b | ||
|
|
47bc3e2dc1 | ||
|
|
49fcaf2420 | ||
|
|
55088e10da | ||
|
|
bdc610c572 | ||
|
|
a151edeff3 | ||
|
|
d8832c382d | ||
|
|
5bbab86a07 | ||
|
|
ab0e77c1cb | ||
|
|
a30846881b | ||
|
|
22c3f56d0a | ||
|
|
66bf81b997 | ||
|
|
03703843b7 | ||
|
|
f61d6d8b84 | ||
|
|
7eb93c60a3 | ||
|
|
9e08770291 | ||
|
|
358493a2c5 | ||
|
|
83965b4be8 | ||
|
|
f9bcf189c9 | ||
|
|
1c74185a71 | ||
|
|
8bf3933972 | ||
|
|
19e94855a1 | ||
|
|
139df20cde | ||
|
|
4c193d4dbc | ||
|
|
aa40934e7f | ||
|
|
6c2c814b5c | ||
|
|
9769380d6d | ||
|
|
be29d877d2 | ||
|
|
e3b2992d6e | ||
|
|
c1bd45f561 | ||
|
|
7214e9f9f6 | ||
|
|
d7e2f18d00 | ||
|
|
6309d08d59 | ||
|
|
afebdd58d1 | ||
|
|
be46d65e3b | ||
|
|
2992d7b4c8 | ||
|
|
80344852b7 | ||
|
|
8210904e95 | ||
|
|
0a25faabbd | ||
|
|
a48ca9ce72 | ||
|
|
8343a4fff8 | ||
|
|
a919dda2ac | ||
|
|
246bbeb408 | ||
|
|
b2af954e7b | ||
|
|
c3f5e997a1 | ||
|
|
d507be2555 | ||
|
|
e3bd6e5c70 | ||
|
|
748060d35e | ||
|
|
d57d69d3a6 | ||
|
|
706ff94f0b | ||
|
|
661d362580 | ||
|
|
6bdcb7a83a | ||
|
|
d1f7dd9c67 | ||
|
|
c718f717cb | ||
|
|
b3197c9b5e | ||
|
|
af0a55a4f3 | ||
|
|
8be255de40 | ||
|
|
f2e322c3c0 | ||
|
|
e58ce1cf39 | ||
|
|
393adc4535 | ||
|
|
e0a3b1c10c | ||
|
|
6c57919647 | ||
|
|
bed1034c2f | ||
|
|
b7d5de8e52 | ||
|
|
348d924c92 |
14
.github/CODEOWNERS
vendored
14
.github/CODEOWNERS
vendored
@@ -1,7 +1,5 @@
|
||||
# Notify all committers of DB migration changes, per SIP-59
|
||||
|
||||
# https://github.com/apache/superset/issues/13351
|
||||
|
||||
/superset/migrations/ @mistercrunch @michael-s-molina @betodealmeida @eschutho @sadpandajoe
|
||||
|
||||
# Notify some committers of changes in the components
|
||||
@@ -12,28 +10,30 @@
|
||||
|
||||
# Notify Helm Chart maintainers about changes in it
|
||||
|
||||
/helm/superset/ @craig-rueda @dpgaspar @villebro @nytai @michael-s-molina @mistercrunch @rusackas @Antonio-RiveroMartnez
|
||||
/helm/superset/ @dpgaspar @villebro @nytai @michael-s-molina @mistercrunch @rusackas @Antonio-RiveroMartnez @hainenber
|
||||
|
||||
# Notify E2E test maintainers of changes
|
||||
|
||||
/superset-frontend/cypress-base/ @sadpandajoe @geido @eschutho @rusackas @betodealmeida @mistercrunch
|
||||
/superset-frontend/playwright/ @sadpandajoe @geido @eschutho @rusackas @mistercrunch
|
||||
/superset-frontend/cypress-base/ @sadpandajoe @geido @eschutho @rusackas @mistercrunch
|
||||
|
||||
# Notify PMC members of changes to GitHub Actions
|
||||
|
||||
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe @hainenber
|
||||
/.github/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @sha174n @dpgaspar @sadpandajoe @hainenber
|
||||
|
||||
# Notify PMC members of changes to CI-executed scripts (supply-chain risk:
|
||||
# scripts/ files run directly in CI workflows and can execute arbitrary code)
|
||||
|
||||
/scripts/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @sadpandajoe @hainenber
|
||||
/scripts/ @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @sadpandajoe @hainenber
|
||||
|
||||
# Notify PMC members of changes to required GitHub Actions
|
||||
|
||||
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @craig-rueda @kgabryje @dpgaspar @Antonio-RiveroMartnez
|
||||
/.asf.yaml @villebro @geido @eschutho @rusackas @betodealmeida @nytai @mistercrunch @kgabryje @dpgaspar @sha174n @Antonio-RiveroMartnez
|
||||
|
||||
# Maps are a finicky contribution process we care about
|
||||
|
||||
**/*.geojson @villebro @rusackas
|
||||
**/*.ipynb @villebro @rusackas
|
||||
/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas
|
||||
|
||||
# Notify translation maintainers of changes to translations
|
||||
|
||||
4
.github/workflows/codeql-analysis.yml
vendored
4
.github/workflows/codeql-analysis.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -37,8 +37,9 @@ jobs:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Dependencies
|
||||
# Versions are pinned to avoid ad-hoc, unpinned package installs
|
||||
# (zizmor adhoc-packages). Bump deliberately when upgrading.
|
||||
# Versions are pinned to avoid ad-hoc, unpinned package installs.
|
||||
# Bump deliberately when upgrading.
|
||||
# zizmor: ignore[adhoc-packages] - @action-validator is a global CLI tool installed to validate the repo's workflows; a global CLI install has no application manifest/lockfile context, and the versions are pinned above
|
||||
run: npm install -g @action-validator/core@0.6.0 @action-validator/cli@0.6.0
|
||||
|
||||
- name: Run Script
|
||||
|
||||
7
.github/workflows/showtime-trigger.yml
vendored
7
.github/workflows/showtime-trigger.yml
vendored
@@ -2,7 +2,8 @@ name: 🎪 Superset Showtime
|
||||
|
||||
# Ultra-simple: just sync on any PR state change
|
||||
on:
|
||||
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; this workflow does not check out or execute PR-provided code
|
||||
# zizmor: ignore[dangerous-triggers] - required to react to PR label changes; PR code is
|
||||
# only checked out and built after the maintainer-authorization gate (write/admin actors)
|
||||
pull_request_target:
|
||||
types: [labeled, unlabeled, synchronize, closed]
|
||||
|
||||
@@ -156,6 +157,10 @@ jobs:
|
||||
with:
|
||||
ref: ${{ steps.check.outputs.target_sha }}
|
||||
persist-credentials: false
|
||||
# Building fork PR code is Showtime's purpose: deploys are gated on the
|
||||
# maintainer-authorization step above (write/admin actors only), so this
|
||||
# checkout is an explicit, authorized opt-in rather than an automatic one.
|
||||
allow-unsafe-pr-checkout: true
|
||||
|
||||
- name: Setup Docker Environment (only if build needed)
|
||||
if: steps.auth.outputs.authorized == 'true' && steps.check.outputs.build_needed == 'true'
|
||||
|
||||
0
.pre-commit-config.yaml
Normal file → Executable file
0
.pre-commit-config.yaml
Normal file → Executable file
@@ -120,7 +120,7 @@ RUN useradd --user-group -d ${SUPERSET_HOME} -m --no-log-init --shell /bin/bash
|
||||
# Some bash scripts needed throughout the layers
|
||||
COPY --chmod=755 docker/*.sh /app/docker/
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||
RUN pip install --no-cache-dir --upgrade uv
|
||||
|
||||
# Using uv as it's faster/simpler than pip
|
||||
RUN uv venv /app/.venv
|
||||
@@ -141,7 +141,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
|
||||
COPY superset/translations/ /app/translations_mo/
|
||||
RUN if [ "${BUILD_TRANSLATIONS}" = "true" ]; then \
|
||||
pybabel compile -d /app/translations_mo | true; \
|
||||
pybabel compile --use-fuzzy -d /app/translations_mo || true; \
|
||||
fi; \
|
||||
rm -f /app/translations_mo/*/*/*.[po,json]
|
||||
|
||||
|
||||
76
UPDATING.md
76
UPDATING.md
@@ -24,6 +24,24 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
- [39925](https://github.com/apache/superset/pull/39925): URL prefixing for `SUPERSET_APP_ROOT` subdirectory deployments is now handled automatically by helpers in `src/utils/navigationUtils` (`openInNewTab`, `redirect`, `getShareableUrl`, `<AppLink>`). Direct imports of `ensureAppRoot` / `makeUrl` from `src/utils/pathUtils` are forbidden outside `navigationUtils.ts` (enforced by a static-invariant test); contributors writing new code should use the focused helpers instead. No runtime behaviour change for existing callers — all 19 prior call sites have been migrated and four pre-existing double-prefix and missing-prefix bugs are fixed as part of the migration.
|
||||
|
||||
- [39925](https://github.com/apache/superset/pull/39925): `SupersetClient.getUrl()` now strips a single leading application-root segment from the supplied `endpoint` before building the request URL, so a caller that accidentally pre-prefixes its endpoint (for example by wrapping it with `ensureAppRoot` before passing it to the client) no longer produces a doubled `/superset/superset/...` URL under subdirectory deployment. The strip is **single-pass** — a genuine `/superset/superset/<slug>` route is preserved, not collapsed — and **silent** (no console warning); the static-invariant test remains the primary signal for pre-prefixing at the call site, and this runtime strip is a safety net beneath it. Code that intentionally targeted a literal `/<app_root>/<app_root>/...` endpoint through `getUrl` (a configuration that has no legitimate use under the prefixing model) would have its first redundant segment removed.
|
||||
|
||||
- **Breaking — `Superset` view class route prefix removed.** The `Superset` view in `superset/views/core.py` now declares `route_base = ""`, overriding Flask-AppBuilder's auto-derived `/superset` prefix. Routes that previously lived at `/superset/welcome/`, `/superset/dashboard/<id>/`, `/superset/dashboard/p/<key>/`, `/superset/explore/`, etc. now respond at `/welcome/`, `/dashboard/<id>/`, `/dashboard/p/<key>/`, `/explore/`, etc. Under subdirectory deployment (`SUPERSET_APP_ROOT=/superset`) the URLs are unchanged from end-user perspective — `AppRootMiddleware` re-applies the prefix via `SCRIPT_NAME`. Under root deployments, any external integration or bookmark that hard-codes `/superset/<endpoint>/` paths must be updated to drop the prefix. This fixes the doubled `/superset/superset/...` URLs that `url_for` emitted for these endpoints under subdirectory deployment and the related 404s on the routes themselves.
|
||||
|
||||
- **Breaking — Three sibling view classes route prefix removed.** Following the same rationale as the `Superset` class above, `ExplorePermalinkView` (`superset/views/explore.py`), `TagModelView`, and `TaggedObjectsModelView` (`superset/views/tags.py`, `superset/views/all_entities.py`) now mount at the application root rather than a hard-coded `/superset/...`. The user-visible URLs `/superset/explore/p/<key>/`, `/superset/tags/`, and `/superset/all_entities/` are unchanged under subdirectory deployment; under root deployments these views now serve `/explore/p/<key>/`, `/tags/`, and `/all_entities/`, so any external integration or bookmark must drop the `/superset/` prefix. `Dashboard.url` and `Dashboard.get_url` likewise return `/dashboard/<id>/` instead of the prior `/superset/dashboard/<id>/` literal so downstream consumers (DashboardList row hrefs, MCP service `dashboard_url`) emit a single, deployment-correct prefix.
|
||||
|
||||
- **Legacy `/superset/*` path support.** A new outermost WSGI middleware `LegacyPrefixRedirectMiddleware` (`superset/middleware/legacy_prefix_redirect.py`) 308-redirects every enumerated legacy `/superset/<canonical>` path to its post-`route_base=""` canonical location (e.g. `/superset/welcome/` → `/welcome/` under root; → `/superset/welcome/` under `SUPERSET_APP_ROOT=/superset`, because the canonical resolves through `AppRootMiddleware`). Bookmarks, email links, and external integrations survive the route-base collapse for one release cycle. POST against a GET-only canonical returns 410 Gone instead of 308 (308 would 405 on retry). The shim is removed at EOL `5.0.0`, matching the `@deprecated(eol_version="5.0.0")` gate on `Superset.explore` and `Superset.explore_json`.
|
||||
|
||||
- **PWA web app manifest served dynamically.** The PWA manifest is now served at `/pwa-manifest.json` (under `APPLICATION_ROOT`) by a new `PwaManifestView` (`superset/views/pwa_manifest.py`) instead of the static file at `/static/assets/pwa-manifest.json`. The legacy static source at `superset-frontend/src/pwa-manifest.json` has been removed (along with its `webpack.config.js` `CopyPlugin` rule). The new endpoint resolves `APPLICATION_ROOT` and `STATIC_ASSETS_PREFIX` at request time so PWA install works under subdirectory deployments and split static-prefix / app-root deployments (where `STATIC_ASSETS_PREFIX` points to a CDN host while the Superset backend stays under `APPLICATION_ROOT`). The `<link rel="manifest">` href in `superset/templates/superset/spa.html` was updated correspondingly (using a new `application_root_rstrip` template global). Operators with a forked `spa.html` should switch any manifest `<link>` to `{{ application_root_rstrip }}/pwa-manifest.json`.
|
||||
|
||||
- **Hard re-bookmark break — `/superset/sql/<database_id>/`.** SQL Lab moved to its own blueprint at `/sqllab/`. The legacy `/superset/sql/<id>/` shape changed to a query-string form (`/sqllab/?dbid=<id>`); no 1:1 path mapping exists, so `LegacyPrefixRedirectMiddleware` does **not** redirect this route — it passes through and surfaces a 404. Users with bookmarks to `/superset/sql/<id>/` must update them to `/sqllab/?dbid=<id>`.
|
||||
|
||||
- **`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.
|
||||
|
||||
### SQL Lab denies large-object and information_schema access by default
|
||||
|
||||
`DISALLOWED_SQL_FUNCTIONS` and `DISALLOWED_SQL_TABLES` now ship with additional default entries, so SQL Lab and chart-data queries that reference them are rejected where they were previously allowed:
|
||||
@@ -61,6 +79,19 @@ When the MCP service has JWT auth enabled (`MCP_AUTH_ENABLED = True`), an audien
|
||||
|
||||
The git SHA and build number surfaced in the "About" section, the bootstrap payload, and the public `/version` endpoint are now only included for admin users by default; the release version string is still shown to everyone. To expose the build details to all users (the previous behavior), set the `SUPERSET_EXPOSE_BUILD_DETAILS` environment variable (or `EXPOSE_BUILD_DETAILS_TO_USERS = True` in `superset_config.py`).
|
||||
|
||||
### Helm chart adopts Kubernetes recommended labels (breaking upgrade)
|
||||
|
||||
The Helm chart now labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. Because a Deployment's `spec.selector.matchLabels` is immutable, `helm upgrade` against an existing release will fail with a `field is immutable` error.
|
||||
|
||||
To upgrade, delete the affected workloads (which selector labels changed) before upgrading, then run the upgrade so they are recreated with the new labels:
|
||||
|
||||
```bash
|
||||
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
|
||||
helm upgrade <release-name> superset/superset
|
||||
```
|
||||
|
||||
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
|
||||
|
||||
### Pivot table First/Last aggregations follow data order
|
||||
|
||||
The pivot table chart's `First` and `Last` aggregations now return the first and last value in data (query result) order, instead of effectively returning the minimum and maximum. Existing pivot tables that use these aggregations for totals/subtotals may show different values after upgrading. For deterministic results, ensure the underlying query has a stable sort order.
|
||||
@@ -271,6 +302,28 @@ Schedule the cutover in a quiet window. Runtime reads use only the single config
|
||||
|
||||
The migration is transactional (all-or-nothing) and idempotent — it can be safely re-run or resumed. Note that AES-GCM, unlike AES-CBC, does not support querying directly over encrypted columns; audit any code that filters on an encrypted column before switching. See the SIP at `docs/sip/authenticated-encryption-at-rest.md` for details.
|
||||
|
||||
### Soft delete and restore for dashboards
|
||||
|
||||
**Everything in this section applies only when the `SOFT_DELETE` feature flag is enabled. The flag defaults to `False`** (`@lifecycle: development`), so on a default deployment `DELETE /api/v1/dashboard/<id>` continues to **hard-delete permanently** — nothing is recoverable. Enable `SOFT_DELETE` to get the behavior described below.
|
||||
|
||||
**Flag-toggle caveat:** the soft-delete visibility filter is evaluated per query while the flag is on. If dashboards are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live dashboards in all lists and lookups (including slug lookups — if a soft-deleted dashboard's slug was reused while the flag was on, both rows become visible with the same slug). The `POST /<uuid>/restore` endpoint and the `dashboard_deleted_state` list filter remain functional regardless of the flag, deliberately, so rows soft-deleted during a flag-on window stay discoverable and restorable after a rollback of the flag.
|
||||
|
||||
With the flag enabled: `DELETE /api/v1/dashboard/<id>` no longer hard-deletes the dashboard (the bulk-delete endpoint behaves the same way). The row is marked with a `deleted_at` timestamp and hidden from the dashboard API's list, detail, and lookup endpoints, which return 404 for soft-deleted dashboards. The embedded-dashboard iframe URL (`/embedded/<uuid>`) keeps rendering because it reads only `embedded.allowed_domains` and `embedded.dashboard_id` (the FK column) without dereferencing the parent dashboard; the frontend's subsequent dashboard-API fetch is what sees the 404 and surfaces "dashboard not found" to the user.
|
||||
|
||||
**New endpoint** — `POST /api/v1/dashboard/<uuid>/restore` clears `deleted_at` and returns the dashboard to active state. Requires `can_write on Dashboard` and ownership of the row (or admin). Soft-deleted dashboards can also be surfaced in the list endpoint via the new `dashboard_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to dashboards they own — the same audience that can restore them.
|
||||
|
||||
**Permissions migration:** existing role grants of `can_write on Dashboard` cover the new restore endpoint automatically; no role migration is required.
|
||||
|
||||
**Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_dashboards_deleted_at`) to the `dashboards` table, and **replaces the full unique constraint on `slug`** with a partial unique index (`ix_dashboards_active_slug`) enforcing slug uniqueness only among active (non-soft-deleted) rows. The column add is instant. On Postgres the constraint swap briefly blocks reads and writes during `ALTER TABLE ... DROP CONSTRAINT` (acquires `ACCESS EXCLUSIVE`), then blocks writes only during `CREATE UNIQUE INDEX` (acquires `ShareLock`); reads pass through during the index build. Both windows are sub-second on a typical `dashboards` table. MySQL InnoDB builds the functional index online (no blocking).
|
||||
|
||||
**Rollback note:** the downgrade restores the original full unique constraint on `slug`. If the partial-index window allowed slug reuse (a soft-deleted row and an active row holding the same slug), `ALTER TABLE ... ADD CONSTRAINT idx_unique_slug UNIQUE (slug)` will abort with a unique-constraint violation. Before downgrading, hard-delete the soft-deleted duplicates (or rename one side) so each slug appears at most once across all rows. Rolling back the application code while leaving the new migration in place is also possible but exposes soft-deleted rows to the older code path; pair the rollback with a data decision (restore, hard-delete, or migrate-down).
|
||||
|
||||
The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WHERE deleted_at IS NULL` partial index; MySQL 8.0.13+ uses a functional index over `(CASE WHEN deleted_at IS NULL THEN slug END)` (8.0.13 is the first release with functional key parts). **MySQL <8.0.13, MariaDB, and SQLite keep the original full unique constraint** (functional indexes / column-level UNIQUE recreation aren't supported cleanly — MariaDB is excluded even at 10.x because its `CASE`-expression index semantics differ), so on those backends a soft-deleted dashboard continues to reserve its slug for the lifetime of the row.
|
||||
|
||||
**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, role grants, owners, tags) — including role grants that were implicitly revoked by the deletion. 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 ownership of the deleted row (or admin) — non-owners get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
### Granular Export Controls
|
||||
|
||||
A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
|
||||
@@ -560,6 +613,29 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides.
|
||||
}
|
||||
```
|
||||
|
||||
### Composite primary keys on many-to-many association tables
|
||||
|
||||
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)` |
|
||||
| `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)` |
|
||||
|
||||
**Before upgrading:**
|
||||
|
||||
- The migration **deletes** two classes of pre-existing rows the composite PK cannot accommodate: duplicate `(fk1, fk2)` pairs (it keeps the lowest `id` and removes the rest) and rows with `NULL` in either FK column. Both are meaningless for `secondary=` association tables, but export the affected rows first if you need an audit record.
|
||||
- External tooling (BI tools, backup scripts) that references the surrogate `id` on these tables will break; no application code references it.
|
||||
- Downgrade restores the `id` column (and the original `UNIQUE` on the two tables that had it) but leaves the FK columns `NOT NULL` (intentional — a `NULL` FK in a junction row is meaningless).
|
||||
|
||||
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.
|
||||
|
||||
@@ -332,15 +332,28 @@ cd superset-frontend
|
||||
npm run build-translation
|
||||
|
||||
# Backend
|
||||
pybabel compile -d superset/translations
|
||||
pybabel compile --use-fuzzy -d superset/translations
|
||||
```
|
||||
|
||||
`--use-fuzzy` includes `#, fuzzy` entries in the compiled `.mo` files. Superset
|
||||
serves fuzzy translations on purpose: the frontend build (`po2json --fuzzy`)
|
||||
already includes them, `flask fab babel-compile` (used by the release images)
|
||||
compiles with `-f`, and the production `Dockerfile` compiles with `--use-fuzzy`
|
||||
as well. This keeps machine-generated (and other draft) translations visible in
|
||||
the UI rather than falling back to English while they await review.
|
||||
|
||||
### Backfilling missing translations with AI
|
||||
|
||||
For languages with many untranslated strings, the repo includes a script that
|
||||
uses Claude AI to generate draft translations for any missing entries. All
|
||||
AI-generated strings are marked `#, fuzzy` and tagged with an attribution
|
||||
comment so that human reviewers know they need to be checked before merging.
|
||||
comment so that human reviewers know they need to be checked.
|
||||
|
||||
Note that `#, fuzzy` marks a translation as *needing review*, not as *withheld*:
|
||||
both the frontend and backend builds serve fuzzy entries (see [Applying
|
||||
translations](#applying-translations) above), so an AI-generated string is shown
|
||||
in the UI as soon as it is built and deployed. Reviewers should verify each
|
||||
entry and remove the `#, fuzzy` flag to promote it to a confirmed translation.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
|
||||
@@ -749,7 +749,7 @@ const config: Config = {
|
||||
showReadingTime: true,
|
||||
// Please change this to your repo.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/edit/main/website/blog/',
|
||||
'https://github.com/apache/superset/tree/master/docs',
|
||||
},
|
||||
theme: {
|
||||
customCss: require.resolve('./src/styles/custom.css'),
|
||||
|
||||
@@ -58,24 +58,14 @@
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@saucelabs/theme-github-codeblock": "^0.3.0",
|
||||
"@storybook/addon-docs": "^8.6.18",
|
||||
"@storybook/blocks": "^8.6.15",
|
||||
"@storybook/channels": "^8.6.18",
|
||||
"@storybook/client-logger": "^8.6.18",
|
||||
"@storybook/components": "^8.6.18",
|
||||
"@storybook/core": "^8.6.18",
|
||||
"@storybook/core-events": "^8.6.18",
|
||||
"@storybook/csf": "^0.1.13",
|
||||
"@storybook/docs-tools": "^8.6.18",
|
||||
"@storybook/preview-api": "^8.6.18",
|
||||
"@storybook/theming": "^8.6.15",
|
||||
"@storybook/addon-docs": "^10.4.5",
|
||||
"@superset-ui/core": "^0.20.4",
|
||||
"@swc/core": "^1.15.43",
|
||||
"antd": "^6.4.5",
|
||||
"baseline-browser-mapping": "^2.10.38",
|
||||
"caniuse-lite": "^1.0.30001799",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.2",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.0",
|
||||
"js-yaml": "^5.1.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
@@ -88,7 +78,7 @@
|
||||
"react-table": "^7.8.0",
|
||||
"remark-import-partial": "^0.0.2",
|
||||
"reselect": "^5.2.0",
|
||||
"storybook": "^8.6.18",
|
||||
"storybook": "^10.4.5",
|
||||
"swagger-ui-react": "^5.32.8",
|
||||
"swc-loader": "^0.2.7",
|
||||
"tinycolor2": "^1.4.2",
|
||||
|
||||
@@ -168,60 +168,6 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
__dirname,
|
||||
'../../superset-frontend/packages/superset-core/src',
|
||||
),
|
||||
// Add proper Storybook aliases
|
||||
'@storybook/blocks': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/blocks',
|
||||
),
|
||||
'@storybook/components': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/components',
|
||||
),
|
||||
'@storybook/theming': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/theming',
|
||||
),
|
||||
'@storybook/client-logger': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/client-logger',
|
||||
),
|
||||
'@storybook/core-events': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/core-events',
|
||||
),
|
||||
// Add internal Storybook aliases
|
||||
'storybook/internal/components': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/components',
|
||||
),
|
||||
'storybook/internal/theming': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/theming',
|
||||
),
|
||||
'storybook/internal/client-logger': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/client-logger',
|
||||
),
|
||||
'storybook/internal/csf': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/csf',
|
||||
),
|
||||
'storybook/internal/preview-api': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/preview-api',
|
||||
),
|
||||
'storybook/internal/docs-tools': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/docs-tools',
|
||||
),
|
||||
'storybook/internal/core-events': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/core-events',
|
||||
),
|
||||
'storybook/internal/channels': path.resolve(
|
||||
__dirname,
|
||||
'../node_modules/@storybook/channels',
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -519,6 +519,80 @@ For a connection to a SQL endpoint you need to use the HTTP path from the endpoi
|
||||
{"connect_args": {"http_path": "/sql/1.0/endpoints/****", "driver_path": "/path/to/odbc/driver"}}
|
||||
```
|
||||
|
||||
##### OAuth2 Authentication
|
||||
|
||||
Superset supports OAuth2 authentication for Databricks, allowing users to authenticate with their personal Databricks accounts instead of using shared access tokens. This provides better security and audit capabilities.
|
||||
|
||||
###### Prerequisites
|
||||
|
||||
1. Create an OAuth2 application in your Databricks account:
|
||||
- Go to your Databricks account console
|
||||
- Navigate to **Settings** → **Developer** → **OAuth apps**
|
||||
- Create a new OAuth app with the redirect URI: `http://your-superset-host:port/api/v1/database/oauth2/`
|
||||
|
||||
2. Configure OAuth2 in your `superset_config.py`:
|
||||
|
||||
```python
|
||||
from datetime import timedelta
|
||||
|
||||
# OAuth2 configuration for Databricks
|
||||
# The authorization endpoint is derived from your Databricks workspace host; the
|
||||
# token endpoint must be set explicitly (see notes below).
|
||||
DATABASE_OAUTH2_CLIENTS = {
|
||||
"Databricks (legacy)": {
|
||||
"id": "your-databricks-client-id",
|
||||
"secret": "your-databricks-client-secret",
|
||||
"scope": "sql",
|
||||
"token_request_uri": "https://your-workspace-host/oidc/v1/token",
|
||||
},
|
||||
"Databricks": {
|
||||
"id": "your-databricks-client-id",
|
||||
"secret": "your-databricks-client-secret",
|
||||
"scope": "sql",
|
||||
"token_request_uri": "https://your-workspace-host/oidc/v1/token",
|
||||
},
|
||||
}
|
||||
|
||||
# OAuth2 redirect URI (adjust hostname/port for your setup)
|
||||
DATABASE_OAUTH2_REDIRECT_URI = "http://your-superset-host:port/api/v1/database/oauth2/"
|
||||
|
||||
# Optional: OAuth2 timeout
|
||||
DATABASE_OAUTH2_TIMEOUT = timedelta(seconds=30)
|
||||
```
|
||||
|
||||
Replace the following placeholders:
|
||||
- `your-databricks-client-id`: Your Databricks OAuth2 application client ID
|
||||
- `your-databricks-client-secret`: Your Databricks OAuth2 application client secret
|
||||
- `your-superset-host:port`: Your Superset instance hostname and port
|
||||
|
||||
**Multi-Cloud Provider Support**
|
||||
|
||||
Databricks fronts the user-to-machine (U2M) OAuth2 flow on every workspace at
|
||||
`https://<workspace-host>/oidc/v1/authorize` and
|
||||
`https://<workspace-host>/oidc/v1/token`, regardless of whether the workspace
|
||||
runs on AWS, Azure, or GCP. Superset derives the **authorization** endpoint
|
||||
directly from your connection's host, so no cloud provider or account/tenant
|
||||
identifier needs to be configured.
|
||||
|
||||
The **token** endpoint cannot be auto-derived (token exchange has no database
|
||||
context to read the host), so you must supply `token_request_uri` in
|
||||
`DATABASE_OAUTH2_CLIENTS`, set to `https://<workspace-host>/oidc/v1/token` for
|
||||
your workspace.
|
||||
|
||||
If you supply a fully-resolved `authorization_request_uri` (and/or
|
||||
`token_request_uri`), those values take precedence over the host-derived
|
||||
defaults.
|
||||
|
||||
###### Usage
|
||||
|
||||
Once configured, users can:
|
||||
|
||||
1. Connect to Databricks databases normally using access tokens
|
||||
2. When querying data, Superset will automatically redirect users to authenticate with Databricks if needed
|
||||
3. User-specific OAuth2 tokens will be used for database connections, providing better security and audit trails
|
||||
|
||||
This feature works with both "Databricks (legacy)" and "Databricks" engine types and automatically supports all major cloud providers (AWS, Azure, GCP).
|
||||
|
||||
#### Denodo
|
||||
|
||||
The recommended connector library for Denodo is
|
||||
|
||||
1591
docs/yarn.lock
1591
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.18.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.19.0 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
@@ -46,6 +46,21 @@ It should be a long random bytes or str.
|
||||
|
||||
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
|
||||
|
||||
## Upgrade Notes
|
||||
|
||||
### Kubernetes recommended labels (breaking)
|
||||
|
||||
This chart labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. A Deployment's `spec.selector.matchLabels` is immutable, so `helm upgrade` against a release created before this change fails with a `field is immutable` error.
|
||||
|
||||
To upgrade an existing release, delete the affected workloads first (their selector labels changed), then upgrade so they are recreated:
|
||||
|
||||
```console
|
||||
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
|
||||
helm upgrade <release-name> superset/superset
|
||||
```
|
||||
|
||||
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
|
||||
|
||||
## Requirements
|
||||
|
||||
| Repository | Name | Version |
|
||||
|
||||
@@ -45,6 +45,21 @@ It should be a long random bytes or str.
|
||||
|
||||
On helm this can be set on `extraSecretEnv.SUPERSET_SECRET_KEY` or `configOverrides.secrets`
|
||||
|
||||
## Upgrade Notes
|
||||
|
||||
### Kubernetes recommended labels (breaking)
|
||||
|
||||
This chart labels and selects workloads using the [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) (`app.kubernetes.io/*`) instead of the legacy `app`/`release` labels. A Deployment's `spec.selector.matchLabels` is immutable, so `helm upgrade` against a release created before this change fails with a `field is immutable` error.
|
||||
|
||||
To upgrade an existing release, delete the affected workloads first (their selector labels changed), then upgrade so they are recreated:
|
||||
|
||||
```console
|
||||
kubectl delete deployment,statefulset -l release=<release-name> -n <namespace>
|
||||
helm upgrade <release-name> superset/superset
|
||||
```
|
||||
|
||||
Alternatively, perform a fresh install. This is a one-time migration; subsequent upgrades are unaffected.
|
||||
|
||||
{{ template "chart.requirementsSection" . }}
|
||||
|
||||
{{ template "chart.valuesSection" . }}
|
||||
|
||||
@@ -61,6 +61,49 @@ Create chart name and version as used by the chart label.
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels for all resources - follows Kubernetes recommended labels
|
||||
https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
|
||||
*/}}
|
||||
{{- define "superset.labels" -}}
|
||||
helm.sh/chart: {{ include "superset.chart" . }}
|
||||
{{ include "superset.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: superset
|
||||
{{- if .Values.extraLabels }}
|
||||
{{ toYaml .Values.extraLabels }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels - used by selectors and matchLabels
|
||||
*/}}
|
||||
{{- define "superset.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "superset.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Component labels - extends superset.labels with component-specific labels
|
||||
Usage: {{ include "superset.componentLabels" (dict "component" "web" "root" .) }}
|
||||
*/}}
|
||||
{{- define "superset.componentLabels" -}}
|
||||
{{ include "superset.labels" .root }}
|
||||
app.kubernetes.io/component: {{ .component }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Component selector labels - for matchLabels with component
|
||||
Usage: {{ include "superset.componentSelectorLabels" (dict "component" "web" "root" .) }}
|
||||
*/}}
|
||||
{{- define "superset.componentSelectorLabels" -}}
|
||||
{{ include "superset.selectorLabels" .root }}
|
||||
app.kubernetes.io/component: {{ .component }}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{- define "superset-config" }}
|
||||
import os
|
||||
@@ -146,27 +189,32 @@ RESULTS_BACKEND = RedisCache(
|
||||
|
||||
{{- end }}
|
||||
|
||||
{{- define "supersetNode.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "superset.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: web
|
||||
{{- end }}
|
||||
|
||||
{{- define "supersetCeleryBeat.selectorLabels" -}}
|
||||
app: {{ include "superset.name" . }}-celerybeat
|
||||
release: {{ .Release.Name }}
|
||||
app.kubernetes.io/name: {{ include "superset.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: celerybeat
|
||||
{{- end }}
|
||||
|
||||
{{- define "supersetCeleryFlower.selectorLabels" -}}
|
||||
app: {{ include "superset.name" . }}-flower
|
||||
release: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "supersetNode.selectorLabels" -}}
|
||||
app: {{ include "superset.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
app.kubernetes.io/name: {{ include "superset.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: flower
|
||||
{{- end }}
|
||||
|
||||
{{- define "supersetWebsockets.selectorLabels" -}}
|
||||
app: {{ include "superset.name" . }}-ws
|
||||
release: {{ .Release.Name }}
|
||||
app.kubernetes.io/name: {{ include "superset.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: websocket
|
||||
{{- end }}
|
||||
|
||||
{{- define "supersetWorker.selectorLabels" -}}
|
||||
app: {{ include "superset.name" . }}-worker
|
||||
release: {{ .Release.Name }}
|
||||
app.kubernetes.io/name: {{ include "superset.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/component: worker
|
||||
{{- end }}
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}-extra-config
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- range $path, $config := .Values.extraConfigs }}
|
||||
{{ $path }}: |
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}-celerybeat
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}-celerybeat
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "celerybeat" "root" .) | nindent 4 }}
|
||||
{{- if .Values.supersetCeleryBeat.deploymentAnnotations }}
|
||||
annotations: {{- toYaml .Values.supersetCeleryBeat.deploymentAnnotations | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -59,11 +53,7 @@ spec:
|
||||
{{- toYaml .Values.supersetCeleryBeat.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: "{{ template "superset.name" . }}-celerybeat"
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "supersetCeleryBeat.selectorLabels" . | nindent 8 }}
|
||||
{{- if .Values.supersetCeleryBeat.podLabels }}
|
||||
{{- toYaml .Values.supersetCeleryBeat.podLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}-flower
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}-flower
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "flower" "root" .) | nindent 4 }}
|
||||
{{- if .Values.supersetCeleryFlower.deploymentAnnotations }}
|
||||
annotations: {{- toYaml .Values.supersetCeleryFlower.deploymentAnnotations | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -48,11 +42,7 @@ spec:
|
||||
{{- toYaml .Values.supersetCeleryFlower.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: "{{ template "superset.name" . }}-flower"
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "supersetCeleryFlower.selectorLabels" . | nindent 8 }}
|
||||
{{- if .Values.supersetCeleryFlower.podLabels }}
|
||||
{{- toYaml .Values.supersetCeleryFlower.podLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -23,15 +23,9 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}-worker
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}-worker
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetWorker.deploymentLabels }}
|
||||
{{- toYaml .Values.supersetWorker.deploymentLabels | nindent 4 }}
|
||||
{{- include "superset.componentLabels" (dict "component" "worker" "root" .) | nindent 4 }}
|
||||
{{- with .Values.supersetWorker.deploymentLabels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetWorker.deploymentAnnotations }}
|
||||
annotations: {{- toYaml .Values.supersetWorker.deploymentAnnotations | nindent 4 }}
|
||||
@@ -65,11 +59,7 @@ spec:
|
||||
{{- toYaml .Values.supersetWorker.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}-worker
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "supersetWorker.selectorLabels" . | nindent 8 }}
|
||||
{{- if .Values.supersetWorker.podLabels }}
|
||||
{{- toYaml .Values.supersetWorker.podLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: "{{ template "superset.fullname" . }}-ws"
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: "{{ template "superset.name" . }}-ws"
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "websocket" "root" .) | nindent 4 }}
|
||||
{{- if .Values.supersetWebsockets.deploymentAnnotations }}
|
||||
annotations: {{- toYaml .Values.supersetWebsockets.deploymentAnnotations | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -51,11 +45,7 @@ spec:
|
||||
{{- toYaml .Values.supersetWebsockets.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: "{{ template "superset.name" . }}-ws"
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "supersetWebsockets.selectorLabels" . | nindent 8 }}
|
||||
{{- if .Values.supersetWebsockets.podLabels }}
|
||||
{{- toYaml .Values.supersetWebsockets.podLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -23,15 +23,9 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetNode.deploymentLabels }}
|
||||
{{- toYaml .Values.supersetNode.deploymentLabels | nindent 4 }}
|
||||
{{- include "superset.componentLabels" (dict "component" "web" "root" .) | nindent 4 }}
|
||||
{{- with .Values.supersetNode.deploymentLabels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetNode.deploymentAnnotations }}
|
||||
annotations: {{- toYaml .Values.supersetNode.deploymentAnnotations | nindent 4 }}
|
||||
@@ -67,11 +61,7 @@ spec:
|
||||
{{- toYaml .Values.supersetNode.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "supersetNode.selectorLabels" . | nindent 8 }}
|
||||
{{- if .Values.supersetNode.podLabels }}
|
||||
{{- toYaml .Values.supersetNode.podLabels | nindent 8 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -23,13 +23,7 @@ kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "superset.fullname" . }}-hpa
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
|
||||
@@ -23,13 +23,7 @@ kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "superset.fullname" . }}-hpa-worker
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
|
||||
@@ -25,13 +25,7 @@ metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "ingress" "root" .) | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}-init-db
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "init" "root" .) | nindent 4 }}
|
||||
{{- if .Values.init.jobAnnotations }}
|
||||
annotations: {{- toYaml .Values.init.jobAnnotations | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -42,9 +36,7 @@ spec:
|
||||
annotations: {{- toYaml .Values.init.podAnnotations | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
{{- include "superset.componentSelectorLabels" (dict "component" "init" "root" .) | nindent 8 }}
|
||||
job: {{ template "superset.fullname" . }}-init-db
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 8 }}
|
||||
|
||||
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "superset.fullname" $ }}-celerybeat-pdb
|
||||
labels:
|
||||
app: {{ template "superset.name" $ }}-celerybeat
|
||||
chart: {{ template "superset.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
{{- if $.Values.extraLabels }}
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "celerybeat" "root" $) | nindent 4 }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
|
||||
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "superset.fullname" $ }}-flower-pdb
|
||||
labels:
|
||||
app: {{ template "superset.name" $ }}-flower
|
||||
chart: {{ template "superset.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
{{- if $.Values.extraLabels }}
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "flower" "root" $) | nindent 4 }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
|
||||
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "superset.fullname" $ }}-worker-pdb
|
||||
labels:
|
||||
app: {{ template "superset.name" $ }}-worker
|
||||
chart: {{ template "superset.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
{{- if $.Values.extraLabels }}
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "worker" "root" $) | nindent 4 }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
|
||||
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "superset.fullname" $ }}-ws-pdb
|
||||
labels:
|
||||
app: {{ template "superset.name" $ }}-ws
|
||||
chart: {{ template "superset.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
{{- if $.Values.extraLabels }}
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "websocket" "root" $) | nindent 4 }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
|
||||
@@ -27,13 +27,7 @@ kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: {{ include "superset.fullname" $ }}-pdb
|
||||
labels:
|
||||
app: {{ template "superset.name" $ }}
|
||||
chart: {{ template "superset.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
{{- if $.Values.extraLabels }}
|
||||
{{- toYaml $.Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "web" "root" $) | nindent 4 }}
|
||||
spec:
|
||||
{{- if .minAvailable }}
|
||||
minAvailable: {{ .minAvailable }}
|
||||
|
||||
@@ -23,13 +23,7 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}-env
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.fullname" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: "{{ .Release.Name }}"
|
||||
heritage: "{{ .Release.Service }}"
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
REDIS_HOST: {{ tpl .Values.supersetNode.connections.redis_host . | quote }}
|
||||
|
||||
@@ -23,13 +23,7 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}-config
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.fullname" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: "{{ .Release.Name }}"
|
||||
heritage: "{{ .Release.Service }}"
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
superset_config.py: |
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: "{{ template "superset.fullname" . }}-ws-config"
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.fullname" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: "{{ .Release.Name }}"
|
||||
heritage: "{{ .Release.Service }}"
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
config.json: |
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: "{{ template "superset.fullname" . }}-flower"
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "flower" "root" .) | nindent 4 }}
|
||||
{{- with .Values.supersetCeleryFlower.service.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -45,8 +39,7 @@ spec:
|
||||
nodePort: {{ .Values.supersetCeleryFlower.service.nodePort.http }}
|
||||
{{- end }}
|
||||
selector:
|
||||
app: {{ template "superset.name" . }}-flower
|
||||
release: {{ .Release.Name }}
|
||||
{{- include "supersetCeleryFlower.selectorLabels" . | nindent 4 }}
|
||||
{{- if .Values.supersetCeleryFlower.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ .Values.supersetCeleryFlower.service.loadBalancerIP }}
|
||||
{{- end }}
|
||||
|
||||
@@ -24,13 +24,7 @@ metadata:
|
||||
name: "{{ template "superset.fullname" . }}-ws"
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "websocket" "root" .) | nindent 4 }}
|
||||
{{- with .Values.supersetWebsockets.service.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -45,8 +39,7 @@ spec:
|
||||
nodePort: {{ .Values.supersetWebsockets.service.nodePort.http }}
|
||||
{{- end }}
|
||||
selector:
|
||||
app: "{{ template "superset.name" . }}-ws"
|
||||
release: {{ .Release.Name }}
|
||||
{{- include "supersetWebsockets.selectorLabels" . | nindent 4 }}
|
||||
{{- if .Values.supersetWebsockets.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ .Values.supersetWebsockets.service.loadBalancerIP }}
|
||||
{{- end }}
|
||||
|
||||
@@ -23,13 +23,7 @@ metadata:
|
||||
name: {{ template "superset.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- include "superset.componentLabels" (dict "component" "web" "root" .) | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -44,8 +38,7 @@ spec:
|
||||
nodePort: {{ .Values.service.nodePort.http }}
|
||||
{{- end }}
|
||||
selector:
|
||||
app: {{ template "superset.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
{{- include "supersetNode.selectorLabels" . | nindent 4 }}
|
||||
{{- if .Values.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
|
||||
{{- end }}
|
||||
|
||||
@@ -24,17 +24,11 @@ metadata:
|
||||
name: {{ include "superset.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "superset.name" . }}
|
||||
helm.sh/chart: {{ include "superset.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- include "superset.labels" . | nindent 4 }}
|
||||
{{- if semverCompare "> 1.6" .Capabilities.KubeVersion.GitVersion }}
|
||||
kubernetes.io/cluster-service: "true"
|
||||
{{- end }}
|
||||
addonmanager.kubernetes.io/mode: Reconcile
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.serviceAccount.annotations }}
|
||||
annotations: {{- toYaml .Values.serviceAccount.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
520
helm/superset/tests/labels_test.yaml
Normal file
520
helm/superset/tests/labels_test.yaml
Normal file
@@ -0,0 +1,520 @@
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
suite: Label Consistency Tests
|
||||
templates:
|
||||
- deployment.yaml
|
||||
- deployment-worker.yaml
|
||||
- deployment-beat.yaml
|
||||
- deployment-flower.yaml
|
||||
- deployment-ws.yaml
|
||||
- service.yaml
|
||||
- service-ws.yaml
|
||||
- service-flower.yaml
|
||||
- init-job.yaml
|
||||
- ingress.yaml
|
||||
- configmap-superset.yaml
|
||||
- secret-superset-config.yaml
|
||||
- secret-ws.yaml
|
||||
- pdb.yaml
|
||||
- pdb-worker.yaml
|
||||
- pdb-beat.yaml
|
||||
- pdb-flower.yaml
|
||||
- pdb-ws.yaml
|
||||
|
||||
# These tests validate that Kubernetes recommended labels are consistently applied
|
||||
# across all chart resources per https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
|
||||
#
|
||||
# Required Labels (app.kubernetes.io/):
|
||||
# - name: The name of the application
|
||||
# - instance: A unique name identifying the instance of an application
|
||||
# - version: The current version of the application
|
||||
# - component: The component within the architecture
|
||||
# - part-of: The name of a higher level application this one is part of
|
||||
# - managed-by: The tool being used to manage the operation of an application
|
||||
#
|
||||
# Helm-specific Labels:
|
||||
# - helm.sh/chart: The chart name and version
|
||||
|
||||
tests:
|
||||
# =============================================================================
|
||||
# Main Deployment Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on main deployment
|
||||
template: deployment.yaml
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/version"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/managed-by"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/part-of"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["helm.sh/chart"]
|
||||
|
||||
- it: should have correct component label on main deployment
|
||||
template: deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: web
|
||||
|
||||
- it: should have part-of label set to superset on main deployment
|
||||
template: deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/part-of"]
|
||||
value: superset
|
||||
|
||||
# =============================================================================
|
||||
# Worker Deployment Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on worker deployment
|
||||
template: deployment-worker.yaml
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/version"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/managed-by"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/part-of"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on worker deployment
|
||||
template: deployment-worker.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: worker
|
||||
|
||||
# =============================================================================
|
||||
# Celery Beat Deployment Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on celerybeat deployment
|
||||
template: deployment-beat.yaml
|
||||
set:
|
||||
supersetCeleryBeat.enabled: true
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on celerybeat deployment
|
||||
template: deployment-beat.yaml
|
||||
set:
|
||||
supersetCeleryBeat.enabled: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: celerybeat
|
||||
|
||||
# =============================================================================
|
||||
# Flower Deployment Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on flower deployment
|
||||
template: deployment-flower.yaml
|
||||
set:
|
||||
supersetCeleryFlower.enabled: true
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on flower deployment
|
||||
template: deployment-flower.yaml
|
||||
set:
|
||||
supersetCeleryFlower.enabled: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: flower
|
||||
|
||||
# =============================================================================
|
||||
# WebSocket Deployment Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on websocket deployment
|
||||
template: deployment-ws.yaml
|
||||
set:
|
||||
supersetWebsockets.enabled: true
|
||||
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on websocket deployment
|
||||
template: deployment-ws.yaml
|
||||
set:
|
||||
supersetWebsockets.enabled: true
|
||||
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: websocket
|
||||
|
||||
# =============================================================================
|
||||
# Service Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on main service
|
||||
template: service.yaml
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on main service
|
||||
template: service.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: web
|
||||
|
||||
- it: should have all recommended labels on websocket service
|
||||
template: service-ws.yaml
|
||||
set:
|
||||
supersetWebsockets.enabled: true
|
||||
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on websocket service
|
||||
template: service-ws.yaml
|
||||
set:
|
||||
supersetWebsockets.enabled: true
|
||||
supersetWebsockets.config.jwtSecret: "test-secret-for-unit-test"
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: websocket
|
||||
|
||||
- it: should have all recommended labels on flower service
|
||||
template: service-flower.yaml
|
||||
set:
|
||||
supersetCeleryFlower.enabled: true
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on flower service
|
||||
template: service-flower.yaml
|
||||
set:
|
||||
supersetCeleryFlower.enabled: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: flower
|
||||
|
||||
# =============================================================================
|
||||
# Init Job Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on init job
|
||||
template: init-job.yaml
|
||||
set:
|
||||
init.enabled: true
|
||||
init.createAdmin: true
|
||||
init.adminUser.password: "test-password"
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on init job
|
||||
template: init-job.yaml
|
||||
set:
|
||||
init.enabled: true
|
||||
init.createAdmin: true
|
||||
init.adminUser.password: "test-password"
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: init
|
||||
|
||||
# =============================================================================
|
||||
# Ingress Labels
|
||||
# =============================================================================
|
||||
- it: should have all recommended labels on ingress
|
||||
template: ingress.yaml
|
||||
set:
|
||||
ingress.enabled: true
|
||||
ingress.hosts:
|
||||
- host: superset.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
|
||||
- it: should have correct component label on ingress
|
||||
template: ingress.yaml
|
||||
set:
|
||||
ingress.enabled: true
|
||||
ingress.hosts:
|
||||
- host: superset.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: ingress
|
||||
|
||||
# =============================================================================
|
||||
# Selector Label Consistency
|
||||
#
|
||||
# These use value assertions (not isNotNull) on purpose: a missing/misscoped
|
||||
# release name renders as the string "<no value>", which is non-null and would
|
||||
# silently pass isNotNull. Asserting the concrete value catches that class of
|
||||
# bug, and asserting the pod template labels equal the selector guards the
|
||||
# immutable spec.selector.matchLabels <-> pod label invariant.
|
||||
# =============================================================================
|
||||
- it: should set selector matchLabels to concrete values on main deployment
|
||||
template: deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/name"]
|
||||
value: superset
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/component"]
|
||||
value: web
|
||||
|
||||
- it: should match pod template labels to the selector on main deployment
|
||||
template: deployment.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.metadata.labels["app.kubernetes.io/name"]
|
||||
value: superset
|
||||
- equal:
|
||||
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: spec.template.metadata.labels["app.kubernetes.io/component"]
|
||||
value: web
|
||||
|
||||
- it: should set selector matchLabels to concrete values on worker deployment
|
||||
template: deployment-worker.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/component"]
|
||||
value: worker
|
||||
|
||||
- it: should match pod template labels to the selector on worker deployment
|
||||
template: deployment-worker.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: spec.template.metadata.labels["app.kubernetes.io/component"]
|
||||
value: worker
|
||||
|
||||
# =============================================================================
|
||||
# Extra Labels Support
|
||||
# =============================================================================
|
||||
- it: should include extraLabels when specified
|
||||
template: deployment.yaml
|
||||
set:
|
||||
extraLabels:
|
||||
custom-label: custom-value
|
||||
environment: production
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels.custom-label
|
||||
value: custom-value
|
||||
- equal:
|
||||
path: metadata.labels.environment
|
||||
value: production
|
||||
|
||||
- it: should include extraLabels in service
|
||||
template: service.yaml
|
||||
set:
|
||||
extraLabels:
|
||||
custom-label: custom-value
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels.custom-label
|
||||
value: custom-value
|
||||
|
||||
# =============================================================================
|
||||
# ConfigMap / Secret Labels
|
||||
# =============================================================================
|
||||
- it: should have recommended labels on extra-config configmap
|
||||
template: configmap-superset.yaml
|
||||
set:
|
||||
extraConfigs:
|
||||
custom.py: "FOO = 1"
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/managed-by"]
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/part-of"]
|
||||
value: superset
|
||||
|
||||
- it: should have recommended labels on superset config secret
|
||||
template: secret-superset-config.yaml
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/part-of"]
|
||||
value: superset
|
||||
|
||||
- it: should have recommended labels on websocket config secret
|
||||
template: secret-ws.yaml
|
||||
set:
|
||||
supersetWebsockets.enabled: true
|
||||
asserts:
|
||||
- isNotNull:
|
||||
path: metadata.labels["app.kubernetes.io/name"]
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
|
||||
# =============================================================================
|
||||
# PodDisruptionBudget Labels (metadata must match the selector)
|
||||
# =============================================================================
|
||||
- it: should have recommended labels and matching selector on main pdb
|
||||
template: pdb.yaml
|
||||
set:
|
||||
supersetNode.podDisruptionBudget.enabled: true
|
||||
supersetNode.podDisruptionBudget.maxUnavailable: null
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: web
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/component"]
|
||||
value: web
|
||||
|
||||
- it: should set correct component on worker pdb
|
||||
template: pdb-worker.yaml
|
||||
set:
|
||||
supersetWorker.podDisruptionBudget.enabled: true
|
||||
supersetWorker.podDisruptionBudget.maxUnavailable: null
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: worker
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/component"]
|
||||
value: worker
|
||||
|
||||
- it: should set correct component on celerybeat pdb
|
||||
template: pdb-beat.yaml
|
||||
set:
|
||||
supersetCeleryBeat.podDisruptionBudget.enabled: true
|
||||
supersetCeleryBeat.podDisruptionBudget.maxUnavailable: null
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: celerybeat
|
||||
|
||||
- it: should set correct component on flower pdb
|
||||
template: pdb-flower.yaml
|
||||
set:
|
||||
supersetCeleryFlower.podDisruptionBudget.enabled: true
|
||||
supersetCeleryFlower.podDisruptionBudget.maxUnavailable: null
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: flower
|
||||
|
||||
- it: should set correct component on websocket pdb
|
||||
template: pdb-ws.yaml
|
||||
set:
|
||||
supersetWebsockets.enabled: true
|
||||
supersetWebsockets.podDisruptionBudget.enabled: true
|
||||
supersetWebsockets.podDisruptionBudget.maxUnavailable: null
|
||||
asserts:
|
||||
- equal:
|
||||
path: metadata.labels["app.kubernetes.io/component"]
|
||||
value: websocket
|
||||
- equal:
|
||||
path: spec.selector.matchLabels["app.kubernetes.io/component"]
|
||||
value: websocket
|
||||
|
||||
- it: should use recommended labels on init job pod template
|
||||
template: init-job.yaml
|
||||
set:
|
||||
init.enabled: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.metadata.labels["app.kubernetes.io/instance"]
|
||||
value: RELEASE-NAME
|
||||
- equal:
|
||||
path: spec.template.metadata.labels["app.kubernetes.io/component"]
|
||||
value: init
|
||||
- isNotNull:
|
||||
path: spec.template.metadata.labels.job
|
||||
@@ -91,22 +91,28 @@ _ASF_LICENSE_HEADER = """\
|
||||
LANGUAGE_NAMES: dict[str, str] = {
|
||||
"ar": "Arabic",
|
||||
"ca": "Catalan",
|
||||
"cs": "Czech",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"fa": "Persian (Farsi)",
|
||||
"fi": "Finnish",
|
||||
"fr": "French",
|
||||
"it": "Italian",
|
||||
"ja": "Japanese",
|
||||
"ko": "Korean",
|
||||
"lv": "Latvian",
|
||||
"mi": "Māori",
|
||||
"nl": "Dutch",
|
||||
"pl": "Polish",
|
||||
"pt": "Portuguese",
|
||||
"pt_BR": "Brazilian Portuguese",
|
||||
"ro": "Romanian",
|
||||
"ru": "Russian",
|
||||
"sk": "Slovak",
|
||||
"sl": "Slovenian",
|
||||
"sr": "Serbian",
|
||||
"sr_Latn": "Serbian (Latin script)",
|
||||
"th": "Thai",
|
||||
"tr": "Turkish",
|
||||
"uk": "Ukrainian",
|
||||
"zh": "Chinese (Simplified)",
|
||||
@@ -346,6 +352,97 @@ def translate_batch(
|
||||
return parse_response(result.stdout.strip(), len(batch))
|
||||
|
||||
|
||||
def _translate_single_plaintext(
|
||||
model: str,
|
||||
target_lang: str,
|
||||
item: dict[str, Any],
|
||||
index: dict[str, Any],
|
||||
) -> str | None:
|
||||
"""Translate a single entry with a plain-text prompt (no JSON envelope).
|
||||
|
||||
Fallback for an entry whose JSON batch response cannot be parsed — typically
|
||||
because the source string contains literal double-quotes that the model
|
||||
echoes back unescaped, corrupting the surrounding JSON. Asking for a bare
|
||||
string sidesteps the JSON contract entirely. Returns the translation text,
|
||||
or None if the CLI call fails.
|
||||
"""
|
||||
claude_bin = shutil.which("claude")
|
||||
if not claude_bin:
|
||||
raise RuntimeError(
|
||||
"claude CLI not found. Install Claude Code or add it to PATH."
|
||||
)
|
||||
lines = [
|
||||
"You are a professional translator specializing in software UI strings.",
|
||||
f"Translate the following English string into {_lang_name(target_lang)} "
|
||||
f"({target_lang}).",
|
||||
"Return ONLY the translation as plain text — no surrounding quotes, no "
|
||||
"JSON, no markdown fences, no explanation.",
|
||||
"Preserve all format placeholders exactly (%(name)s, {name}, %s, %d), any "
|
||||
"HTML tags, and any inner quotation marks.",
|
||||
"",
|
||||
f"English: {item['msgid']}",
|
||||
]
|
||||
if item.get("msgid_plural"):
|
||||
lines.append(f"English plural: {item['msgid_plural']}")
|
||||
refs = index.get(item["index_key"], {})
|
||||
ref_lines = [
|
||||
f"{_lang_name(lang)}: {val}"
|
||||
for lang, val in sorted(refs.items())
|
||||
if lang != target_lang and isinstance(val, str) and val
|
||||
]
|
||||
if ref_lines:
|
||||
lines.append("")
|
||||
lines.append("Reference translations in other languages:")
|
||||
lines.extend(ref_lines)
|
||||
prompt = "\n".join(lines)
|
||||
# claude_bin is resolved via shutil.which — not user-controlled input
|
||||
result = subprocess.run( # noqa: S603
|
||||
[claude_bin, "--model", model, "-p"],
|
||||
input=prompt,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
text = result.stdout.strip()
|
||||
# Strip accidental markdown fences or wrapping quotes the model may add.
|
||||
text = re.sub(r"^```[^\n]*\n?", "", text)
|
||||
text = re.sub(r"\n?```$", "", text).strip()
|
||||
if len(text) >= 2 and text[0] == '"' and text[-1] == '"':
|
||||
text = text[1:-1]
|
||||
return text or None
|
||||
|
||||
|
||||
def _resilient_translate(
|
||||
model: str,
|
||||
target_lang: str,
|
||||
batch: list[dict[str, Any]],
|
||||
index: dict[str, Any],
|
||||
) -> dict[int, str]:
|
||||
"""Translate a batch, isolating entries that break the JSON response contract.
|
||||
|
||||
``translate_batch`` sends the whole batch in one request and parses a single
|
||||
JSON object back. A source string containing literal double-quotes can make
|
||||
the model emit unescaped quotes, so ``json.loads`` fails and the ENTIRE batch
|
||||
would be lost. To salvage the rest, on a parse failure (ValueError) we bisect
|
||||
the batch and recurse; a lone entry that still fails falls back to a
|
||||
plain-text prompt via ``_translate_single_plaintext``. Returned keys are
|
||||
positions within ``batch``. RuntimeError (CLI failure) is left to propagate
|
||||
to the caller, preserving the existing per-batch failure handling.
|
||||
"""
|
||||
try:
|
||||
return translate_batch(model, target_lang, batch, index)
|
||||
except ValueError:
|
||||
if len(batch) == 1:
|
||||
text = _translate_single_plaintext(model, target_lang, batch[0], index)
|
||||
return {0: text} if text else {}
|
||||
mid = len(batch) // 2
|
||||
left = _resilient_translate(model, target_lang, batch[:mid], index)
|
||||
right = _resilient_translate(model, target_lang, batch[mid:], index)
|
||||
return {**left, **{k + mid: v for k, v in right.items()}}
|
||||
|
||||
|
||||
def _apply_plural_translation(entry: polib.POEntry, translation: str) -> None:
|
||||
"""Distribute a model response across the entry's plural forms.
|
||||
|
||||
@@ -462,7 +559,7 @@ def _process_batches(
|
||||
file=sys.stderr,
|
||||
)
|
||||
try:
|
||||
translations = translate_batch(model, lang, batch_items, index)
|
||||
translations = _resilient_translate(model, lang, batch_items, index)
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
print(f" ERROR in batch starting at {batch_start}: {exc}", file=sys.stderr)
|
||||
failed_count += len(batch_entries)
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
|
||||
export const DASHBOARD_LIST = '/dashboard/list/';
|
||||
export const CHART_LIST = '/chart/list/';
|
||||
export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/';
|
||||
export const SAMPLE_DASHBOARD_1 = '/superset/dashboard/1-sample-dashboard/';
|
||||
export const SUPPORTED_CHARTS_DASHBOARD =
|
||||
'/superset/dashboard/supported_charts_dash/';
|
||||
export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/';
|
||||
export const WORLD_HEALTH_DASHBOARD = '/dashboard/world_health/';
|
||||
export const SAMPLE_DASHBOARD_1 = '/dashboard/1-sample-dashboard/';
|
||||
export const SUPPORTED_CHARTS_DASHBOARD = '/dashboard/supported_charts_dash/';
|
||||
export const TABBED_DASHBOARD = '/dashboard/tabbed_dash/';
|
||||
export const DATABASE_LIST = '/databaseview/list';
|
||||
|
||||
538
superset-frontend/package-lock.json
generated
538
superset-frontend/package-lock.json
generated
@@ -24,7 +24,6 @@
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@deck.gl/react": "~9.2.5",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
@@ -99,10 +98,10 @@
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
"geostyler-openlayers-parser": "^5.7.0",
|
||||
"geostyler-openlayers-parser": "^5.7.1",
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.7.0",
|
||||
"google-auth-library": "^10.9.0",
|
||||
"immer": "^11.1.8",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
@@ -117,7 +116,7 @@
|
||||
"memoize-one": "^6.0.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.15",
|
||||
"nanoid": "^5.1.16",
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
@@ -181,11 +180,11 @@
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.15",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.5",
|
||||
"@storybook/addon-links": "10.4.4",
|
||||
"@storybook/react-webpack5": "10.4.4",
|
||||
"@storybook/addon-docs": "10.4.6",
|
||||
"@storybook/addon-links": "10.4.6",
|
||||
"@storybook/react-webpack5": "10.4.6",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.43",
|
||||
@@ -203,7 +202,7 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
@@ -3048,20 +3047,6 @@
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/modifiers": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
|
||||
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dnd-kit/core": "^6.3.0",
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dnd-kit/sortable": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||
@@ -8454,9 +8439,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8474,9 +8456,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8494,9 +8473,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8514,9 +8490,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8534,9 +8507,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8554,9 +8524,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8574,9 +8541,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8594,9 +8558,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -8864,13 +8825,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz",
|
||||
"integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==",
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.0"
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -9513,9 +9474,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sigstore/core": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.0.tgz",
|
||||
"integrity": "sha512-kxHrDQ9YgfrWUSXU0cjsQGv8JykOFZQ9ErNKbFPWzk3Hgpwu8x2hHrQ9IdA8yl+j9RTLTC3sAF3Tdq1IQCP4oA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz",
|
||||
"integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -9681,14 +9642,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sigstore/verify": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.0.tgz",
|
||||
"integrity": "sha512-mNe0Iigql08YupSOGv197YdHpPPr+EzDZmfCgMc7RPNaZTw5aLN01nBl6CHJOh3BGtnMIj83EeN4butBchc8Ag==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz",
|
||||
"integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/bundle": "^4.0.0",
|
||||
"@sigstore/core": "^3.1.0",
|
||||
"@sigstore/core": "^3.2.1",
|
||||
"@sigstore/protobuf-specs": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -9757,16 +9718,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@storybook/addon-docs": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.5.tgz",
|
||||
"integrity": "sha512-9mIV0maIxixfuvdpNhr3QMeU/gbJKeaBcWhPYuf176cqDZAG9EUhZ50TIinxeFRbyEGRJqaLPoiYwIu4GJu3jA==",
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.6.tgz",
|
||||
"integrity": "sha512-aWAfP5JMiT5a3zBJizwroCRzOCqZwDTJmvsYvwMD3ilIEa/kT1vhf6Xrbk4XIPhDwbh8Hpb/Gfnka1xBYEISWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@storybook/csf-plugin": "10.4.5",
|
||||
"@storybook/csf-plugin": "10.4.6",
|
||||
"@storybook/icons": "^2.0.2",
|
||||
"@storybook/react-dom-shim": "10.4.5",
|
||||
"@storybook/react-dom-shim": "10.4.6",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
@@ -9777,7 +9738,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.5"
|
||||
"storybook": "^10.4.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9785,36 +9746,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.5.tgz",
|
||||
"integrity": "sha512-fKdikHC7cDgSuaBirPwvgFBmfO//3cln0y3GmDEQchUV2VFDrZ7ZL1/iH7dA21XuiFFhQcDRRkArJmvMAGG5Cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-links": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.4.tgz",
|
||||
"integrity": "sha512-sWydPWLgduT24p/NJ/hXHcHsPlAyzQP+cOtCGliSI989K9yBP/TOL3A8sz7LIDfukI9DVAsylPhJ1jDSiAEI1w==",
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.4.6.tgz",
|
||||
"integrity": "sha512-VGfERTsGRFmfvNP3SKprFWkC6Od5kXzSutT5PSZjQ/O9NnCdHhd/RILxFDN2TzZn9ywDc7t5b4AldKmSYCv3EQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9827,7 +9762,7 @@
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.4"
|
||||
"storybook": "^10.4.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
@@ -9838,10 +9773,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack5": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.6.tgz",
|
||||
"integrity": "sha512-klJUCBlkr1mFqXxsyYc7akIBhT5VOBTRcHChSFXq6Kbh+qQYOvfmgBpY0Tv+f7ffBmzp6gxpTorg7eGxlklxeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.6",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"cjs-module-lexer": "^1.2.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"es-module-lexer": "^1.5.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"magic-string": "^0.30.5",
|
||||
"style-loader": "^4.0.0",
|
||||
"terser-webpack-plugin": "^5.3.17",
|
||||
"ts-dedent": "^2.0.0",
|
||||
"webpack": "5",
|
||||
"webpack-dev-middleware": "^6.1.2",
|
||||
"webpack-hot-middleware": "^2.25.1",
|
||||
"webpack-virtual-modules": "^0.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/core-webpack": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.6.tgz",
|
||||
"integrity": "sha512-DDhpgaFGb1AC0lzfR2LOozCj+uT14tsr2R9551PHgcC3ru1yfhL2DNbIjdiMuQP8nVm+2HKeXWhybJGYtk9z9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/csf-plugin": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.5.tgz",
|
||||
"integrity": "sha512-OsSsSLulBmdKTz7MIKLgoWADZB8bjYaAjZZy/THdI50G/TTd6FVSXQMCM7GO7xQZ/EguRY1PmjOVCLbgcnXsDA==",
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.6.tgz",
|
||||
"integrity": "sha512-NILLxDqpA/JR/AazGWpsz+4fadJwRU4uhHephGtYpVOWnQA/DkJfKT6zpcJVq8+QA8A2zKMLX3GVKsXIrxjuDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -9854,7 +9842,7 @@
|
||||
"peerDependencies": {
|
||||
"esbuild": "*",
|
||||
"rollup": "*",
|
||||
"storybook": "^10.4.5",
|
||||
"storybook": "^10.4.6",
|
||||
"vite": "*",
|
||||
"webpack": "*"
|
||||
},
|
||||
@@ -9891,6 +9879,74 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/preset-react-webpack": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.6.tgz",
|
||||
"integrity": "sha512-D6EjK1sknC/1GmdWQjV38HEP+tBxJ5ObNuCUWQzGsJqsiq45HaOemZbIyJqxp3dYx2/YpFd0bawn7PlOP4yNOQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.6",
|
||||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"magic-string": "^0.30.5",
|
||||
"react-docgen": "^7.1.1",
|
||||
"resolve": "^1.22.8",
|
||||
"semver": "^7.7.3",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"webpack": "5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.6.tgz",
|
||||
"integrity": "sha512-9Y7YecrVFe1/01KYjfOLxVqTg2Aq+IO6TEv6sC2U0PfD0AWCSCmQ91QqgBpN/XW4aFFWoiZNinyXMUlU8zxy2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.4.6",
|
||||
"react-docgen": "^8.0.2",
|
||||
"react-docgen-typescript": "^2.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-docgen-typescript-plugin": {
|
||||
"version": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.6--canary.9.0c3f3b7.0.tgz",
|
||||
@@ -9955,16 +10011,42 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.6.tgz",
|
||||
"integrity": "sha512-iGNmKzrq9vgl2PDrYAnZKI+yvac3Ym+lJXXuQaqlFRS23zA5MNm4EBX+rAG7WulqchoK6NaZ0KQOs2mAgEpTMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.4.tgz",
|
||||
"integrity": "sha512-LAYz4BlyOKNHhD/DR7f7eRFevJdl9lvoT1QsKEiT3ngFNxn9wMI2o8IMx7fOQYz0v1UTExZFYtwV2Ur/psJMEw==",
|
||||
"version": "10.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-10.4.6.tgz",
|
||||
"integrity": "sha512-34rOHFRa4NrBnMKsQMq8QTvw65QQJUtasYfEOw1/Pa9+CFWfFQZ3kfDLRxatphUyc0rbKxmVrVE4SFEMY/eMsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/builder-webpack5": "10.4.4",
|
||||
"@storybook/preset-react-webpack": "10.4.4",
|
||||
"@storybook/react": "10.4.4"
|
||||
"@storybook/builder-webpack5": "10.4.6",
|
||||
"@storybook/preset-react-webpack": "10.4.6",
|
||||
"@storybook/react": "10.4.6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -9973,7 +10055,7 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.4",
|
||||
"storybook": "^10.4.6",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -9982,171 +10064,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.4.tgz",
|
||||
"integrity": "sha512-R/hUX+uPFXjh3CqRtwMTG4PXywpSn0Qb08GIHOrNihb2bmszbC3nZ89vReZsBYL1lm4IpgBQbz+XzmY8LOHaVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.4",
|
||||
"case-sensitive-paths-webpack-plugin": "^2.4.0",
|
||||
"cjs-module-lexer": "^1.2.3",
|
||||
"css-loader": "^7.1.2",
|
||||
"es-module-lexer": "^1.5.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.1.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"magic-string": "^0.30.5",
|
||||
"style-loader": "^4.0.0",
|
||||
"terser-webpack-plugin": "^5.3.17",
|
||||
"ts-dedent": "^2.0.0",
|
||||
"webpack": "5",
|
||||
"webpack-dev-middleware": "^6.1.2",
|
||||
"webpack-hot-middleware": "^2.25.1",
|
||||
"webpack-virtual-modules": "^0.6.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5/node_modules/@storybook/core-webpack": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.4.tgz",
|
||||
"integrity": "sha512-tZ3cMhMunC/84KStq1vGhWanhNiFL99M0KFyKToEeULYx2WOyd/dC3oFIaWSNns3jRvykwR4Z11CwZzW6khjrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-10.4.4.tgz",
|
||||
"integrity": "sha512-n7JDUfY2mYW/5VqkfkHePWUWmUATCG1lF278pX+gaahvbMACJyM/bbZFcL56mlc1dEQIg6czC8VkA9y2Xm5nTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/core-webpack": "10.4.4",
|
||||
"@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"magic-string": "^0.30.5",
|
||||
"react-docgen": "^7.1.1",
|
||||
"resolve": "^1.22.8",
|
||||
"semver": "^7.7.3",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"webpack": "5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack/node_modules/@storybook/core-webpack": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-10.4.4.tgz",
|
||||
"integrity": "sha512-tZ3cMhMunC/84KStq1vGhWanhNiFL99M0KFyKToEeULYx2WOyd/dC3oFIaWSNns3jRvykwR4Z11CwZzW6khjrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"storybook": "^10.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.4.4.tgz",
|
||||
"integrity": "sha512-6K5/uHrvjswrueyVpUt6IWGuSgYCMtMOYyVs86XJZYqKBV3Pv7nGsGNH7YSMLAVQBZW4CQqm2etd5Op0GHY9Kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/react-dom-shim": "10.4.4",
|
||||
"react-docgen": "^8.0.2",
|
||||
"react-docgen-typescript": "^2.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.4",
|
||||
"typescript": ">= 4.9.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": {
|
||||
"version": "10.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.4.tgz",
|
||||
"integrity": "sha512-y6SObmoW78AydE6VfKQSUmCkuqiaMPy9LgMpMdMEyWfJ/pSxBDMIKycr9dlRMJP1cvNgByaJgrusWtA46ndSQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"storybook": "^10.4.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/react-webpack5/node_modules/@storybook/react/node_modules/react-docgen": {
|
||||
"node_modules/@storybook/react/node_modules/react-docgen": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.3.tgz",
|
||||
"integrity": "sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==",
|
||||
@@ -10666,9 +10584,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10685,9 +10600,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10704,9 +10616,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10723,9 +10632,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10742,9 +10648,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10761,9 +10664,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11763,9 +11663,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz",
|
||||
"integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==",
|
||||
"version": "26.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz",
|
||||
"integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
@@ -20330,9 +20230,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
||||
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -21538,9 +21438,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/geostyler-openlayers-parser": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/geostyler-openlayers-parser/-/geostyler-openlayers-parser-5.7.0.tgz",
|
||||
"integrity": "sha512-FRNTNPLoKJzKYnWas+E4hb4h38SGaK3KeNPZmLUqO5EcTootJjAJyTbCy/Cuv9afk56HYIBpM2gHh6q/fLwqsg==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/geostyler-openlayers-parser/-/geostyler-openlayers-parser-5.7.1.tgz",
|
||||
"integrity": "sha512-GKkFdki1XbNIWS8onAU2CatGCJ/BB3QzknligxTXtTuLOa6Gqp2RshgExt3BzVyQOlXMmb8zmhtd5Z0CbvrrgA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"css-font-parser": "^2.0.0",
|
||||
@@ -22250,9 +22150,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz",
|
||||
"integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==",
|
||||
"version": "10.9.0",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz",
|
||||
"integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
@@ -31406,9 +31306,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz",
|
||||
"integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==",
|
||||
"version": "5.1.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
|
||||
"integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -33741,13 +33641,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz",
|
||||
"integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.0"
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -33760,9 +33660,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz",
|
||||
"integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -38490,18 +38390,18 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sigstore": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.0.tgz",
|
||||
"integrity": "sha512-/fUgUhYghuLzVT/gaJoeVehLCgZiUxPCPMcyVNY0lIf/cTCz58K/WTI7PefDarXxp9nUKpEwg1yyz3eSBMTtgA==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz",
|
||||
"integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/bundle": "^4.0.0",
|
||||
"@sigstore/core": "^3.1.0",
|
||||
"@sigstore/core": "^3.2.1",
|
||||
"@sigstore/protobuf-specs": "^0.5.0",
|
||||
"@sigstore/sign": "^4.1.0",
|
||||
"@sigstore/tuf": "^4.0.1",
|
||||
"@sigstore/verify": "^3.1.0"
|
||||
"@sigstore/sign": "^4.1.1",
|
||||
"@sigstore/tuf": "^4.0.2",
|
||||
"@sigstore/verify": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
@@ -44372,7 +44272,7 @@
|
||||
"handlebars": "^4.7.9",
|
||||
"jed": "^1.1.1",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-es": "^4.18.1",
|
||||
"math-expression-evaluator": "^2.0.7",
|
||||
"parse-ms": "^4.0.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
@@ -44402,7 +44302,7 @@
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/jquery": "^4.0.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-table": "^7.7.20",
|
||||
|
||||
@@ -107,7 +107,6 @@
|
||||
"@deck.gl/mesh-layers": "~9.2.5",
|
||||
"@deck.gl/react": "~9.2.5",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/cache": "^11.4.0",
|
||||
@@ -182,10 +181,10 @@
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
"geostyler-data": "^1.1.0",
|
||||
"geostyler-openlayers-parser": "^5.7.0",
|
||||
"geostyler-openlayers-parser": "^5.7.1",
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.7.0",
|
||||
"google-auth-library": "^10.9.0",
|
||||
"immer": "^11.1.8",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
@@ -199,7 +198,7 @@
|
||||
"memoize-one": "^6.0.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^5.1.15",
|
||||
"nanoid": "^5.1.16",
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.0",
|
||||
"re-resizable": "^6.11.2",
|
||||
@@ -264,11 +263,11 @@
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.15",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
"@storybook/addon-docs": "10.4.5",
|
||||
"@storybook/addon-links": "10.4.4",
|
||||
"@storybook/react-webpack5": "10.4.4",
|
||||
"@storybook/addon-docs": "10.4.6",
|
||||
"@storybook/addon-links": "10.4.6",
|
||||
"@storybook/react-webpack5": "10.4.6",
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.43",
|
||||
@@ -285,7 +284,7 @@
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
@@ -414,6 +413,7 @@
|
||||
"@luma.gl/shadertools": "~9.2.5",
|
||||
"@luma.gl/webgl": "~9.2.5",
|
||||
"fast-xml-parser": "^5.8.0",
|
||||
"fast-uri": "^3.1.3",
|
||||
"jest-mock": "^30.4.0",
|
||||
"jest-runtime": "^30.4.0",
|
||||
"@jest/globals": "^30.4.0",
|
||||
|
||||
@@ -237,6 +237,27 @@ export interface SupersetSpecificTokens {
|
||||
* Fallback: transparent
|
||||
*/
|
||||
buttonSecondaryActiveBorderColor?: string;
|
||||
|
||||
// Component flexibility tokens (sizing, radius, behavior)
|
||||
selectOptionActiveOutline?: boolean;
|
||||
labelBorderRadius?: number;
|
||||
buttonControlHeight?: number;
|
||||
buttonControlHeightSM?: number;
|
||||
buttonControlHeightXS?: number;
|
||||
buttonPaddingInline?: number;
|
||||
buttonPaddingInlineSM?: number;
|
||||
buttonFontSize?: number;
|
||||
buttonBorderRadius?: number;
|
||||
buttonStyleMap?: Record<
|
||||
string,
|
||||
{ type?: string; variant?: string; color?: string }
|
||||
>;
|
||||
|
||||
// Dashboard tile tokens (opt-in, fallbacks: colorBgContainer bg, no border, borderRadius, hairline box-shadow)
|
||||
dashboardTileBg?: string;
|
||||
dashboardTileBorder?: string;
|
||||
dashboardTileBorderRadius?: number;
|
||||
dashboardTileBoxShadow?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -713,4 +713,5 @@ export interface DataColumnMeta {
|
||||
isChildColumn?: boolean;
|
||||
description?: string;
|
||||
currencyCodeColumn?: string;
|
||||
isFilterable?: boolean;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"rison": "^0.1.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"xss": "^1.0.15",
|
||||
"lodash-es": "^4.17.21"
|
||||
"lodash-es": "^4.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/styled": "^11.14.1",
|
||||
@@ -78,7 +78,7 @@
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/jquery": "^4.0.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-table": "^7.7.20",
|
||||
|
||||
@@ -109,7 +109,7 @@ export default class ChartClient {
|
||||
(await buildQueryRegistry.get(visType)) ?? (() => formData);
|
||||
const requestConfig: RequestConfig = useLegacyApi
|
||||
? {
|
||||
endpoint: '/superset/explore_json/',
|
||||
endpoint: '/explore_json/',
|
||||
postPayload: {
|
||||
form_data: buildQuery(formData),
|
||||
},
|
||||
@@ -139,7 +139,7 @@ export default class ChartClient {
|
||||
): Promise<Datasource> {
|
||||
return this.client
|
||||
.get({
|
||||
endpoint: `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
|
||||
endpoint: `/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
|
||||
...options,
|
||||
} as RequestConfig)
|
||||
.then(response => response.json as Datasource);
|
||||
|
||||
@@ -263,9 +263,7 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
if (!useLegacyApi && !queryContext.queries) {
|
||||
queryContext = { queries: [queryContext] };
|
||||
}
|
||||
const endpoint = useLegacyApi
|
||||
? '/superset/explore_json/'
|
||||
: '/api/v1/chart/data';
|
||||
const endpoint = useLegacyApi ? '/explore_json/' : '/api/v1/chart/data';
|
||||
|
||||
const requestConfig: RequestConfig = {
|
||||
endpoint,
|
||||
|
||||
@@ -125,6 +125,11 @@ InteractiveButton.argTypes = {
|
||||
options: buttonSizes,
|
||||
control: { type: 'select' },
|
||||
},
|
||||
styleConfig: {
|
||||
description:
|
||||
'Optional visual overrides (controlHeight, paddingInline, fontSize, fontWeight, borderRadius, ctaMinWidth, ctaMinHeight, iconGap).',
|
||||
control: { type: 'object' },
|
||||
},
|
||||
target: {
|
||||
name: TARGETS.label,
|
||||
control: { type: 'select' },
|
||||
|
||||
@@ -186,6 +186,68 @@ test('getSecondaryButtonHoverStyles supports partial token overrides', () => {
|
||||
expect(hoverStyles['&:active'].backgroundColor).toBe('#99d3df !important');
|
||||
});
|
||||
|
||||
test('styleConfig overrides theme defaults', () => {
|
||||
const { getByRole } = render(
|
||||
<Button
|
||||
buttonStyle="primary"
|
||||
styleConfig={{
|
||||
controlHeight: 50,
|
||||
fontSize: 20,
|
||||
fontWeight: 900,
|
||||
paddingInline: 30,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
Custom
|
||||
</Button>,
|
||||
);
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('styleConfig partial override merges with defaults', () => {
|
||||
const { getByRole } = render(
|
||||
<Button buttonStyle="primary" styleConfig={{ controlHeight: 44 }}>
|
||||
Partial
|
||||
</Button>,
|
||||
);
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('xsmall size resolves with theme tokens', () => {
|
||||
const { getByRole } = render(
|
||||
<Button buttonSize="xsmall" buttonStyle="primary">
|
||||
XSmall
|
||||
</Button>,
|
||||
);
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
expect(getByRole('button')).toHaveClass('superset-button');
|
||||
});
|
||||
|
||||
test('small size resolves with theme tokens', () => {
|
||||
const { getByRole } = render(
|
||||
<Button buttonSize="small" buttonStyle="primary">
|
||||
Small
|
||||
</Button>,
|
||||
);
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
expect(getByRole('button')).toHaveClass('superset-button');
|
||||
});
|
||||
|
||||
test('primary buttonStyle applies ant-btn-primary class', () => {
|
||||
const { getByRole } = render(<Button buttonStyle="primary">Primary</Button>);
|
||||
expect(getByRole('button')).toHaveClass('ant-btn-primary');
|
||||
});
|
||||
|
||||
test('danger buttonStyle applies ant-btn-dangerous class', () => {
|
||||
const { getByRole } = render(<Button buttonStyle="danger">Danger</Button>);
|
||||
expect(getByRole('button')).toHaveClass('ant-btn-dangerous');
|
||||
});
|
||||
|
||||
test('link buttonStyle applies ant-btn-link class', () => {
|
||||
const { getByRole } = render(<Button buttonStyle="link">Link</Button>);
|
||||
expect(getByRole('button')).toHaveClass('ant-btn-link');
|
||||
});
|
||||
|
||||
test('getSecondaryButtonStyle falls back when tokens are empty strings', () => {
|
||||
const mockTheme = {
|
||||
colorPrimary: '#2893B3',
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { SupersetTheme } from '@apache-superset/core/theme';
|
||||
import type {
|
||||
ButtonColorType,
|
||||
ButtonProps,
|
||||
ButtonStyleConfig,
|
||||
ButtonStyle,
|
||||
ButtonType,
|
||||
ButtonVariantType,
|
||||
@@ -84,14 +85,13 @@ export const getSecondaryButtonHoverStyles = (theme: SupersetTheme) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const BUTTON_STYLE_MAP: Record<
|
||||
ButtonStyle,
|
||||
{
|
||||
type?: ButtonType;
|
||||
variant?: ButtonVariantType;
|
||||
color?: ButtonColorType;
|
||||
}
|
||||
> = {
|
||||
type ButtonStyleMapping = {
|
||||
type?: ButtonType;
|
||||
variant?: ButtonVariantType;
|
||||
color?: ButtonColorType;
|
||||
};
|
||||
|
||||
const BUTTON_STYLE_MAP: Record<ButtonStyle, ButtonStyleMapping> = {
|
||||
primary: { type: 'primary', variant: 'solid', color: 'primary' },
|
||||
secondary: { variant: 'filled', color: 'primary' },
|
||||
tertiary: { variant: 'outlined', color: 'default' },
|
||||
@@ -113,30 +113,56 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
href,
|
||||
showMarginRight = true,
|
||||
icon,
|
||||
styleConfig,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const { fontSizeSM, fontWeightStrong } = theme;
|
||||
const { fontWeightStrong } = theme;
|
||||
const btnFontSize = theme.buttonFontSize ?? theme.fontSizeSM;
|
||||
|
||||
let height = 32;
|
||||
let padding = 18;
|
||||
const resolvedStyleMap: Record<ButtonStyle, ButtonStyleMapping> =
|
||||
theme.buttonStyleMap
|
||||
? (Object.fromEntries(
|
||||
Object.entries(BUTTON_STYLE_MAP).map(([key, value]) => [
|
||||
key,
|
||||
{ ...value, ...theme.buttonStyleMap?.[key as ButtonStyle] },
|
||||
]),
|
||||
) as Record<ButtonStyle, ButtonStyleMapping>)
|
||||
: BUTTON_STYLE_MAP;
|
||||
|
||||
let defaultHeight = theme.buttonControlHeight ?? 32;
|
||||
let defaultPaddingInline = theme.buttonPaddingInline ?? 18;
|
||||
let defaultBorderRadius = theme.buttonBorderRadius ?? theme.borderRadius;
|
||||
if (buttonSize === 'xsmall') {
|
||||
height = 22;
|
||||
padding = 5;
|
||||
defaultHeight = theme.buttonControlHeightXS ?? 22;
|
||||
defaultPaddingInline = 5;
|
||||
defaultBorderRadius = theme.buttonBorderRadius ?? theme.borderRadiusSM;
|
||||
} else if (buttonSize === 'small') {
|
||||
height = 30;
|
||||
padding = 10;
|
||||
defaultHeight = theme.buttonControlHeightSM ?? 30;
|
||||
defaultPaddingInline = theme.buttonPaddingInlineSM ?? 10;
|
||||
defaultBorderRadius = theme.buttonBorderRadius ?? theme.borderRadiusSM;
|
||||
}
|
||||
if (buttonStyle === 'link') {
|
||||
padding = 4;
|
||||
defaultPaddingInline = 4;
|
||||
}
|
||||
|
||||
const resolvedStyleConfig: Required<ButtonStyleConfig> = {
|
||||
controlHeight: styleConfig?.controlHeight ?? defaultHeight,
|
||||
paddingInline: styleConfig?.paddingInline ?? defaultPaddingInline,
|
||||
fontSize: styleConfig?.fontSize ?? btnFontSize,
|
||||
fontWeight: styleConfig?.fontWeight ?? fontWeightStrong,
|
||||
ctaMinWidth: styleConfig?.ctaMinWidth ?? theme.sizeUnit * 36,
|
||||
ctaMinHeight: styleConfig?.ctaMinHeight ?? theme.sizeUnit * 8,
|
||||
iconGap: styleConfig?.iconGap ?? theme.sizeUnit * 2,
|
||||
borderRadius: styleConfig?.borderRadius ?? defaultBorderRadius,
|
||||
};
|
||||
|
||||
const {
|
||||
type: antdType = 'default',
|
||||
variant,
|
||||
color,
|
||||
} = BUTTON_STYLE_MAP[buttonStyle ?? 'primary'];
|
||||
} = resolvedStyleMap[buttonStyle ?? 'primary'] ?? BUTTON_STYLE_MAP.primary;
|
||||
|
||||
const element = children as ReactElement;
|
||||
|
||||
@@ -148,7 +174,9 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
renderedChildren = Children.toArray(children);
|
||||
}
|
||||
const firstChildMargin =
|
||||
showMarginRight && renderedChildren.length > 1 ? theme.sizeUnit * 2 : 0;
|
||||
showMarginRight && renderedChildren.length > 1
|
||||
? resolvedStyleConfig.iconGap
|
||||
: 0;
|
||||
|
||||
const effectiveButtonStyle: ButtonStyle = buttonStyle ?? 'primary';
|
||||
|
||||
@@ -167,11 +195,10 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
variant={variant}
|
||||
danger={effectiveButtonStyle === 'danger'}
|
||||
color={color}
|
||||
// Static class names for embedded-dashboard CSS targeting
|
||||
className={cx(
|
||||
className,
|
||||
'superset-button',
|
||||
// A static class name containing the button style is available to
|
||||
// support customizing button styles in embedded dashboards.
|
||||
`superset-button-${buttonStyle}`,
|
||||
{ cta: !!cta },
|
||||
)}
|
||||
@@ -180,12 +207,13 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
lineHeight: 1,
|
||||
fontSize: fontSizeSM,
|
||||
fontWeight: fontWeightStrong,
|
||||
height,
|
||||
padding: `0px ${padding}px`,
|
||||
minWidth: cta ? theme.sizeUnit * 36 : undefined,
|
||||
minHeight: cta ? theme.sizeUnit * 8 : undefined,
|
||||
fontSize: resolvedStyleConfig.fontSize,
|
||||
fontWeight: resolvedStyleConfig.fontWeight,
|
||||
height: resolvedStyleConfig.controlHeight,
|
||||
padding: `0px ${resolvedStyleConfig.paddingInline}px`,
|
||||
borderRadius: resolvedStyleConfig.borderRadius,
|
||||
minWidth: cta ? resolvedStyleConfig.ctaMinWidth : undefined,
|
||||
minHeight: cta ? resolvedStyleConfig.ctaMinHeight : undefined,
|
||||
marginLeft: 0,
|
||||
'& + .superset-button:not(.ant-btn-compact-item)': {
|
||||
marginLeft: theme.sizeUnit * 2,
|
||||
|
||||
@@ -40,6 +40,17 @@ export type ButtonStyle =
|
||||
|
||||
export type ButtonSize = 'default' | 'small' | 'xsmall';
|
||||
|
||||
export type ButtonStyleConfig = {
|
||||
controlHeight?: number;
|
||||
paddingInline?: number;
|
||||
fontSize?: number;
|
||||
fontWeight?: number;
|
||||
ctaMinWidth?: number;
|
||||
ctaMinHeight?: number;
|
||||
iconGap?: number;
|
||||
borderRadius?: number;
|
||||
};
|
||||
|
||||
export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
|
||||
placement?: TooltipPlacement;
|
||||
tooltip?: ReactNode;
|
||||
@@ -49,4 +60,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
|
||||
cta?: boolean;
|
||||
showMarginRight?: boolean;
|
||||
icon?: ReactNode;
|
||||
styleConfig?: ButtonStyleConfig;
|
||||
};
|
||||
|
||||
@@ -60,6 +60,11 @@ InteractiveDropdownButton.args = {
|
||||
};
|
||||
|
||||
InteractiveDropdownButton.argTypes = {
|
||||
styleConfig: {
|
||||
description:
|
||||
'Optional visual overrides (controlHeight, fontSize, fontWeight, boxShadow).',
|
||||
control: { type: 'object' },
|
||||
},
|
||||
placement: {
|
||||
defaultValue: 'top',
|
||||
control: { type: 'select' },
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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 { fireEvent, render, waitFor } from '@superset-ui/core/spec';
|
||||
import { DropdownButton } from '.';
|
||||
|
||||
const menuProps = { items: [{ key: '1', label: 'Item 1' }] };
|
||||
|
||||
test('renders without crashing when no styleConfig is provided', () => {
|
||||
const { container } = render(
|
||||
<DropdownButton menu={menuProps}>Click</DropdownButton>,
|
||||
);
|
||||
expect(container.querySelector('.ant-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders without crashing with full styleConfig', () => {
|
||||
const { container } = render(
|
||||
<DropdownButton
|
||||
menu={menuProps}
|
||||
styleConfig={{
|
||||
controlHeight: 40,
|
||||
fontSize: 16,
|
||||
fontWeight: 700,
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
Click
|
||||
</DropdownButton>,
|
||||
);
|
||||
expect(container.querySelector('.ant-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders tooltip when tooltip prop is provided', async () => {
|
||||
const { getByText } = render(
|
||||
<DropdownButton menu={menuProps} tooltip="My Tooltip">
|
||||
Click
|
||||
</DropdownButton>,
|
||||
);
|
||||
fireEvent.mouseEnter(getByText('Click'));
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('[role="tooltip"]')).toHaveTextContent(
|
||||
'My Tooltip',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('does not render tooltip wrapper when tooltip is not provided', () => {
|
||||
const { container } = render(
|
||||
<DropdownButton menu={menuProps}>Click</DropdownButton>,
|
||||
);
|
||||
expect(container.querySelector('[id$="-tooltip"]')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('passes button type to underlying Dropdown.Button', () => {
|
||||
const { container } = render(
|
||||
<DropdownButton menu={menuProps} type="primary">
|
||||
Click
|
||||
</DropdownButton>,
|
||||
);
|
||||
expect(container.querySelector('.ant-btn-primary')).toBeInTheDocument();
|
||||
});
|
||||
@@ -27,6 +27,7 @@ export const DropdownButton = ({
|
||||
tooltip,
|
||||
tooltipPlacement,
|
||||
children,
|
||||
styleConfig,
|
||||
...rest
|
||||
}: DropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -57,10 +58,14 @@ export const DropdownButton = ({
|
||||
defaultBtnCss,
|
||||
css`
|
||||
.ant-btn {
|
||||
height: 30px;
|
||||
box-shadow: none;
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
font-weight: ${theme.fontWeightStrong};
|
||||
height: ${styleConfig?.controlHeight ??
|
||||
theme.buttonControlHeightSM ??
|
||||
30}px;
|
||||
box-shadow: ${styleConfig?.boxShadow ?? 'none'};
|
||||
font-size: ${styleConfig?.fontSize ??
|
||||
theme.buttonFontSize ??
|
||||
theme.fontSizeSM}px;
|
||||
font-weight: ${styleConfig?.fontWeight ?? theme.fontWeightStrong};
|
||||
}
|
||||
`,
|
||||
]}
|
||||
@@ -85,4 +90,4 @@ export const DropdownButton = ({
|
||||
return button;
|
||||
};
|
||||
|
||||
export type { DropdownButtonProps };
|
||||
export type { DropdownButtonProps, DropdownButtonStyleConfig } from './types';
|
||||
|
||||
@@ -21,7 +21,15 @@ import { type ComponentProps } from 'react';
|
||||
import { Dropdown } from 'antd';
|
||||
import type { TooltipPlacement } from '../Tooltip/types';
|
||||
|
||||
export type DropdownButtonStyleConfig = {
|
||||
controlHeight?: number;
|
||||
fontSize?: number;
|
||||
fontWeight?: number;
|
||||
boxShadow?: string;
|
||||
};
|
||||
|
||||
export type DropdownButtonProps = ComponentProps<typeof Dropdown.Button> & {
|
||||
tooltip?: string;
|
||||
tooltipPlacement?: TooltipPlacement;
|
||||
styleConfig?: DropdownButtonStyleConfig;
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export const LabeledErrorBoundInput = ({
|
||||
isValidating ? 'validating' : hasError ? 'error' : 'success'
|
||||
}
|
||||
help={errorMessage || helpText}
|
||||
hasFeedback={!!hasError}
|
||||
hasFeedback={isValidating || !!hasError}
|
||||
>
|
||||
{visibilityToggle || props.name === 'password' ? (
|
||||
<StyledInputPassword
|
||||
|
||||
@@ -31,5 +31,6 @@ export interface LabeledErrorBoundInputProps {
|
||||
id?: string;
|
||||
classname?: string;
|
||||
visibilityToggle?: boolean;
|
||||
isValidating?: boolean;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: ${backgroundColor};
|
||||
border-radius: 8px;
|
||||
border-radius: ${theme.labelBorderRadius ?? 8}px;
|
||||
border-color: ${borderColor};
|
||||
padding: 0.35em 0.8em;
|
||||
line-height: 1;
|
||||
|
||||
@@ -32,10 +32,11 @@ import { CertifiedBadge } from '../CertifiedBadge';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export const menuTriggerStyles = (theme: SupersetTheme) => css`
|
||||
width: ${theme.sizeUnit * 8}px;
|
||||
height: ${theme.sizeUnit * 8}px;
|
||||
width: ${theme.buttonControlHeight ?? theme.sizeUnit * 8}px;
|
||||
height: ${theme.buttonControlHeight ?? theme.sizeUnit * 8}px;
|
||||
padding: 0;
|
||||
border: 1px solid ${theme.colorPrimary};
|
||||
border-radius: ${theme.buttonBorderRadius ?? theme.borderRadius}px;
|
||||
|
||||
&.ant-btn > span.anticon {
|
||||
line-height: 0;
|
||||
|
||||
@@ -45,10 +45,11 @@ export const StyledContainer = styled.div<{ headerPosition: string }>`
|
||||
export const StyledSelect = styled(Select, {
|
||||
shouldForwardProp: prop => prop !== 'headerPosition' && prop !== 'oneLine',
|
||||
})<{ headerPosition?: string; oneLine?: boolean }>`
|
||||
${({ theme, headerPosition, oneLine }) => `
|
||||
${({ theme, headerPosition, oneLine }) => {
|
||||
const useSubtleOptionHover = theme.selectOptionActiveOutline === false;
|
||||
return `
|
||||
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
|
||||
outline: 2px solid ${theme.colorPrimary};
|
||||
outline-offset: -2px;
|
||||
${useSubtleOptionHover ? 'outline: none;' : `outline: 2px solid ${theme.colorPrimary}; outline-offset: -2px;`}
|
||||
}
|
||||
flex: ${headerPosition === 'left' ? 1 : 0};
|
||||
line-height: ${theme.sizeXL}px;
|
||||
@@ -82,7 +83,8 @@ export const StyledSelect = styled(Select, {
|
||||
}
|
||||
`
|
||||
};
|
||||
`}
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
export const NoElement = styled.span`
|
||||
|
||||
@@ -82,7 +82,11 @@ export default class SupersetClientClass {
|
||||
unauthorizedHandler = undefined,
|
||||
}: ClientConfig = {}) {
|
||||
const url = new URL(`${protocol || 'https:'}//${host || 'localhost'}`);
|
||||
this.appRoot = appRoot;
|
||||
// Strip a trailing slash so the getUrl dedupe comparisons and the final
|
||||
// `${this.appRoot}/${...}` build stay correct regardless of how the root
|
||||
// was supplied. Mirrors normalizeBackendUrlString / AppRootMiddleware /
|
||||
// LegacyPrefixRedirectMiddleware, which all rstrip the root.
|
||||
this.appRoot = appRoot.replace(/\/$/, '');
|
||||
this.host = url.host;
|
||||
this.protocol = url.protocol as Protocol;
|
||||
this.headers = { Accept: 'application/json', ...headers }; // defaulting accept to json
|
||||
@@ -296,8 +300,26 @@ export default class SupersetClientClass {
|
||||
const host = inputHost ?? this.host;
|
||||
const cleanHost = host.slice(-1) === '/' ? host.slice(0, -1) : host; // no backslash
|
||||
|
||||
// Strip a single leading appRoot segment so callers that accidentally
|
||||
// pre-prefix their endpoint (e.g. by wrapping with ensureAppRoot before
|
||||
// passing to the client) do not produce a doubled `/superset/superset/...`
|
||||
// URL. Single-pass strip mirrors
|
||||
// `stripAppRoot` in `src/utils/pathUtils` and `normalizeBackendUrlString`
|
||||
// exactly: a genuine `/superset/superset/<slug>` is a legitimate route, not
|
||||
// a double-prefix bug. The L2 static invariant still flags pre-prefixing as
|
||||
// a migration issue; this is the runtime safety net.
|
||||
let cleanEndpoint = endpoint;
|
||||
const root = this.appRoot;
|
||||
if (root) {
|
||||
if (cleanEndpoint === root) {
|
||||
cleanEndpoint = '';
|
||||
} else if (cleanEndpoint.startsWith(`${root}/`)) {
|
||||
cleanEndpoint = cleanEndpoint.slice(root.length);
|
||||
}
|
||||
}
|
||||
|
||||
return `${this.protocol}//${cleanHost}${this.appRoot}/${
|
||||
endpoint[0] === '/' ? endpoint.slice(1) : endpoint
|
||||
cleanEndpoint[0] === '/' ? cleanEndpoint.slice(1) : cleanEndpoint
|
||||
}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,24 +55,25 @@ export default async function parseResponse<T extends ParseMethod = 'json'>(
|
||||
if (parseMethod === 'json-bigint') {
|
||||
const rawData = await response.text();
|
||||
const json = JSONbig.parse(rawData);
|
||||
const decoded = cloneDeepWith(json, (value: any) => {
|
||||
if (
|
||||
value?.isInteger?.() === true &&
|
||||
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
|
||||
) {
|
||||
// toFixed() avoids scientific notation, which BigInt() rejects.
|
||||
return BigInt(value.toFixed());
|
||||
}
|
||||
// // `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
||||
// // TODO: clean up after json-bigint>1.0.1 is released
|
||||
if (value?.isNaN?.() === false) {
|
||||
return value?.toNumber?.();
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
const result: JsonResponse = {
|
||||
response,
|
||||
json: cloneDeepWith(json, (value: any) => {
|
||||
if (
|
||||
value?.isInteger?.() === true &&
|
||||
(value?.isGreaterThan?.(Number.MAX_SAFE_INTEGER) ||
|
||||
value?.isLessThan?.(Number.MIN_SAFE_INTEGER))
|
||||
) {
|
||||
// toFixed() avoids scientific notation, which BigInt() rejects.
|
||||
return BigInt(value.toFixed());
|
||||
}
|
||||
// // `json-bigint` could not handle floats well, see sidorares/json-bigint#62
|
||||
// // TODO: clean up after json-bigint>1.0.1 is released
|
||||
if (value?.isNaN?.() === false) {
|
||||
return value?.toNumber?.();
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
json: decoded,
|
||||
};
|
||||
return result as ReturnType;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ export { default as callApi } from './callApi';
|
||||
export { default as SupersetClient } from './SupersetClient';
|
||||
export { default as SupersetClientClass } from './SupersetClientClass';
|
||||
|
||||
export { normalizeBackendUrlString } from './normalizeBackendUrls';
|
||||
export type { NormalizeOptions } from './normalizeBackendUrls';
|
||||
|
||||
export * from './types';
|
||||
export * from './constants';
|
||||
export { default as __hack_reexport_connection } from './types';
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Strips the configured application root from a single backend-supplied URL
|
||||
* string so the frontend speaks router-relative paths. Apply it at the few call
|
||||
* sites that surface a router-relative URL from an API response (e.g. a
|
||||
* dataset's `explore_url`) before handing the value to a consumer that
|
||||
* re-prefixes the root — `SupersetClient.getUrl`, `makeUrl`, or a react-router
|
||||
* `<Link>` resolving against the Router `basename`. Without it those consumers
|
||||
* would re-prefix an already-rooted path into `/superset/superset/...`.
|
||||
*
|
||||
* Absolute (`https:`, `ftp:`, `mailto:`, `tel:`) and protocol-relative (`//`)
|
||||
* URLs pass through untouched, so an operator-configured external
|
||||
* `default_endpoint` on a dataset is left alone.
|
||||
*/
|
||||
|
||||
export interface NormalizeOptions {
|
||||
/** Application root to strip. Empty string disables normalisation. */
|
||||
applicationRoot: string;
|
||||
}
|
||||
|
||||
const SAFE_ABSOLUTE_URL_RE = /^(?:https?|ftp|mailto|tel):/i;
|
||||
|
||||
function stripTrailingSlash(root: string): string {
|
||||
return root.endsWith('/') ? root.slice(0, -1) : root;
|
||||
}
|
||||
|
||||
/** Normalise a single router-relative URL string. */
|
||||
export function normalizeBackendUrlString(
|
||||
value: string,
|
||||
options: NormalizeOptions,
|
||||
): string {
|
||||
const root = stripTrailingSlash(options.applicationRoot);
|
||||
if (!root) return value;
|
||||
if (SAFE_ABSOLUTE_URL_RE.test(value)) return value;
|
||||
if (value.startsWith('//')) return value;
|
||||
if (value === root) return '/';
|
||||
if (value.startsWith(`${root}/`)) {
|
||||
return value.slice(root.length);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export default function getDatasourceMetadata({
|
||||
}: Params) {
|
||||
return client
|
||||
.get({
|
||||
endpoint: `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
|
||||
endpoint: `/fetch_datasource_metadata?datasourceKey=${datasourceKey}`,
|
||||
...requestConfig,
|
||||
})
|
||||
.then(response => response.json as Datasource);
|
||||
|
||||
@@ -106,6 +106,7 @@ export type ChartCustomization = {
|
||||
};
|
||||
description?: string;
|
||||
removed?: boolean;
|
||||
time_grains?: string[];
|
||||
};
|
||||
|
||||
export type ChartCustomizationDivider = Partial<
|
||||
|
||||
@@ -283,6 +283,22 @@ test('lookalike OpenStreetMap hostnames do not receive OSM attribution', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('relative raster tile templates do not receive OSM attribution', () => {
|
||||
// A host-relative template cannot be parsed by `new URL`, so the OSM
|
||||
// hostname check must fall through to "not OSM" rather than throw.
|
||||
const relativeTileUrl = '/local-tiles/{z}/{x}/{y}.png';
|
||||
const style = resolveMapStyle(
|
||||
`tile://${relativeTileUrl}`,
|
||||
'default-style.json',
|
||||
);
|
||||
|
||||
expect(typeof style).toBe('object');
|
||||
if (typeof style !== 'string') {
|
||||
expect(style.sources['osm-raster-tiles'].tiles).toEqual([relativeTileUrl]);
|
||||
expect(style.sources['osm-raster-tiles']).not.toHaveProperty('attribution');
|
||||
}
|
||||
});
|
||||
|
||||
test('style JSON URLs pass through without raster wrapping', () => {
|
||||
const styleUrl = 'https://example.com/styles/custom-style.json';
|
||||
|
||||
|
||||
@@ -88,6 +88,9 @@ type BootstrapData = {
|
||||
};
|
||||
|
||||
export function getBootstrapDataFromDocument(): unknown {
|
||||
/* istanbul ignore if -- a missing document only occurs in SSR/worker
|
||||
contexts, which Jest cannot simulate: jsdom pins `document` as a
|
||||
non-configurable global */
|
||||
if (typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,9 @@ describe('ChartClient', () => {
|
||||
Promise.reject(new Error('Unexpected all to v1 API')),
|
||||
);
|
||||
|
||||
fetchMock.post('glob:*/superset/explore_json/', {
|
||||
// post `Superset.route_base = ""`, the legacy endpoint
|
||||
// collapsed from `/superset/explore_json/` to `/explore_json/`.
|
||||
fetchMock.post('glob:*/explore_json/', {
|
||||
field1: 'abc',
|
||||
field2: 'def',
|
||||
});
|
||||
@@ -198,13 +200,10 @@ describe('ChartClient', () => {
|
||||
|
||||
describe('.loadDatasource(datasourceKey, options)', () => {
|
||||
test('fetches datasource', () => {
|
||||
fetchMock.get(
|
||||
'glob:*/superset/fetch_datasource_metadata?datasourceKey=1__table',
|
||||
{
|
||||
field1: 'abc',
|
||||
field2: 'def',
|
||||
},
|
||||
);
|
||||
fetchMock.get('glob:*/fetch_datasource_metadata?datasourceKey=1__table', {
|
||||
field1: 'abc',
|
||||
field2: 'def',
|
||||
});
|
||||
|
||||
return expect(chartClient.loadDatasource('1__table')).resolves.toEqual({
|
||||
field1: 'abc',
|
||||
@@ -264,13 +263,10 @@ describe('ChartClient', () => {
|
||||
color: 'living-coral',
|
||||
});
|
||||
|
||||
fetchMock.get(
|
||||
'glob:*/superset/fetch_datasource_metadata?datasourceKey=1__table',
|
||||
{
|
||||
name: 'transactions',
|
||||
schema: 'staging',
|
||||
},
|
||||
);
|
||||
fetchMock.get('glob:*/fetch_datasource_metadata?datasourceKey=1__table', {
|
||||
name: 'transactions',
|
||||
schema: 'staging',
|
||||
});
|
||||
|
||||
fetchMock.post('glob:*/api/v1/chart/data', {
|
||||
lorem: 'ipsum',
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 { SupersetClientClass } from '@superset-ui/core';
|
||||
|
||||
// SupersetClient is expected to apply the configured appRoot exactly once.
|
||||
// Callers must pass router-relative endpoints; pre-prefixing causes the
|
||||
// double-prefix bug documented below.
|
||||
|
||||
describe('SupersetClient applies the application root exactly once', () => {
|
||||
const buildClient = () =>
|
||||
new SupersetClientClass({
|
||||
protocol: 'https:',
|
||||
host: 'config_host',
|
||||
appRoot: '/superset',
|
||||
});
|
||||
|
||||
test('endpoint without leading slash is concatenated correctly', () => {
|
||||
expect(buildClient().getUrl({ endpoint: 'api/v1/chart' })).toBe(
|
||||
'https://config_host/superset/api/v1/chart',
|
||||
);
|
||||
});
|
||||
|
||||
test('endpoint with leading slash is normalised to a single root segment', () => {
|
||||
expect(buildClient().getUrl({ endpoint: '/api/v1/chart' })).toBe(
|
||||
'https://config_host/superset/api/v1/chart',
|
||||
);
|
||||
});
|
||||
|
||||
// A trailing slash on the configured appRoot is stripped at construction
|
||||
// (SupersetClientClass `appRoot.replace(/\/$/, '')`). Without it, a root of
|
||||
// '/superset/' produced 'https://host/superset//foo', and the dedupe block's
|
||||
// `startsWith('/superset//')` check silently failed to dedupe a pre-prefixed
|
||||
// endpoint. This pins both behaviours against regression.
|
||||
test('trailing-slash appRoot is normalised to a single root segment', () => {
|
||||
const client = new SupersetClientClass({
|
||||
protocol: 'https:',
|
||||
host: 'config_host',
|
||||
appRoot: '/superset/',
|
||||
});
|
||||
expect(client.getUrl({ endpoint: '/api/v1/chart' })).toBe(
|
||||
'https://config_host/superset/api/v1/chart',
|
||||
);
|
||||
// and a pre-prefixed endpoint is still deduped, not doubled
|
||||
expect(client.getUrl({ endpoint: '/superset/api/v1/chart' })).toBe(
|
||||
'https://config_host/superset/api/v1/chart',
|
||||
);
|
||||
});
|
||||
|
||||
// Runtime safety net: if a caller pre-prefixes the endpoint (e.g. by wrapping
|
||||
// with ensureAppRoot before calling), getUrl strips the duplicate. The L2
|
||||
// static invariant still flags the pattern at the call site — this guards
|
||||
// against the bug reaching production if the static check is bypassed.
|
||||
test('dedupes a leading application-root segment from a pre-prefixed endpoint', () => {
|
||||
expect(buildClient().getUrl({ endpoint: '/superset/api/v1/chart' })).toBe(
|
||||
'https://config_host/superset/api/v1/chart',
|
||||
);
|
||||
});
|
||||
|
||||
// Single-pass strip preserves a legitimate `/superset/superset/<slug>`
|
||||
// route. Backend-supplied router-relative URLs are stripped of the root at
|
||||
// the call sites that surface them (via `normalizeBackendUrlString`) before
|
||||
// any re-prefixing helper sees them, so a doubled leading segment reaching
|
||||
// `getUrl` is a real route, not a double-prefix bug. This pin guards against
|
||||
// silent regression to a greedy strip.
|
||||
test('strips exactly one application-root segment (single-pass)', () => {
|
||||
expect(
|
||||
buildClient().getUrl({ endpoint: '/superset/superset/api/v1/chart' }),
|
||||
).toBe('https://config_host/superset/superset/api/v1/chart');
|
||||
expect(
|
||||
buildClient().getUrl({
|
||||
endpoint: '/superset/superset/superset/api/v1/chart',
|
||||
}),
|
||||
).toBe('https://config_host/superset/superset/superset/api/v1/chart');
|
||||
});
|
||||
|
||||
test('dedupe is segment-boundary aware — `/supersetfoo` is not a prefix match', () => {
|
||||
expect(buildClient().getUrl({ endpoint: '/supersetfoo/x' })).toBe(
|
||||
'https://config_host/superset/supersetfoo/x',
|
||||
);
|
||||
});
|
||||
|
||||
test('dedupes the bare application root to an empty endpoint', () => {
|
||||
expect(buildClient().getUrl({ endpoint: '/superset' })).toBe(
|
||||
'https://config_host/superset/',
|
||||
);
|
||||
});
|
||||
|
||||
test('empty application root produces no prefix segment', () => {
|
||||
const client = new SupersetClientClass({
|
||||
protocol: 'https:',
|
||||
host: 'config_host',
|
||||
});
|
||||
expect(client.getUrl({ endpoint: '/api/v1/chart' })).toBe(
|
||||
'https://config_host/api/v1/chart',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 { normalizeBackendUrlString } from '../../src/connection/normalizeBackendUrls';
|
||||
|
||||
const PREFIX = '/superset';
|
||||
|
||||
describe('normalizeBackendUrlString', () => {
|
||||
test('strips application root from a router-relative path', () => {
|
||||
expect(
|
||||
normalizeBackendUrlString('/superset/explore/?slice_id=1', {
|
||||
applicationRoot: PREFIX,
|
||||
}),
|
||||
).toBe('/explore/?slice_id=1');
|
||||
});
|
||||
|
||||
test('strips a value that equals the application root exactly', () => {
|
||||
expect(
|
||||
normalizeBackendUrlString('/superset', { applicationRoot: PREFIX }),
|
||||
).toBe('/');
|
||||
});
|
||||
|
||||
test('tolerates a trailing slash on applicationRoot', () => {
|
||||
expect(
|
||||
normalizeBackendUrlString('/superset/foo', {
|
||||
applicationRoot: '/superset/',
|
||||
}),
|
||||
).toBe('/foo');
|
||||
});
|
||||
|
||||
// The negative cases below prove the helper is conservative: it doesn't
|
||||
// mutate external URLs or path segments that merely share text with the root.
|
||||
test('passes absolute URLs through unchanged', () => {
|
||||
expect(
|
||||
normalizeBackendUrlString('https://external.example.com/superset/foo', {
|
||||
applicationRoot: PREFIX,
|
||||
}),
|
||||
).toBe('https://external.example.com/superset/foo');
|
||||
});
|
||||
|
||||
test('passes protocol-relative URLs through unchanged', () => {
|
||||
expect(
|
||||
normalizeBackendUrlString('//cdn.example.com/superset/foo', {
|
||||
applicationRoot: PREFIX,
|
||||
}),
|
||||
).toBe('//cdn.example.com/superset/foo');
|
||||
});
|
||||
|
||||
test('does not strip a similar-but-different prefix segment', () => {
|
||||
// /superset-public/... shares text with /superset but is a different path
|
||||
// segment. Only /superset followed by / or end-of-string counts.
|
||||
expect(
|
||||
normalizeBackendUrlString('/superset-public/explore/?slice_id=1', {
|
||||
applicationRoot: PREFIX,
|
||||
}),
|
||||
).toBe('/superset-public/explore/?slice_id=1');
|
||||
});
|
||||
|
||||
test('is a no-op when application root is empty', () => {
|
||||
expect(
|
||||
normalizeBackendUrlString('/superset/explore/?slice_id=1', {
|
||||
applicationRoot: '',
|
||||
}),
|
||||
).toBe('/superset/explore/?slice_id=1');
|
||||
});
|
||||
|
||||
test('is idempotent: normalize(normalize(x)) === normalize(x)', () => {
|
||||
const once = normalizeBackendUrlString('/superset/explore/?id=1', {
|
||||
applicationRoot: PREFIX,
|
||||
});
|
||||
const twice = normalizeBackendUrlString(once, { applicationRoot: PREFIX });
|
||||
expect(twice).toBe(once);
|
||||
});
|
||||
});
|
||||
@@ -35,8 +35,10 @@ describe('getFormData()', () => {
|
||||
field2: 'def',
|
||||
};
|
||||
|
||||
// post-`route_base=""`, the legacy endpoint collapsed
|
||||
// from `/superset/fetch_datasource_metadata` to `/fetch_datasource_metadata`.
|
||||
fetchMock.get(
|
||||
'glob:*/superset/fetch_datasource_metadata?datasourceKey=1__table',
|
||||
'glob:*/fetch_datasource_metadata?datasourceKey=1__table',
|
||||
mockData,
|
||||
);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ export class DashboardPage {
|
||||
* @param slug - The dashboard slug (e.g., 'world_health')
|
||||
*/
|
||||
async gotoBySlug(slug: string): Promise<void> {
|
||||
await gotoWithRetry(this.page, `superset/dashboard/${slug}/`);
|
||||
await gotoWithRetry(this.page, `dashboard/${slug}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ export class DashboardPage {
|
||||
* @param id - The dashboard ID
|
||||
*/
|
||||
async gotoById(id: number): Promise<void> {
|
||||
await gotoWithRetry(this.page, `superset/dashboard/${id}/`);
|
||||
await gotoWithRetry(this.page, `dashboard/${id}/`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,7 @@ import { waitForPost } from '../../helpers/api/intercepts';
|
||||
import { expectStatusOneOf } from '../../helpers/api/assertions';
|
||||
import { getDatabaseByName } from '../../helpers/api/database';
|
||||
import { apiExecuteSql } from '../../helpers/api/sqllab';
|
||||
import { TIMEOUT } from '../../utils/constants';
|
||||
|
||||
interface ExamplesSetupResult {
|
||||
tableName: string;
|
||||
@@ -116,7 +117,7 @@ async function dropTempTable(
|
||||
// Uses test.describe only because Playwright's serial mode API requires it -
|
||||
// (Deviation from "avoid describe" guideline is necessary for functional reasons)
|
||||
test.describe('create dataset wizard', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
test.describe.configure({ mode: 'serial', timeout: TIMEOUT.SLOW_TEST });
|
||||
|
||||
test('should create a dataset via wizard', async ({ page, testAssets }) => {
|
||||
const { tableName, dbId, createDatasetPage } = await setupExamplesDataset(
|
||||
|
||||
@@ -35,5 +35,5 @@ export const URL = {
|
||||
LOGIN: 'login/',
|
||||
SAVED_QUERIES_LIST: 'savedqueryview/list/',
|
||||
SQLLAB: 'sqllab',
|
||||
WELCOME: 'superset/welcome/',
|
||||
WELCOME: 'welcome/',
|
||||
} as const;
|
||||
|
||||
@@ -44,3 +44,8 @@ export const FILTER_CONDITION_BODY_INDEX = {
|
||||
} as const;
|
||||
|
||||
export const ROW_NUMBER_COL_ID = '__row_number__';
|
||||
|
||||
// Non-enumerable key used to attach a row's basic (increase/decrease) color
|
||||
// formatter to the row data object so it travels with the row through AG Grid
|
||||
// client-side sorting (#105973).
|
||||
export const BASIC_COLOR_FORMATTERS_ROW_KEY = '__basicColorFormatters__';
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { CustomCellRendererProps } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { BasicColorFormatterType, InputColumn, ValueRange } from '../types';
|
||||
import { useIsDark } from '../utils/useTableTheme';
|
||||
import getRowBasicColorFormatter from '../utils/getRowBasicColorFormatter';
|
||||
|
||||
const StyledTotalCell = styled.div`
|
||||
${() => `
|
||||
@@ -163,13 +164,13 @@ export const NumericCellRenderer = (
|
||||
let arrow = '';
|
||||
let arrowColor = '';
|
||||
if (hasBasicColorFormatters && col?.metricName) {
|
||||
arrow =
|
||||
basicColorFormatters?.[node?.rowIndex as number]?.[col.metricName]
|
||||
?.mainArrow;
|
||||
arrowColor =
|
||||
basicColorFormatters?.[node?.rowIndex as number]?.[
|
||||
col.metricName
|
||||
]?.arrowColor?.toLowerCase();
|
||||
const rowFormatter = getRowBasicColorFormatter(
|
||||
node,
|
||||
node?.rowIndex,
|
||||
basicColorFormatters,
|
||||
)?.[col.metricName];
|
||||
arrow = rowFormatter?.mainArrow;
|
||||
arrowColor = rowFormatter?.arrowColor?.toLowerCase();
|
||||
}
|
||||
|
||||
const alignment =
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
} from '@superset-ui/chart-controls';
|
||||
import isEqualColumns from './utils/isEqualColumns';
|
||||
import DateWithFormatter from './utils/DateWithFormatter';
|
||||
import { BASIC_COLOR_FORMATTERS_ROW_KEY } from './consts';
|
||||
import {
|
||||
DataColumnMeta,
|
||||
TableChartProps,
|
||||
@@ -703,6 +704,23 @@ const transformProps = (
|
||||
|
||||
const basicColorFormatters =
|
||||
comparisonColorEnabled && getBasicColorFormatter(baseQuery?.data, columns);
|
||||
|
||||
// Attach each row's basic (increase/decrease) color formatter to the row data
|
||||
// object so it travels with the row through AG Grid client-side sorting.
|
||||
// basicColorFormatters is built in the original query order and was previously
|
||||
// read positionally by the displayed rowIndex, which applied colors to the
|
||||
// wrong rows once the table was sorted (#105973). The property is
|
||||
// non-enumerable so it never leaks into exports, cross-filters or spreads.
|
||||
if (basicColorFormatters) {
|
||||
passedData.forEach((row, index) => {
|
||||
Object.defineProperty(row, BASIC_COLOR_FORMATTERS_ROW_KEY, {
|
||||
value: basicColorFormatters[index],
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
const columnColorFormatters =
|
||||
getColorFormatters(conditionalFormatting, passedData, theme) ?? [];
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { CellClassParams } from '@superset-ui/core/components/ThemedAgGridReact';
|
||||
import { BasicColorFormatterType, InputColumn } from '../types';
|
||||
import getRowBasicColorFormatter from './getRowBasicColorFormatter';
|
||||
|
||||
type CellStyleParams = CellClassParams & {
|
||||
hasColumnColorFormatters: boolean | undefined;
|
||||
@@ -84,8 +85,11 @@ const getCellStyle = (params: CellStyleParams) => {
|
||||
col?.metricName &&
|
||||
node?.rowPinned !== 'bottom'
|
||||
) {
|
||||
backgroundColor =
|
||||
basicColorFormatters?.[rowIndex]?.[col.metricName]?.backgroundColor;
|
||||
backgroundColor = getRowBasicColorFormatter(
|
||||
node,
|
||||
rowIndex,
|
||||
basicColorFormatters,
|
||||
)?.[col.metricName]?.backgroundColor;
|
||||
}
|
||||
|
||||
const textAlign =
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 { BASIC_COLOR_FORMATTERS_ROW_KEY } from '../consts';
|
||||
import { BasicColorFormatterType } from '../types';
|
||||
|
||||
type RowFormatters = { [key: string]: BasicColorFormatterType };
|
||||
|
||||
/**
|
||||
* Resolves the basic (increase/decrease) color formatters for a given AG Grid
|
||||
* row node.
|
||||
*
|
||||
* The formatter is attached to the row data object itself (see transformProps),
|
||||
* so it follows the row through client-side sorting. Looking it up positionally
|
||||
* by the displayed `rowIndex` was wrong once the user sorted the table, because
|
||||
* the displayed index no longer matched the original data order (#105973).
|
||||
*
|
||||
* Falls back to the positional array for safety when no attached formatter is
|
||||
* present.
|
||||
*/
|
||||
export default function getRowBasicColorFormatter(
|
||||
node: { data?: Record<string, unknown> } | undefined,
|
||||
rowIndex: number | null | undefined,
|
||||
basicColorFormatters: RowFormatters[] | undefined,
|
||||
): RowFormatters | undefined {
|
||||
const attached = node?.data?.[BASIC_COLOR_FORMATTERS_ROW_KEY] as
|
||||
| RowFormatters
|
||||
| undefined;
|
||||
if (attached) {
|
||||
return attached;
|
||||
}
|
||||
if (rowIndex == null) {
|
||||
return undefined;
|
||||
}
|
||||
return basicColorFormatters?.[rowIndex];
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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 getRowBasicColorFormatter from '../../src/utils/getRowBasicColorFormatter';
|
||||
import { BASIC_COLOR_FORMATTERS_ROW_KEY } from '../../src/consts';
|
||||
|
||||
const red = { sales: { backgroundColor: 'red', mainArrow: '↓', arrowColor: 'red' } };
|
||||
const green = {
|
||||
sales: { backgroundColor: 'green', mainArrow: '↑', arrowColor: 'green' },
|
||||
};
|
||||
|
||||
// Positional array in the original (unsorted) query order: row 0 -> green, row 1 -> red.
|
||||
const positional = [green, red] as any;
|
||||
|
||||
test('uses the formatter attached to the row, not the displayed rowIndex (#105973)', () => {
|
||||
// After sorting, the row whose original formatter is `red` is displayed first
|
||||
// (rowIndex 0). The positional lookup would wrongly return `green`.
|
||||
const rowData: Record<string, unknown> = { sales: 5 };
|
||||
Object.defineProperty(rowData, BASIC_COLOR_FORMATTERS_ROW_KEY, {
|
||||
value: red,
|
||||
enumerable: false,
|
||||
});
|
||||
const node = { data: rowData };
|
||||
|
||||
expect(getRowBasicColorFormatter(node, 0, positional)).toBe(red);
|
||||
expect(
|
||||
getRowBasicColorFormatter(node, 0, positional)?.sales.backgroundColor,
|
||||
).toBe('red');
|
||||
});
|
||||
|
||||
test('falls back to positional lookup when no formatter is attached', () => {
|
||||
const node = { data: { sales: 5 } };
|
||||
expect(getRowBasicColorFormatter(node, 1, positional)).toBe(red);
|
||||
});
|
||||
|
||||
test('returns undefined when nothing matches', () => {
|
||||
expect(getRowBasicColorFormatter(undefined, null, positional)).toBeUndefined();
|
||||
expect(
|
||||
getRowBasicColorFormatter({ data: {} }, null, positional),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('attached formatter is non-enumerable so it does not leak into the row', () => {
|
||||
const rowData: Record<string, unknown> = { sales: 5 };
|
||||
Object.defineProperty(rowData, BASIC_COLOR_FORMATTERS_ROW_KEY, {
|
||||
value: green,
|
||||
enumerable: false,
|
||||
});
|
||||
expect(Object.keys(rowData)).toEqual(['sales']);
|
||||
});
|
||||
@@ -33,8 +33,9 @@ import { getLayerConfig } from '../util/controlPanelUtil';
|
||||
export default class CartodiagramPlugin extends ChartPlugin {
|
||||
constructor(opts: CartodiagramPluginConstructorOpts) {
|
||||
const metadata = new ChartMetadata({
|
||||
description:
|
||||
description: t(
|
||||
'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.',
|
||||
),
|
||||
name: t('Cartodiagram'),
|
||||
thumbnail,
|
||||
thumbnailDark,
|
||||
|
||||
@@ -28,8 +28,9 @@ export default class PopKPIPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('KPI'),
|
||||
description:
|
||||
description: t(
|
||||
'Showcases a metric along with a comparison of value, change, and percent change for a selected time period.',
|
||||
),
|
||||
name: t('Big Number with Time Period Comparison'),
|
||||
tags: [
|
||||
t('Comparison'),
|
||||
|
||||
@@ -282,6 +282,11 @@ export default function transformProps(
|
||||
? formatTime
|
||||
: numberFormatter;
|
||||
|
||||
const lineWidth = 2;
|
||||
// Pad the grid by half the stroke width so the trendline isn't clipped at
|
||||
// the edges of the chart area (the stroke extends beyond the data point).
|
||||
const strokePad = lineWidth / 2;
|
||||
|
||||
const echartOptions: EChartsCoreOption = trendLineData
|
||||
? {
|
||||
series: [
|
||||
@@ -293,6 +298,9 @@ export default function transformProps(
|
||||
symbolSize: 10,
|
||||
showSymbol: false,
|
||||
color: mainColor ?? BRAND_COLOR,
|
||||
lineStyle: {
|
||||
width: lineWidth,
|
||||
},
|
||||
areaStyle: {
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
@@ -346,10 +354,10 @@ export default function transformProps(
|
||||
top: TIMESERIES_CONSTANTS.gridOffsetTop,
|
||||
}
|
||||
: {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: strokePad,
|
||||
left: strokePad,
|
||||
right: strokePad,
|
||||
top: strokePad,
|
||||
},
|
||||
tooltip: {
|
||||
...getDefaultTooltip(refs),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { DatasourceType, TimeGranularity, VizType } from '@superset-ui/core';
|
||||
import type { LineSeriesOption } from 'echarts';
|
||||
import { supersetTheme } from '@apache-superset/core/theme';
|
||||
import transformProps from '../../src/BigNumber/BigNumberWithTrendline/transformProps';
|
||||
import {
|
||||
@@ -269,11 +270,18 @@ describe('BigNumberWithTrendline', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const series = (
|
||||
transformed.echartOptions?.series as LineSeriesOption[]
|
||||
)?.[0];
|
||||
const lineWidth = series?.lineStyle?.width;
|
||||
expect(lineWidth).toBe(2);
|
||||
|
||||
const expectedPad = (lineWidth as number) / 2;
|
||||
expect(transformed.echartOptions?.grid).toEqual({
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: expectedPad,
|
||||
left: expectedPad,
|
||||
right: expectedPad,
|
||||
top: expectedPad,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ import { PAGE_SIZE_OPTIONS } from '../consts';
|
||||
import { sortAlphanumericCaseInsensitive } from './utils/sortAlphanumericCaseInsensitive';
|
||||
import { SearchOption, SortByItem } from '../types';
|
||||
import SearchSelectDropdown from './components/SearchSelectDropdown';
|
||||
import { SupersetTheme, css } from '@apache-superset/core/theme';
|
||||
|
||||
export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
tableClassName?: string;
|
||||
@@ -561,6 +562,9 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
align="center"
|
||||
justify="space-between"
|
||||
gap="middle"
|
||||
css={(theme: SupersetTheme) => css`
|
||||
font-size: ${theme.fontSizeSM}px;
|
||||
`}
|
||||
>
|
||||
{hasPagination ? (
|
||||
<SelectPageSize
|
||||
@@ -576,30 +580,32 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
/>
|
||||
) : null}
|
||||
<Flex wrap align="center" gap="middle">
|
||||
{serverPagination && searchInput && (
|
||||
<Space size="small" className="search-select-container">
|
||||
<span className="search-by-label">{t('Search by')}:</span>
|
||||
<SearchSelectDropdown
|
||||
searchOptions={searchOptions}
|
||||
value={serverPaginationData?.searchColumn || ''}
|
||||
onChange={onSearchColChange}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
{searchInput && (
|
||||
<GlobalFilter<D>
|
||||
searchInput={
|
||||
typeof searchInput === 'boolean' ? undefined : searchInput
|
||||
}
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
setGlobalFilter={
|
||||
manualSearch ? handleSearchChange : setGlobalFilter
|
||||
}
|
||||
filterValue={manualSearch ? initialSearchText : filterValue}
|
||||
id={searchInputId}
|
||||
serverPagination={!!serverPagination}
|
||||
rowCount={rowCount}
|
||||
/>
|
||||
<>
|
||||
{serverPagination && (
|
||||
<Space direction="vertical" size={4}>
|
||||
{t('Search by')}
|
||||
<SearchSelectDropdown
|
||||
searchOptions={searchOptions}
|
||||
value={serverPaginationData?.searchColumn || ''}
|
||||
onChange={onSearchColChange}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
<GlobalFilter<D>
|
||||
searchInput={
|
||||
typeof searchInput === 'boolean' ? undefined : searchInput
|
||||
}
|
||||
preGlobalFilteredRows={preGlobalFilteredRows}
|
||||
setGlobalFilter={
|
||||
manualSearch ? handleSearchChange : setGlobalFilter
|
||||
}
|
||||
filterValue={manualSearch ? initialSearchText : filterValue}
|
||||
id={searchInputId}
|
||||
serverPagination={!!serverPagination}
|
||||
rowCount={rowCount}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{renderTimeComparisonDropdown
|
||||
? renderTimeComparisonDropdown()
|
||||
|
||||
@@ -17,14 +17,9 @@
|
||||
* under the License.
|
||||
*/
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { RawAntdSelect } from '@superset-ui/core/components';
|
||||
import { RawAntdSelect as Select } from '@superset-ui/core/components';
|
||||
import { SearchOption } from '../../types';
|
||||
|
||||
const StyledSelect = styled(RawAntdSelect)`
|
||||
width: 120px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
import { SupersetTheme, css } from '@apache-superset/core/theme';
|
||||
|
||||
interface SearchSelectDropdownProps {
|
||||
/** The currently selected search column value */
|
||||
@@ -41,10 +36,14 @@ function SearchSelectDropdown({
|
||||
searchOptions,
|
||||
}: SearchSelectDropdownProps) {
|
||||
return (
|
||||
<StyledSelect
|
||||
<Select
|
||||
className="search-select"
|
||||
value={value || (searchOptions?.[0]?.value ?? '')}
|
||||
css={(theme: SupersetTheme) => css`
|
||||
width: ${theme.sizeUnit * 30}px;
|
||||
`}
|
||||
value={value ?? searchOptions?.[0]?.value}
|
||||
options={searchOptions}
|
||||
size="small"
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -58,7 +58,7 @@ import {
|
||||
useTheme,
|
||||
SupersetTheme,
|
||||
} from '@apache-superset/core/theme';
|
||||
import { t, tn } from '@apache-superset/core/translation';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import {
|
||||
Input,
|
||||
@@ -254,12 +254,13 @@ function SearchInput({
|
||||
inputRef,
|
||||
}: SearchInputProps) {
|
||||
return (
|
||||
<Space direction="horizontal" size={4} className="dt-global-filter">
|
||||
{t('Search')}
|
||||
<Space direction="vertical" size={4} className="dt-global-filter">
|
||||
<span aria-hidden="true">{t('Search')}</span>
|
||||
<Input
|
||||
aria-label={t('Search %s records', count)}
|
||||
placeholder={tn('%s record', '%s records...', count, count)}
|
||||
aria-label={t('Search records')}
|
||||
placeholder={t('Search records')}
|
||||
value={value}
|
||||
size="small"
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
ref={inputRef}
|
||||
@@ -276,18 +277,18 @@ function SelectPageSize({
|
||||
const { Option } = Select;
|
||||
|
||||
return (
|
||||
<span className="dt-select-page-size">
|
||||
<Space direction="vertical" size={4} className="dt-select-page-size">
|
||||
<VisuallyHidden htmlFor="pageSizeSelect">
|
||||
{t('Select page size')}
|
||||
</VisuallyHidden>
|
||||
{t('Show')}{' '}
|
||||
{t('Entries per page')}
|
||||
<Select<number>
|
||||
id="pageSizeSelect"
|
||||
value={current}
|
||||
onChange={value => onChange(value)}
|
||||
size="small"
|
||||
css={(theme: SupersetTheme) => css`
|
||||
width: ${theme.sizeUnit * 18}px;
|
||||
width: ${theme.sizeUnit * 30}px;
|
||||
`}
|
||||
aria-label={t('Show entries per page')}
|
||||
>
|
||||
@@ -301,9 +302,8 @@ function SelectPageSize({
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</Select>{' '}
|
||||
{t('entries per page')}
|
||||
</span>
|
||||
</Select>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1204,8 +1204,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
onClick:
|
||||
emitCrossFilters && !valueRange && !isMetric
|
||||
? () => {
|
||||
const isFilterable = columnsMeta.find(
|
||||
(cm: DataColumnMeta) => cm.key === key,
|
||||
)?.isFilterable;
|
||||
// allow selecting text in a cell
|
||||
if (!getSelectedText()) {
|
||||
if (!getSelectedText() && isFilterable !== false) {
|
||||
toggleFilter(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +232,9 @@ const processColumns = memoizeOne(function processColumns(
|
||||
const metricsSet = new Set(metrics);
|
||||
const percentMetricsSet = new Set(percentMetrics);
|
||||
const rawPercentMetricsSet = new Set(rawPercentMetrics);
|
||||
const columnsByName = new Map(
|
||||
(props.datasource.columns ?? []).map(col => [col.column_name, col]),
|
||||
);
|
||||
|
||||
const columns: DataColumnMeta[] = (colnames || [])
|
||||
.filter(
|
||||
@@ -244,6 +247,7 @@ const processColumns = memoizeOne(function processColumns(
|
||||
const config = columnConfig[key] || {};
|
||||
// for the purpose of presentation, only numeric values are treated as metrics
|
||||
// because users can also add things like `MAX(str_col)` as a metric.
|
||||
const isFilterable = columnsByName.get(key)?.filterable;
|
||||
const isMetric = metricsSet.has(key) && isNumeric(key, records);
|
||||
const isPercentMetric = percentMetricsSet.has(key);
|
||||
const label =
|
||||
@@ -326,6 +330,7 @@ const processColumns = memoizeOne(function processColumns(
|
||||
isPercentMetric,
|
||||
formatter,
|
||||
config,
|
||||
isFilterable,
|
||||
description,
|
||||
currencyCodeColumn,
|
||||
};
|
||||
|
||||
@@ -2106,7 +2106,7 @@ describe('plugin-chart-table', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('textbox')).toHaveValue('Michael');
|
||||
expect(screen.getByLabelText('Search 0 records')).toHaveValue(
|
||||
expect(screen.getByLabelText('Search records')).toHaveValue(
|
||||
'Michael',
|
||||
);
|
||||
});
|
||||
@@ -2534,3 +2534,33 @@ test('sorts genuinely string columns alphanumerically', () => {
|
||||
const values = Array.from(cells).map(td => td.textContent);
|
||||
expect(values).toEqual(['apple', 'banana', 'cherry']);
|
||||
});
|
||||
|
||||
test('TableChart should NOT emit cross-filter when clicking a cell in a not-filterable column', () => {
|
||||
const setDataMask = jest.fn();
|
||||
const props = transformProps({
|
||||
...testData.basic,
|
||||
datasource: {
|
||||
...testData.basic.datasource,
|
||||
columns: [{ column_name: 'name', filterable: false } as any],
|
||||
},
|
||||
hooks: { setDataMask },
|
||||
emitCrossFilters: true,
|
||||
});
|
||||
render(
|
||||
<ProviderWrapper>
|
||||
<TableChart
|
||||
{...props}
|
||||
emitCrossFilters
|
||||
setDataMask={setDataMask}
|
||||
sticky={false}
|
||||
/>
|
||||
</ProviderWrapper>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Michael'));
|
||||
|
||||
const crossFilterCall = setDataMask.mock.calls.find(
|
||||
(call: any[]) => call[0]?.filterState?.filters,
|
||||
);
|
||||
expect(crossFilterCall).toBeUndefined();
|
||||
});
|
||||
|
||||
183
superset-frontend/spec/helpers/sourceTreeScanner.ts
Normal file
183
superset-frontend/spec/helpers/sourceTreeScanner.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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 { readdirSync, readFileSync, statSync } from 'fs';
|
||||
import { join, relative, resolve, sep } from 'path';
|
||||
|
||||
const DEFAULT_ROOTS = ['src', 'packages/superset-ui-core/src'];
|
||||
|
||||
const ALWAYS_SKIP_SEGMENTS = new Set([
|
||||
'node_modules',
|
||||
'dist',
|
||||
'build',
|
||||
'coverage',
|
||||
'__mocks__',
|
||||
'cypress-base',
|
||||
'playwright',
|
||||
]);
|
||||
|
||||
const ALWAYS_SKIP_SUFFIXES = [
|
||||
'.test.ts',
|
||||
'.test.tsx',
|
||||
'.stories.ts',
|
||||
'.stories.tsx',
|
||||
];
|
||||
|
||||
const SOURCE_EXTENSIONS = ['.ts', '.tsx'];
|
||||
|
||||
export interface ScanOptions {
|
||||
/** Workspace-relative directories to scan. Defaults to the source tree. */
|
||||
roots?: string[];
|
||||
/** Extra path segments to skip on top of {@link ALWAYS_SKIP_SEGMENTS}. */
|
||||
ignoreSegments?: string[];
|
||||
/** Regex run against each line of each file. */
|
||||
pattern: RegExp;
|
||||
/** Workspace-relative paths (forward slashes) exempt from this scan. */
|
||||
allowlist?: string[];
|
||||
}
|
||||
|
||||
export interface ScanHit {
|
||||
/** Workspace-relative path with forward slashes. */
|
||||
file: string;
|
||||
/** 1-based line number. */
|
||||
line: number;
|
||||
/** The text of the matching line, trimmed. */
|
||||
text: string;
|
||||
/** The substring captured by `pattern`. */
|
||||
match: string;
|
||||
}
|
||||
|
||||
// __dirname resolves to <workspace>/spec/helpers regardless of cwd.
|
||||
const WORKSPACE_ROOT = resolve(__dirname, '..', '..');
|
||||
|
||||
function isSourceFile(name: string): boolean {
|
||||
return (
|
||||
SOURCE_EXTENSIONS.some(ext => name.endsWith(ext)) &&
|
||||
!ALWAYS_SKIP_SUFFIXES.some(suffix => name.endsWith(suffix))
|
||||
);
|
||||
}
|
||||
|
||||
function walk(directory: string, ignoreSegments: Set<string>): string[] {
|
||||
const found: string[] = [];
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(directory, { withFileTypes: true });
|
||||
} catch {
|
||||
return found;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (ignoreSegments.has(entry.name)) continue;
|
||||
const absolute = join(directory, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
found.push(...walk(absolute, ignoreSegments));
|
||||
} else if (entry.isFile() && isSourceFile(entry.name)) {
|
||||
found.push(absolute);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
function toForwardSlashes(path: string): string {
|
||||
return sep === '/' ? path : path.split(sep).join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Line-by-line regex scan over the source tree. Returns one {@link ScanHit}
|
||||
* per matching line. Textual (not AST-based) — false positives on string
|
||||
* literals should be fixed by tightening the regex.
|
||||
*/
|
||||
export function scanSource(options: ScanOptions): ScanHit[] {
|
||||
const {
|
||||
roots = DEFAULT_ROOTS,
|
||||
ignoreSegments = [],
|
||||
pattern,
|
||||
allowlist = [],
|
||||
} = options;
|
||||
|
||||
const ignoreSet = new Set([...ALWAYS_SKIP_SEGMENTS, ...ignoreSegments]);
|
||||
const allowSet = new Set(allowlist);
|
||||
const hits: ScanHit[] = [];
|
||||
|
||||
const seen = new Set<string>();
|
||||
for (const root of roots) {
|
||||
const absoluteRoot = resolve(WORKSPACE_ROOT, root);
|
||||
let stat;
|
||||
try {
|
||||
stat = statSync(absoluteRoot);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!stat.isDirectory()) continue;
|
||||
|
||||
for (const absoluteFile of walk(absoluteRoot, ignoreSet)) {
|
||||
if (seen.has(absoluteFile)) continue;
|
||||
seen.add(absoluteFile);
|
||||
|
||||
const relativePath = toForwardSlashes(
|
||||
relative(WORKSPACE_ROOT, absoluteFile),
|
||||
);
|
||||
if (allowSet.has(relativePath)) continue;
|
||||
|
||||
const contents = readFileSync(absoluteFile, 'utf8');
|
||||
const lines = contents.split('\n');
|
||||
|
||||
// Reuse the regex per file. Without the `g` flag, `.exec` ignores
|
||||
// lastIndex, so recompiling per-line was wasted allocation.
|
||||
const lineRegex = pattern.flags.includes('g')
|
||||
? new RegExp(pattern.source, pattern.flags.replace('g', ''))
|
||||
: pattern;
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const lineText = lines[index];
|
||||
const match = lineRegex.exec(lineText);
|
||||
if (match) {
|
||||
hits.push({
|
||||
file: relativePath,
|
||||
line: index + 1,
|
||||
text: lineText.trim(),
|
||||
match: match[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
/** Format hits as a multi-line failure message: ` file:line — text`. */
|
||||
export function formatHits(hits: ScanHit[], header: string): string {
|
||||
if (hits.length === 0) return header;
|
||||
const lines = hits
|
||||
.slice(0, 50)
|
||||
.map(hit => ` ${hit.file}:${hit.line} — ${hit.text}`);
|
||||
const overflow =
|
||||
hits.length > 50 ? `\n ... and ${hits.length - 50} more` : '';
|
||||
return `${header}\n${lines.join('\n')}${overflow}`;
|
||||
}
|
||||
|
||||
/** Throw with a formatted message if `hits` is non-empty. */
|
||||
export function expectNoHits(hits: ScanHit[], header: string): void {
|
||||
if (hits.length > 0) {
|
||||
throw new Error(formatHits(hits, header));
|
||||
}
|
||||
}
|
||||
53
superset-frontend/spec/helpers/withApplicationRoot.ts
Normal file
53
superset-frontend/spec/helpers/withApplicationRoot.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Run `callback` with `getBootstrapData().common.application_root` set to
|
||||
* `applicationRoot`. Resets modules so any imports inside the callback see
|
||||
* the configured value, then restores the prior DOM and module cache on exit.
|
||||
* Pass `''` to simulate the default root-of-domain deployment.
|
||||
*/
|
||||
export async function withApplicationRoot<T>(
|
||||
applicationRoot: string,
|
||||
callback: () => Promise<T> | T,
|
||||
): Promise<T> {
|
||||
const previousBody = document.body.innerHTML;
|
||||
|
||||
try {
|
||||
const bootstrapData = { common: { application_root: applicationRoot } };
|
||||
document.body.innerHTML = `<div id="app" data-bootstrap='${JSON.stringify(bootstrapData)}'></div>`;
|
||||
jest.resetModules();
|
||||
await import('src/utils/getBootstrapData');
|
||||
return await callback();
|
||||
} finally {
|
||||
document.body.innerHTML = previousBody;
|
||||
jest.resetModules();
|
||||
}
|
||||
}
|
||||
|
||||
/** Run `body` once per scenario, each under a different application root. */
|
||||
export async function applicationRootScenarios<S extends { root: string }>(
|
||||
scenarios: S[],
|
||||
body: (scenario: S) => Promise<void> | void,
|
||||
): Promise<void> {
|
||||
for (const scenario of scenarios) {
|
||||
// eslint-disable-next-line no-await-in-loop -- intentional: scenarios share document state.
|
||||
await withApplicationRoot(scenario.root, () => body(scenario));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@
|
||||
import { Global } from '@emotion/react';
|
||||
import { css } from '@apache-superset/core/theme';
|
||||
|
||||
// Class applied to the SQL Lab tab bar's overflow ("...") dropdown so its menu
|
||||
// items truncate long tab names. The dropdown is portaled to the body, outside
|
||||
// the tabs' emotion scope, so it is styled here via a global rule.
|
||||
export const SQLLAB_TAB_OVERFLOW_POPUP_CLASS = 'sqllab-tab-overflow-popup';
|
||||
|
||||
export const SqlLabGlobalStyles = () => (
|
||||
<Global
|
||||
styles={theme => css`
|
||||
@@ -30,6 +35,31 @@ export const SqlLabGlobalStyles = () => (
|
||||
); // Set a min height so the gutter is always visible when resizing
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// The tab label is a flex node (icon menu + title + status icon). antd's
|
||||
// overflow dropdown styles each menu item for a plain-text label, so the
|
||||
// nested flex defeats its ellipsis and very long names render blank. Cap
|
||||
// the item width and let the title truncate inside it.
|
||||
.${SQLLAB_TAB_OVERFLOW_POPUP_CLASS} {
|
||||
.ant-tabs-dropdown-menu-item {
|
||||
max-width: ${theme.sizeUnit * 80}px;
|
||||
}
|
||||
.ant-tabs-dropdown-menu-item > span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
[data-test='sql-editor-tab-header'] {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
[data-test='sql-editor-tab-title'] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
import { fDuration, extendedDayjs } from '@superset-ui/core/utils/dates';
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
|
||||
import { makeUrl } from 'src/utils/pathUtils';
|
||||
import { openInNewTab } from 'src/utils/navigationUtils';
|
||||
import ResultSet from '../ResultSet';
|
||||
import HighlightedSql from '../HighlightedSql';
|
||||
import { StaticPosition, StyledTooltip, ModalResultSetWrapper } from './styles';
|
||||
@@ -80,8 +80,7 @@ interface QueryTableProps {
|
||||
}
|
||||
|
||||
const openQuery = (id: number) => {
|
||||
const url = makeUrl(`/sqllab?queryId=${id}`);
|
||||
window.open(url);
|
||||
openInNewTab(`/sqllab?queryId=${id}`);
|
||||
};
|
||||
|
||||
const QueryTable = ({
|
||||
|
||||
@@ -53,7 +53,28 @@ jest.mock('@superset-ui/core', () => ({
|
||||
isFeatureEnabled: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
// Mock openInNewTab so the Create-chart "new window" branch can be asserted
|
||||
// without spawning a real window. The rest of navigationUtils stays real so
|
||||
// existing CSV-download tests keep using the genuine `redirect`/`makeUrl`.
|
||||
jest.mock('src/utils/navigationUtils', () => ({
|
||||
...jest.requireActual('src/utils/navigationUtils'),
|
||||
openInNewTab: jest.fn(),
|
||||
}));
|
||||
// eslint-disable-next-line import/order, import/first
|
||||
import { openInNewTab } from 'src/utils/navigationUtils';
|
||||
|
||||
// Stub postFormData so the Create-chart click resolves quickly; this lets
|
||||
// the test focus on the URL composition that happens after the resolve.
|
||||
jest.mock('src/explore/exploreUtils/formData', () => ({
|
||||
...jest.requireActual('src/explore/exploreUtils/formData'),
|
||||
postFormData: jest.fn(),
|
||||
}));
|
||||
// eslint-disable-next-line import/order, import/first
|
||||
import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
|
||||
const mockIsFeatureEnabled = isFeatureEnabled as jest.Mock;
|
||||
const mockOpenInNewTab = openInNewTab as jest.Mock;
|
||||
const mockPostFormData = postFormData as jest.Mock;
|
||||
|
||||
jest.mock('src/components/ErrorMessage', () => ({
|
||||
ErrorMessageWithStackTrace: () => <div data-test="error-message">Error</div>,
|
||||
@@ -160,6 +181,9 @@ describe('ResultSet', () => {
|
||||
beforeEach(() => {
|
||||
applicationRootMock.mockReturnValue('');
|
||||
mockStartExport.mockClear();
|
||||
mockOpenInNewTab.mockClear();
|
||||
mockPostFormData.mockReset();
|
||||
mockPostFormData.mockResolvedValue('test-form-data-key');
|
||||
});
|
||||
|
||||
// Add cleanup after each test
|
||||
@@ -1009,4 +1033,148 @@ describe('ResultSet', () => {
|
||||
screen.getByRole('button', { name: 'Results Action' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Create chart in new window opens single-prefixed explore URL under subdirectory deployment', async () => {
|
||||
// When the user metaKey-clicks "Create chart", the SQL-Lab result handoff
|
||||
// composes an explore URL via mountExploreUrl(..., includeAppRoot=true).
|
||||
// Under SUPERSET_APP_ROOT=/superset, the resulting URL must contain the
|
||||
// prefix exactly once. A doubled prefix (/superset/superset/explore/…)
|
||||
// produces a blank Explore page.
|
||||
const appRoot = '/superset';
|
||||
applicationRootMock.mockReturnValue(appRoot);
|
||||
|
||||
const queryWithId = {
|
||||
...queries[0],
|
||||
results: {
|
||||
...queries[0].results,
|
||||
query_id: 42,
|
||||
},
|
||||
};
|
||||
|
||||
const { getByTestId } = setup(
|
||||
{
|
||||
...mockedProps,
|
||||
queryId: queryWithId.id,
|
||||
database: { allows_subquery: true, allows_virtual_table_explore: true },
|
||||
},
|
||||
mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[queryWithId.id]: queryWithId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const exploreButton = await waitFor(() =>
|
||||
getByTestId('explore-results-button'),
|
||||
);
|
||||
fireEvent.click(exploreButton, { metaKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOpenInNewTab).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
const url = mockOpenInNewTab.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/^\/superset\/explore\/\?.*form_data_key=/);
|
||||
expect(url).not.toMatch(/\/superset\/superset\//);
|
||||
});
|
||||
|
||||
test('Create chart in same window pushes router-relative explore URL under subdirectory deployment', async () => {
|
||||
// Same-tab click (no metaKey) goes through history.push under the SPA
|
||||
// basename Router, so mountExploreUrl is called with includeAppRoot=false.
|
||||
// The composed URL must NOT carry an app-root prefix — the router applies
|
||||
// it once via <Router basename={applicationRoot()}>. A premature prefix
|
||||
// here would compound with the basename and yield /superset/superset/…
|
||||
const appRoot = '/superset';
|
||||
applicationRootMock.mockReturnValue(appRoot);
|
||||
|
||||
const queryWithId = {
|
||||
...queries[0],
|
||||
results: {
|
||||
...queries[0].results,
|
||||
query_id: 99,
|
||||
},
|
||||
};
|
||||
|
||||
const store = mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[queryWithId.id]: queryWithId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<ResultSet
|
||||
{...mockedProps}
|
||||
queryId={queryWithId.id}
|
||||
database={{
|
||||
allows_subquery: true,
|
||||
allows_virtual_table_explore: true,
|
||||
}}
|
||||
/>,
|
||||
{ useRedux: true, store, useRouter: true },
|
||||
);
|
||||
|
||||
const exploreButton = await waitFor(() =>
|
||||
getByTestId('explore-results-button'),
|
||||
);
|
||||
fireEvent.click(exploreButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPostFormData).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mockOpenInNewTab).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('filters the results table when typing in the search box', async () => {
|
||||
const filterableQuery = {
|
||||
...queries[0],
|
||||
id: 'filterableQueryId',
|
||||
cached: false,
|
||||
results: {
|
||||
columns: [{ is_dttm: false, column_name: 'fruit', type: 'STRING' }],
|
||||
selected_columns: [
|
||||
{ is_dttm: false, column_name: 'fruit', type: 'STRING' },
|
||||
],
|
||||
data: [{ fruit: 'apple' }, { fruit: 'banana' }],
|
||||
},
|
||||
};
|
||||
const { getByTestId, getByPlaceholderText, queryByText } = setup(
|
||||
{ ...mockedProps, cache: false, queryId: filterableQuery.id },
|
||||
mockStore({
|
||||
...initialState,
|
||||
user,
|
||||
sqlLab: {
|
||||
...initialState.sqlLab,
|
||||
queries: {
|
||||
[filterableQuery.id]: filterableQuery,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('table-container')).toBeInTheDocument();
|
||||
});
|
||||
// Both rows are visible before filtering
|
||||
expect(queryByText('apple')).toBeInTheDocument();
|
||||
expect(queryByText('banana')).toBeInTheDocument();
|
||||
|
||||
// Typing in the search box filters the rows (case-insensitive substring)
|
||||
fireEvent.change(getByPlaceholderText('Filter results'), {
|
||||
target: { value: 'APP' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryByText('banana')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(queryByText('apple')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@@ -88,7 +87,7 @@ import { usePermissions } from 'src/hooks/usePermissions';
|
||||
import { StreamingExportModal } from 'src/components/StreamingExportModal';
|
||||
import { useStreamingExport } from 'src/components/StreamingExportModal/useStreamingExport';
|
||||
import { useConfirmModal } from 'src/hooks/useConfirmModal';
|
||||
import { makeUrl } from 'src/utils/pathUtils';
|
||||
import { makeUrl, openInNewTab, redirect } from 'src/utils/navigationUtils';
|
||||
import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
|
||||
import ExploreResultsButton from '../ExploreResultsButton';
|
||||
import HighlightedSql from '../HighlightedSql';
|
||||
@@ -312,7 +311,9 @@ const ResultSet = ({
|
||||
includeAppRoot,
|
||||
);
|
||||
if (openInNewWindow) {
|
||||
window.open(url, '_blank', 'noreferrer');
|
||||
// `url` is from `mountExploreUrl(..., includeAppRoot=true)`; the
|
||||
// helper re-applies `ensureAppRoot` idempotently.
|
||||
openInNewTab(url);
|
||||
} else {
|
||||
history.push(url);
|
||||
}
|
||||
@@ -379,7 +380,13 @@ const ResultSet = ({
|
||||
{ rows: rowsCount.toLocaleString() },
|
||||
),
|
||||
onConfirm: () => {
|
||||
window.location.href = sanitizeUrl(getExportCsvUrl(query.id));
|
||||
// `getExportCsvUrl` already runs the path through `makeUrl`;
|
||||
// `redirect` re-applies `ensureAppRoot` idempotently and routes
|
||||
// the sink through navigationUtils' barriers (scheme allowlist,
|
||||
// userinfo rejection, backslash rejection), which is a
|
||||
// strict superset of what `sanitizeUrl` from master PR #40546
|
||||
// provides.
|
||||
redirect(getExportCsvUrl(query.id));
|
||||
},
|
||||
confirmText: t('OK'),
|
||||
cancelText: t('Close'),
|
||||
|
||||
@@ -127,6 +127,28 @@ describe('SaveDatasetModal', () => {
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('disables the save button when the dataset name is empty or whitespace-only', async () => {
|
||||
renderModal();
|
||||
|
||||
const nameInput = screen.getByRole('textbox');
|
||||
const saveBtn = screen.getByRole('button', { name: /save/i });
|
||||
|
||||
// Default name is present, so save starts enabled
|
||||
expect(saveBtn).toBeEnabled();
|
||||
|
||||
// Clearing the name disables save
|
||||
await userEvent.clear(nameInput);
|
||||
await waitFor(() => expect(saveBtn).toBeDisabled());
|
||||
|
||||
// Whitespace-only name keeps save disabled
|
||||
await userEvent.type(nameInput, ' ');
|
||||
await waitFor(() => expect(saveBtn).toBeDisabled());
|
||||
|
||||
// A non-empty name re-enables save
|
||||
await userEvent.type(nameInput, 'My dataset');
|
||||
await waitFor(() => expect(saveBtn).toBeEnabled());
|
||||
});
|
||||
|
||||
test('renders an overwrite button when "Overwrite existing" is selected', () => {
|
||||
renderModal();
|
||||
|
||||
@@ -245,6 +267,22 @@ describe('SaveDatasetModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('trims surrounding whitespace from the dataset name on save', async () => {
|
||||
renderModal();
|
||||
|
||||
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
|
||||
fireEvent.change(inputFieldText, { target: { value: ' my dataset ' } });
|
||||
|
||||
const saveConfirmationBtn = screen.getByRole('button', {
|
||||
name: /save/i,
|
||||
});
|
||||
userEvent.click(saveConfirmationBtn);
|
||||
|
||||
expect(createDatasource).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ datasourceName: 'my dataset' }),
|
||||
);
|
||||
});
|
||||
|
||||
test('sends the catalog when creating the dataset', async () => {
|
||||
renderModal({
|
||||
datasource: { ...mockedProps.datasource, catalog: 'public' },
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { sanitizeUrl } from '@braintree/sanitize-url';
|
||||
import { useCallback, useState, FormEvent } from 'react';
|
||||
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
|
||||
import { Radio, RadioChangeEvent } from '@superset-ui/core/components/Radio';
|
||||
@@ -58,6 +57,7 @@ import { postFormData } from 'src/explore/exploreUtils/formData';
|
||||
import { URL_PARAMS } from 'src/constants';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { clearDatasetCache } from 'src/utils/cachedSupersetGet';
|
||||
import { openInNewTab, redirect } from 'src/utils/navigationUtils';
|
||||
|
||||
interface QueryDatabase {
|
||||
id?: number;
|
||||
@@ -244,10 +244,16 @@ export const SaveDatasetModal = ({
|
||||
useState(false);
|
||||
|
||||
const createWindow = (url: string) => {
|
||||
// `url` is from `mountExploreUrl(..., includeAppRoot=true)`; the
|
||||
// navigationUtils helpers re-apply `ensureAppRoot` idempotently.
|
||||
if (openWindow) {
|
||||
window.open(sanitizeUrl(url), '_blank', 'noreferrer');
|
||||
// `openInNewTab` / `redirect` route the sink through navigationUtils'
|
||||
// barriers (scheme allowlist, userinfo rejection, backslash
|
||||
// rejection) — strictly stronger than master PR #40546's `sanitizeUrl`
|
||||
// wrap, which only rejects `javascript:` / `data:` / `vbscript:`.
|
||||
openInNewTab(url);
|
||||
} else {
|
||||
window.location.href = sanitizeUrl(url);
|
||||
redirect(url);
|
||||
}
|
||||
};
|
||||
const formDataWithDefaults = {
|
||||
@@ -365,7 +371,7 @@ export const SaveDatasetModal = ({
|
||||
catalog: datasource?.catalog ?? null,
|
||||
schema: datasource?.schema ?? '',
|
||||
templateParams,
|
||||
datasourceName: datasetName,
|
||||
datasourceName: datasetName.trim(),
|
||||
}),
|
||||
)
|
||||
.then((data: { id: number }) => {
|
||||
@@ -411,7 +417,7 @@ export const SaveDatasetModal = ({
|
||||
|
||||
const disableSaveAndExploreBtn =
|
||||
(newOrOverwrite === DatasetRadioState.SaveNew &&
|
||||
datasetName.length === 0) ||
|
||||
datasetName.trim().length === 0) ||
|
||||
(newOrOverwrite === DatasetRadioState.OverwriteDataset &&
|
||||
isEmpty(selectedDatasetToOverwrite));
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user