mirror of
https://github.com/apache/superset.git
synced 2026-07-07 23:35:36 +00:00
Compare commits
136 Commits
fix/export
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1a76797d9 | ||
|
|
77b98a5d67 | ||
|
|
dd0277e1e3 | ||
|
|
9b508ebe0d | ||
|
|
53c3b1ceb1 | ||
|
|
0dcb09b163 | ||
|
|
3a514dc8eb | ||
|
|
aa330422f1 | ||
|
|
7bdde4a36a | ||
|
|
5e16c4eaaf | ||
|
|
ffd0982942 | ||
|
|
4dc45aa958 | ||
|
|
4167a21f7d | ||
|
|
5e384f54d3 | ||
|
|
ec4e5deb01 | ||
|
|
de5a233ccf | ||
|
|
c846da8dfb | ||
|
|
1c3b070d34 | ||
|
|
a8a1b220fd | ||
|
|
e52240b9a6 | ||
|
|
87fa7e5e64 | ||
|
|
2b74e140f3 | ||
|
|
847bdad960 | ||
|
|
4b78758fd2 | ||
|
|
101c1e8827 | ||
|
|
85eaf15c5e | ||
|
|
2aa43f6f0f | ||
|
|
ee524b3471 | ||
|
|
9e50d9eca6 | ||
|
|
033b7bc385 | ||
|
|
c0b0a2fdaf | ||
|
|
44c1ad5c71 | ||
|
|
fb496e158a | ||
|
|
69ce27f173 | ||
|
|
51e708da6b | ||
|
|
21b4185819 | ||
|
|
689b4f623e | ||
|
|
ad8e6d4140 | ||
|
|
83d93b8b42 | ||
|
|
06f421ed4a | ||
|
|
dc671ae44a | ||
|
|
c49d01223c | ||
|
|
69f052985f | ||
|
|
f4cc871f8c | ||
|
|
2f11240339 | ||
|
|
a7a051777c | ||
|
|
b4218136be | ||
|
|
fd8d7009a5 | ||
|
|
beff76cb4d | ||
|
|
d6b31376a5 | ||
|
|
1575b83f96 | ||
|
|
83826839a0 | ||
|
|
a8ab5f973d | ||
|
|
7cdfe3ffe7 | ||
|
|
9a24d42d65 | ||
|
|
3303d4e09e | ||
|
|
8229c0183d | ||
|
|
ea40b87e2f | ||
|
|
dc739e4fb9 | ||
|
|
a687e87b2c | ||
|
|
d91c5cf09b | ||
|
|
3ac19470ec | ||
|
|
687fafd424 | ||
|
|
8fcc0f8b48 | ||
|
|
e08c2c12da | ||
|
|
1e50316bcc | ||
|
|
3e152d9bb7 | ||
|
|
004c401c97 | ||
|
|
a9aabdaedf | ||
|
|
210389478b | ||
|
|
43f2816240 | ||
|
|
c3fe0a40eb | ||
|
|
71ac6c64e2 | ||
|
|
81a437826f | ||
|
|
fd1f313b30 | ||
|
|
b23cef136e | ||
|
|
681275077b | ||
|
|
d7ad7fbb49 | ||
|
|
59d35d16ce | ||
|
|
4e2160079a | ||
|
|
ceeba01305 | ||
|
|
6d7344750f | ||
|
|
7d90684f93 | ||
|
|
e7b825e26b | ||
|
|
b6f8267ed2 | ||
|
|
d443dd17b9 | ||
|
|
2702113d99 | ||
|
|
0b14f1c226 | ||
|
|
114c258145 | ||
|
|
9f8ff1e87f | ||
|
|
2b30605e3c | ||
|
|
d93098f853 | ||
|
|
eaf6daa7eb | ||
|
|
0e6c5838e4 | ||
|
|
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 |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -10,11 +10,7 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
ignore:
|
||||
- dependency-name: "react-error-boundary"
|
||||
- dependency-name: "@rjsf/*"
|
||||
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
|
||||
- dependency-name: "remark-gfm"
|
||||
- dependency-name: "react-markdown"
|
||||
# TODO: remove below entries until React >= 19.0.0
|
||||
- dependency-name: "react-icons"
|
||||
# JSDOM v30 doesn't play well with Jest v30
|
||||
@@ -78,8 +74,6 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/docs/"
|
||||
ignore:
|
||||
- dependency-name: "react-error-boundary"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -376,11 +370,6 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-core/"
|
||||
ignore:
|
||||
# not until React >= 18.0.0
|
||||
- dependency-name: "react-markdown"
|
||||
- dependency-name: "remark-gfm"
|
||||
- dependency-name: "react-error-boundary"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
|
||||
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'
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
37
UPDATING.md
37
UPDATING.md
@@ -42,6 +42,8 @@ assists people when migrating to a new version.
|
||||
|
||||
- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` *and* who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host.
|
||||
|
||||
- [41651](https://github.com/apache/superset/pull/41651): **New do-not-translate standard for translation catalogs.** Strings that must stay identical to the source — icon names (e.g. `bolt`), enum/option values (`step-after`), SQL keywords, API field names (`error_message`), code constants, and example placeholders — are now marked with a `#. do-not-translate` extracted comment. The list lives in the `superset/translations/do-not-translate.txt` registry; `scripts/translations/apply_do_not_translate.py` stamps the marker onto `messages.pot` during `babel_update.sh`, and `pybabel update` propagates it to every `.po`, so the status is consistent across all languages. The AI backfill (`backfill_po.py`) and translators leave these entries untranslated (source fallback). The legacy per-catalog convention (a `# Не переводить` translator comment in the `ru` catalog) is still honored for back-compat but is superseded by this standard; contributors adding new machine-read strings should add the msgid to the registry rather than annotating individual catalogs.
|
||||
|
||||
### 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:
|
||||
@@ -79,6 +81,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.
|
||||
@@ -289,6 +304,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 charts
|
||||
|
||||
**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/chart/<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 charts are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live charts in all lists, lookups, and relationship loads (including dashboards that contained them). The `POST /<uuid>/restore` endpoint and the `chart_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/chart/<id>` no longer hard-deletes the chart (the bulk-delete endpoint behaves the same way). The row is marked with a `deleted_at` timestamp and hidden from all list, detail, and lookup endpoints. Charts in this state are excluded from default queries and from relationship loads (e.g. `dashboard.slices`).
|
||||
|
||||
**Operational notes:** a report schedule whose target chart is soft-deleted now fails its runs with an explicit error ("The chart this report targets was deleted...") until the chart is restored or the report re-pointed — chart deletion is blocked while a report references the chart, but a validate/commit race or a flag toggle can still produce this state. Dashboards **preserve** their membership rows for soft-deleted charts: saving a dashboard does not sever a trashed member, and restoring the chart re-attaches it to its dashboards.
|
||||
|
||||
**New endpoint** — `POST /api/v1/chart/<uuid>/restore` clears `deleted_at` and returns the chart to active state. Requires `can_write on Chart` and ownership of the row (or admin). Soft-deleted charts can also be surfaced in the list endpoint via the new `chart_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 charts they own — the same audience that can restore them.
|
||||
|
||||
**Permissions migration:** existing role grants of `can_write on Chart` 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_slices_deleted_at`) to the `slices` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `slices` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking).
|
||||
|
||||
**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
|
||||
|
||||
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-owners get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
{/*
|
||||
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.
|
||||
*/}
|
||||
|
||||
---
|
||||
title: AWS IAM Authentication
|
||||
sidebar_label: AWS IAM Authentication
|
||||
version: 1
|
||||
sidebar_position: 15
|
||||
---
|
||||
|
||||
|
||||
@@ -332,15 +332,39 @@ 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.
|
||||
|
||||
The script never touches entries that must stay literal — icon names, enum
|
||||
values, SQL keywords, API field names, and example placeholders. These are
|
||||
registered in `superset/translations/do-not-translate.txt`;
|
||||
`scripts/translations/apply_do_not_translate.py` stamps them in `messages.pot`
|
||||
with a `#. do-not-translate` extracted comment (run automatically
|
||||
from `babel_update.sh`), which `pybabel update` then propagates to every
|
||||
catalog. To mark a new string do-not-translate, add its msgid to the registry.
|
||||
The backfill also honors that marker and any legacy do-not-translate translator
|
||||
comment (e.g. the `ru` catalog's `# Не переводить`), leaving such entries
|
||||
untranslated so they fall back to the source token.
|
||||
|
||||
#### 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,26 +58,15 @@
|
||||
"@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",
|
||||
"antd": "^6.5.0",
|
||||
"baseline-browser-mapping": "^2.10.40",
|
||||
"caniuse-lite": "^1.0.30001799",
|
||||
"docusaurus-plugin-openapi-docs": "^5.0.2",
|
||||
"docusaurus-theme-openapi-docs": "^5.0.2",
|
||||
"js-yaml": "^5.1.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.0",
|
||||
"js-yaml": "^5.2.0",
|
||||
"json-bigint": "^1.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
@@ -88,7 +77,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",
|
||||
@@ -107,10 +96,10 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.7.0",
|
||||
"prettier": "^3.8.4",
|
||||
"prettier": "^3.9.1",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.62.0",
|
||||
"webpack": "^5.107.2"
|
||||
"webpack": "^5.108.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -128,14 +117,16 @@
|
||||
"react-redux": "^9.2.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"swagger-client": "3.37.3",
|
||||
"lodash": "4.18.1",
|
||||
"lodash-es": "4.18.1",
|
||||
"yaml": "1.10.3",
|
||||
"uuid": "11.1.1",
|
||||
"serialize-javascript": "7.0.5",
|
||||
"d3-color": "3.1.0",
|
||||
"ws": "^8.21.0"
|
||||
"ws": "^8.21.0",
|
||||
"@docusaurus/core/@docusaurus/utils/gray-matter/js-yaml": "^3.15.0",
|
||||
"@docusaurus/core/**/js-yaml": "^4.3.0",
|
||||
"docusaurus-plugin-openapi-docs/**/js-yaml": "^4.3.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ export interface DatabaseInfo {
|
||||
expressions_in_orderby?: boolean;
|
||||
|
||||
// Platform features
|
||||
limit_method?: string;
|
||||
limit_method?: number;
|
||||
limit_clause?: boolean;
|
||||
max_column_name?: number;
|
||||
supports_file_upload?: boolean;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import Layout from '@theme/Layout';
|
||||
import { Avatar, Card, Col, Collapse, Row, Typography } from 'antd';
|
||||
import { load } from 'js-yaml';
|
||||
import BlurredSection from '../components/BlurredSection';
|
||||
import SectionHeader from '../components/SectionHeader';
|
||||
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
|
||||
@@ -36,7 +37,7 @@ interface DataSetType {
|
||||
categories: Record<string, Organization[]>;
|
||||
}
|
||||
|
||||
const typedDataSet = DataSet as DataSetType;
|
||||
const typedDataSet = load(DataSet) as DataSetType;
|
||||
|
||||
const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
|
||||
if (!contributors?.length) return null;
|
||||
|
||||
@@ -22,6 +22,7 @@ import Link from '@docusaurus/Link';
|
||||
import { Card, Carousel, Flex } from 'antd';
|
||||
import styled from '@emotion/styled';
|
||||
import GitHubButton from 'react-github-btn';
|
||||
import { load } from 'js-yaml';
|
||||
import { mq } from '../utils';
|
||||
import SectionHeader from '../components/SectionHeader';
|
||||
import databaseData from '../data/databases.json';
|
||||
@@ -60,7 +61,7 @@ interface DataSetType {
|
||||
categories: Record<string, Organization[]>;
|
||||
}
|
||||
|
||||
const typedDataSet = DataSet as DataSetType;
|
||||
const typedDataSet = load(DataSet) as DataSetType;
|
||||
|
||||
// Extract all organizations with logos for the carousel
|
||||
const companiesWithLogos = Object.values(typedDataSet.categories)
|
||||
|
||||
4
docs/src/types/yaml.d.ts
vendored
4
docs/src/types/yaml.d.ts
vendored
@@ -18,11 +18,11 @@
|
||||
*/
|
||||
|
||||
declare module '*.yaml' {
|
||||
const content: unknown;
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.yml' {
|
||||
const content: unknown;
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
@@ -61,10 +61,10 @@ export default function webpackExtendPlugin(): Plugin<void> {
|
||||
);
|
||||
});
|
||||
|
||||
// Add YAML loader rule directly to existing rules
|
||||
// Load YAML file as raw text string
|
||||
config.module?.rules?.push({
|
||||
test: /\.ya?ml$/,
|
||||
use: 'js-yaml-loader',
|
||||
type: 'asset/source',
|
||||
});
|
||||
|
||||
// Add swc-loader rule for superset-frontend files
|
||||
@@ -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',
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
4
docs/static/feature-flags.json
vendored
4
docs/static/feature-flags.json
vendored
@@ -122,9 +122,9 @@
|
||||
},
|
||||
{
|
||||
"name": "ALERT_REPORT_SLACK_V2",
|
||||
"default": false,
|
||||
"default": true,
|
||||
"lifecycle": "testing",
|
||||
"description": "Enables Slack V2 integration for Alerts and Reports"
|
||||
"description": "Enables Slack V2 integration for Alerts and Reports. Defaults to True; the legacy Slack v1 path is deprecated and will be removed in the next major release. Operators must grant the Slack bot both the `channels:read` and `groups:read` scopes so existing v1 recipients can be auto-upgraded on their next send. Without those scopes, file uploads fail (Slack retired the `files.upload` endpoint in 2025) and only text-only `chat_postMessage` sends will continue to work via the legacy path."
|
||||
},
|
||||
{
|
||||
"name": "ALERT_REPORT_WEBHOOK",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"../superset-frontend/**/*",
|
||||
"src/webpack.extend.ts",
|
||||
"src/shims/**"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
8954
docs/yarn.lock
8954
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
|
||||
@@ -54,7 +54,7 @@ dependencies = [
|
||||
"cryptography>=48.0.0, <49.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <4.0.0",
|
||||
"flask-appbuilder>=5.2.1, <6.0.0",
|
||||
"flask-appbuilder>=5.2.2, <6.0.0",
|
||||
"flask-caching>=2.1.0, <3",
|
||||
"flask-compress>=1.13, <2.0",
|
||||
"flask-talisman>=1.0.0, <2.0",
|
||||
@@ -64,7 +64,7 @@ dependencies = [
|
||||
"flask-wtf>=1.3.0, <2.0",
|
||||
"geopy",
|
||||
"greenlet<=3.5.1, >=3.5.1",
|
||||
"gunicorn>=25.3.0, <26; sys_platform != 'win32'",
|
||||
"gunicorn>=26.0.0, <27; sys_platform != 'win32'",
|
||||
"hashids>=1.3.1, <2",
|
||||
# holidays>=0.45 required for security fix
|
||||
"holidays>=0.45, <1",
|
||||
@@ -101,7 +101,7 @@ dependencies = [
|
||||
"pyyaml>=6.0.3, <7.0.0",
|
||||
"PyJWT>=2.4.0, <3.0",
|
||||
"redis>=5.0.0, <6.0",
|
||||
"rison>=2.0.0, <3.0",
|
||||
"rison>=2.0.1, <3.0",
|
||||
"selenium>=4.45.0, <5.0",
|
||||
"shillelagh[gsheetsapi]>=1.4.4, <2.0",
|
||||
"sshtunnel>=0.4.0, <0.5",
|
||||
@@ -137,10 +137,10 @@ d1 = [
|
||||
"sqlalchemy-d1>=0.1.0",
|
||||
"dbapi-d1>=0.1.0",
|
||||
]
|
||||
databend = ["databend-sqlalchemy>=0.3.2, <1.0"]
|
||||
databend = ["databend-sqlalchemy>=0.5.5, <1.0"]
|
||||
databricks = [
|
||||
"databricks-sql-connector==4.2.6",
|
||||
"databricks-sqlalchemy==1.0.5",
|
||||
"databricks-sqlalchemy==2.0.9",
|
||||
]
|
||||
db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
|
||||
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
|
||||
@@ -174,11 +174,11 @@ hive = [
|
||||
]
|
||||
impala = ["impyla>0.16.2, <0.23"]
|
||||
kusto = ["sqlalchemy-kusto>=3.1.2, <4"]
|
||||
kylin = ["kylinpy>=2.8.1, <2.9"]
|
||||
kylin = ["kylinpy>=2.8.4, <2.9"]
|
||||
mssql = ["pymssql>=2.3.13, <3"]
|
||||
# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver
|
||||
motherduck = ["apache-superset[duckdb]"]
|
||||
mysql = ["mysqlclient>=2.1.0, <3"]
|
||||
mysql = ["mysqlclient>=2.2.8, <3"]
|
||||
ocient = [
|
||||
"sqlalchemy-ocient>=1.0.0",
|
||||
"pyocient>=1.0.15, <4",
|
||||
@@ -186,7 +186,7 @@ ocient = [
|
||||
"geojson",
|
||||
]
|
||||
oracle = ["oracledb>=2.0.0, <5"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
|
||||
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
|
||||
pinot = ["pinotdb>=5.0.0, <10.0.0"]
|
||||
playwright = ["playwright>=1.60.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.12"]
|
||||
@@ -196,7 +196,7 @@ prophet = ["prophet>=1.1.6, <2"]
|
||||
redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
|
||||
risingwave = ["sqlalchemy-risingwave"]
|
||||
shillelagh = ["shillelagh[all]>=1.4.4, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.1.1, <2"]
|
||||
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
|
||||
snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
|
||||
sqlite = ["syntaqlite>=0.1.0,<0.5.0"]
|
||||
spark = [
|
||||
@@ -216,7 +216,7 @@ netezza = ["nzalchemy>=11.0.2, < 11.2"]
|
||||
starrocks = ["starrocks>=1.3.3, <2"]
|
||||
doris = ["pydoris>=1.0.0, <2.0.0"]
|
||||
oceanbase = ["oceanbase_py>=0.0.1.2"]
|
||||
ydb = ["ydb-sqlalchemy>=0.1.22", "ydb-sqlglot-plugin>=0.2.5"]
|
||||
ydb = ["ydb-sqlalchemy>=0.1.22", "ydb-sqlglot-plugin>=0.2.8"]
|
||||
development = [
|
||||
# no bounds for apache-superset-extensions-cli until a stable version
|
||||
"apache-superset-extensions-cli",
|
||||
|
||||
@@ -122,7 +122,7 @@ flask==2.3.3
|
||||
# flask-session
|
||||
# flask-sqlalchemy
|
||||
# flask-wtf
|
||||
flask-appbuilder==5.2.1
|
||||
flask-appbuilder==5.2.2
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
# apache-superset-core
|
||||
@@ -169,7 +169,7 @@ greenlet==3.5.1
|
||||
# apache-superset (pyproject.toml)
|
||||
# shillelagh
|
||||
# sqlalchemy
|
||||
gunicorn==25.3.0
|
||||
gunicorn==26.0.0
|
||||
# via apache-superset (pyproject.toml)
|
||||
h11==0.16.0
|
||||
# via wsproto
|
||||
@@ -369,7 +369,7 @@ rfc3339-validator==0.1.4
|
||||
# via openapi-schema-validator
|
||||
rich==13.9.4
|
||||
# via flask-limiter
|
||||
rison==2.0.0
|
||||
rison==2.0.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
rpds-py==0.25.0
|
||||
# via
|
||||
|
||||
@@ -263,7 +263,7 @@ flask==2.3.3
|
||||
# flask-sqlalchemy
|
||||
# flask-testing
|
||||
# flask-wtf
|
||||
flask-appbuilder==5.2.1
|
||||
flask-appbuilder==5.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -393,7 +393,7 @@ grpcio==1.81.1
|
||||
# grpcio-status
|
||||
grpcio-status==1.60.1
|
||||
# via google-api-core
|
||||
gunicorn==25.3.0
|
||||
gunicorn==26.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -572,7 +572,7 @@ msgspec==0.19.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# flask-session
|
||||
mysqlclient==2.2.6
|
||||
mysqlclient==2.2.8
|
||||
# via apache-superset
|
||||
nh3==0.3.5
|
||||
# via
|
||||
@@ -913,7 +913,7 @@ rich==13.9.4
|
||||
# rich-rst
|
||||
rich-rst==1.3.1
|
||||
# via cyclopts
|
||||
rison==2.0.0
|
||||
rison==2.0.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
|
||||
128
scripts/translations/apply_do_not_translate.py
Normal file
128
scripts/translations/apply_do_not_translate.py
Normal file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
# 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.
|
||||
"""Stamp do-not-translate msgids in a .pot with an extracted-comment marker.
|
||||
|
||||
For every msgid listed in ``superset/translations/do-not-translate.txt`` that is
|
||||
present in the target .pot, add a ``#. do-not-translate`` extracted
|
||||
comment. gettext extracted comments (``#.``) propagate from the .pot into every
|
||||
language .po on ``pybabel update``, so the do-not-translate status stays
|
||||
consistent across all catalogs from a single registry.
|
||||
|
||||
Run from ``babel_update.sh`` after the .pot is extracted and normalized (and
|
||||
before ``pybabel update``). Idempotent: re-running makes no further changes.
|
||||
|
||||
Usage:
|
||||
python scripts/translations/apply_do_not_translate.py [POT_PATH]
|
||||
# POT_PATH defaults to superset/translations/messages.pot
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# The standardized extracted-comment marker. Kept in sync with backfill_po.py.
|
||||
MARKER: str = "do-not-translate"
|
||||
_MARKER_LINE: str = f"#. {MARKER}"
|
||||
|
||||
TRANSLATIONS_DIR: Path = (
|
||||
Path(__file__).parent.parent.parent / "superset" / "translations"
|
||||
)
|
||||
DEFAULT_POT: Path = TRANSLATIONS_DIR / "messages.pot"
|
||||
REGISTRY: Path = TRANSLATIONS_DIR / "do-not-translate.txt"
|
||||
|
||||
|
||||
def load_registry(path: Path = REGISTRY) -> set[str]:
|
||||
"""Return the set of do-not-translate msgids (skips comments/blank lines).
|
||||
|
||||
Each line is stripped before the blank/comment check, so trailing
|
||||
whitespace or an indented comment never yields a msgid that fails to match
|
||||
the .pot.
|
||||
"""
|
||||
if not path.exists():
|
||||
return set()
|
||||
entries: set[str] = set()
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line: str = raw_line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
entries.add(line)
|
||||
return entries
|
||||
|
||||
|
||||
def _escape(msgid: str) -> str:
|
||||
"""Escape a msgid the way gettext writes it on a `msgid "..."` line."""
|
||||
return msgid.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def apply_markers(pot_path: Path, registry: set[str]) -> int:
|
||||
"""Insert the marker comment above each registry msgid via text edit.
|
||||
|
||||
Text manipulation (rather than a polib round-trip) preserves the .pot's
|
||||
exact wrapping/layout, so the only change is the added marker lines.
|
||||
Idempotent. Returns the number of entries newly marked.
|
||||
"""
|
||||
lines: list[str] = pot_path.read_text(encoding="utf-8").split("\n")
|
||||
targets: set[str] = {f'msgid "{_escape(m)}"' for m in registry}
|
||||
out: list[str] = []
|
||||
changed: int = 0
|
||||
for line in lines:
|
||||
if line in targets and (not out or out[-1] != _MARKER_LINE):
|
||||
# `#.` extracted comments precede `msgid`; these registry entries are
|
||||
# bare single-line msgids, so inserting directly above is correct.
|
||||
out.append(_MARKER_LINE)
|
||||
changed += 1
|
||||
out.append(line)
|
||||
if changed:
|
||||
pot_path.write_text("\n".join(out), encoding="utf-8")
|
||||
return changed
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Stamp the marker onto the target .pot from the registry."""
|
||||
pot_path: Path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_POT
|
||||
if not pot_path.exists():
|
||||
print(f"POT file not found: {pot_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# Fail fast if the registry file is absent: babel_update.sh depends on this
|
||||
# step to stamp the .pot, and continuing would silently publish catalogs
|
||||
# without any do-not-translate markers. An existing-but-empty registry is a
|
||||
# valid state (nothing to mark), so only a missing file is an error.
|
||||
if not REGISTRY.exists():
|
||||
print(
|
||||
f"do-not-translate registry not found at {REGISTRY}; refusing to "
|
||||
"produce unmarked translation artifacts.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
registry: set[str] = load_registry()
|
||||
if not registry:
|
||||
print(
|
||||
f"do-not-translate registry {REGISTRY} is empty; nothing to mark.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
changed: int = apply_markers(pot_path, registry)
|
||||
print(
|
||||
f"do-not-translate: marked {changed} new entr(y/ies) with {MARKER} "
|
||||
f"in {pot_path.name} ({len(registry)} msgids in registry).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -55,6 +55,14 @@ msgcat --sort-by-msgid --no-wrap --no-location superset/translations/messages.po
|
||||
cat $LICENSE_TMP superset/translations/messages.pot > messages.pot.tmp \
|
||||
&& mv messages.pot.tmp superset/translations/messages.pot
|
||||
|
||||
# Stamp do-not-translate msgids (superset/translations/do-not-translate.txt) with
|
||||
# a `#. do-not-translate` extracted comment. Extracted comments
|
||||
# propagate from the .pot into every catalog on the `pybabel update` below, so
|
||||
# the do-not-translate status stays consistent across all languages.
|
||||
# Fail fast: without this guard the script would continue past a marker-stamping
|
||||
# failure and `pybabel update` would publish catalogs missing the markers.
|
||||
python scripts/translations/apply_do_not_translate.py superset/translations/messages.pot || exit 1
|
||||
|
||||
# --no-fuzzy-matching: when a *new* source string is added, Babel's fuzzy
|
||||
# matcher otherwise guesses a "close" existing translation and marks it
|
||||
# `#, fuzzy` in every language catalog. Those guesses are (a) usually wrong
|
||||
|
||||
@@ -152,6 +152,60 @@ def _is_missing(entry: polib.POEntry) -> bool:
|
||||
return not entry.msgstr
|
||||
|
||||
|
||||
# Canonical registry of msgids that must never be machine-translated: literal
|
||||
# tokens compared against source (SQL keywords, confirmation words), enum values
|
||||
# (d3 interpolation modes), icon names (e.g. "bolt" -> the ⚡ Explore control
|
||||
# icon), API field names, code constants, and example placeholders. Translating
|
||||
# them can break icon lookups, enum matching, or API contracts, or is simply
|
||||
# meaningless (proper nouns, example values). apply_do_not_translate.py stamps
|
||||
# these msgids in messages.pot with a `#. do-not-translate`
|
||||
# extracted comment that propagates to every catalog on `pybabel update`.
|
||||
DO_NOT_TRANSLATE_REGISTRY: Path = TRANSLATIONS_DIR / "do-not-translate.txt"
|
||||
|
||||
|
||||
def _load_do_not_translate(path: Path = DO_NOT_TRANSLATE_REGISTRY) -> frozenset[str]:
|
||||
"""Load the do-not-translate msgids (skips comment/blank lines).
|
||||
|
||||
Lines are stripped before the blank/comment checks, matching the parsing in
|
||||
apply_do_not_translate.py, so trailing whitespace or an indented comment
|
||||
never yields a msgid that fails to match a catalog entry.
|
||||
"""
|
||||
if not path.exists():
|
||||
return frozenset()
|
||||
return frozenset(
|
||||
stripped
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if (stripped := line.strip()) and not stripped.startswith("#")
|
||||
)
|
||||
|
||||
|
||||
DO_NOT_TRANSLATE: frozenset[str] = _load_do_not_translate()
|
||||
|
||||
# An explicit do-not-translate marker on an entry, matched in either the
|
||||
# extracted comment (`#. do-not-translate`, the standard propagated
|
||||
# from the .pot) or a translator comment (e.g. the ru catalog's legacy
|
||||
# "# Не переводить"). Honored so a human's deliberate decision is never
|
||||
# overridden even if a msgid is missing from the registry.
|
||||
_DO_NOT_TRANSLATE_COMMENT: re.Pattern[str] = re.compile(
|
||||
r"не\s+переводить|do[\s-]?not[\s-]?translate|don'?t\s+translate",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _is_do_not_translate(entry: polib.POEntry) -> bool:
|
||||
"""Return True if an entry must be left for a human (never machine-filled).
|
||||
|
||||
Either its msgid is in the do-not-translate registry, or the entry carries
|
||||
an explicit do-not-translate marker in its extracted or translator comment.
|
||||
"""
|
||||
if entry.msgid in DO_NOT_TRANSLATE:
|
||||
return True
|
||||
return any(
|
||||
comment and _DO_NOT_TRANSLATE_COMMENT.search(comment)
|
||||
for comment in (entry.comment, entry.tcomment)
|
||||
)
|
||||
|
||||
|
||||
def _context_langs(
|
||||
item: dict[str, Any], index: dict[str, Any], target_lang: str
|
||||
) -> list[str]:
|
||||
@@ -647,6 +701,15 @@ def backfill(
|
||||
missing: list[polib.POEntry] = [e for e in cat if e.msgid and _is_missing(e)]
|
||||
print(f"Found {len(missing)} untranslated entries for '{lang}'.", file=sys.stderr)
|
||||
|
||||
skipped_dnt: list[polib.POEntry] = [e for e in missing if _is_do_not_translate(e)]
|
||||
if skipped_dnt:
|
||||
missing = [e for e in missing if not _is_do_not_translate(e)]
|
||||
print(
|
||||
f"Skipping {len(skipped_dnt)} do-not-translate entries (literal "
|
||||
f"tokens / translator-marked); they are left untranslated.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if min_context > 0:
|
||||
before = len(missing)
|
||||
missing = [
|
||||
|
||||
1412
superset-embedded-sdk/package-lock.json
generated
1412
superset-embedded-sdk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,10 +37,10 @@
|
||||
"jwt-decode": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.25.6",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@babel/cli": "^7.29.7",
|
||||
"@babel/core": "^7.29.7",
|
||||
"@babel/preset-env": "^7.29.7",
|
||||
"@babel/preset-typescript": "7.29.7",
|
||||
"@types/node": "^25.4.0",
|
||||
"babel-loader": "^9.1.3",
|
||||
"tscw-config": "^1.1.2",
|
||||
|
||||
@@ -72,7 +72,10 @@ module.exports = {
|
||||
],
|
||||
coverageReporters: ['lcov', 'json-summary', 'html', 'text'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
|
||||
// @ant-design/colors and @ant-design/fast-color are allowed through because
|
||||
// @ant-design/icons >= 6.3 deep-imports the ESM build of @ant-design/colors
|
||||
// from its CJS output, so babel-jest must transform those files.
|
||||
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
4626
superset-frontend/package-lock.json
generated
4626
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -176,7 +176,7 @@
|
||||
"dom-to-pdf": "^0.3.2",
|
||||
"echarts": "^6.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fs-extra": "^11.3.6",
|
||||
"fuse.js": "^7.4.2",
|
||||
"geolib": "^3.3.14",
|
||||
"geostyler": "^18.6.0",
|
||||
@@ -192,18 +192,19 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lodash-es": "^4.18.1",
|
||||
"mapbox-gl": "^3.25.0",
|
||||
"markdown-to-jsx": "^9.8.2",
|
||||
"match-sorter": "^8.3.0",
|
||||
"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",
|
||||
"query-string": "9.4.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^18.3.0",
|
||||
"react-arborist": "^3.10.5",
|
||||
"react-arborist": "^3.12.0",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
@@ -239,8 +240,7 @@
|
||||
"use-query-params": "^2.2.2",
|
||||
"uuid": "^14.0.1",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||
"yargs": "^18.0.0",
|
||||
"lodash-es": "^4.17.21"
|
||||
"yargs": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.29.7",
|
||||
@@ -265,9 +265,9 @@
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@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",
|
||||
@@ -283,6 +283,7 @@
|
||||
"@types/jquery": "^4.0.1",
|
||||
"@types/js-levenshtein": "^1.1.3",
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/react": "^18.3.0",
|
||||
@@ -297,20 +298,19 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.61.1",
|
||||
"@typescript-eslint/parser": "^8.61.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
"babel-jest": "^30.4.1",
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-dynamic-import-node": "^2.3.3",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"baseline-browser-mapping": "^2.10.38",
|
||||
"baseline-browser-mapping": "^2.10.40",
|
||||
"cheerio": "1.2.0",
|
||||
"concurrently": "^10.0.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"css-loader": "^7.1.4",
|
||||
"css-minimizer-webpack-plugin": "^8.0.0",
|
||||
"eslint": "^10.5.0",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
@@ -342,10 +342,11 @@
|
||||
"lerna": "^9.0.4",
|
||||
"lightningcss": "^1.32.0",
|
||||
"mini-css-extract-plugin": "^2.10.2",
|
||||
"minimizer-webpack-plugin": "^5.6.1",
|
||||
"open-cli": "^9.0.0",
|
||||
"oxlint": "^1.71.0",
|
||||
"oxlint": "^1.72.0",
|
||||
"po2json": "^0.4.5",
|
||||
"prettier": "3.8.4",
|
||||
"prettier": "3.9.4",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"process": "^0.11.10",
|
||||
"react-dnd-test-backend": "^16.0.1",
|
||||
@@ -358,7 +359,6 @@
|
||||
"storybook": "10.4.6",
|
||||
"style-loader": "^4.0.0",
|
||||
"swc-loader": "^0.2.7",
|
||||
"terser-webpack-plugin": "^5.6.1",
|
||||
"ts-jest": "^29.4.11",
|
||||
"tscw-config": "^1.1.2",
|
||||
"tsx": "^4.22.4",
|
||||
@@ -366,14 +366,13 @@
|
||||
"unzipper": "^0.12.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"wait-on": "^9.0.10",
|
||||
"webpack": "^5.107.2",
|
||||
"webpack": "^5.108.3",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.5",
|
||||
"webpack-manifest-plugin": "^6.0.1",
|
||||
"webpack-sources": "^3.5.0",
|
||||
"webpack-visualizer-plugin2": "^2.0.0",
|
||||
"@types/lodash-es": "^4.17.12"
|
||||
"webpack-visualizer-plugin2": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ace-builds": "^1.41.0",
|
||||
@@ -394,7 +393,6 @@
|
||||
"http-proxy-middleware": "^2.0.10",
|
||||
"tar": "^7.5.16",
|
||||
"puppeteer": "^22.4.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"underscore": "^1.13.7",
|
||||
"jspdf": "^4.2.0",
|
||||
"nwsapi": "^2.2.13",
|
||||
@@ -429,6 +427,9 @@
|
||||
},
|
||||
"eslint-plugin-jest-dom": {
|
||||
"eslint": "$eslint"
|
||||
},
|
||||
"lerna": {
|
||||
"js-yaml": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"readme": "ERROR: No README data found!",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^10.1.0",
|
||||
"fs-extra": "^11.3.5",
|
||||
"fs-extra": "^11.3.6",
|
||||
"jest": "^30.4.2",
|
||||
"yeoman-test": "^11.6.0"
|
||||
},
|
||||
|
||||
@@ -122,6 +122,6 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash-es": "^4.17.21"
|
||||
"lodash-es": "^4.18.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +237,37 @@ 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;
|
||||
|
||||
/**
|
||||
* Results grid customization tokens.
|
||||
* Control the appearance of AG Grid-backed result tables (e.g. SQL Lab).
|
||||
*/
|
||||
resultsGridRowHeight?: number;
|
||||
resultsGridHeaderFontWeight?: number;
|
||||
resultsGridHeaderFontSize?: number;
|
||||
resultsGridBorderRadius?: number;
|
||||
resultsGridNoStriping?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -173,3 +173,82 @@ test('resetTranslation does nothing when not yet configured', () => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
// --- autoConfigureFromWindow ----------------------------------------------
|
||||
// These cover the bootstrap-injection path used to dodge the
|
||||
// module-level `const X = t(...)` race across code-split chunks
|
||||
// (upstream issue #35330).
|
||||
|
||||
test('t() self-configures from window.__SUPERSET_LANGUAGE_PACK__ on first call', () => {
|
||||
jest.isolateModules(() => {
|
||||
window.__SUPERSET_LANGUAGE_PACK__ = {
|
||||
domain: 'superset',
|
||||
locale_data: {
|
||||
superset: {
|
||||
'': {
|
||||
domain: 'superset',
|
||||
lang: 'fr',
|
||||
plural_forms: 'nplurals=2; plural=(n > 1);',
|
||||
},
|
||||
hello: ['bonjour'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { t } = require('./TranslatorSingleton');
|
||||
expect(t('hello')).toBe('bonjour');
|
||||
// No "should call configure" warning because we self-configured first.
|
||||
expect(consoleSpy).not.toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
delete window.__SUPERSET_LANGUAGE_PACK__;
|
||||
});
|
||||
});
|
||||
|
||||
test('t() falls back to msgid when window has no language pack', () => {
|
||||
jest.isolateModules(() => {
|
||||
delete window.__SUPERSET_LANGUAGE_PACK__;
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const { t } = require('./TranslatorSingleton');
|
||||
expect(t('hello')).toBe('hello');
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/was called before configure\(\)/),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
test('explicit configure() takes precedence over window pack', () => {
|
||||
jest.isolateModules(() => {
|
||||
window.__SUPERSET_LANGUAGE_PACK__ = {
|
||||
domain: 'superset',
|
||||
locale_data: {
|
||||
superset: {
|
||||
'': {
|
||||
domain: 'superset',
|
||||
lang: 'fr',
|
||||
plural_forms: 'nplurals=2; plural=(n > 1);',
|
||||
},
|
||||
hello: ['bonjour'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const { configure, t } = require('./TranslatorSingleton');
|
||||
configure({
|
||||
languagePack: {
|
||||
domain: 'superset',
|
||||
locale_data: {
|
||||
superset: {
|
||||
'': {
|
||||
domain: 'superset',
|
||||
lang: 'es',
|
||||
plural_forms: 'nplurals=2; plural=(n != 1);',
|
||||
},
|
||||
hello: ['hola'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(t('hello')).toBe('hola');
|
||||
delete window.__SUPERSET_LANGUAGE_PACK__;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,31 @@ function configure(config?: TranslatorConfig) {
|
||||
return singleton;
|
||||
}
|
||||
|
||||
// When webpack splits @apache-superset/core across chunks, each
|
||||
// chunk-local copy of this module has its own `singleton` and
|
||||
// `isConfigured` state. The first `t()` call in a late chunk hits a
|
||||
// fresh, unconfigured Translator and returns the English msgid. To
|
||||
// make translations survive chunk duplication, the HTML template
|
||||
// stashes the language pack on window and we self-configure from it
|
||||
// on first access. See upstream issue #35330.
|
||||
declare global {
|
||||
interface Window {
|
||||
__SUPERSET_LANGUAGE_PACK__?: TranslatorConfig['languagePack'];
|
||||
}
|
||||
}
|
||||
|
||||
function autoConfigureFromWindow() {
|
||||
if (isConfigured) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
const pack = window.__SUPERSET_LANGUAGE_PACK__;
|
||||
if (pack) {
|
||||
configure({ languagePack: pack });
|
||||
}
|
||||
}
|
||||
|
||||
function getInstance() {
|
||||
autoConfigureFromWindow();
|
||||
|
||||
if (typeof singleton === 'undefined') {
|
||||
singleton = new Translator();
|
||||
}
|
||||
@@ -85,11 +109,16 @@ function addLocaleData(data: LocaleData) {
|
||||
}
|
||||
|
||||
function t(input: string, ...args: unknown[]) {
|
||||
// Self-configure from the bootstrap-injected window pack before deciding
|
||||
// whether to warn, so a chunk-local copy that hasn't seen configure() yet
|
||||
// doesn't warn (or fall back to English) when the pack is available.
|
||||
autoConfigureFromWindow();
|
||||
if (!isConfigured) warnPreConfigure('t', input);
|
||||
return getInstance().translate(input, ...args);
|
||||
}
|
||||
|
||||
function tn(key: string, ...args: unknown[]) {
|
||||
autoConfigureFromWindow();
|
||||
if (!isConfigured) warnPreConfigure('tn', key);
|
||||
return getInstance().translateWithNumber(key, ...args);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@types/react": "*",
|
||||
"lodash": "^4.18.1",
|
||||
"tinycolor2": "*",
|
||||
"lodash-es": "^4.17.21"
|
||||
"lodash-es": "^4.18.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
|
||||
@@ -330,8 +330,7 @@ export interface SelectControlConfig<
|
||||
optionRenderer?: (option: O) => ReactNode;
|
||||
valueRenderer?: (option: O) => ReactNode;
|
||||
filterOption?:
|
||||
| ((option: FilterOption<O>, rawInput: string) => boolean)
|
||||
| null;
|
||||
((option: FilterOption<O>, rawInput: string) => boolean) | null;
|
||||
}
|
||||
|
||||
export type SharedControlConfig<
|
||||
@@ -396,9 +395,7 @@ export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
|
||||
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
|
||||
|
||||
export type ControlSetItem =
|
||||
| SharedControlAlias
|
||||
| OverrideSharedControlItem
|
||||
| ExpandedControlItem;
|
||||
SharedControlAlias | OverrideSharedControlItem | ExpandedControlItem;
|
||||
|
||||
export type ControlSetRow = ControlSetItem[];
|
||||
|
||||
@@ -702,10 +699,7 @@ export interface DataColumnMeta {
|
||||
originalLabel?: string;
|
||||
dataType: GenericDataType;
|
||||
formatter?:
|
||||
| TimeFormatter
|
||||
| NumberFormatter
|
||||
| CustomFormatter
|
||||
| CurrencyFormatter;
|
||||
TimeFormatter | NumberFormatter | CustomFormatter | CurrencyFormatter;
|
||||
isMetric?: boolean;
|
||||
isPercentMetric?: boolean;
|
||||
isNumeric?: boolean;
|
||||
@@ -713,4 +707,5 @@ export interface DataColumnMeta {
|
||||
isChildColumn?: boolean;
|
||||
description?: string;
|
||||
currencyCodeColumn?: string;
|
||||
isFilterable?: boolean;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"lib"
|
||||
],
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.2.5",
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
"@apache-superset/core": "*",
|
||||
"@babel/runtime": "^7.29.7",
|
||||
"@braintree/sanitize-url": "^7.1.2",
|
||||
@@ -53,21 +53,21 @@
|
||||
"re-resizable": "^6.11.2",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-draggable": "^4.7.0",
|
||||
"react-error-boundary": "6.0.0",
|
||||
"react-error-boundary": "^6.1.2",
|
||||
"react-js-cron": "^5.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-markdown": "^9.0.7",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"reselect": "^5.2.0",
|
||||
"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",
|
||||
|
||||
@@ -29,13 +29,7 @@ import {
|
||||
import type { AsyncAceEditorOptions } from './types';
|
||||
|
||||
type EditorType =
|
||||
| 'sql'
|
||||
| 'full-sql'
|
||||
| 'markdown'
|
||||
| 'text-area'
|
||||
| 'css'
|
||||
| 'json'
|
||||
| 'config';
|
||||
'sql' | 'full-sql' | 'markdown' | 'text-area' | 'css' | 'json' | 'config';
|
||||
|
||||
const editorTypes: EditorType[] = [
|
||||
'sql',
|
||||
|
||||
@@ -70,6 +70,199 @@ test('SQLEditor uses fontFamilyCode from theme', async () => {
|
||||
expect(fontFamily).toMatch(/mono|courier|consolas/i);
|
||||
});
|
||||
|
||||
test('re-measures Ace font metrics on load and preserves a consumer onLoad (#41664)', async () => {
|
||||
// Ace caches glyph width at construction; if the editor font settles later,
|
||||
// the caret drifts. The editor forces a re-measure on load and again once
|
||||
// `document.fonts.ready` resolves. Mock `document.fonts` so the re-measure
|
||||
// path is deterministic regardless of the jsdom FontFaceSet implementation.
|
||||
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
|
||||
const fontsReady = Promise.resolve();
|
||||
Object.defineProperty(document, 'fonts', {
|
||||
configurable: true,
|
||||
value: { ready: fontsReady },
|
||||
});
|
||||
|
||||
const ref = createRef<AceEditor>();
|
||||
let updateFontSizeSpy: jest.SpyInstance | undefined;
|
||||
// The spy is installed from inside the consumer onLoad, so it captures the
|
||||
// asynchronous (post-fonts-ready) re-measure that runs after this callback.
|
||||
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
|
||||
// Cast to a minimal shape so `jest.spyOn` resolves cleanly; it is the
|
||||
// same renderer instance the component re-measures, so the spy still
|
||||
// observes the production calls.
|
||||
const renderer = editor.renderer as unknown as {
|
||||
updateFontSize: () => void;
|
||||
};
|
||||
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
|
||||
});
|
||||
|
||||
try {
|
||||
const { container } = render(
|
||||
<SQLEditor
|
||||
ref={ref as React.Ref<never>}
|
||||
onLoad={consumerOnLoad as never}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(selector)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The wrapper must call through to the consumer's onLoad with the editor.
|
||||
expect(consumerOnLoad).toHaveBeenCalledTimes(1);
|
||||
expect(consumerOnLoad).toHaveBeenCalledWith(ref.current?.editor);
|
||||
|
||||
// Once fonts settle, the editor re-measures (Ace itself resizes and
|
||||
// re-renders when the measured character size changed) so the caret
|
||||
// realigns.
|
||||
await fontsReady;
|
||||
await waitFor(() => {
|
||||
expect(updateFontSizeSpy).toHaveBeenCalled();
|
||||
});
|
||||
} finally {
|
||||
updateFontSizeSpy?.mockRestore();
|
||||
if (originalFonts) {
|
||||
Object.defineProperty(document, 'fonts', originalFonts);
|
||||
} else {
|
||||
delete (document as { fonts?: unknown }).fonts;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('does not crash when the Font Loading API is unavailable (#41697)', async () => {
|
||||
// jsdom has no `document.fonts` by default; some embedded webviews don't
|
||||
// either. The load-time re-measure must still run and the editor must mount.
|
||||
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
|
||||
delete (document as { fonts?: unknown }).fonts;
|
||||
|
||||
const consumerOnLoad = jest.fn();
|
||||
try {
|
||||
const { container } = render(
|
||||
<SQLEditor onLoad={consumerOnLoad as never} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(selector)).toBeInTheDocument();
|
||||
});
|
||||
expect(consumerOnLoad).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
if (originalFonts) {
|
||||
Object.defineProperty(document, 'fonts', originalFonts);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('explicitly loads the editor font and re-measures when it resolves (#41664)', async () => {
|
||||
// `fonts.ready` can settle BEFORE a lazily-referenced editor font even
|
||||
// starts loading, so the fix must request the font itself via
|
||||
// `fonts.load()` and re-measure when that specific load resolves.
|
||||
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
|
||||
let resolveFontLoad: () => void = () => {};
|
||||
const fontLoad = new Promise<void>(resolve => {
|
||||
resolveFontLoad = resolve;
|
||||
});
|
||||
const load = jest.fn((_font: string) => fontLoad);
|
||||
Object.defineProperty(document, 'fonts', {
|
||||
configurable: true,
|
||||
// `ready` already settled: the pre-fix code would never re-measure.
|
||||
value: {
|
||||
ready: Promise.resolve(),
|
||||
load,
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
let updateFontSizeSpy: jest.SpyInstance | undefined;
|
||||
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
|
||||
const renderer = editor.renderer as unknown as {
|
||||
updateFontSize: () => void;
|
||||
};
|
||||
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
|
||||
});
|
||||
|
||||
try {
|
||||
const { container } = render(
|
||||
<SQLEditor onLoad={consumerOnLoad as never} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(selector)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The editor font itself is requested (family from the theme's code font).
|
||||
expect(load).toHaveBeenCalledTimes(1);
|
||||
expect(load.mock.calls[0][0]).toMatch(/^12px /);
|
||||
|
||||
const callsBefore = updateFontSizeSpy?.mock.calls.length ?? 0;
|
||||
resolveFontLoad();
|
||||
await waitFor(() => {
|
||||
expect(updateFontSizeSpy!.mock.calls.length).toBeGreaterThan(callsBefore);
|
||||
});
|
||||
} finally {
|
||||
updateFontSizeSpy?.mockRestore();
|
||||
if (originalFonts) {
|
||||
Object.defineProperty(document, 'fonts', originalFonts);
|
||||
} else {
|
||||
delete (document as { fonts?: unknown }).fonts;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('re-measures on later loadingdone events and unsubscribes on unmount (#41664)', async () => {
|
||||
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
|
||||
const listeners: Record<string, (() => void)[]> = {};
|
||||
const addEventListener = jest.fn((event: string, handler: () => void) => {
|
||||
(listeners[event] ??= []).push(handler);
|
||||
});
|
||||
const removeEventListener = jest.fn((event: string, handler: () => void) => {
|
||||
listeners[event] = (listeners[event] ?? []).filter(h => h !== handler);
|
||||
});
|
||||
Object.defineProperty(document, 'fonts', {
|
||||
configurable: true,
|
||||
value: {
|
||||
ready: Promise.resolve(),
|
||||
load: jest.fn(() => Promise.resolve()),
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
},
|
||||
});
|
||||
|
||||
let updateFontSizeSpy: jest.SpyInstance | undefined;
|
||||
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
|
||||
const renderer = editor.renderer as unknown as {
|
||||
updateFontSize: () => void;
|
||||
};
|
||||
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
|
||||
});
|
||||
|
||||
try {
|
||||
const { container, unmount } = render(
|
||||
<SQLEditor onLoad={consumerOnLoad as never} />,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector(selector)).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(listeners.loadingdone?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// A font finishing later (e.g. a user-config font) re-measures again.
|
||||
const callsBefore = updateFontSizeSpy?.mock.calls.length ?? 0;
|
||||
listeners.loadingdone.forEach(handler => handler());
|
||||
expect(updateFontSizeSpy!.mock.calls.length).toBeGreaterThan(callsBefore);
|
||||
|
||||
// Unmount removes the listener so destroyed editors are not re-measured.
|
||||
unmount();
|
||||
expect(listeners.loadingdone?.length ?? 0).toBe(0);
|
||||
} finally {
|
||||
updateFontSizeSpy?.mockRestore();
|
||||
if (originalFonts) {
|
||||
Object.defineProperty(document, 'fonts', originalFonts);
|
||||
} else {
|
||||
delete (document as { fonts?: unknown }).fonts;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('renders FullSQLEditor', async () => {
|
||||
const { container } = render(<FullSQLEditor />);
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 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 ace from 'ace-builds/src-noconflict/ace';
|
||||
import {
|
||||
emojiWidth,
|
||||
isBmpEmojiPresentation,
|
||||
patchAceEmojiWidths,
|
||||
} from './emojiWidthPatch';
|
||||
|
||||
const { EditSession } = ace.require('ace/edit_session');
|
||||
const { Text: TextLayer } = ace.require('ace/layer/text');
|
||||
|
||||
beforeAll(() => {
|
||||
patchAceEmojiWidths(EditSession, TextLayer);
|
||||
});
|
||||
|
||||
test('classifies BMP emoji-presentation codepoints and nothing else', () => {
|
||||
expect(isBmpEmojiPresentation('✨'.charCodeAt(0))).toBe(true); // U+2728
|
||||
expect(isBmpEmojiPresentation('⭐'.charCodeAt(0))).toBe(true); // U+2B50
|
||||
expect(isBmpEmojiPresentation('❤'.charCodeAt(0))).toBe(false); // text-default
|
||||
expect(isBmpEmojiPresentation('A'.charCodeAt(0))).toBe(false);
|
||||
expect(isBmpEmojiPresentation('中'.charCodeAt(0))).toBe(false); // CJK, ace's own tables
|
||||
expect(isBmpEmojiPresentation(0xd83d)).toBe(false); // lone surrogate
|
||||
});
|
||||
|
||||
test('emojiWidth: VS16 tops its base up to 2 columns total', () => {
|
||||
const VS16 = 0xfe0f;
|
||||
expect(emojiWidth('✨'.charCodeAt(0), 0)).toBe(2);
|
||||
// ❤️ = U+2764 (1 by default rules) + VS16 (1) = 2 total
|
||||
expect(emojiWidth('❤'.charCodeAt(0), 0)).toBe(null);
|
||||
expect(emojiWidth(VS16, '❤'.charCodeAt(0))).toBe(1);
|
||||
// Redundant ✨️ = ✨ (2) + VS16 (0) = 2 total
|
||||
expect(emojiWidth(VS16, '✨'.charCodeAt(0))).toBe(0);
|
||||
expect(emojiWidth('A'.charCodeAt(0), 0)).toBe(null);
|
||||
});
|
||||
|
||||
test('caret math counts emoji as two columns (issue #41664 repro line)', () => {
|
||||
const session = new EditSession('### ✨Header 3');
|
||||
// Before "H" (doc column 5): "### " = 4 + ✨ = 2 → screen column 6.
|
||||
expect(session.documentToScreenColumn(0, 5)).toBe(6);
|
||||
// End of line: 4 + 2 + "Header 3".length (8) = 14.
|
||||
expect(session.documentToScreenColumn(0, 13)).toBe(14);
|
||||
});
|
||||
|
||||
test('astral emoji keep their pre-existing two-column accounting', () => {
|
||||
const session = new EditSession('💡X');
|
||||
// 💡 is two code units (doc columns 0-1) and two screen columns.
|
||||
expect(session.documentToScreenColumn(0, 2)).toBe(2);
|
||||
});
|
||||
|
||||
test('$getStringScreenWidth counts emoji, VS16 pairs, and plain text', () => {
|
||||
const session = new EditSession('');
|
||||
expect(session.$getStringScreenWidth('✨')[0]).toBe(2);
|
||||
expect(session.$getStringScreenWidth('❤️')[0]).toBe(2);
|
||||
expect(session.$getStringScreenWidth('✨️')[0]).toBe(2);
|
||||
expect(session.$getStringScreenWidth('abc')[0]).toBe(3);
|
||||
expect(session.$getStringScreenWidth('中')[0]).toBe(2); // ace's own CJK path
|
||||
});
|
||||
|
||||
test('renderer draws emoji in a forced 2×charWidth box like CJK', () => {
|
||||
const container = document.createElement('div');
|
||||
const layer = new TextLayer(container);
|
||||
layer.config = { characterWidth: 10 };
|
||||
|
||||
const parent = document.createElement('div');
|
||||
const screenColumn = layer.$renderToken(
|
||||
parent,
|
||||
0,
|
||||
{ type: 'text', value: '# ✨Header' },
|
||||
'# ✨Header',
|
||||
);
|
||||
|
||||
const box = parent.querySelector<HTMLElement>('.ace_cjk');
|
||||
expect(box).not.toBeNull();
|
||||
expect(box?.textContent).toBe('✨');
|
||||
expect(box?.style.width).toBe('20px');
|
||||
// "# " (2) + ✨ (2) + "Header" (6) = 10 columns.
|
||||
expect(screenColumn).toBe(10);
|
||||
// Text around the emoji is preserved in order.
|
||||
expect(parent.textContent).toBe('# ✨Header');
|
||||
});
|
||||
|
||||
test('renderer handles a leading emoji (empty split part) cleanly', () => {
|
||||
const container = document.createElement('div');
|
||||
const layer = new TextLayer(container);
|
||||
layer.config = { characterWidth: 10 };
|
||||
|
||||
const parent = document.createElement('div');
|
||||
const screenColumn = layer.$renderToken(
|
||||
parent,
|
||||
0,
|
||||
{ type: 'text', value: '✨x' },
|
||||
'✨x',
|
||||
);
|
||||
|
||||
expect(parent.querySelector('.ace_cjk')?.textContent).toBe('✨');
|
||||
expect(parent.textContent).toBe('✨x');
|
||||
expect(screenColumn).toBe(3);
|
||||
});
|
||||
|
||||
test('renderer leaves emoji-free tokens entirely to the original path', () => {
|
||||
const container = document.createElement('div');
|
||||
const layer = new TextLayer(container);
|
||||
layer.config = { characterWidth: 10 };
|
||||
|
||||
const parent = document.createElement('div');
|
||||
const screenColumn = layer.$renderToken(
|
||||
parent,
|
||||
0,
|
||||
{ type: 'text', value: 'SELECT 1' },
|
||||
'SELECT 1',
|
||||
);
|
||||
|
||||
expect(parent.querySelector('.ace_cjk')).toBeNull();
|
||||
expect(parent.textContent).toBe('SELECT 1');
|
||||
expect(screenColumn).toBe(8);
|
||||
});
|
||||
|
||||
test('emoji boxes carry the token-class styling of their token', () => {
|
||||
const container = document.createElement('div');
|
||||
const layer = new TextLayer(container);
|
||||
layer.config = { characterWidth: 10 };
|
||||
|
||||
const parent = document.createElement('div');
|
||||
layer.$renderToken(
|
||||
parent,
|
||||
0,
|
||||
{ type: 'constant.language', value: '✨' },
|
||||
'✨',
|
||||
);
|
||||
|
||||
const box = parent.querySelector<HTMLElement>('.ace_cjk');
|
||||
expect(box?.className).toBe('ace_cjk ace_constant ace_language');
|
||||
});
|
||||
|
||||
test('emoji boxes in plain text tokens carry no extra classes', () => {
|
||||
const container = document.createElement('div');
|
||||
const layer = new TextLayer(container);
|
||||
layer.config = { characterWidth: 10 };
|
||||
|
||||
const parent = document.createElement('div');
|
||||
layer.$renderToken(parent, 0, { type: 'text', value: '✨' }, '✨');
|
||||
|
||||
const box = parent.querySelector<HTMLElement>('.ace_cjk');
|
||||
expect(box?.className).toBe('ace_cjk');
|
||||
});
|
||||
|
||||
test('patch is idempotent', () => {
|
||||
const before = EditSession.prototype.$getStringScreenWidth;
|
||||
patchAceEmojiWidths(EditSession, TextLayer);
|
||||
expect(EditSession.prototype.$getStringScreenWidth).toBe(before);
|
||||
});
|
||||
|
||||
// --- copied-internals parity ------------------------------------------------
|
||||
// The two overridden EditSession methods are verbatim copies of the
|
||||
// ace-builds internals plus the emoji branch; these pin the non-emoji
|
||||
// behavior so a future ace upgrade that drifts is caught here.
|
||||
|
||||
test('$getStringScreenWidth keeps tab, early-return, and break semantics', () => {
|
||||
const session = new EditSession('');
|
||||
// Tab expands to the next tab stop (default tab size 4).
|
||||
expect(session.$getStringScreenWidth('\ta')[0]).toBe(5);
|
||||
// maxScreenColumn === 0 short-circuits.
|
||||
expect(session.$getStringScreenWidth('abc', 0)).toEqual([0, 0]);
|
||||
// Stops consuming once past maxScreenColumn.
|
||||
const [screen, column] = session.$getStringScreenWidth('abcdef', 3);
|
||||
expect(screen).toBe(4);
|
||||
expect(column).toBe(3);
|
||||
});
|
||||
|
||||
test('$getDisplayTokens keeps tab, space, punctuation, and CJK tokens', () => {
|
||||
const session = new EditSession('');
|
||||
const CHAR = 1;
|
||||
const CHAR_EXT = 2;
|
||||
const PUNCTUATION = 9;
|
||||
const SPACE = 10;
|
||||
const TAB = 11;
|
||||
const TAB_SPACE = 12;
|
||||
|
||||
// Tab at offset 0 with default tab size 4: TAB + 3 TAB_SPACEs.
|
||||
expect(session.$getDisplayTokens('\t', 0)).toEqual([
|
||||
TAB,
|
||||
TAB_SPACE,
|
||||
TAB_SPACE,
|
||||
TAB_SPACE,
|
||||
]);
|
||||
expect(session.$getDisplayTokens(' a', 0)).toEqual([SPACE, CHAR]);
|
||||
expect(session.$getDisplayTokens('*:', 0)).toEqual([
|
||||
PUNCTUATION,
|
||||
PUNCTUATION,
|
||||
]);
|
||||
expect(session.$getDisplayTokens('中', 0)).toEqual([CHAR, CHAR_EXT]);
|
||||
expect(session.$getDisplayTokens('✨', 0)).toEqual([CHAR, CHAR_EXT]);
|
||||
// VS16 keeps one entry per code unit so wrap offsets stay aligned.
|
||||
expect(session.$getDisplayTokens('✨️', 0)).toEqual([
|
||||
CHAR,
|
||||
CHAR_EXT,
|
||||
CHAR_EXT,
|
||||
]);
|
||||
expect(session.$getDisplayTokens('❤️', 0)).toEqual([CHAR, CHAR_EXT]);
|
||||
// Second pass hits the memoized emoji-base classification.
|
||||
expect(session.$getDisplayTokens('❤️', 0)).toEqual([CHAR, CHAR_EXT]);
|
||||
});
|
||||
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ace positions the caret on a monospace grid (`column × charWidth`) and
|
||||
* renders anything it classifies as "full width" inside a forced
|
||||
* `2 × charWidth` inline-block (`.ace_cjk`), so the grid model and the pixels
|
||||
* agree. Its width tables only cover East-Asian ranges, though:
|
||||
*
|
||||
* - Astral emoji (💡, surrogate pairs) happen to work: the renderer's
|
||||
* surrogate-pair branch forces the 2-cell box and the column counter
|
||||
* counts the two code units.
|
||||
* - BMP emoji with default emoji presentation (✨ U+2728, ⭐ U+2B50, …) fall
|
||||
* through BOTH nets: they count as 1 column but render ~1.6 cells wide, so
|
||||
* the caret drifts on any line containing one (issue #41664, upstream
|
||||
* ajaxorg/ace#3404).
|
||||
* - VS16 sequences (❤️ = U+2764 U+FE0F) render as one color glyph while the
|
||||
* two code units count as two ordinary columns, without the forced box.
|
||||
*
|
||||
* The patch extends the exact mechanism Ace already uses — count 2 columns
|
||||
* and force the 2-cell box — to emoji-presentation codepoints and VS16
|
||||
* sequences, by overriding the two `EditSession` width methods (verbatim
|
||||
* copies of the ace-builds internals plus one branch; the originals call a
|
||||
* module-private `isFullWidth`, so they cannot be extended in place) and
|
||||
* wrapping the text layer's `$renderToken` to emit the forced-width span for
|
||||
* emoji runs before delegating everything else to the original.
|
||||
*
|
||||
* Deliberately out of scope: ZWJ sequences (family emoji) and flag pairs
|
||||
* render as one glyph but count per component; handling them needs grapheme
|
||||
* segmentation across every Ace layer, and they were equally misaligned
|
||||
* before this patch.
|
||||
*/
|
||||
|
||||
const VS16 = 0xfe0f;
|
||||
|
||||
// Tokens ace renders as bare text nodes with no token-class span; mirrors
|
||||
// ace/layer/text_util's textTokens set.
|
||||
const TEXT_TOKENS = new Set(['text', 'rparen', 'lparen']);
|
||||
|
||||
// Emoji_Presentation covers exactly the codepoints browsers render as color
|
||||
// emoji without a variation selector. Lone surrogates never match it (the
|
||||
// astral path is already consistent), so only BMP emoji change behavior.
|
||||
const EMOJI_PRESENTATION_RE = /\p{Emoji_Presentation}/u;
|
||||
// One renderable emoji cluster: an emoji-presentation codepoint with an
|
||||
// optional (redundant) VS16, or a text-presentation emoji forced to color
|
||||
// presentation by VS16.
|
||||
const EMOJI_CLUSTER_SOURCE =
|
||||
'(?:\\p{Emoji_Presentation}\\uFE0F?|\\p{Emoji}\\uFE0F)';
|
||||
const EMOJI_RUN_TEST_RE = new RegExp(`${EMOJI_CLUSTER_SOURCE}`, 'u');
|
||||
const EMOJI_RUN_SPLIT_RE = new RegExp(`(${EMOJI_CLUSTER_SOURCE}+)`, 'gu');
|
||||
const EMOJI_CLUSTER_RE = new RegExp(EMOJI_CLUSTER_SOURCE, 'gu');
|
||||
|
||||
// Memoized per-code-unit classification; $getStringScreenWidth runs per
|
||||
// character per rendered line, so the regexes must not run every time.
|
||||
const EMOJI_BASE_RE = /\p{Emoji}/u;
|
||||
const emojiBaseCache = new Map<number, boolean>();
|
||||
function isEmojiBase(code: number): boolean {
|
||||
let result = emojiBaseCache.get(code);
|
||||
if (result === undefined) {
|
||||
result = EMOJI_BASE_RE.test(String.fromCharCode(code));
|
||||
emojiBaseCache.set(code, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const bmpEmojiCache = new Map<number, boolean>();
|
||||
export function isBmpEmojiPresentation(code: number): boolean {
|
||||
if (code < 0x2000 || code > 0xffff || (code >= 0xd800 && code <= 0xdfff)) {
|
||||
return false;
|
||||
}
|
||||
let result = bmpEmojiCache.get(code);
|
||||
if (result === undefined) {
|
||||
result = EMOJI_PRESENTATION_RE.test(String.fromCharCode(code));
|
||||
bmpEmojiCache.set(code, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen-column contribution of one code unit under the emoji rules, or null
|
||||
* when Ace's default handling should apply. Emoji-presentation chars are 2
|
||||
* columns; VS16 tops its base up to 2 in total (so ❤️ = 1 + 1 and a
|
||||
* redundant ✨️ = 2 + 0).
|
||||
*/
|
||||
export function emojiWidth(code: number, prevCode: number): number | null {
|
||||
if (isBmpEmojiPresentation(code)) {
|
||||
return 2;
|
||||
}
|
||||
if (code === VS16) {
|
||||
return isBmpEmojiPresentation(prevCode) ? 0 : 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface PatchableEditSessionClass {
|
||||
prototype: {
|
||||
isFullWidth: (c: number) => boolean;
|
||||
getScreenTabSize: (screenColumn: number) => number;
|
||||
$getStringScreenWidth: (
|
||||
str: string,
|
||||
maxScreenColumn?: number,
|
||||
screenColumn?: number,
|
||||
) => [number, number];
|
||||
$getDisplayTokens: (str: string, offset: number) => number[];
|
||||
};
|
||||
}
|
||||
|
||||
interface TextLayerInstance {
|
||||
config: { characterWidth: number };
|
||||
dom: { createElement: (tag: string) => HTMLElement };
|
||||
}
|
||||
|
||||
type RenderToken = (
|
||||
this: TextLayerInstance,
|
||||
parent: HTMLElement,
|
||||
screenColumn: number,
|
||||
token: { type: string; value: string },
|
||||
value: string,
|
||||
) => number;
|
||||
|
||||
interface PatchableTextLayerClass {
|
||||
prototype: { $renderToken: RenderToken };
|
||||
}
|
||||
|
||||
let patched = false;
|
||||
|
||||
export function patchAceEmojiWidths(
|
||||
EditSession: PatchableEditSessionClass,
|
||||
TextLayer: PatchableTextLayerClass,
|
||||
): void {
|
||||
if (patched) {
|
||||
return;
|
||||
}
|
||||
patched = true;
|
||||
|
||||
const sessionProto = EditSession.prototype;
|
||||
const { isFullWidth } = sessionProto;
|
||||
|
||||
// Mirrors EditSession.prototype.$getStringScreenWidth from ace-builds
|
||||
// (src-noconflict/ace.js), adding the emojiWidth branch.
|
||||
sessionProto.$getStringScreenWidth = function $getStringScreenWidth(
|
||||
this: PatchableEditSessionClass['prototype'],
|
||||
str: string,
|
||||
maxScreenColumn?: number,
|
||||
screenColumn?: number,
|
||||
): [number, number] {
|
||||
if (maxScreenColumn === 0) {
|
||||
return [0, 0];
|
||||
}
|
||||
const max = maxScreenColumn ?? Infinity;
|
||||
let screen = screenColumn || 0;
|
||||
let column;
|
||||
for (column = 0; column < str.length; column += 1) {
|
||||
const c = str.charCodeAt(column);
|
||||
const emoji = emojiWidth(c, column > 0 ? str.charCodeAt(column - 1) : 0);
|
||||
if (c === 9) {
|
||||
screen += this.getScreenTabSize(screen);
|
||||
} else if (emoji !== null) {
|
||||
screen += emoji;
|
||||
} else if (c >= 0x1100 && isFullWidth(c)) {
|
||||
screen += 2;
|
||||
} else {
|
||||
screen += 1;
|
||||
}
|
||||
if (screen > max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return [screen, column];
|
||||
};
|
||||
|
||||
// Display-token codes from ace's edit_session module.
|
||||
const CHAR = 1;
|
||||
const CHAR_EXT = 2;
|
||||
const PUNCTUATION = 9;
|
||||
const SPACE = 10;
|
||||
const TAB = 11;
|
||||
const TAB_SPACE = 12;
|
||||
|
||||
// Mirrors EditSession.prototype.$getDisplayTokens, adding the emoji branch.
|
||||
// One entry per code unit is preserved (VS16 pushes a lone CHAR_EXT) so the
|
||||
// wrap-split offset math stays aligned with document columns.
|
||||
sessionProto.$getDisplayTokens = function $getDisplayTokens(
|
||||
this: PatchableEditSessionClass['prototype'],
|
||||
str: string,
|
||||
offset: number,
|
||||
): number[] {
|
||||
const arr: number[] = [];
|
||||
let tabSize: number;
|
||||
for (let i = 0; i < str.length; i += 1) {
|
||||
const c = str.charCodeAt(i);
|
||||
const emoji = emojiWidth(c, i > 0 ? str.charCodeAt(i - 1) : 0);
|
||||
if (c === 9) {
|
||||
tabSize = this.getScreenTabSize(arr.length + offset);
|
||||
arr.push(TAB);
|
||||
for (let n = 1; n < tabSize; n += 1) {
|
||||
arr.push(TAB_SPACE);
|
||||
}
|
||||
} else if (emoji === 2) {
|
||||
arr.push(CHAR, CHAR_EXT);
|
||||
} else if (emoji === 0) {
|
||||
arr.push(CHAR_EXT);
|
||||
} else if (c === VS16 && i > 0 && isEmojiBase(str.charCodeAt(i - 1))) {
|
||||
// VS16 after a text-presentation emoji base (❤️): keep the pair
|
||||
// atomic for wrap splitting; its width contribution stays 1.
|
||||
arr.push(CHAR_EXT);
|
||||
} else if (c === 32) {
|
||||
arr.push(SPACE);
|
||||
} else if ((c > 39 && c < 48) || (c > 57 && c < 64)) {
|
||||
arr.push(PUNCTUATION);
|
||||
} else if (c >= 0x1100 && isFullWidth(c)) {
|
||||
arr.push(CHAR, CHAR_EXT);
|
||||
} else {
|
||||
arr.push(CHAR);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
|
||||
// Pre-split token text on emoji runs: emoji clusters get the same forced
|
||||
// 2 × charWidth `.ace_cjk` box Ace gives CJK (so rendered geometry matches
|
||||
// the 2-column model above); everything else delegates to the original.
|
||||
const origRenderToken = TextLayer.prototype.$renderToken;
|
||||
TextLayer.prototype.$renderToken = function $renderToken(
|
||||
this: TextLayerInstance,
|
||||
parent: HTMLElement,
|
||||
screenColumn: number,
|
||||
token: { type: string; value: string },
|
||||
value: string,
|
||||
): number {
|
||||
if (!EMOJI_RUN_TEST_RE.test(value)) {
|
||||
return origRenderToken.call(this, parent, screenColumn, token, value);
|
||||
}
|
||||
// Non-text tokens normally have their whole fragment wrapped in a
|
||||
// token-class span ("ace_" + dotted type); carry those classes on the
|
||||
// emoji box itself so syntax styling that isn't glyph color (comment
|
||||
// italics, invalid-token backgrounds, …) still applies to emoji.
|
||||
// Mirrors ace's text_util.isTextToken and $renderToken class handling.
|
||||
const tokenClasses = TEXT_TOKENS.has(token.type)
|
||||
? ''
|
||||
: ` ace_${token.type.replace(/\./g, ' ace_')}`;
|
||||
let column = screenColumn;
|
||||
value.split(EMOJI_RUN_SPLIT_RE).forEach((part, index) => {
|
||||
if (!part) {
|
||||
return;
|
||||
}
|
||||
const isEmojiRun = index % 2 === 1;
|
||||
if (isEmojiRun) {
|
||||
// Parts at odd indices are the split's capture group, so every one
|
||||
// is a non-empty sequence of clusters; matchAll never comes up empty.
|
||||
Array.from(part.matchAll(EMOJI_CLUSTER_RE), m => m[0]).forEach(
|
||||
cluster => {
|
||||
const span = this.dom.createElement('span');
|
||||
span.style.width = `${this.config.characterWidth * 2}px`;
|
||||
span.className = `ace_cjk${tokenClasses}`;
|
||||
span.textContent = cluster;
|
||||
parent.appendChild(span);
|
||||
column += 2;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
column = origRenderToken.call(this, parent, column, token, part);
|
||||
}
|
||||
});
|
||||
return column;
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,13 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { forwardRef, useEffect, useCallback, ComponentType } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
ComponentType,
|
||||
} from 'react';
|
||||
|
||||
import type {
|
||||
Editor as OrigEditor,
|
||||
@@ -35,6 +41,8 @@ import {
|
||||
import { useTheme, css, type SupersetTheme } from '@apache-superset/core/theme';
|
||||
import { Global } from '@emotion/react';
|
||||
|
||||
import { patchAceEmojiWidths } from './emojiWidthPatch';
|
||||
|
||||
export { getTooltipHTML } from './Tooltip';
|
||||
export { useJsonValidation } from './useJsonValidation';
|
||||
export type {
|
||||
@@ -162,6 +170,14 @@ export function AsyncAceEditor(
|
||||
config.setModuleUrl('ace/mode/javascript_worker', javascriptWorkerUrl);
|
||||
config.setModuleUrl('ace/mode/html_worker', htmlWorkerUrl);
|
||||
|
||||
// Align caret math and rendered glyph geometry for emoji (issue #41664);
|
||||
// see emojiWidthPatch for the full story. Applied once, globally, since
|
||||
// the prototypes are shared by every Ace editor instance.
|
||||
patchAceEmojiWidths(
|
||||
acequire('ace/edit_session').EditSession,
|
||||
acequire('ace/layer/text').Text,
|
||||
);
|
||||
|
||||
await Promise.all(aceModules.map(x => aceModuleLoaders[x]()));
|
||||
|
||||
const inferredMode =
|
||||
@@ -179,6 +195,7 @@ export function AsyncAceEditor(
|
||||
theme = inferredTheme,
|
||||
tabSize = defaultTabSize,
|
||||
defaultValue = '',
|
||||
onLoad,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -281,6 +298,62 @@ export function AsyncAceEditor(
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
// Ace caches the measured glyph width in its internal FontMetrics and
|
||||
// only re-measures when its hidden measure node's own size changes. If
|
||||
// the editor font finishes loading after construction, the cached width
|
||||
// can stop matching the rendered glyphs and the caret drifts further
|
||||
// from the text the longer the line, the residual misalignment in issue
|
||||
// #41664 that the font-family CSS from #38928 does not address.
|
||||
// `updateFontSize` runs Ace's `checkForSizeChanges`, which on a metrics
|
||||
// change emits `changeCharacterSize`; Ace's renderer reacts with a
|
||||
// forced resize and full re-render.
|
||||
//
|
||||
// Re-measure on every signal that glyph geometry may have changed:
|
||||
// - immediately, for the already-loaded case;
|
||||
// - after explicitly loading the editor font via `fonts.load()` —
|
||||
// `fonts.ready` alone is NOT enough, because it can settle before a
|
||||
// lazily-referenced editor font even starts loading, in which case
|
||||
// waiting on it misses the swap entirely;
|
||||
// - after `fonts.ready`, for everything in flight at mount;
|
||||
// - on every subsequent `loadingdone` event while mounted, for fonts
|
||||
// that load even later (removed on unmount via fontCleanupsRef).
|
||||
const fontCleanupsRef = useRef<(() => void)[]>([]);
|
||||
useEffect(
|
||||
() => () => {
|
||||
fontCleanupsRef.current.forEach(cleanup => cleanup());
|
||||
fontCleanupsRef.current = [];
|
||||
},
|
||||
[],
|
||||
);
|
||||
const handleLoad = useCallback(
|
||||
(editor: Ace.Editor) => {
|
||||
const remeasure = () => {
|
||||
try {
|
||||
editor.renderer.updateFontSize();
|
||||
} catch {
|
||||
// The editor was destroyed before a pending font event fired.
|
||||
}
|
||||
};
|
||||
remeasure();
|
||||
const { fonts } = document;
|
||||
if (fonts) {
|
||||
// The size in the shorthand is irrelevant; any value triggers
|
||||
// the load of the family's faces.
|
||||
fonts
|
||||
.load?.(`12px ${editorFontFamily}`)
|
||||
?.then(remeasure)
|
||||
?.catch(() => {});
|
||||
fonts.ready?.then(remeasure)?.catch(() => {});
|
||||
fonts.addEventListener?.('loadingdone', remeasure);
|
||||
fontCleanupsRef.current.push(() =>
|
||||
fonts.removeEventListener?.('loadingdone', remeasure),
|
||||
);
|
||||
}
|
||||
onLoad?.(editor);
|
||||
},
|
||||
[onLoad, editorFontFamily],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Global
|
||||
@@ -316,8 +389,9 @@ export function AsyncAceEditor(
|
||||
}
|
||||
/* Adjust selection color */
|
||||
.ace_editor .ace_selection {
|
||||
background-color: ${token.colorEditorSelection ??
|
||||
token.colorPrimaryBgHover} !important;
|
||||
background-color: ${
|
||||
token.colorEditorSelection ?? token.colorPrimaryBgHover
|
||||
} !important;
|
||||
}
|
||||
|
||||
/* Improve active line highlighting */
|
||||
@@ -468,6 +542,7 @@ export function AsyncAceEditor(
|
||||
tabSize={tabSize}
|
||||
defaultValue={defaultValue}
|
||||
setOptions={{ fontFamily: editorFontFamily }}
|
||||
onLoad={handleLoad}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -31,15 +31,21 @@ export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
|
||||
export type OnClickHandler = MouseEventHandler<HTMLElement>;
|
||||
|
||||
export type ButtonStyle =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'tertiary'
|
||||
| 'danger'
|
||||
| 'link'
|
||||
| 'dashed';
|
||||
'primary' | 'secondary' | 'tertiary' | 'danger' | 'link' | 'dashed';
|
||||
|
||||
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 +55,5 @@ export type ButtonProps = Omit<AntdButtonProps, 'css'> & {
|
||||
cta?: boolean;
|
||||
showMarginRight?: boolean;
|
||||
icon?: ReactNode;
|
||||
styleConfig?: ButtonStyleConfig;
|
||||
};
|
||||
|
||||
@@ -37,13 +37,7 @@ import 'ace-builds/src-noconflict/theme-github';
|
||||
import 'ace-builds/src-noconflict/theme-monokai';
|
||||
|
||||
export type CodeEditorMode =
|
||||
| 'handlebars'
|
||||
| 'css'
|
||||
| 'json'
|
||||
| 'sql'
|
||||
| 'markdown'
|
||||
| 'javascript'
|
||||
| 'html';
|
||||
'handlebars' | 'css' | 'json' | 'sql' | 'markdown' | 'javascript' | 'html';
|
||||
|
||||
export type CodeEditorTheme = 'light' | 'dark';
|
||||
|
||||
|
||||
@@ -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,14 +27,16 @@ export const DropdownButton = ({
|
||||
tooltip,
|
||||
tooltipPlacement,
|
||||
children,
|
||||
styleConfig,
|
||||
...rest
|
||||
}: DropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const { type: buttonType } = rest;
|
||||
// divider implementation for default (non-primary) buttons
|
||||
const defaultBtnCss = css`
|
||||
${(!buttonType || buttonType === 'default') &&
|
||||
`.ant-dropdown-trigger {
|
||||
${
|
||||
(!buttonType || buttonType === 'default') &&
|
||||
`.ant-dropdown-trigger {
|
||||
position: relative;
|
||||
&:before {
|
||||
content: '';
|
||||
@@ -47,7 +49,8 @@ export const DropdownButton = ({
|
||||
.anticon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}`}
|
||||
}`
|
||||
}
|
||||
`;
|
||||
const button = (
|
||||
<Dropdown.Button
|
||||
@@ -57,10 +60,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 +92,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;
|
||||
};
|
||||
|
||||
@@ -221,18 +221,22 @@ export const DynamicEditableTitle = memo(
|
||||
onPressEnter={handleKeyPress}
|
||||
placeholder={placeholder}
|
||||
css={css`
|
||||
${!canEdit &&
|
||||
`&[disabled] {
|
||||
${
|
||||
!canEdit &&
|
||||
`&[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
`}
|
||||
`
|
||||
}
|
||||
font-size: ${theme.fontSizeXL}px;
|
||||
transition: auto;
|
||||
${inputWidth &&
|
||||
inputWidth > 0 &&
|
||||
css`
|
||||
width: ${inputWidth}px;
|
||||
`}
|
||||
${
|
||||
inputWidth &&
|
||||
inputWidth > 0 &&
|
||||
css`
|
||||
width: ${inputWidth}px;
|
||||
`
|
||||
}
|
||||
`}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
|
||||
@@ -167,9 +167,11 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
{image && <ImageContainer image={image} size={size} />}
|
||||
<div
|
||||
css={(theme: SupersetTheme) => css`
|
||||
max-width: ${containerSize === 'large'
|
||||
? theme.sizeUnit * 150
|
||||
: theme.sizeUnit * 100}px;
|
||||
max-width: ${
|
||||
containerSize === 'large'
|
||||
? theme.sizeUnit * 150
|
||||
: theme.sizeUnit * 100
|
||||
}px;
|
||||
`}
|
||||
>
|
||||
{title && <Title size={effectiveTextSize}>{title}</Title>}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,8 +21,7 @@ export type { FormProps, FormInstance, FormItemProps } from 'antd/es/form';
|
||||
export interface LabeledErrorBoundInputProps {
|
||||
label?: string;
|
||||
validationMethods:
|
||||
| { onBlur: (value: any) => void }
|
||||
| { onChange: (value: any) => void };
|
||||
{ onBlur: (value: any) => void } | { onChange: (value: any) => void };
|
||||
errorMessage?: string | null;
|
||||
helpText?: string;
|
||||
required?: boolean;
|
||||
@@ -31,5 +30,6 @@ export interface LabeledErrorBoundInputProps {
|
||||
id?: string;
|
||||
classname?: string;
|
||||
visibilityToggle?: boolean;
|
||||
isValidating?: boolean;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
@@ -77,8 +77,7 @@ customIcons.forEach(customIcon => {
|
||||
});
|
||||
|
||||
export type IconNameType =
|
||||
| keyof typeof antdEnhancedIcons
|
||||
| keyof typeof iconOverrides;
|
||||
keyof typeof antdEnhancedIcons | keyof typeof iconOverrides;
|
||||
|
||||
type IconComponentType = Record<IconNameType, IconComponent>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,12 +27,7 @@ import type {
|
||||
export type OnClickHandler = MouseEventHandler<HTMLElement>;
|
||||
|
||||
export type LabelType =
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'error'
|
||||
| 'info'
|
||||
| 'default'
|
||||
| 'primary';
|
||||
'success' | 'warning' | 'error' | 'info' | 'default' | 'primary';
|
||||
|
||||
export interface LabelProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
key?: string;
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
export type PositionOption =
|
||||
| 'floating'
|
||||
| 'inline'
|
||||
| 'inline-centered'
|
||||
| 'normal';
|
||||
'floating' | 'inline' | 'inline-centered' | 'normal';
|
||||
|
||||
export type SizeOption = 's' | 'm' | 'l';
|
||||
|
||||
|
||||
@@ -81,11 +81,4 @@ export type Tags = {
|
||||
};
|
||||
|
||||
export type ContentType =
|
||||
| Dashboards
|
||||
| Description
|
||||
| LastModified
|
||||
| Owner
|
||||
| Rows
|
||||
| Sql
|
||||
| Table
|
||||
| Tags;
|
||||
Dashboards | Description | LastModified | Owner | Rows | Sql | Table | Tags;
|
||||
|
||||
@@ -73,14 +73,16 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
const closeButtonWidth = theme.sizeUnit * 14;
|
||||
|
||||
return css`
|
||||
${responsive &&
|
||||
css`
|
||||
max-width: ${maxWidth ?? '900px'};
|
||||
padding-left: ${theme.sizeUnit * 3}px;
|
||||
padding-right: ${theme.sizeUnit * 3}px;
|
||||
padding-bottom: 0;
|
||||
top: 0;
|
||||
`}
|
||||
${
|
||||
responsive &&
|
||||
css`
|
||||
max-width: ${maxWidth ?? '900px'};
|
||||
padding-left: ${theme.sizeUnit * 3}px;
|
||||
padding-right: ${theme.sizeUnit * 3}px;
|
||||
padding-bottom: 0;
|
||||
top: 0;
|
||||
`
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
background-color: ${theme.colorBgContainer};
|
||||
@@ -168,40 +170,46 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
${draggable &&
|
||||
css`
|
||||
.ant-modal-header {
|
||||
padding: 0;
|
||||
${
|
||||
draggable &&
|
||||
css`
|
||||
.ant-modal-header {
|
||||
padding: 0;
|
||||
|
||||
.draggable-trigger {
|
||||
cursor: move;
|
||||
padding: ${theme.sizeUnit * 4}px ${closeButtonWidth}px
|
||||
${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`}
|
||||
|
||||
${resizable &&
|
||||
css`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.resizable-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100%;
|
||||
|
||||
.ant-modal-body {
|
||||
height: ${hideFooter
|
||||
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
|
||||
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`};
|
||||
.draggable-trigger {
|
||||
cursor: move;
|
||||
padding: ${theme.sizeUnit * 4}px ${closeButtonWidth}px
|
||||
${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
`
|
||||
}
|
||||
|
||||
${
|
||||
resizable &&
|
||||
css`
|
||||
.resizable {
|
||||
pointer-events: all;
|
||||
|
||||
.resizable-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100%;
|
||||
|
||||
.ant-modal-body {
|
||||
height: ${
|
||||
hideFooter
|
||||
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
|
||||
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
// TODO: Upgrade to remark-gfm v4+ after migrating to React 18.
|
||||
// remark-gfm v4+ requires react-markdown v9+, which requires React 18.
|
||||
// Currently pinned to v3.0.1 for compatibility with react-markdown v8 and React 17.
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { cloneDeep, mergeWith } from 'lodash-es';
|
||||
import { FeatureFlag, isFeatureEnabled } from '../../utils';
|
||||
@@ -139,7 +136,7 @@ export function SafeMarkdown({
|
||||
rehypePlugins={rehypePlugins}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
skipHtml={false}
|
||||
transformLinkUri={transformLinkUri}
|
||||
urlTransform={transformLinkUri}
|
||||
>
|
||||
{source}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -398,6 +398,25 @@ test('removes duplicated values', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('trims whitespace from pasted comma-separated values', async () => {
|
||||
render(<AsyncSelect {...defaultProps} mode="multiple" allowNewOptions />);
|
||||
const input = getElementByClassName('.ant-select-selection-search-input');
|
||||
const paste = createEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => 'a, b, c , d',
|
||||
},
|
||||
});
|
||||
fireEvent(input, paste);
|
||||
await waitFor(async () => {
|
||||
const values = await findAllSelectValues();
|
||||
expect(values.length).toBe(4);
|
||||
expect(values[0]).toHaveTextContent('a');
|
||||
expect(values[1]).toHaveTextContent('b');
|
||||
expect(values[2]).toHaveTextContent('c');
|
||||
expect(values[3]).toHaveTextContent('d');
|
||||
});
|
||||
});
|
||||
|
||||
test('renders a custom label', async () => {
|
||||
const loadOptions = jest.fn(async () => ({
|
||||
data: [
|
||||
|
||||
@@ -694,7 +694,14 @@ const AsyncSelect = forwardRef(
|
||||
}
|
||||
} else {
|
||||
const token = tokenSeparators.find(token => pastedText.includes(token));
|
||||
const array = token ? uniq(pastedText.split(token)) : [pastedText];
|
||||
const array = token
|
||||
? uniq(
|
||||
pastedText
|
||||
.split(token)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
)
|
||||
: [pastedText.trim()].filter(Boolean);
|
||||
const values = (
|
||||
await Promise.all(array.map(item => getPastedTextValue(item)))
|
||||
).filter(item => item !== undefined) as AntdLabeledValue[];
|
||||
|
||||
@@ -378,6 +378,23 @@ test('removes duplicated values', async () => {
|
||||
expect(values[3]).toHaveTextContent('d');
|
||||
});
|
||||
|
||||
test('trims whitespace from pasted comma-separated values', async () => {
|
||||
render(<Select {...defaultProps} mode="multiple" allowNewOptions />);
|
||||
const input = getElementByClassName('.ant-select-selection-search-input');
|
||||
const paste = createEvent.paste(input, {
|
||||
clipboardData: {
|
||||
getData: () => 'a, b, c , d',
|
||||
},
|
||||
});
|
||||
fireEvent(input, paste);
|
||||
const values = await findAllSelectValues();
|
||||
expect(values.length).toBe(4);
|
||||
expect(values[0]).toHaveTextContent('a');
|
||||
expect(values[1]).toHaveTextContent('b');
|
||||
expect(values[2]).toHaveTextContent('c');
|
||||
expect(values[3]).toHaveTextContent('d');
|
||||
});
|
||||
|
||||
test('renders a custom label', async () => {
|
||||
const options = [
|
||||
{ value: 'John', label: <h1>John</h1> },
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -51,8 +51,10 @@ const StyledTabs = ({
|
||||
.ant-tabs-content-holder {
|
||||
overflow: ${allowOverflow ? 'visible' : 'auto'};
|
||||
${fullHeight && 'height: 100%;'}
|
||||
${contentHeight &&
|
||||
`height: ${typeof contentHeight === 'number' ? `${contentHeight}px` : contentHeight};`}
|
||||
${
|
||||
contentHeight &&
|
||||
`height: ${typeof contentHeight === 'number' ? `${contentHeight}px` : contentHeight};`
|
||||
}
|
||||
${contentPadding}
|
||||
}
|
||||
.ant-tabs-content {
|
||||
@@ -66,9 +68,11 @@ const StyledTabs = ({
|
||||
margin: 0;
|
||||
}
|
||||
.ant-tabs-nav-wrap {
|
||||
${!(tabBarStyle && 'paddingLeft' in tabBarStyle)
|
||||
? `padding: 0 ${theme.sizeUnit * 4}px;`
|
||||
: ''}
|
||||
${
|
||||
!(tabBarStyle && 'paddingLeft' in tabBarStyle)
|
||||
? `padding: 0 ${theme.sizeUnit * 4}px;`
|
||||
: ''
|
||||
}
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
flex: 1 1 auto;
|
||||
|
||||
@@ -211,6 +211,7 @@ export type {
|
||||
GridState,
|
||||
GridReadyEvent,
|
||||
CellClickedEvent,
|
||||
CellKeyDownEvent,
|
||||
CellClassParams,
|
||||
IMenuActionParams,
|
||||
IHeaderParams,
|
||||
|
||||
@@ -51,9 +51,7 @@ export type JsonPrimitive = string | number | boolean | null;
|
||||
* (Ref: https://github.com/microsoft/TypeScript/issues/15300).
|
||||
*/
|
||||
export type StrictJsonValue =
|
||||
| JsonPrimitive
|
||||
| StrictJsonObject
|
||||
| StrictJsonArray;
|
||||
JsonPrimitive | StrictJsonObject | StrictJsonArray;
|
||||
export type StrictJsonArray = StrictJsonValue[];
|
||||
/**
|
||||
* More strict JSON objects that makes sure all values are plain objects.
|
||||
@@ -79,12 +77,7 @@ export type Mode = RequestInit['mode'];
|
||||
export type Redirect = RequestInit['redirect'];
|
||||
export type ClientTimeout = number | undefined;
|
||||
export type ParseMethod =
|
||||
| 'json'
|
||||
| 'json-bigint'
|
||||
| 'text'
|
||||
| 'raw'
|
||||
| null
|
||||
| undefined;
|
||||
'json' | 'json-bigint' | 'text' | 'raw' | null | undefined;
|
||||
export type Signal = RequestInit['signal'];
|
||||
export type Stringify = boolean;
|
||||
export type Url = string;
|
||||
|
||||
@@ -18,12 +18,6 @@
|
||||
*/
|
||||
|
||||
export type RowDataValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Date
|
||||
| bigint
|
||||
| null
|
||||
| undefined;
|
||||
string | number | boolean | Date | bigint | null | undefined;
|
||||
|
||||
export type RowData = Record<string, RowDataValue>;
|
||||
|
||||
@@ -36,9 +36,7 @@ export interface ItemWithLoader<T> {
|
||||
type InclusiveLoaderResult<V> = V | Promise<V>;
|
||||
|
||||
export type RegistryValue<V, W extends InclusiveLoaderResult<V>> =
|
||||
| V
|
||||
| W
|
||||
| undefined;
|
||||
V | W | undefined;
|
||||
|
||||
export type RegistryEntry<V, W extends InclusiveLoaderResult<V>> = {
|
||||
key: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user