Compare commits
103 Commits
chart-samp
...
enxdev/fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a10f39c20 | ||
|
|
a0d9e82c60 | ||
|
|
15f8b8e9b4 | ||
|
|
31351eb52a | ||
|
|
8e549e4975 | ||
|
|
e65b2c49d7 | ||
|
|
2c8cdbbd9b | ||
|
|
83716cb8c0 | ||
|
|
33f0fc93ed | ||
|
|
01e872c105 | ||
|
|
34d4966f8b | ||
|
|
001d6c9692 | ||
|
|
e333135a0d | ||
|
|
dc5c5af467 | ||
|
|
deefe897f1 | ||
|
|
49ed206381 | ||
|
|
8a599d70b4 | ||
|
|
d81b135a04 | ||
|
|
211bcbc635 | ||
|
|
3b1abf8260 | ||
|
|
01cd215202 | ||
|
|
2dab31c130 | ||
|
|
f74de0eb6c | ||
|
|
206155f986 | ||
|
|
f6d03e4a68 | ||
|
|
0efcd54250 | ||
|
|
3fa000ba0b | ||
|
|
9617155289 | ||
|
|
e49945fc2f | ||
|
|
866f8c51ee | ||
|
|
98fc857613 | ||
|
|
ddca50c820 | ||
|
|
c253b68d7b | ||
|
|
050acc6c30 | ||
|
|
7b95d8b17c | ||
|
|
0d9533fdf8 | ||
|
|
6b1311a6c7 | ||
|
|
f1c0a461fd | ||
|
|
ef10501e6b | ||
|
|
17ae25b8d4 | ||
|
|
2a77c0156a | ||
|
|
8f4c411765 | ||
|
|
0936160633 | ||
|
|
d9e1f34527 | ||
|
|
4038c9d1e6 | ||
|
|
8108496e2a | ||
|
|
098b95560d | ||
|
|
6edbf425f7 | ||
|
|
8e98ae701b | ||
|
|
054be4d45e | ||
|
|
fbc24959ab | ||
|
|
fe7d9b4724 | ||
|
|
5f27f88e73 | ||
|
|
04f8b700d7 | ||
|
|
a01a4ae511 | ||
|
|
0d9b3f3b05 | ||
|
|
51a171f821 | ||
|
|
19c982d8d3 | ||
|
|
973366d30b | ||
|
|
7b0969131f | ||
|
|
00a9546de9 | ||
|
|
de5a31a2cf | ||
|
|
af058ccecb | ||
|
|
d0b42d8c19 | ||
|
|
f0e25fb978 | ||
|
|
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 |
300
.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
|
||||
@@ -50,7 +46,6 @@ updates:
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -78,8 +73,6 @@ updates:
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/docs/"
|
||||
ignore:
|
||||
- dependency-name: "react-error-boundary"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
@@ -109,296 +102,3 @@ updates:
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
# Now for all of our plugins and packages!
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-calendar/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-partition/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-world-map/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-pivot-table/"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 19.0.0
|
||||
- dependency-name: "react-icons"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-chord/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-horizon/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-rose/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-preset-chart-deckgl/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-table/"
|
||||
ignore:
|
||||
# TODO: remove below entries until React >= 19.0.0
|
||||
- dependency-name: "react-icons"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-country-map/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-map-box/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-preset-chart-nvd3/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-word-cloud/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-echarts/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-ag-grid-table/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-cartodiagram/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/plugins/plugin-chart-handlebars/"
|
||||
ignore:
|
||||
# `just-handlerbars-helpers` library in plugin-chart-handlebars requires `currencyformatter`` to be < 2
|
||||
- dependency-name: "currencyformatter.js"
|
||||
update-types: ["version-update:semver-major"]
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/generator-superset/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-chart-controls/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- 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:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/superset-frontend/packages/superset-ui-switchboard/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- npm
|
||||
- dependabot
|
||||
open-pull-requests-limit: 5
|
||||
versioning-strategy: increase
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
@@ -619,14 +619,34 @@ categories:
|
||||
- name: Symmetrics
|
||||
url: https://www.symmetrics.fyi
|
||||
|
||||
Government:
|
||||
Government / Public authority:
|
||||
- name: Centre Régional Auvergnat de l'Information Géographique (France)
|
||||
url: https://www.craig.fr/
|
||||
logo: craig.png
|
||||
|
||||
- name: City of Ann Arbor, MI
|
||||
url: https://www.a2gov.org/
|
||||
contributors: ["@sfirke"]
|
||||
|
||||
- name: Métropole Européenne de Lille (France)
|
||||
url: https://data.lillemetropole.fr/accueil/
|
||||
logo: mel.jpg
|
||||
|
||||
- name: NRLM - Sarathi, India
|
||||
url: https://pib.gov.in/PressReleasePage.aspx?PRID=1999586
|
||||
|
||||
- name: Région Bretagne (France)
|
||||
url: https://geobretagne.fr/
|
||||
logo: geobretagne.svg
|
||||
|
||||
- name: Région Hauts-de-France (France)
|
||||
url: https://www.geo2france.fr/
|
||||
logo: geo2france.svg
|
||||
|
||||
- name: Rennes Métropole (France)
|
||||
url: https://metropole.rennes.fr/
|
||||
logo: rennes-ville-metropole_noir.svg
|
||||
|
||||
- name: RIS3 Strategy of CZ, MIT CR
|
||||
url: https://www.ris3.cz/
|
||||
contributors: ["@RIS3CZ"]
|
||||
@@ -682,6 +702,10 @@ categories:
|
||||
url: https://automattic.com/
|
||||
contributors: ["@Khrol", "@Usiel"]
|
||||
|
||||
- name: CamptoCamp
|
||||
url: https://camptocamp.com/
|
||||
logo: camptocamp.svg
|
||||
|
||||
- name: Dropbox
|
||||
url: https://www.dropbox.com/
|
||||
contributors: ["@bkyryliuk"]
|
||||
@@ -690,6 +714,10 @@ categories:
|
||||
url: https://flowbird.com
|
||||
contributors: ["@EmmanuelCbd"]
|
||||
|
||||
- name: geOrchestra
|
||||
url: https://www.georchestra.org
|
||||
logo: georchestra.svg
|
||||
|
||||
- name: GEOTAB
|
||||
url: https://www.geotab.com
|
||||
contributors: ["@JZ6"]
|
||||
@@ -706,6 +734,10 @@ categories:
|
||||
url: https://www.increff.com/
|
||||
contributors: ["@ishansinghania"]
|
||||
|
||||
- name: jDev
|
||||
url: https://jdev.fr/
|
||||
logo: jDev.svg
|
||||
|
||||
- name: Let's Roam
|
||||
url: https://www.letsroam.com/
|
||||
|
||||
@@ -716,6 +748,11 @@ categories:
|
||||
url: https://1beat.com/
|
||||
contributors: ["@GuyAttia"]
|
||||
|
||||
- name: pi-Geosolutions
|
||||
url: https://www.pi-geosolutions.fr/
|
||||
logo: pi-Geosolutions.png
|
||||
contributors: ["@jeanpommier"]
|
||||
|
||||
- name: X
|
||||
url: https://x.com/
|
||||
|
||||
|
||||
128
UPDATING.md
@@ -24,6 +24,94 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Owners, dashboard roles, and RLS roles replaced by Subjects
|
||||
|
||||
Superset now uses subject-based access assignments for dashboards, charts, datasets,
|
||||
alerts/reports, and Row Level Security. A Subject can represent a user, role, or group.
|
||||
|
||||
This is a breaking API and metadata change:
|
||||
|
||||
- `owners` is replaced by `editors` for dashboards, charts, datasets, and alerts/reports.
|
||||
- Dashboard `roles` and the `DASHBOARD_RBAC` feature flag are replaced by dashboard/chart
|
||||
`viewers`, enabled with `ENABLE_VIEWERS`.
|
||||
- RLS `roles` is replaced by `subjects`.
|
||||
- The legacy `dashboard_user`, `slice_user`, `sqlatable_user`, `report_schedule_user`,
|
||||
`dashboard_roles`, and `rls_filter_roles` tables are migrated into subject junction tables
|
||||
and dropped on upgrade.
|
||||
|
||||
For deployments that previously used `DASHBOARD_RBAC` and later disabled it: remove stale rows
|
||||
from the legacy `dashboard_roles` table before upgrading, otherwise those role assignments will
|
||||
become active dashboard Viewers after migration.
|
||||
|
||||
API clients and automation should send and read `editors`, `viewers`, and `subjects` instead
|
||||
of the legacy fields.
|
||||
|
||||
Subject pickers support users, groups, and roles, but only users and groups are selectable by
|
||||
default. Roles remain supported as Subject types for backwards compatibility with RLS role
|
||||
assignments and the previous `DASHBOARD_RBAC` model, but they are not recommended for new
|
||||
resource-specific assignments. Prefer groups for membership-based access and keep roles focused
|
||||
on capability grants. Existing Role subject assignments remain effective after migration even when
|
||||
Roles are hidden from the default dropdown values; configure the relevant `SUBJECTS_RELATED_TYPES_*`
|
||||
setting to make Roles selectable when editing subject lists. See the [Security documentation](docs/admin_docs/security/security.mdx#subjects)
|
||||
for the full Subject model and picker configuration guidance.
|
||||
|
||||
To make roles selectable everywhere:
|
||||
|
||||
```python
|
||||
from superset.subjects.types import SubjectType
|
||||
|
||||
SUBJECTS_RELATED_TYPES = [
|
||||
SubjectType.USER,
|
||||
SubjectType.GROUP,
|
||||
SubjectType.ROLE,
|
||||
]
|
||||
```
|
||||
|
||||
To make roles selectable for RLS while other pickers keep the user and group default, use the
|
||||
RLS-specific override:
|
||||
|
||||
```python
|
||||
from superset.subjects.types import SubjectType
|
||||
|
||||
SUBJECTS_RELATED_TYPES_RLS = [
|
||||
SubjectType.USER,
|
||||
SubjectType.GROUP,
|
||||
SubjectType.ROLE,
|
||||
]
|
||||
```
|
||||
|
||||
Entity-specific `SUBJECTS_RELATED_TYPES_*` settings replace `SUBJECTS_RELATED_TYPES` for that
|
||||
picker.
|
||||
|
||||
Deployments using `EXTRA_OWNERS_RESOLVER` must migrate to `EXTRA_EDITORS_RESOLVER`. The new
|
||||
resolver should return editor Subjects, subject IDs, or dicts with an `id` key instead of FAB
|
||||
User objects. API responses expose these dynamic assignments as `extra_editors` instead of
|
||||
`extra_owners`.
|
||||
|
||||
`DASHBOARD_RBAC` has been removed. To preserve the previous Dashboard RBAC behavior, enable both
|
||||
subject viewers and viewer datasource bypass:
|
||||
|
||||
```python
|
||||
FEATURE_FLAGS = {
|
||||
"ENABLE_VIEWERS": True,
|
||||
}
|
||||
VIEWER_PROMISCUOUS_MODE = True
|
||||
```
|
||||
|
||||
Enabling only `ENABLE_VIEWERS` allows assigning dashboard/chart viewer subjects, but viewers still
|
||||
need normal datasource permissions unless `VIEWER_PROMISCUOUS_MODE` is also enabled.
|
||||
|
||||
For backwards compatibility, enabling `ENABLE_VIEWERS` does not change access for dashboards or
|
||||
charts that have no assigned viewers. Those resources continue to use the implicit dataset-access
|
||||
model: users who can access the underlying dataset can still see published dashboards that use that
|
||||
dataset and charts backed by that dataset.
|
||||
Assigning one or more viewers opts that resource into explicit viewer access for non-editors. To
|
||||
return a resource to the implicit dataset-access model, remove all viewers from it. Explicit Viewers
|
||||
are the intended model going forward; deprecating and removing implicit viewership can be considered
|
||||
in a later major version.
|
||||
|
||||
- [41044](https://github.com/apache/superset/issues/41044): Removes the deprecated `AVOID_COLORS_COLLISION` feature flag (it defaulted to `True`). Color-collision avoidance is now permanently enabled; any config override setting it to `False` is ignored.
|
||||
|
||||
- [39925](https://github.com/apache/superset/pull/39925): URL prefixing for `SUPERSET_APP_ROOT` subdirectory deployments is now handled automatically by helpers in `src/utils/navigationUtils` (`openInNewTab`, `redirect`, `getShareableUrl`, `<AppLink>`). Direct imports of `ensureAppRoot` / `makeUrl` from `src/utils/pathUtils` are forbidden outside `navigationUtils.ts` (enforced by a static-invariant test); contributors writing new code should use the focused helpers instead. No runtime behaviour change for existing callers — all 19 prior call sites have been migrated and four pre-existing double-prefix and missing-prefix bugs are fixed as part of the migration.
|
||||
|
||||
- [39925](https://github.com/apache/superset/pull/39925): `SupersetClient.getUrl()` now strips a single leading application-root segment from the supplied `endpoint` before building the request URL, so a caller that accidentally pre-prefixes its endpoint (for example by wrapping it with `ensureAppRoot` before passing it to the client) no longer produces a doubled `/superset/superset/...` URL under subdirectory deployment. The strip is **single-pass** — a genuine `/superset/superset/<slug>` route is preserved, not collapsed — and **silent** (no console warning); the static-invariant test remains the primary signal for pre-prefixing at the call site, and this runtime strip is a safety net beneath it. Code that intentionally targeted a literal `/<app_root>/<app_root>/...` endpoint through `getUrl` (a configuration that has no legitimate use under the prefixing model) would have its first redundant segment removed.
|
||||
@@ -42,6 +130,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:
|
||||
@@ -302,6 +392,36 @@ 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 datasets
|
||||
|
||||
**The soft-delete behavior 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/dataset/<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 datasets are soft-deleted during a flag-on window and the flag is later turned **off**, those rows reappear as live datasets in all lists, lookups, and relationship loads (including charts that reference them). The `POST /<uuid>/restore` endpoint and the `dataset_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.
|
||||
|
||||
**Flag-independent parts of this work** (active even with `SOFT_DELETE` off): the restore endpoint and deleted-state filter (above); the database-deletion guard counting soft-deleted datasets; the `get_or_create_dataset` soft-deleted-twin pre-check; the combined datasource listing (`GET /api/v1/datasource/...`) always excluding soft-deleted datasets; and the two uniqueness-validation changes documented at the end of this section. Everything else — the soft DELETE itself and the visibility filtering — is flag-gated.
|
||||
|
||||
With the flag enabled: `DELETE /api/v1/dataset/<id>` no longer hard-deletes the dataset (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. Datasets in this state are excluded from default queries and from relationship loads (e.g. `database.tables`).
|
||||
|
||||
**No cascade in v1.** Soft-delete does not propagate to dependent charts or dashboards: they remain visible. Loading a chart whose dataset is soft-deleted surfaces a "datasource not found" error at chart-load time. Restore the dataset to recover.
|
||||
|
||||
**Database deletion is blocked by soft-deleted datasets.** Superset already refuses to delete a database that still has datasets (`DatabaseDeleteDatasetsExistFailedError`); that check now explicitly counts soft-deleted datasets too (it bypasses the visibility filter), since the soft-deleted `tables` rows still reference the database via `database_id` and must not be orphaned. Consequence: because dataset `DELETE` is soft and v1 ships no hard-delete/purge, **a database that has ever had datasets cannot be deleted through the API once those datasets are soft-deleted** — the rows remain and keep blocking the delete. Until a purge capability lands, operators who must remove such a database have to hard-delete the underlying `tables` rows out-of-band first. This is a deliberate trade-off (no orphaned rows / restorable datasets) and is expected to be resolved by the planned purge work.
|
||||
|
||||
**Side-effect change for operators.** Because the row is no longer physically deleted, FAB `ab_view_menu` / permission-view rows tied to the dataset are also preserved. Downstream automation that relied on `DELETE /api/v1/dataset/<id>` cleaning up those rows must now react to the new `POST /api/v1/dataset/<uuid>/restore` lifecycle, or call the eventual hard-delete endpoint.
|
||||
|
||||
**New endpoint** — `POST /api/v1/dataset/<uuid>/restore` clears `deleted_at` and returns the dataset to active state. Requires `can_write on Dataset` and editorship of the row (or admin). Soft-deleted datasets can also be surfaced in the list endpoint via the new `dataset_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 datasets they can edit — the same audience that can restore them.
|
||||
|
||||
**Permissions migration:** existing role grants of `can_write on Dataset` 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_tables_deleted_at`) to the `tables` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `tables` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking). Production deployments with many thousands of datasets should run this migration during a maintenance window.
|
||||
|
||||
**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback.
|
||||
|
||||
**SQL Lab / dataset-creation flows:** creating a dataset over a table whose dataset sits in the trash is refused. The SQL Lab "save as dataset" flow (`get_or_create_dataset`) and file uploads return a **422 naming the hidden twin and the restore endpoint**; the plain create, update, and duplicate paths currently fail with the generic "already exists" 422. In all cases the remediation is the same: restore the hidden dataset (or use a different table name). Perm-string maintenance also covers hidden rows: renaming a database rewrites `perm`/`schema_perm`/`catalog_perm` on soft-deleted datasets and their charts, so a later restore does not resurrect stale permission strings.
|
||||
|
||||
**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
**Uniqueness-validation changes that apply regardless of the feature flag:** two dataset uniqueness checks were tightened alongside this work and are active even with `SOFT_DELETE` off. (1) Create/update uniqueness treats a dataset whose `catalog` is `NULL` as belonging to the database's default catalog, so a legacy twin pair (`catalog=NULL` vs. `catalog=<default>`, same database/schema/name) that older versions allowed now fails validation with "already exists" when either row is edited — resolve by renaming or removing one of the twins. (2) Duplicating a dataset now checks name collisions scoped to the target (database, catalog, schema) instead of globally by name alone: duplicates into other databases that were previously blocked are now allowed.
|
||||
|
||||
### 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.
|
||||
@@ -312,7 +432,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
|
||||
|
||||
**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.
|
||||
**New endpoint** — `POST /api/v1/chart/<uuid>/restore` clears `deleted_at` and returns the chart to active state. Requires `can_write on Chart` and editorship 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 can edit — 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.
|
||||
|
||||
@@ -320,7 +440,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
|
||||
|
||||
**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.
|
||||
**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path.
|
||||
|
||||
@@ -332,7 +452,7 @@ With the flag enabled: `DELETE /api/v1/chart/<id>` no longer hard-deletes the ch
|
||||
|
||||
With the flag enabled: `DELETE /api/v1/dashboard/<id>` no longer hard-deletes the dashboard (the bulk-delete endpoint behaves the same way). The row is marked with a `deleted_at` timestamp and hidden from the dashboard API's list, detail, and lookup endpoints, which return 404 for soft-deleted dashboards. The embedded-dashboard iframe URL (`/embedded/<uuid>`) keeps rendering because it reads only `embedded.allowed_domains` and `embedded.dashboard_id` (the FK column) without dereferencing the parent dashboard; the frontend's subsequent dashboard-API fetch is what sees the 404 and surfaces "dashboard not found" to the user.
|
||||
|
||||
**New endpoint** — `POST /api/v1/dashboard/<uuid>/restore` clears `deleted_at` and returns the dashboard to active state. Requires `can_write on Dashboard` and ownership of the row (or admin). Soft-deleted dashboards can also be surfaced in the list endpoint via the new `dashboard_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to dashboards they own — the same audience that can restore them.
|
||||
**New endpoint** — `POST /api/v1/dashboard/<uuid>/restore` clears `deleted_at` and returns the dashboard to active state. Requires `can_write on Dashboard` and editorship of the row (or admin). Soft-deleted dashboards can also be surfaced in the list endpoint via the new `dashboard_deleted_state` rison filter: `include` returns both live and soft-deleted rows, `only` returns just the soft-deleted ones. Any other value is ignored. For non-admin users, soft-deleted rows are limited to dashboards they can edit — the same audience that can restore them.
|
||||
|
||||
**Permissions migration:** existing role grants of `can_write on Dashboard` cover the new restore endpoint automatically; no role migration is required.
|
||||
|
||||
@@ -344,7 +464,7 @@ The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WH
|
||||
|
||||
**Slug semantics:** on PostgreSQL and MySQL 8.0.13+, the slug of a soft-deleted dashboard is **free for reuse**. A new active dashboard can claim it immediately. Restoring a soft-deleted dashboard whose slug has since been claimed returns **422 with a clean error** (`DashboardSlugConflictError`) — rename one of the dashboards and retry; the restore is not silently rejected by a database-level constraint violation.
|
||||
|
||||
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, role grants, owners, tags) — including role grants that were implicitly revoked by the deletion. Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and ownership of the deleted row (or admin) — non-owners get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row.
|
||||
|
||||
### Granular Export Controls
|
||||
|
||||
|
||||
@@ -287,8 +287,10 @@ to specify on behalf of which username to render the dashboards. In general, das
|
||||
are not accessible to unauthorized requests, that is why the worker needs to take over credentials
|
||||
of an existing user to take a snapshot.
|
||||
|
||||
By default, Alerts and Reports are executed as the owner of the alert/report object. To use a fixed user account,
|
||||
just change the config as follows (`admin` in this example):
|
||||
By default, Alerts and Reports are executed as a physical user represented by the alert/report editors
|
||||
(giving priority to the last modifier, then the creator, then the first direct user editor, then a
|
||||
deterministic user from editor roles or groups). To use a fixed user account, just change the config
|
||||
as follows (`admin` in this example):
|
||||
|
||||
```python
|
||||
from superset.tasks.types import FixedExecutor
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -139,6 +139,10 @@ from superset.tasks.types import FixedExecutor
|
||||
THUMBNAIL_EXECUTORS = [FixedExecutor("admin")]
|
||||
```
|
||||
|
||||
When using `ExecutorType.EDITOR`, thumbnails are rendered as a physical user represented by
|
||||
the dashboard or chart editors. Superset prioritizes the last modifier, then the creator,
|
||||
then the first direct user editor, then a deterministic user from editor roles or groups.
|
||||
|
||||
For this feature you will need a cache system and celery workers. All thumbnails are stored on cache
|
||||
and are processed asynchronously by the workers.
|
||||
|
||||
@@ -179,10 +183,10 @@ WEBDRIVER_BASEURL = "https://superset.company.com"
|
||||
|
||||
To control which user account is used for rendering thumbnails and warming up caches, configure
|
||||
`THUMBNAIL_EXECUTORS` and `CACHE_WARMUP_EXECUTORS`. Each accepts a list of executor types (which
|
||||
resolve to an owner, creator, modifier, or the currently-logged-in user) and/or a `FixedExecutor`
|
||||
pinned to a specific username. By default, thumbnails render as the current user
|
||||
(`ExecutorType.CURRENT_USER`) and cache warmup runs as the chart/dashboard owner
|
||||
(`ExecutorType.OWNER`).
|
||||
resolve to an editor, creator, modifier, or the currently-logged-in user) and/or a
|
||||
`FixedExecutor` pinned to a specific username. By default, thumbnails render as the current user
|
||||
(`ExecutorType.CURRENT_USER`) and cache warmup uses editor-based execution
|
||||
(`ExecutorType.EDITOR`) where executor-based cache warmup is used.
|
||||
|
||||
To force both to run as a dedicated service account (`admin` in this example):
|
||||
|
||||
|
||||
@@ -60,9 +60,9 @@ There are two approaches to making dashboards publicly accessible:
|
||||
|
||||
**Option 2: Dashboard-level access (selective control)**
|
||||
1. Set `PUBLIC_ROLE_LIKE = "Public"` in `superset_config.py`
|
||||
2. Add the `'DASHBOARD_RBAC': True` [Feature Flag](/admin-docs/configuration/feature-flags)
|
||||
3. Edit each dashboard's properties and add the "Public" role
|
||||
4. Only dashboards with the Public role explicitly assigned are visible to anonymous users
|
||||
2. Add the `'ENABLE_VIEWERS': True` [Feature Flag](/admin-docs/configuration/feature-flags)
|
||||
3. Edit each dashboard's properties and add the "Public" role subject as a viewer
|
||||
4. Only published dashboards with the Public role subject explicitly assigned are visible to anonymous users
|
||||
|
||||
See the [Public role documentation](/admin-docs/security/#public) for more details.
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ https://superset.apache.org/admin-docs/configuration/configuring-superset/#rotat
|
||||
|
||||
### **Appendix C: Secrets Register and Rotation Schedule**
|
||||
|
||||
`SUPERSET_SECRET_KEY` is not the only security-critical secret in a Superset deployment. Maintain an inventory of all such secrets, store each in a secrets manager (not in `superset_config.py` or version control), assign an owner, and rotate them on a defined cadence as well as after any suspected compromise.
|
||||
`SUPERSET_SECRET_KEY` is not the only security-critical secret in a Superset deployment. Maintain an inventory of all such secrets, store each in a secrets manager (not in `superset_config.py` or version control), assign a responsible maintainer, and rotate them on a defined cadence as well as after any suspected compromise.
|
||||
|
||||
| Secret | Purpose | Risk if leaked | Suggested rotation |
|
||||
|---|---|---|---|
|
||||
|
||||
@@ -91,24 +91,24 @@ PUBLIC_ROLE_LIKE = "Public"
|
||||
# PUBLIC_ROLE_LIKE = "Gamma"
|
||||
```
|
||||
|
||||
If you prefer to manually configure the Public role's permissions (or use `DASHBOARD_RBAC` to
|
||||
grant access at the dashboard level), you do not need to set `PUBLIC_ROLE_LIKE`.
|
||||
If you prefer to manually configure the Public role's permissions, or use subject-based
|
||||
dashboard viewer assignments, you do not need to set `PUBLIC_ROLE_LIKE`.
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- **Data access is still required:** The Public role only grants UI/API permissions. You must
|
||||
also grant access to specific datasets necessary to view a dashboard. As with other roles,
|
||||
this can be done in two ways:
|
||||
- **Data access is still required by default:** The Public role only grants UI/API permissions.
|
||||
You must also grant access to specific datasets necessary to view a dashboard. As with other
|
||||
roles, this can be done in two ways:
|
||||
|
||||
- **Without `DASHBOARD_RBAC`:** Dashboards only appear in the list and are accessible if
|
||||
the user has permission to at least one of their datasets. Grant dataset access by editing
|
||||
the Public role in the Superset UI (Menu → Security → List Roles → Public) and adding the
|
||||
relevant data sources. All published dashboards using those datasets become visible.
|
||||
- **Dataset-based access:** Dashboards only appear in the list and are accessible if the user
|
||||
has permission to at least one of their datasets. Grant dataset access by editing the Public
|
||||
role in the Superset UI (Menu -> Security -> List Roles -> Public) and adding the relevant
|
||||
data sources. All published dashboards using those datasets become visible.
|
||||
|
||||
- **With `DASHBOARD_RBAC` enabled:** Anonymous users will only see dashboards where the
|
||||
"Public" role has been explicitly added in the dashboard's properties. Dataset permissions
|
||||
are not required—DASHBOARD_RBAC handles the cascading permissions check. This provides
|
||||
fine-grained control over which dashboards are publicly visible.
|
||||
- **Subject-based dashboard access:** When `ENABLE_VIEWERS` is enabled, anonymous users can
|
||||
see published dashboards where the "Public" role subject has been explicitly added as a
|
||||
viewer. If `VIEWER_PROMISCUOUS_MODE` is enabled, viewer access can bypass dataset
|
||||
permission checks for dashboard rendering.
|
||||
|
||||
- **Role synchronization:** Built-in role permissions (Admin, Alpha, Gamma, sql_lab, and Public
|
||||
when `PUBLIC_ROLE_LIKE = "Public"`) are synchronized when you run `superset init`. Any manual
|
||||
@@ -131,10 +131,76 @@ tables in the **Permissions** dropdown. To select the data sources you want to a
|
||||
You can then confirm with users assigned to the **Gamma** role that they see the
|
||||
objects (dashboards and slices) associated with the tables you just extended them.
|
||||
|
||||
### Subjects
|
||||
|
||||
A **subject** is a unified identity that can be granted access to Superset resources such as
|
||||
dashboards, charts, and datasets. Subjects come in three types:
|
||||
|
||||
- **User** — an individual Superset user
|
||||
- **Group** — a FAB group
|
||||
- **Role** — a FAB role (e.g. Alpha, Gamma, or any custom role)
|
||||
|
||||
Subjects are used throughout Superset to assign **editors** (who can modify a resource) and
|
||||
**viewers** (who can view it). For new resource-specific assignments, Superset defaults to
|
||||
**Users** and **Groups** in subject pickers. Groups are the recommended way to grant access to a
|
||||
set of people because they represent organizational membership, while roles remain focused on
|
||||
capability grants such as "can read dashboards" or "can write charts".
|
||||
|
||||
Role subjects are still supported for backwards compatibility with features that previously used
|
||||
roles directly, including Row Level Security role assignments and `DASHBOARD_RBAC`. Existing role
|
||||
subject assignments continue to be enforced even when roles are not exposed in the default picker
|
||||
configuration. However, when an administrator edits a subject list, roles are not available as new
|
||||
dropdown values unless that picker has been configured to expose role subjects. Roles are not
|
||||
recommended for new subject assignments, because using roles for both permissions and resource
|
||||
membership couples two separate concerns.
|
||||
|
||||
Administrators can control which subject types are available in pickers with
|
||||
`SUBJECTS_RELATED_TYPES`. By default, this setting exposes only users and groups:
|
||||
|
||||
```python
|
||||
from superset.subjects.types import SubjectType
|
||||
|
||||
SUBJECTS_RELATED_TYPES = [
|
||||
SubjectType.USER,
|
||||
SubjectType.GROUP,
|
||||
]
|
||||
```
|
||||
|
||||
Set `SUBJECTS_RELATED_TYPES = None` to expose all subject types, including roles. To expose roles
|
||||
only for a specific entity, set that entity's override:
|
||||
|
||||
```python
|
||||
from superset.subjects.types import SubjectType
|
||||
|
||||
SUBJECTS_RELATED_TYPES = [
|
||||
SubjectType.USER,
|
||||
SubjectType.GROUP,
|
||||
]
|
||||
|
||||
# Leave dashboard, chart, and alert/report overrides unset so they inherit
|
||||
# the users + groups default. Expose roles only in the RLS subject picker for
|
||||
# compatibility with existing role-based RLS workflows.
|
||||
SUBJECTS_RELATED_TYPES_RLS = [
|
||||
SubjectType.USER,
|
||||
SubjectType.GROUP,
|
||||
SubjectType.ROLE,
|
||||
]
|
||||
```
|
||||
|
||||
Available per-entity overrides are:
|
||||
|
||||
- `SUBJECTS_RELATED_TYPES_DASHBOARDS` for dashboard editor/viewer pickers
|
||||
- `SUBJECTS_RELATED_TYPES_CHARTS` for chart editor/viewer pickers
|
||||
- `SUBJECTS_RELATED_TYPES_RLS` for Row Level Security subject pickers
|
||||
- `SUBJECTS_RELATED_TYPES_ALERT_REPORTS` for alert/report editor pickers
|
||||
|
||||
When an override is set, it replaces `SUBJECTS_RELATED_TYPES` for that picker. When it is `None`,
|
||||
the picker inherits the global default.
|
||||
|
||||
### Dashboard Access Control
|
||||
|
||||
Access to dashboards is managed via owners (users that have edit permissions to the dashboard).
|
||||
Non-owner user access can be managed in two ways. Note that dashboards must be published to be
|
||||
Access to dashboards is managed via editors (subjects that have edit permissions to the dashboard).
|
||||
Non-editor user access can be managed in two ways. Note that dashboards must be published to be
|
||||
visible to other users.
|
||||
|
||||
#### Dataset-Based Access (Default)
|
||||
@@ -146,30 +212,40 @@ to a role (Menu → Security → List Roles).
|
||||
This is the simplest approach but provides all-or-nothing access based on dataset permissions—
|
||||
if a user has access to a dataset, they can see all published dashboards using that dataset.
|
||||
|
||||
#### Dashboard-Level Access (DASHBOARD_RBAC)
|
||||
#### Dashboard and Chart-Level Access
|
||||
|
||||
For fine-grained control over which dashboards specific roles can access, enable the
|
||||
`DASHBOARD_RBAC` feature flag:
|
||||
For fine-grained control over which dashboards and charts specific users or groups can access,
|
||||
enable subject-based viewers:
|
||||
|
||||
```python
|
||||
FEATURE_FLAGS = {
|
||||
"DASHBOARD_RBAC": True,
|
||||
"ENABLE_VIEWERS": True,
|
||||
}
|
||||
```
|
||||
|
||||
With this enabled, you can assign specific roles to each dashboard in its properties. Users
|
||||
will only see dashboards where their role is explicitly added.
|
||||
With this enabled, you can assign viewer subjects to each dashboard or chart in its properties.
|
||||
Users will see resources where they are assigned directly, or indirectly through a group subject.
|
||||
Role subjects can also be enabled for compatibility, but groups are recommended for new viewer
|
||||
assignments.
|
||||
|
||||
`ENABLE_VIEWERS` can be adopted incrementally. Dashboards and charts with no assigned viewers keep
|
||||
the existing implicit dataset-access behavior: users who can access the underlying dataset can still
|
||||
see published dashboards that use that dataset and charts backed by that dataset. Assigning one or
|
||||
more viewers changes that specific resource to explicit viewer access for non-editors. Removing all
|
||||
viewers returns the resource to the dataset-based fallback. Explicit Viewers are the preferred model
|
||||
going forward; the implicit fallback may be deprecated and removed in a later major version.
|
||||
|
||||
**Important considerations:**
|
||||
- Dashboard access **bypasses** dataset-level checks—granting a role access to a dashboard
|
||||
implicitly grants read access to all charts and datasets in that dashboard
|
||||
- Dashboards without any assigned roles fall back to dataset-based access
|
||||
- Viewer access uses normal dataset checks unless `VIEWER_PROMISCUOUS_MODE` is enabled
|
||||
- With `VIEWER_PROMISCUOUS_MODE`, dashboard viewer access can bypass dataset-level checks for
|
||||
charts and datasets in that dashboard
|
||||
- Dashboards and charts without assigned viewers fall back to dataset-based access
|
||||
- The dashboard must still be published to be visible
|
||||
|
||||
This feature is particularly useful for:
|
||||
- Making specific dashboards public while keeping others private
|
||||
- Granting access to dashboards without exposing the underlying datasets for other uses
|
||||
- Creating dashboard-specific access patterns that don't align with dataset ownership
|
||||
- Creating dashboard-specific access patterns that don't align with dataset permissions
|
||||
|
||||
### SQL Execution Security Considerations
|
||||
|
||||
@@ -301,16 +377,16 @@ based on the roles and permissions that were attributed.
|
||||
### Row Level Security
|
||||
|
||||
Using Row Level Security filters (under the **Security** menu) you can create filters
|
||||
that are assigned to a particular dataset, as well as a set of roles.
|
||||
that are assigned to a particular dataset, as well as a set of subjects.
|
||||
If you want members of the Finance team to only have access to
|
||||
rows where `department = "finance"`, you could:
|
||||
|
||||
- Create a Row Level Security filter with that clause (`department = "finance"`)
|
||||
- Then assign the clause to the **Finance** role and the dataset it applies to
|
||||
- Then assign the clause to the **Finance** role subject and the dataset it applies to
|
||||
|
||||
The **clause** field, which can contain arbitrary text, is then added to the generated
|
||||
SQL statement's WHERE clause. So you could even do something like create a filter
|
||||
for the last 30 days and apply it to a specific role, with a clause
|
||||
for the last 30 days and apply it to a specific subject, with a clause
|
||||
like `date_field > DATE_SUB(NOW(), INTERVAL 30 DAY)`. It can also support
|
||||
multiple conditions: `client_id = 6` AND `advertiser="foo"`, etc.
|
||||
|
||||
@@ -321,12 +397,14 @@ RLS clauses also support **Jinja templating** when `ENABLE_TEMPLATE_PROCESSING`
|
||||
|
||||
There are two types of RLS filters:
|
||||
|
||||
- **Regular** — The filter clause is applied when the querying user belongs to one of the
|
||||
roles assigned to the filter. Use this to restrict what specific roles can see.
|
||||
- **Base** — The filter clause is applied to **all** users _except_ those in the assigned
|
||||
roles. Use this to define a default restriction that privileged roles (e.g. Admin) are
|
||||
exempt from. For example, a Base filter with clause `1 = 0` and the Admin role would
|
||||
hide all rows from everyone except Admin — useful as a deny-by-default baseline.
|
||||
- **Regular** — The filter clause is applied when the querying user matches one of the
|
||||
subjects assigned to the filter. Use this to restrict what specific users, roles, or groups
|
||||
can see.
|
||||
- **Base** — The filter clause is applied to **all** users _except_ those matching the assigned
|
||||
subjects. Use this to define a default restriction that privileged subjects (e.g. the Admin
|
||||
role subject) are exempt from. For example, a Base filter with clause `1 = 0` and the Admin
|
||||
role subject would hide all rows from everyone except Admin - useful as a deny-by-default
|
||||
baseline.
|
||||
|
||||
#### Group Keys and Filter Combination
|
||||
|
||||
@@ -416,21 +494,20 @@ GET /api/v1/rowlevelsecurity/
|
||||
GET /api/v1/rowlevelsecurity/?q=(filters:!((col:tables,opr:rel_m_m,value:<dataset_id>)))
|
||||
```
|
||||
|
||||
**Filter RLS rules by role:**
|
||||
**Filter RLS rules by subject:**
|
||||
|
||||
```
|
||||
GET /api/v1/rowlevelsecurity/?q=(filters:!((col:roles,opr:rel_m_m,value:<role_id>)))
|
||||
GET /api/v1/rowlevelsecurity/?q=(filters:!((col:subjects,opr:rel_m_m,value:<subject_id>)))
|
||||
```
|
||||
|
||||
**View details of a specific rule** (including clause, assigned datasets, and roles):
|
||||
**View details of a specific rule** (including clause, assigned datasets, and subjects):
|
||||
|
||||
```
|
||||
GET /api/v1/rowlevelsecurity/<id>
|
||||
```
|
||||
|
||||
The response includes the filter's `name`, `filter_type` (Regular or Base), `clause`,
|
||||
`group_key`, assigned `tables` (with id, schema, and table\_name), and assigned `roles`
|
||||
(with id and name).
|
||||
`group_key`, assigned `tables` (with id, schema, and table\_name), and assigned `subjects`.
|
||||
|
||||
:::tip Auditing RLS for virtual datasets
|
||||
To find all RLS rules that could affect a particular virtual dataset, query the list
|
||||
|
||||
@@ -26,7 +26,7 @@ import { StoryWithControls } from '../../../src/components/StorybookWrapper';
|
||||
|
||||
# MetadataBar
|
||||
|
||||
MetadataBar displays a row of metadata items (SQL info, owners, last modified, tags, dashboards, etc.) that collapse responsively based on available width.
|
||||
MetadataBar displays a row of metadata items (SQL info, editors, last modified, tags, dashboards, etc.) that collapse responsively based on available width.
|
||||
|
||||
## Live Example
|
||||
|
||||
@@ -43,9 +43,9 @@ MetadataBar displays a row of metadata items (SQL info, owners, last modified, t
|
||||
title: "Click to view query"
|
||||
},
|
||||
{
|
||||
type: "owner",
|
||||
type: "editor",
|
||||
createdBy: "Jane Smith",
|
||||
owners: [
|
||||
editors: [
|
||||
"John Doe",
|
||||
"Mary Wilson"
|
||||
],
|
||||
@@ -104,9 +104,9 @@ function Demo() {
|
||||
const items = [
|
||||
{ type: 'sql', title: 'Click to view query' },
|
||||
{
|
||||
type: 'owner',
|
||||
type: 'editor',
|
||||
createdBy: 'Jane Smith',
|
||||
owners: ['John Doe', 'Mary Wilson'],
|
||||
editors: ['John Doe', 'Mary Wilson'],
|
||||
createdOn: 'a week ago',
|
||||
},
|
||||
{
|
||||
@@ -125,7 +125,7 @@ function Demo() {
|
||||
```tsx live
|
||||
function MinimalMetadata() {
|
||||
const items = [
|
||||
{ type: 'owner', createdBy: 'Admin', owners: ['Admin'], createdOn: 'yesterday' },
|
||||
{ type: 'editor', createdBy: 'Admin', editors: ['Admin'], createdOn: 'yesterday' },
|
||||
{ type: 'lastModified', value: '2 hours ago', modifiedBy: 'Admin' },
|
||||
];
|
||||
return <MetadataBar items={items} />;
|
||||
@@ -138,7 +138,7 @@ function MinimalMetadata() {
|
||||
function FullMetadata() {
|
||||
const items = [
|
||||
{ type: 'sql', title: 'SELECT * FROM ...' },
|
||||
{ type: 'owner', createdBy: 'Jane Smith', owners: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
|
||||
{ type: 'editor', createdBy: 'Jane Smith', editors: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
|
||||
{ type: 'lastModified', value: '3 days ago', modifiedBy: 'John Doe' },
|
||||
{ type: 'tags', values: ['production', 'finance', 'quarterly'] },
|
||||
{ type: 'dashboards', title: 'Used in 12 dashboards' },
|
||||
@@ -158,7 +158,7 @@ function FullMetadata() {
|
||||
| `createdBy` | `string` | `"Jane Smith"` | - |
|
||||
| `modifiedBy` | `string` | `"Jane Smith"` | - |
|
||||
| `description` | `string` | `"To preview the list of dashboards go to More settings."` | - |
|
||||
| `items` | `any` | `[{"type":"sql","title":"Click to view query"},{"type":"owner","createdBy":"Jane Smith","owners":["John Doe","Mary Wilson"],"createdOn":"a week ago"},{"type":"lastModified","value":"a week ago","modifiedBy":"Jane Smith"},{"type":"tags","values":["management","research","poc"]},{"type":"dashboards","title":"Added to 3 dashboards","description":"To preview the list of dashboards go to More settings."}]` | - |
|
||||
| `items` | `any` | `[{"type":"sql","title":"Click to view query"},{"type":"editor","createdBy":"Jane Smith","editors":["John Doe","Mary Wilson"],"createdOn":"a week ago"},{"type":"lastModified","value":"a week ago","modifiedBy":"Jane Smith"},{"type":"tags","values":["management","research","poc"]},{"type":"dashboards","title":"Added to 3 dashboards","description":"To preview the list of dashboards go to More settings."}]` | Items displayed in the metadata bar. The `editor` type shows editors/creators. |
|
||||
|
||||
## Import
|
||||
|
||||
|
||||
@@ -355,6 +355,17 @@ 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
|
||||
|
||||
```bash
|
||||
|
||||
@@ -128,7 +128,7 @@ from superset.dashboards.commands.create import CreateDashboardCommand
|
||||
def test_create_dashboard_success():
|
||||
properties = {
|
||||
"dashboard_title": "Test Dashboard",
|
||||
"owners": [1]
|
||||
"editors": [1]
|
||||
}
|
||||
|
||||
command = CreateDashboardCommand(properties)
|
||||
@@ -275,9 +275,11 @@ def handle_validation_error(error):
|
||||
from typing import List, Optional, Dict, Any
|
||||
from superset.models.dashboard import Dashboard
|
||||
|
||||
def get_dashboards_by_owner(owner_id: int) -> List[Dashboard]:
|
||||
"""Get all dashboards owned by a specific user"""
|
||||
return db.session.query(Dashboard).filter_by(owner_id=owner_id).all()
|
||||
def get_dashboards_by_editor(editor_id: int) -> List[Dashboard]:
|
||||
"""Get all dashboards editable by a specific subject"""
|
||||
return db.session.query(Dashboard).filter(
|
||||
Dashboard.editors.any(id=editor_id)
|
||||
).all()
|
||||
|
||||
def create_dashboard(properties: Dict[str, Any]) -> Optional[Dashboard]:
|
||||
"""Create a new dashboard with the given properties"""
|
||||
|
||||
@@ -153,10 +153,10 @@ class DashboardDAO:
|
||||
return db.session.query(Dashboard).filter_by(slug=slug).first()
|
||||
|
||||
@classmethod
|
||||
def find_by_owner(cls, owner_id: int) -> List[Dashboard]:
|
||||
"""Find all dashboards owned by a user"""
|
||||
return db.session.query(Dashboard).filter_by(
|
||||
created_by_fk=owner_id
|
||||
def find_by_editor(cls, editor_id: int) -> List[Dashboard]:
|
||||
"""Find all dashboards editable by a subject"""
|
||||
return db.session.query(Dashboard).filter(
|
||||
Dashboard.editors.any(id=editor_id)
|
||||
).all()
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -120,7 +120,7 @@ SMTP_MAIL_FROM = "superset@localhost"
|
||||
# Must match where your frontend is running
|
||||
WEBDRIVER_BASEURL = "http://localhost:9000/"
|
||||
|
||||
ALERT_REPORTS_EXECUTE_AS = [ExecutorType.OWNER]
|
||||
ALERT_REPORTS_EXECUTORS = [ExecutorType.EDITOR]
|
||||
|
||||
FEATURE_FLAGS = {
|
||||
"ALERT_REPORTS": True,
|
||||
|
||||
@@ -211,8 +211,8 @@ slices and dashboards of your own.
|
||||
|
||||
### Manage access to Dashboards
|
||||
|
||||
Access to dashboards is managed via owners and permissions. Non-owner access can be controlled
|
||||
through dataset permissions or dashboard-level roles (using the `DASHBOARD_RBAC` feature flag).
|
||||
Access to dashboards is managed via editors and permissions. Non-editor access can be controlled
|
||||
through dataset permissions, or through dashboard viewers when `ENABLE_VIEWERS` is enabled.
|
||||
|
||||
For detailed information on configuring dashboard access, see the
|
||||
[Dashboard Access Control](/admin-docs/security/#dashboard-access-control) section in the
|
||||
@@ -228,7 +228,9 @@ title of your dashboard.
|
||||
<img src={useBaseUrl("/img/tutorial/publish_button_dashboard.png" )} />
|
||||
|
||||
:::warning
|
||||
Draft dashboards are only visible to the dashboard owners and admins. Published dashboards are visible to all users with access to the underlying datasets or if RBAC is enabled, to the roles that have been granted access to the dashboard.
|
||||
Draft dashboards are only visible to dashboard editors and admins. Published dashboards are visible
|
||||
to users with access to the underlying datasets, or, when `ENABLE_VIEWERS` is enabled, to users
|
||||
who have viewer access directly or through an assigned role or group subject.
|
||||
:::
|
||||
|
||||
### Mark a Dashboard as Favorite
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
"docusaurus-plugin-openapi-docs": "^5.1.0",
|
||||
"docusaurus-theme-openapi-docs": "^5.1.0",
|
||||
"js-yaml": "^5.2.0",
|
||||
"js-yaml-loader": "^1.2.2",
|
||||
"json-bigint": "^1.0.0",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
@@ -118,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
@@ -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
|
||||
|
||||
21
docs/static/feature-flags.json
vendored
@@ -182,6 +182,13 @@
|
||||
"description": "Allows users to add a superset:// DB that can query across databases. Experimental with potential security/performance risks. See SUPERSET_META_DB_LIMIT.",
|
||||
"docs": "https://superset.apache.org/user-docs/databases/supported/superset-meta-database"
|
||||
},
|
||||
{
|
||||
"name": "ENABLE_VIEWERS",
|
||||
"default": false,
|
||||
"lifecycle": "testing",
|
||||
"description": "Subject-based viewer access control for dashboards and charts. When enabled, resources can have explicit viewer Subject assignments.",
|
||||
"category": "security"
|
||||
},
|
||||
{
|
||||
"name": "ESTIMATE_QUERY_COST",
|
||||
"default": false,
|
||||
@@ -256,14 +263,6 @@
|
||||
"description": "Enables CSS Templates in Settings menu and dashboard forms",
|
||||
"category": "runtime_config"
|
||||
},
|
||||
{
|
||||
"name": "DASHBOARD_RBAC",
|
||||
"default": false,
|
||||
"lifecycle": "stable",
|
||||
"description": "Role-based access control for dashboards",
|
||||
"docs": "https://superset.apache.org/docs/using-superset/creating-your-first-dashboard",
|
||||
"category": "runtime_config"
|
||||
},
|
||||
{
|
||||
"name": "DASHBOARD_VIRTUALIZATION",
|
||||
"default": true,
|
||||
@@ -401,12 +400,6 @@
|
||||
}
|
||||
],
|
||||
"deprecated": [
|
||||
{
|
||||
"name": "AVOID_COLORS_COLLISION",
|
||||
"default": true,
|
||||
"lifecycle": "deprecated",
|
||||
"description": "Avoid color collisions in charts by using distinct colors"
|
||||
},
|
||||
{
|
||||
"name": "DRILL_TO_DETAIL",
|
||||
"default": true,
|
||||
|
||||
4
docs/static/img/logos/camptocamp.svg
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/static/img/logos/craig.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
docs/static/img/logos/geo2france.png
vendored
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
2
docs/static/img/logos/geobretagne.svg
vendored
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
108
docs/static/img/logos/georchestra.svg
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="76.919907mm"
|
||||
height="26.149284mm"
|
||||
viewBox="0 0 76.919907 26.149284"
|
||||
version="1.1"
|
||||
id="svg855"
|
||||
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
|
||||
sodipodi:docname="georchestra-logo.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview857"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
fit-margin-top="2"
|
||||
fit-margin-right="2"
|
||||
fit-margin-bottom="2"
|
||||
fit-margin-left="2"
|
||||
inkscape:zoom="4.0920134"
|
||||
inkscape:cx="120.47859"
|
||||
inkscape:cy="47.775992"
|
||||
inkscape:window-width="2400"
|
||||
inkscape:window-height="1271"
|
||||
inkscape:window-x="2391"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs852" />
|
||||
<g
|
||||
inkscape:label="Calque 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-73.097929,-135.79112)">
|
||||
<g
|
||||
id="g53648"
|
||||
transform="matrix(1.1048988,0,0,1.1048988,-54.141498,96.632368)">
|
||||
<path
|
||||
id="path164290-3-6-4"
|
||||
style="fill:#85127e;fill-opacity:0.4;stroke-width:0.606168;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill"
|
||||
d="m 126.99257,37.251152 a 10.023325,10.023325 0 0 0 -10.0231,10.023355 10.023325,10.023325 0 0 0 10.0231,10.023083 10.023325,10.023325 0 0 0 9.62187,-7.232846 3.0318022,3.5371027 0 0 1 -2.01469,-3.330589 3.0318022,3.5371027 0 0 1 1.70117,-3.175791 10.023325,10.023325 0 0 0 -9.30835,-6.307212 z"
|
||||
inkscape:export-filename="C:\Users\Agathe\Desktop\georchestra_univers.png"
|
||||
inkscape:export-xdpi="200"
|
||||
inkscape:export-ydpi="200" />
|
||||
<path
|
||||
id="path20251-5-9-8-2"
|
||||
style="fill:#85127e;fill-opacity:1;stroke-width:0.337452;stroke-linecap:round;stroke-linejoin:round;paint-order:markers stroke fill"
|
||||
d="m 138.54037,41.724815 a 4.988567,4.988567 0 0 0 -4.9887,4.988712 4.988567,4.988567 0 0 0 4.9887,4.98844 4.988567,4.988567 0 0 0 4.98844,-4.98844 4.988567,4.988567 0 0 0 -4.98844,-4.988712 z m 0,2.683748 a 2.3048444,2.3048444 0 0 1 2.30472,2.304964 2.3048444,2.3048444 0 0 1 -2.30472,2.304704 2.3048444,2.3048444 0 0 1 -2.30496,-2.304704 2.3048444,2.3048444 0 0 1 2.30496,-2.304964 z"
|
||||
inkscape:export-filename="C:\Users\Agathe\Desktop\georchestra_univers.png"
|
||||
inkscape:export-xdpi="200"
|
||||
inkscape:export-ydpi="200" />
|
||||
<g
|
||||
aria-label="ge rchestra"
|
||||
id="text10089-26-6-9"
|
||||
style="font-size:9.62958px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:'Roboto, Normal';letter-spacing:0px;fill:#85127e;stroke-width:0.601849">
|
||||
<path
|
||||
d="m 123.84851,51.576041 q -0.66682,0 -1.14556,-0.177819 -0.47875,-0.177819 -0.76941,-0.475324 -0.29067,-0.297505 -0.40351,-0.653142 l 1.11478,-0.270148 q 0.0752,0.140203 0.21886,0.287245 0.14362,0.150463 0.38641,0.249631 0.24621,0.102588 0.61895,0.102588 0.52662,0 0.872,-0.25305 0.34537,-0.25305 0.34537,-0.817283 v -0.995103 h -0.0615 q -0.0957,0.191498 -0.28041,0.393254 -0.18123,0.201756 -0.48216,0.33854 -0.2975,0.136784 -0.74889,0.136784 -0.60527,0 -1.09769,-0.283827 -0.489,-0.287246 -0.77967,-0.854899 -0.28724,-0.571072 -0.28724,-1.42939 0,-0.865158 0.28724,-1.460167 0.29067,-0.59843 0.78309,-0.906193 0.49242,-0.311183 1.09769,-0.311183 0.46165,0 0.75915,0.157301 0.30092,0.153882 0.47874,0.372736 0.17782,0.215434 0.27015,0.406932 h 0.0684 v -0.868577 h 1.2208 v 5.337987 q 0,0.659982 -0.32144,1.09769 -0.32144,0.441128 -0.87884,0.656563 -0.55739,0.218854 -1.26525,0.218854 z m 0.0103,-3.104992 q 0.39325,0 0.67024,-0.191498 0.27699,-0.191497 0.42061,-0.550555 0.14362,-0.359057 0.14362,-0.861738 0,-0.495841 -0.14362,-0.868577 -0.1402,-0.372736 -0.41719,-0.577911 -0.27357,-0.208596 -0.67366,-0.208596 -0.41377,0 -0.69076,0.215435 -0.27699,0.215434 -0.41719,0.59159 -0.1402,0.372736 -0.1402,0.848059 0,0.482163 0.1402,0.84464 0.14362,0.359058 0.42061,0.560814 0.28041,0.198337 0.68734,0.198337 z"
|
||||
id="path1811" />
|
||||
<path
|
||||
d="m 129.91145,49.620032 q -0.78993,0 -1.36442,-0.328281 -0.57107,-0.3317 -0.87883,-0.936969 -0.30777,-0.608688 -0.30777,-1.43281 0,-0.810444 0.30777,-1.422552 0.31118,-0.615527 0.86857,-0.957486 0.5574,-0.345379 1.30971,-0.345379 0.48558,0 0.91645,0.157301 0.43429,0.153882 0.76599,0.478743 0.33512,0.324862 0.52662,0.827542 0.19149,0.499261 0.19149,1.19002 v 0.379575 h -4.30527 v -0.834382 h 3.11867 q -0.003,-0.355638 -0.15388,-0.632625 -0.15046,-0.280406 -0.42061,-0.441127 -0.26673,-0.160721 -0.62236,-0.160721 -0.37958,0 -0.66683,0.184658 -0.28724,0.181238 -0.44796,0.478743 -0.1573,0.294085 -0.16072,0.646303 v 0.728374 q 0,0.458226 0.16756,0.786507 0.16756,0.324862 0.46848,0.499261 0.30093,0.17098 0.70444,0.17098 0.27015,0 0.489,-0.07523 0.21885,-0.07865 0.37957,-0.229113 0.16073,-0.150462 0.2428,-0.372736 l 1.15582,0.129944 q -0.10943,0.458226 -0.41719,0.800186 -0.30435,0.33854 -0.77967,0.526617 -0.47532,0.184658 -1.08743,0.184658 z"
|
||||
id="path1813" />
|
||||
<path
|
||||
d="m 145.2449,49.517445 v -5.252498 h 1.20027 v 0.875416 h 0.0547 q 0.14362,-0.454806 0.49242,-0.701017 0.35222,-0.24963 0.8036,-0.24963 0.10259,0 0.22912,0.01026 0.12994,0.0068 0.21543,0.02394 v 1.138725 q -0.0787,-0.02736 -0.24963,-0.04787 -0.16756,-0.02394 -0.32486,-0.02394 -0.33854,0 -0.60869,0.147043 -0.26673,0.143623 -0.42061,0.400092 -0.15388,0.25647 -0.15388,0.59159 v 3.087895 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1815" />
|
||||
<path
|
||||
d="m 151.13685,49.620032 q -0.7865,0 -1.35074,-0.345379 -0.56081,-0.345379 -0.86516,-0.954067 -0.30092,-0.612107 -0.30092,-1.408873 0,-0.800185 0.30776,-1.412293 0.30777,-0.615527 0.86858,-0.957486 0.56423,-0.345379 1.33364,-0.345379 0.63947,0 1.13189,0.235952 0.49584,0.232532 0.78993,0.659982 0.29408,0.424029 0.33512,0.991682 h -1.18318 q -0.0718,-0.379575 -0.34196,-0.632625 -0.26673,-0.256469 -0.7147,-0.256469 -0.37957,0 -0.66682,0.205175 -0.28725,0.201756 -0.44797,0.581331 -0.1573,0.379575 -0.1573,0.909613 0,0.536876 0.1573,0.92329 0.1573,0.382995 0.44113,0.59159 0.28725,0.205176 0.67366,0.205176 0.27357,0 0.489,-0.102588 0.21886,-0.106007 0.3659,-0.304344 0.14704,-0.198336 0.20176,-0.482163 h 1.18318 q -0.0445,0.557394 -0.32829,0.988263 -0.28382,0.42745 -0.77282,0.670241 -0.48901,0.239371 -1.14899,0.239371 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1817" />
|
||||
<path
|
||||
d="m 155.64387,46.439809 v 3.077636 h -1.2379 v -7.003331 h 1.21054 v 2.643347 h 0.0615 q 0.18466,-0.444547 0.57107,-0.701017 0.38984,-0.259889 0.99169,-0.259889 0.54713,0 0.95406,0.229113 0.40694,0.229113 0.62921,0.67024 0.22569,0.441128 0.22569,1.077173 v 3.344364 h -1.23789 v -3.152867 q 0,-0.530037 -0.27357,-0.824122 -0.27015,-0.297505 -0.75915,-0.297505 -0.32828,0 -0.58817,0.143623 -0.25647,0.140203 -0.40351,0.406932 -0.14362,0.266728 -0.14362,0.646303 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1819" />
|
||||
<path
|
||||
d="m 162.6335,49.620032 q -0.78992,0 -1.36441,-0.328281 -0.57108,-0.3317 -0.87884,-0.936969 -0.30776,-0.608688 -0.30776,-1.43281 0,-0.810444 0.30776,-1.422552 0.31118,-0.615527 0.86858,-0.957486 0.55739,-0.345379 1.3097,-0.345379 0.48558,0 0.91645,0.157301 0.43429,0.153882 0.76599,0.478743 0.33512,0.324862 0.52662,0.827542 0.1915,0.499261 0.1915,1.19002 v 0.379575 h -4.30527 v -0.834382 h 3.11867 q -0.003,-0.355638 -0.15388,-0.632625 -0.15047,-0.280406 -0.42061,-0.441127 -0.26673,-0.160721 -0.62237,-0.160721 -0.37958,0 -0.66682,0.184658 -0.28725,0.181238 -0.44797,0.478743 -0.1573,0.294085 -0.16072,0.646303 v 0.728374 q 0,0.458226 0.16756,0.786507 0.16756,0.324862 0.46849,0.499261 0.30092,0.17098 0.70443,0.17098 0.27015,0 0.489,-0.07523 0.21886,-0.07865 0.37958,-0.229113 0.16072,-0.150462 0.24279,-0.372736 l 1.15582,0.129944 q -0.10942,0.458226 -0.41719,0.800186 -0.30434,0.33854 -0.77967,0.526617 -0.47532,0.184658 -1.08743,0.184658 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1821" />
|
||||
<path
|
||||
d="m 170.16686,45.653302 -1.12846,0.123106 q -0.0479,-0.17098 -0.16756,-0.321442 -0.11627,-0.150462 -0.31461,-0.242792 -0.19833,-0.09233 -0.48558,-0.09233 -0.38641,0 -0.64972,0.167561 -0.25989,0.16756 -0.25647,0.434288 -0.003,0.229113 0.16756,0.372736 0.1744,0.143623 0.57449,0.235952 l 0.89593,0.191497 q 0.74548,0.160721 1.10795,0.50952 0.3659,0.348799 0.36932,0.913032 -0.003,0.495841 -0.29067,0.875416 -0.28382,0.376156 -0.78992,0.588171 -0.5061,0.212014 -1.16267,0.212014 -0.96432,0 -1.55249,-0.403512 -0.58817,-0.406932 -0.70102,-1.131886 l 1.20712,-0.116266 q 0.0821,0.355638 0.3488,0.536876 0.26673,0.181239 0.69417,0.181239 0.44113,0 0.70786,-0.181239 0.27015,-0.181238 0.27015,-0.447966 0,-0.225694 -0.1744,-0.372736 -0.17098,-0.147043 -0.53346,-0.225694 l -0.89593,-0.188077 q -0.75573,-0.157302 -1.11821,-0.530038 -0.36248,-0.376155 -0.35906,-0.950647 -0.003,-0.485582 0.26331,-0.84122 0.27015,-0.359058 0.74889,-0.553975 0.48217,-0.198336 1.11137,-0.198336 0.92329,0 1.45333,0.393253 0.53346,0.393254 0.65998,1.063495 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1823" />
|
||||
<path
|
||||
d="m 173.925,44.264947 v 0.957486 h -3.01951 v -0.957486 z m -2.27403,-1.258411 h 1.23789 v 4.931056 q 0,0.24963 0.0752,0.382994 0.0787,0.129945 0.20518,0.177819 0.12652,0.04788 0.2804,0.04788 0.11627,0 0.21202,-0.0171 0.0992,-0.0171 0.15046,-0.03078 l 0.2086,0.967746 q -0.0992,0.0342 -0.28383,0.07523 -0.18124,0.04104 -0.44455,0.04787 -0.46506,0.01368 -0.8378,-0.140203 -0.37274,-0.157302 -0.59159,-0.485583 -0.21543,-0.328281 -0.21201,-0.820703 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1825" />
|
||||
<path
|
||||
d="m 174.96112,49.517445 v -5.252498 h 1.20028 v 0.875416 h 0.0547 q 0.14363,-0.454806 0.49242,-0.701017 0.35222,-0.24963 0.80361,-0.24963 0.10259,0 0.22911,0.01026 0.12995,0.0068 0.21544,0.02394 v 1.138725 q -0.0787,-0.02736 -0.24963,-0.04787 -0.16756,-0.02394 -0.32487,-0.02394 -0.33853,0 -0.60868,0.147043 -0.26673,0.143623 -0.42061,0.400092 -0.15388,0.25647 -0.15388,0.59159 v 3.087895 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1827" />
|
||||
<path
|
||||
d="m 180.19309,49.623452 q -0.49926,0 -0.89936,-0.177819 -0.39667,-0.181238 -0.6292,-0.533457 -0.22911,-0.352218 -0.22911,-0.868577 0,-0.444547 0.16414,-0.735213 0.16414,-0.290665 0.44796,-0.465065 0.28383,-0.174399 0.63947,-0.263309 0.35905,-0.09233 0.74205,-0.133364 0.46164,-0.04787 0.74889,-0.08549 0.28725,-0.04104 0.41719,-0.123105 0.13337,-0.08549 0.13337,-0.263309 v -0.02052 q 0,-0.386415 -0.22912,-0.59843 -0.22911,-0.212014 -0.65998,-0.212014 -0.45481,0 -0.72153,0.198336 -0.26331,0.198337 -0.35564,0.468485 L 178.6064,45.64646 q 0.13678,-0.478743 0.45138,-0.800185 0.3146,-0.324862 0.76941,-0.485583 0.45481,-0.16414 1.00536,-0.16414 0.37958,0 0.75573,0.08891 0.37616,0.08891 0.68734,0.294085 0.31118,0.201757 0.49926,0.550555 0.1915,0.348799 0.1915,0.871997 v 3.515344 h -1.19002 V 48.79591 h -0.041 q -0.11284,0.218854 -0.31802,0.410351 -0.20175,0.188078 -0.50952,0.304344 -0.30434,0.112847 -0.71469,0.112847 z m 0.32144,-0.909612 q 0.37274,0 0.6463,-0.147043 0.27357,-0.150462 0.42061,-0.396673 0.15047,-0.246211 0.15047,-0.536876 v -0.618947 q -0.0581,0.04787 -0.19834,0.08891 -0.13679,0.04104 -0.30776,0.07181 -0.17098,0.03078 -0.33854,0.05471 -0.16756,0.02394 -0.29067,0.04104 -0.27699,0.03761 -0.49584,0.123105 -0.21886,0.08549 -0.34538,0.239372 -0.12653,0.150462 -0.12653,0.389834 0,0.341959 0.24963,0.516358 0.24964,0.1744 0.63605,0.1744 z"
|
||||
style="font-weight:600;font-family:inter;-inkscape-font-specification:'inter Semi-Bold'"
|
||||
id="path1829" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
1
docs/static/img/logos/jDev.svg
vendored
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/static/img/logos/mel.jpg
vendored
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/static/img/logos/pi-Geosolutions.png
vendored
Normal file
|
After Width: | Height: | Size: 22 KiB |
1
docs/static/img/logos/rennes-ville-metropole_noir.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="178" height="34" version="1.0" viewBox="27 27 367 70"><switch><foreignObject width="1" height="1" x="0" y="0" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"/><g><path d="M221.8 47.9c1.3 4.4 5 7.1 10.9 7.1 6 0 10.1-2.8 10.1-7.9 0-4.2-2.6-6.8-9.9-7.8-4.1-.6-5.5-1.6-5.5-3.6 0-2.2 1.8-3.2 4.5-3.2 3.3 0 5.1 1.5 5.8 4.4l4.2-1.2c-1.1-4.6-4.5-7.2-10.1-7.2-5.5 0-9.1 2.6-9.1 7.5 0 3.5 1.8 6.4 9 7.4 4.7.6 6.2 1.7 6.2 3.9 0 2.4-1.9 3.6-5.1 3.6-3.8 0-6-1.7-7.1-4.7zm-21.1-8.3c.5-4 2.9-6.8 7.1-6.8 3.9 0 6.5 2.4 6.5 6.8zm18.3 1.1c0-7.9-4.4-12.1-11.1-12.1-7.6 0-11.9 5.3-11.9 13.2 0 8 4.3 13.2 12.1 13.2 5.6 0 9.2-2.7 10.5-7l-4.3-1.4c-.9 3.2-3.5 4.1-6.2 4.1-4.9 0-7.1-3.1-7.4-7.2H219zm-44.7 13.8V35.9c2.5 2.4 9.6 12.8 12.4 18.6h4.6V29h-4.5v17.1c-3.3-5.9-7.9-12-12.8-17.1h-4.3v25.5zm-27.5 0V35.9c2.5 2.4 9.6 12.8 12.4 18.6h4.6V29h-4.5v17.1c-3.3-5.9-7.9-12-12.7-17.1h-4.3v25.5zm-27.4-14.9c.5-4 2.9-6.8 7.1-6.8 3.9 0 6.5 2.4 6.5 6.8zm18.2 1.1c0-7.9-4.4-12.1-11.1-12.1-7.6 0-11.9 5.3-11.9 13.2 0 8 4.3 13.2 12.1 13.2 5.6 0 9.2-2.7 10.5-7l-4.3-1.4c-.9 3.2-3.5 4.1-6.2 4.1-4.9 0-7.1-3.1-7.4-7.2h18.3zM101 33.2c3.6 0 5.5 1.7 5.5 4.6 0 2.6-1.6 4.4-5 4.4h-6.1v-9zm4.4 12.3c4.5-1.5 5.9-4.9 5.9-8.2 0-5.2-3.6-8.3-9.8-8.3H90.9v25.5h4.5v-8.3h5.2l1.7 2.8c3 4.9 4.2 5.7 9 5.7v-4.3c-2.5 0-3.4-.8-5.3-3.9zM383.9 71.9c2.8 0 4.8 1.8 4.8 5.1h-9.9c.3-3 2-5.1 5.1-5.1m8.8 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.4 4.1-9.4 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-21.5-15.5H367v26.2h4.2zm-17.4 22.7c-3.3 0-5.3-2.4-5.3-6.4s2.1-6.4 5.3-6.4 5.3 2.4 5.3 6.4c0 3.9-2.1 6.4-5.3 6.4m0 3.8c5.8 0 9.7-3.9 9.7-10.2s-3.9-10.2-9.7-10.2c-5.9 0-9.8 3.9-9.8 10.2.1 6.3 3.9 10.2 9.8 10.2m-22-3.7c-3.3 0-5.4-2.5-5.4-6.5s2.1-6.5 5.4-6.5 5.4 2.5 5.4 6.5c.1 4-2.1 6.5-5.4 6.5M326.4 96V85.5h.2c1.2 2.4 3.7 3.5 6.2 3.5 5 0 8.9-3.9 8.9-10.3s-3.9-10.3-8.9-10.3c-2.5 0-5.1 1.1-6.2 3.5h-.2v-3.1h-4.2V96zm-17.3-10.9c-3.3 0-5.3-2.4-5.3-6.4s2.1-6.4 5.3-6.4 5.3 2.4 5.3 6.4c.1 3.9-2 6.4-5.3 6.4m0 3.8c5.8 0 9.7-3.9 9.7-10.2s-3.9-10.2-9.7-10.2c-5.9 0-9.8 3.9-9.8 10.2.1 6.3 4 10.2 9.8 10.2m-10.8-20.3c-.5-.1-1-.1-1.5-.1-2.5 0-4.4.8-5.4 3.5h-.2v-3.2H287v19.7h4.2v-9.7c0-5.6 2.4-6.1 4.7-6.1.8 0 1.5.1 2.3.2v-4.3zm-17.6 16.2c-3 0-3.4-.9-3.4-6.3v-5.9h5.7v-3.7h-5.7v-4.8H273v4.8h-3.7v3.7h3.7v7c0 7 1.8 9.1 7 9.1 1 0 2.3 0 3.5-.1v-3.8zM259 71.9c2.8 0 4.8 1.8 4.8 5.1h-9.9c.4-3 2.1-5.1 5.1-5.1m8.9 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.4 4.1-9.4 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-3.9-17-9.1 3.2 1.1 3.1 9.1-3.1zm-20 2.2h-4.4c-2.9 5.9-5.5 11.8-7.6 18.6h-.3c-2.1-6.8-4.8-12.7-7.6-18.6h-4.4l-3.1 25.5h4.5l2-17.6c2.4 5.4 4.5 10.9 6.3 16.8h5q2.7-8.7 6.3-16.8l2 17.6h4.5zm-41.2 21.7c-3 0-3.4-.9-3.4-6.3v-5.9h5.7v-3.7h-5.7v-4.8h-4.2v4.8h-3.7v3.7h3.7v7c0 7 1.8 9.1 7 9.1 1 0 2.3 0 3.5-.1v-3.8zm-21.7-12.9c2.8 0 4.8 1.8 4.8 5.1H176c.4-3 2.1-5.1 5.1-5.1m8.9 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.4 4.1-9.4 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-37.3-6c2.8 0 4.8 1.8 4.8 5.1h-9.9c.3-3 2-5.1 5.1-5.1m8.8 6c0-6.1-3.4-9.5-8.8-9.5-6 0-9.3 4.1-9.3 10.3 0 6.1 3.4 10.3 9.7 10.3 4.5 0 7.2-2.1 8.2-5.6l-3.5-1.2c-.6 2-2.3 3-4.7 3-3.5 0-5.1-2.2-5.4-5.2h14v-2.1zm-21.9-15.5h-4.2v26.2h4.2zm-9.1 0h-4.2v26.2h4.2zm-9.4 6.4h-4.2v19.7h4.2zm-2-2.2c1.4 0 2.6-1.2 2.6-2.7 0-1.4-1.2-2.6-2.6-2.6-1.5 0-2.7 1.2-2.7 2.6s1.2 2.7 2.7 2.7m-14.3 22 9-25.5H109l-7.2 21h-.3l-7.2-21h-4.8l9 25.5zM57.4 74.6c-3.7-5.3-6.2-6.9-11.4-7.7v4.9c3.6.5 5.3 2.4 7.6 5.7 6.2 8.9 9.7 12.5 22.3 11.9v-4.7c-9.7.7-13-2.3-18.5-10.1m3.1-2.4c5.3 7.3 8.1 9.8 15.4 9.2v-4.7c-5.4.6-8-2.1-11.7-7.4-4.5-6.4-9.6-9.9-18.2-10.3v4.8c6.1.2 10.1 2.3 14.5 8.4M28.3 88.8h4.9V47.9c-1.5.5-3.4 1.1-4.9 1.8zm13.6-43.2c-1.5.2-3.4.5-4.9.9v42.2h4.9zm26 4.7c0-8.1-6.4-13.5-19-13.5-6.6 0-14.5 1.5-20.6 4v5c5.6-2.5 13.7-4.3 20.6-4.3 9.1 0 14.1 3.1 14.1 8.9 0 3.4-1.7 6.4-4.8 8.3 1.3.7 2.7 1.8 3.8 2.8 3.8-2.4 5.9-6.5 5.9-11.2m-18.4-22c-7 0-14.2 1.5-21.1 3.8v5C36 34.3 43.2 33 49.5 33c14.3 0 22.7 6.5 22.7 16.8 0 6-2.6 11.1-7.5 14.5 1 1.1 2.1 2.4 3 3.5 6.1-4.2 9.4-10.9 9.4-18-.1-13-10.6-21.5-27.6-21.5"/></g></switch></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -30,7 +30,6 @@
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"../superset-frontend/**/*",
|
||||
"src/webpack.extend.ts",
|
||||
"src/shims/**"
|
||||
]
|
||||
}
|
||||
|
||||
7598
docs/yarn.lock
@@ -44,14 +44,14 @@ dependencies = [
|
||||
# explicitly to keep fresh ``pip install apache-superset`` working
|
||||
# without the ``base.txt`` lock file (#40962).
|
||||
"cachetools>=6.2.1, <7",
|
||||
"celery>=5.3.6, <6.0.0",
|
||||
"celery>=5.6.3, <6.0.0",
|
||||
"click>=8.4.0",
|
||||
"click-option-group",
|
||||
"colorama",
|
||||
"flask-cors>=6.0.5, <7.0",
|
||||
"croniter>=6.2.2",
|
||||
"cron-descriptor",
|
||||
"cryptography>=48.0.0, <49.0.0",
|
||||
"cryptography>=49.0.0, <50.0.0",
|
||||
"deprecation>=2.1.0, <2.2.0",
|
||||
"flask>=2.2.5, <4.0.0",
|
||||
"flask-appbuilder>=5.2.2, <6.0.0",
|
||||
@@ -127,7 +127,7 @@ aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
|
||||
bigquery = [
|
||||
"pandas-gbq>=0.19.1",
|
||||
"sqlalchemy-bigquery>=1.17.0",
|
||||
"google-cloud-bigquery>=3.10.0",
|
||||
"google-cloud-bigquery>=3.42.1",
|
||||
]
|
||||
clickhouse = ["clickhouse-connect>=1.1.1, <2.0"]
|
||||
cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
|
||||
@@ -137,7 +137,7 @@ 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",
|
||||
@@ -180,15 +180,15 @@ mssql = ["pymssql>=2.3.13, <3"]
|
||||
motherduck = ["apache-superset[duckdb]"]
|
||||
mysql = ["mysqlclient>=2.2.8, <3"]
|
||||
ocient = [
|
||||
"sqlalchemy-ocient>=1.0.0",
|
||||
"sqlalchemy-ocient>=1.0.0, <4",
|
||||
"pyocient>=1.0.15, <4",
|
||||
"shapely",
|
||||
"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"]
|
||||
playwright = ["playwright>=1.61.0, <2"]
|
||||
postgres = ["psycopg2-binary==2.9.12"]
|
||||
presto = ["pyhive[presto]>=0.6.5"]
|
||||
trino = ["trino>=0.337.0"]
|
||||
@@ -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 = [
|
||||
@@ -235,7 +235,7 @@ development = [
|
||||
"pyfakefs",
|
||||
"pyinstrument>=5.1.2,<6",
|
||||
"pylint",
|
||||
"pytest<8.0.0", # hairy issue with pytest >=8 where current_app proxies are not set in time
|
||||
"pytest<10.0.0", # cap below the next major until validated; the earlier <8 pin (current_app proxy timing) no longer reproduces
|
||||
"pytest-asyncio",
|
||||
"pytest-cov",
|
||||
"pytest-mock",
|
||||
|
||||
@@ -26,7 +26,7 @@ filelock>=3.20.3,<4.0.0
|
||||
brotli>=1.2.0,<2.0.0
|
||||
numexpr>=2.9.0
|
||||
# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation
|
||||
cryptography>=48.0.0,<49.0.0
|
||||
cryptography>=49.0.0,<50.0.0
|
||||
# Security: Snyk - XSS vulnerability in Mako templates
|
||||
mako>=1.3.11,<2.0.0
|
||||
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
|
||||
|
||||
@@ -48,7 +48,7 @@ cachetools==6.2.1
|
||||
# via apache-superset (pyproject.toml)
|
||||
cattrs==25.1.1
|
||||
# via requests-cache
|
||||
celery==5.5.2
|
||||
celery==5.6.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
certifi==2026.5.20
|
||||
# via
|
||||
@@ -86,7 +86,7 @@ cron-descriptor==1.4.5
|
||||
# via apache-superset (pyproject.toml)
|
||||
croniter==6.2.2
|
||||
# via apache-superset (pyproject.toml)
|
||||
cryptography==48.0.1
|
||||
cryptography==49.0.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# apache-superset (pyproject.toml)
|
||||
@@ -207,7 +207,7 @@ jsonschema-specifications==2025.4.1
|
||||
# via
|
||||
# jsonschema
|
||||
# openapi-schema-validator
|
||||
kombu==5.5.3
|
||||
kombu==5.6.2
|
||||
# via celery
|
||||
limits==5.1.0
|
||||
# via flask-limiter
|
||||
@@ -272,6 +272,7 @@ packaging==25.0
|
||||
# apispec
|
||||
# deprecation
|
||||
# gunicorn
|
||||
# kombu
|
||||
# limits
|
||||
# shillelagh
|
||||
pandas==2.1.4
|
||||
@@ -323,7 +324,7 @@ pyjwt==2.13.0
|
||||
# redis
|
||||
pynacl==1.6.2
|
||||
# via paramiko
|
||||
pyopenssl==26.2.0
|
||||
pyopenssl==26.3.0
|
||||
# via
|
||||
# -r requirements/base.in
|
||||
# shillelagh
|
||||
@@ -444,6 +445,8 @@ tzdata==2025.2
|
||||
# via
|
||||
# kombu
|
||||
# pandas
|
||||
tzlocal==5.4.4
|
||||
# via celery
|
||||
url-normalize==2.2.1
|
||||
# via requests-cache
|
||||
urllib3==2.7.0
|
||||
|
||||
@@ -108,7 +108,7 @@ cattrs==25.1.1
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# requests-cache
|
||||
celery==5.5.2
|
||||
celery==5.6.3
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -178,7 +178,7 @@ croniter==6.2.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
cryptography==48.0.1
|
||||
cryptography==49.0.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -360,7 +360,7 @@ google-auth-oauthlib==1.2.1
|
||||
# via
|
||||
# pandas-gbq
|
||||
# pydata-google-auth
|
||||
google-cloud-bigquery==3.27.0
|
||||
google-cloud-bigquery==3.42.1
|
||||
# via
|
||||
# apache-superset
|
||||
# pandas-gbq
|
||||
@@ -503,7 +503,7 @@ keyring==25.7.0
|
||||
# via py-key-value-aio
|
||||
kiwisolver==1.4.7
|
||||
# via matplotlib
|
||||
kombu==5.5.3
|
||||
kombu==5.6.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# celery
|
||||
@@ -635,6 +635,7 @@ packaging==25.0
|
||||
# fastmcp-slim
|
||||
# google-cloud-bigquery
|
||||
# gunicorn
|
||||
# kombu
|
||||
# limits
|
||||
# matplotlib
|
||||
# pytest
|
||||
@@ -790,9 +791,10 @@ pynacl==1.6.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# paramiko
|
||||
pyopenssl==26.2.0
|
||||
pyopenssl==26.3.0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# google-auth
|
||||
# shillelagh
|
||||
pyparsing==3.2.3
|
||||
# via
|
||||
@@ -1071,8 +1073,11 @@ tzdata==2025.2
|
||||
# -c requirements/base-constraint.txt
|
||||
# kombu
|
||||
# pandas
|
||||
tzlocal==5.2
|
||||
# via trino
|
||||
tzlocal==5.4.4
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# celery
|
||||
# trino
|
||||
uncalled-for==0.2.0
|
||||
# via fastmcp-slim
|
||||
url-normalize==2.2.1
|
||||
|
||||
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 = [
|
||||
|
||||
@@ -42,7 +42,7 @@ classifiers = [
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
dependencies = [
|
||||
"flask-appbuilder>=5.0.2,<6",
|
||||
"flask-appbuilder>=5.2.0,<6",
|
||||
"isodate>=0.7.0",
|
||||
"pyarrow>=16.0.0",
|
||||
"pydantic>=2.8.0",
|
||||
|
||||
1412
superset-embedded-sdk/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -57,9 +57,9 @@ export function saveChartToDashboard(chartName: string, dashboardName: string) {
|
||||
'.ant-select-selection-search-input[aria-label*="Select a dashboard"]',
|
||||
)
|
||||
.type(dashboardName, { force: true });
|
||||
cy.wrap($modal)
|
||||
.find(`.ant-select-item-option[title="${dashboardName}"]`)
|
||||
.click();
|
||||
|
||||
cy.get(`.ant-select-item-option[title="${dashboardName}"]`).click();
|
||||
|
||||
cy.getBySel('btn-modal-save').click();
|
||||
cy.wait('@update');
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{
|
||||
"slice_name": "1 - Sample chart",
|
||||
"description": "chart description",
|
||||
"owners": [1],
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 2,
|
||||
@@ -12,7 +11,6 @@
|
||||
{
|
||||
"slice_name": "2 - Sample chart",
|
||||
"description": "chart description",
|
||||
"owners": [1],
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 2,
|
||||
@@ -22,7 +20,6 @@
|
||||
{
|
||||
"slice_name": "3 - Sample chart",
|
||||
"description": "chart description",
|
||||
"owners": [1],
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 2,
|
||||
@@ -32,7 +29,6 @@
|
||||
{
|
||||
"slice_name": "4 - Sample chart",
|
||||
"description": "chart description",
|
||||
"owners": [1],
|
||||
"viz_type": "echarts_timeseries_line",
|
||||
"cache_timeout": 1000,
|
||||
"datasource_id": 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: {
|
||||
|
||||
3251
superset-frontend/package-lock.json
generated
@@ -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",
|
||||
@@ -185,7 +185,7 @@
|
||||
"geostyler-style": "11.0.2",
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"google-auth-library": "^10.9.0",
|
||||
"immer": "^11.1.8",
|
||||
"immer": "^11.1.9",
|
||||
"interweave": "^13.1.1",
|
||||
"jquery": "^4.0.0",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
@@ -271,7 +271,7 @@
|
||||
"@storybook/test-runner": "0.24.4",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.15.43",
|
||||
"@swc/plugin-emotion": "^14.14.0",
|
||||
"@swc/plugin-emotion": "^14.14.1",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
@@ -285,7 +285,7 @@
|
||||
"@types/json-bigint": "^1.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
@@ -298,8 +298,8 @@
|
||||
"@types/rison": "0.1.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/unzipper": "^0.10.11",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.0",
|
||||
"@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",
|
||||
@@ -314,7 +314,6 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-cypress": "^3.6.0",
|
||||
"eslint-plugin-i18n-strings": "file:eslint-rules/eslint-plugin-i18n-strings",
|
||||
"eslint-plugin-icons": "file:eslint-rules/eslint-plugin-icons",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
@@ -324,7 +323,7 @@
|
||||
"eslint-plugin-prettier": "^5.5.6",
|
||||
"eslint-plugin-react-prefer-function-component": "^5.0.0",
|
||||
"eslint-plugin-react-you-might-not-need-an-effect": "^1.0.1",
|
||||
"eslint-plugin-storybook": "10.4.5",
|
||||
"eslint-plugin-storybook": "10.4.6",
|
||||
"eslint-plugin-testing-library": "^7.16.2",
|
||||
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
|
||||
"fetch-mock": "^12.6.0",
|
||||
@@ -344,9 +343,9 @@
|
||||
"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.9.3",
|
||||
"prettier": "3.9.4",
|
||||
"prettier-plugin-packagejson": "^3.0.2",
|
||||
"process": "^0.11.10",
|
||||
"react-dnd-test-backend": "^16.0.1",
|
||||
@@ -393,7 +392,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",
|
||||
@@ -428,6 +426,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"
|
||||
},
|
||||
|
||||
@@ -258,6 +258,16 @@ export interface SupersetSpecificTokens {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,18 +25,35 @@ export { default as __hack_reexport_jed } from './jed';
|
||||
* Superset supported languages.
|
||||
*/
|
||||
export type Locale =
|
||||
| 'ar'
|
||||
| 'ca'
|
||||
| 'cs'
|
||||
| 'de'
|
||||
| 'en'
|
||||
| 'es'
|
||||
| 'fa'
|
||||
| 'fi'
|
||||
| 'fr'
|
||||
| 'it'
|
||||
| 'ja'
|
||||
| 'ko'
|
||||
| 'lv'
|
||||
| 'mi'
|
||||
| 'nl'
|
||||
| 'pl'
|
||||
| 'pt'
|
||||
| 'pt_BR'
|
||||
| 'ro'
|
||||
| 'ru'
|
||||
| 'sk'
|
||||
| 'sl'
|
||||
| 'sr'
|
||||
| 'sr_Latn'
|
||||
| 'th'
|
||||
| 'tr'
|
||||
| 'uk'
|
||||
| 'zh'
|
||||
| 'zh_TW'; // supported locales in Superset
|
||||
| 'zh_TW'; // every locale with a catalog in superset/translations
|
||||
|
||||
/**
|
||||
* Language pack provided to `jed`.
|
||||
|
||||
@@ -138,7 +138,7 @@ export const TestDataset: Dataset = {
|
||||
],
|
||||
name: 'public.birth_names',
|
||||
order_by_choices: [],
|
||||
owners: [
|
||||
editors: [
|
||||
{
|
||||
first_name: 'admin',
|
||||
id: 1,
|
||||
|
||||
@@ -67,7 +67,7 @@ export const renameOperator: PostProcessingFactory<PostProcessingRename> = (
|
||||
[...metricOffsetMap.entries()].forEach(
|
||||
([metricWithOffset, metricOnly]) => {
|
||||
const offsetLabel = timeOffsets.find(offset =>
|
||||
metricWithOffset.includes(offset),
|
||||
metricWithOffset.endsWith(`${TIME_COMPARISON_SEPARATOR}${offset}`),
|
||||
);
|
||||
renamePairs.push([
|
||||
formData.comparison_type === ComparisonType.Values
|
||||
|
||||
@@ -26,7 +26,7 @@ export const getTimeOffset = (
|
||||
timeCompare.find(
|
||||
timeOffset =>
|
||||
// offset is represented as <offset>, group by list
|
||||
series.name.includes(`${timeOffset},`) ||
|
||||
series.name.startsWith(`${timeOffset},`) ||
|
||||
// offset is represented as <metric>__<offset>
|
||||
series.name.includes(`__${timeOffset}`) ||
|
||||
// offset is represented as <metric>, <offset>
|
||||
@@ -50,7 +50,9 @@ export const getOriginalSeries = (
|
||||
// offset in the middle: <metric>, <offset>, <dimension>
|
||||
result = result.replace(`, ${compare},`, ',');
|
||||
// offset at start: <offset>, <dimension>
|
||||
result = result.replace(`${compare},`, '');
|
||||
if (result.startsWith(`${compare},`)) {
|
||||
result = result.slice(`${compare},`.length);
|
||||
}
|
||||
// offset with double underscore: <metric>__<offset>
|
||||
result = result.replace(`__${compare}`, '');
|
||||
// offset at end: <metric>, <offset>
|
||||
|
||||
@@ -86,7 +86,7 @@ export interface Dataset {
|
||||
name?: string;
|
||||
description: string | null;
|
||||
uid?: string;
|
||||
owners?: Owner[];
|
||||
editors?: Owner[];
|
||||
filter_select?: boolean;
|
||||
filter_select_enabled?: boolean;
|
||||
column_names?: string[];
|
||||
@@ -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;
|
||||
|
||||
@@ -303,6 +303,30 @@ test('should add renameOperator if multiple metrics exist', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should correctly match offsets that share a numeric prefix', () => {
|
||||
expect(
|
||||
renameOperator(
|
||||
{
|
||||
...formData,
|
||||
|
||||
comparison_type: ComparisonType.Values,
|
||||
time_compare: ['1 year ago', '11 year ago'],
|
||||
},
|
||||
queryObject,
|
||||
),
|
||||
).toEqual({
|
||||
operation: 'rename',
|
||||
options: {
|
||||
columns: {
|
||||
'count(*)__1 year ago': '1 year ago',
|
||||
'count(*)__11 year ago': '11 year ago',
|
||||
},
|
||||
inplace: true,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should remove renameOperator', () => {
|
||||
expect(
|
||||
renameOperator(
|
||||
|
||||
@@ -114,3 +114,26 @@ test('hasTimeOffset returns false when series name is not a string', () => {
|
||||
const timeCompare = ['1 year ago'];
|
||||
expect(hasTimeOffset(series, timeCompare)).toBe(false);
|
||||
});
|
||||
|
||||
test('getTimeOffset correctly matches offsets that share a numeric prefix', () => {
|
||||
const timeCompare = ['1 year ago', '11 year ago'];
|
||||
expect(
|
||||
getTimeOffset({ name: '11 year ago, Alexander' }, timeCompare),
|
||||
).toEqual('11 year ago');
|
||||
expect(getTimeOffset({ name: '1 year ago, Alexander' }, timeCompare)).toEqual(
|
||||
'1 year ago',
|
||||
);
|
||||
expect(getTimeOffset({ name: 'Births__11 year ago' }, timeCompare)).toEqual(
|
||||
'11 year ago',
|
||||
);
|
||||
});
|
||||
|
||||
test('getOriginalSeries correctly strips offsets that share a numeric prefix', () => {
|
||||
const timeCompare = ['1 year ago', '11 year ago'];
|
||||
expect(getOriginalSeries('11 year ago, Alexander', timeCompare)).toEqual(
|
||||
'Alexander',
|
||||
);
|
||||
expect(getOriginalSeries('1 year ago, Alexander', timeCompare)).toEqual(
|
||||
'Alexander',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,16 +53,16 @@
|
||||
"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": "^10.1.0",
|
||||
"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",
|
||||
@@ -73,12 +73,12 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/d3-format": "^3.0.4",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-scale": "^2.1.1",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-time": "^3.0.4",
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/jquery": "^4.0.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/prop-types": "^15.7.15",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-table": "^7.7.20",
|
||||
|
||||
@@ -154,18 +154,13 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
this.incrementColorRange();
|
||||
}
|
||||
|
||||
if (
|
||||
// feature flag to be deprecated (will become standard behaviour)
|
||||
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
|
||||
this.isColorUsed(color)
|
||||
) {
|
||||
if (this.isColorUsed(color)) {
|
||||
// fallback to least used color
|
||||
color = this.getNextAvailableColor(cleanedValue, color);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isFeatureEnabled(FeatureFlag.AvoidColorsCollision) &&
|
||||
source === LabelsColorMapSource.Dashboard &&
|
||||
(forcedColor || isExistingLabel)
|
||||
) {
|
||||
@@ -396,17 +391,24 @@ class CategoricalColorScale extends ExtensibleFunction {
|
||||
/**
|
||||
* Returns the current unknown value, which defaults to "implicit".
|
||||
*/
|
||||
unknown(): string | { name: 'implicit' };
|
||||
unknown(): { name: 'implicit' };
|
||||
|
||||
/**
|
||||
* Sets the output value of the scale for unknown input values and returns this scale.
|
||||
* The implicit value enables implicit domain construction. scaleImplicit can be used as a convenience to set the implicit value.
|
||||
*
|
||||
* The signatures mirror d3's `ScaleOrdinal.unknown` so this class remains
|
||||
* assignable to `ScaleOrdinal<{ toString(): string }, string>`.
|
||||
*
|
||||
* @param value Unknown value to be used or scaleImplicit to set implicit scale generation.
|
||||
*/
|
||||
unknown(value: string | { name: 'implicit' }): this;
|
||||
unknown<NewUnknown>(
|
||||
value: NewUnknown,
|
||||
): NewUnknown extends { name: 'implicit' }
|
||||
? ScaleOrdinal<{ toString(): string }, string>
|
||||
: ScaleOrdinal<{ toString(): string }, string, NewUnknown>;
|
||||
|
||||
unknown(value?: string | { name: 'implicit' }): unknown {
|
||||
unknown(value?: unknown): unknown {
|
||||
if (typeof value === 'undefined') {
|
||||
return this.scale.unknown();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -31,12 +31,7 @@ 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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -34,8 +34,9 @@ export const DropdownButton = ({
|
||||
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: '';
|
||||
@@ -48,7 +49,8 @@ export const DropdownButton = ({
|
||||
.anticon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}`}
|
||||
}`
|
||||
}
|
||||
`;
|
||||
const button = (
|
||||
<Dropdown.Button
|
||||
@@ -58,13 +60,13 @@ export const DropdownButton = ({
|
||||
defaultBtnCss,
|
||||
css`
|
||||
.ant-btn {
|
||||
height: ${styleConfig?.controlHeight ??
|
||||
theme.buttonControlHeightSM ??
|
||||
30}px;
|
||||
height: ${
|
||||
styleConfig?.controlHeight ?? theme.buttonControlHeightSM ?? 30
|
||||
}px;
|
||||
box-shadow: ${styleConfig?.boxShadow ?? 'none'};
|
||||
font-size: ${styleConfig?.fontSize ??
|
||||
theme.buttonFontSize ??
|
||||
theme.fontSizeSM}px;
|
||||
font-size: ${
|
||||
styleConfig?.fontSize ?? theme.buttonFontSize ?? theme.fontSizeSM
|
||||
}px;
|
||||
font-weight: ${styleConfig?.fontWeight ?? theme.fontWeightStrong};
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
InfoCircleFilled,
|
||||
InsertRowAboveOutlined,
|
||||
InsertRowBelowOutlined,
|
||||
LayoutOutlined,
|
||||
LeftOutlined,
|
||||
LineChartOutlined,
|
||||
LineOutlined,
|
||||
@@ -249,6 +250,7 @@ const AntdIcons = {
|
||||
InfoCircleFilled,
|
||||
InsertRowAboveOutlined,
|
||||
InsertRowBelowOutlined,
|
||||
LayoutOutlined,
|
||||
LeftOutlined,
|
||||
LineChartOutlined,
|
||||
LineOutlined,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -84,15 +84,15 @@ const config = (contentType: ContentType) => {
|
||||
),
|
||||
};
|
||||
|
||||
case MetadataType.Owner:
|
||||
case MetadataType.Editor:
|
||||
return {
|
||||
icon: Icons.UserOutlined,
|
||||
title: contentType.createdBy,
|
||||
tooltip: (
|
||||
<div>
|
||||
<Info header={t('Created by')} text={contentType.createdBy} />
|
||||
{!!contentType.owners && (
|
||||
<Info header={t('Owners')} text={contentType.owners} />
|
||||
{!!contentType.editors && (
|
||||
<Info header={t('Editors')} text={contentType.editors} />
|
||||
)}
|
||||
<Info header={t('Created on')} text={contentType.createdOn} />
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export enum MetadataType {
|
||||
Dashboards = 'dashboards',
|
||||
Description = 'description',
|
||||
LastModified = 'lastModified',
|
||||
Owner = 'owner',
|
||||
Editor = 'editor',
|
||||
Rows = 'rows',
|
||||
Sql = 'sql',
|
||||
Table = 'table',
|
||||
@@ -48,10 +48,10 @@ export type LastModified = {
|
||||
onClick?: (type: string) => void;
|
||||
};
|
||||
|
||||
export type Owner = {
|
||||
type: MetadataType.Owner;
|
||||
export type Editor = {
|
||||
type: MetadataType.Editor;
|
||||
createdBy: string;
|
||||
owners?: string[] | string;
|
||||
editors?: string[] | string;
|
||||
createdOn: string;
|
||||
onClick?: (type: string) => void;
|
||||
};
|
||||
@@ -81,11 +81,4 @@ export type Tags = {
|
||||
};
|
||||
|
||||
export type ContentType =
|
||||
| Dashboards
|
||||
| Description
|
||||
| LastModified
|
||||
| Owner
|
||||
| Rows
|
||||
| Sql
|
||||
| Table
|
||||
| Tags;
|
||||
Dashboards | Description | LastModified | Editor | Rows | Sql | Table | Tags;
|
||||
|
||||
@@ -27,7 +27,7 @@ export default {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'MetadataBar displays a row of metadata items (SQL info, owners, last modified, tags, dashboards, etc.) that collapse responsively based on available width.',
|
||||
'MetadataBar displays a row of metadata items (SQL info, editors, last modified, tags, dashboards, etc.) that collapse responsively based on available width.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -75,9 +75,9 @@ Basic.args = {
|
||||
title: 'Click to view query',
|
||||
},
|
||||
{
|
||||
type: MetadataType.Owner,
|
||||
type: MetadataType.Editor,
|
||||
createdBy: 'Jane Smith',
|
||||
owners: ['John Doe', 'Mary Wilson'],
|
||||
editors: ['John Doe', 'Mary Wilson'],
|
||||
createdOn: A_WEEK_AGO,
|
||||
},
|
||||
{
|
||||
@@ -119,9 +119,9 @@ InteractiveMetadataBar.args = {
|
||||
title: 'Click to view query',
|
||||
},
|
||||
{
|
||||
type: MetadataType.Owner,
|
||||
type: MetadataType.Editor,
|
||||
createdBy: 'Jane Smith',
|
||||
owners: ['John Doe', 'Mary Wilson'],
|
||||
editors: ['John Doe', 'Mary Wilson'],
|
||||
createdOn: A_WEEK_AGO,
|
||||
},
|
||||
{
|
||||
@@ -149,9 +149,9 @@ InteractiveMetadataBar.parameters = {
|
||||
items: [
|
||||
{ type: 'sql', title: 'Click to view query' },
|
||||
{
|
||||
type: 'owner',
|
||||
type: 'editor',
|
||||
createdBy: 'Jane Smith',
|
||||
owners: ['John Doe', 'Mary Wilson'],
|
||||
editors: ['John Doe', 'Mary Wilson'],
|
||||
createdOn: 'a week ago',
|
||||
},
|
||||
{
|
||||
@@ -171,9 +171,9 @@ InteractiveMetadataBar.parameters = {
|
||||
const items = [
|
||||
{ type: 'sql', title: 'Click to view query' },
|
||||
{
|
||||
type: 'owner',
|
||||
type: 'editor',
|
||||
createdBy: 'Jane Smith',
|
||||
owners: ['John Doe', 'Mary Wilson'],
|
||||
editors: ['John Doe', 'Mary Wilson'],
|
||||
createdOn: 'a week ago',
|
||||
},
|
||||
{
|
||||
@@ -190,7 +190,7 @@ InteractiveMetadataBar.parameters = {
|
||||
title: 'Minimal Metadata',
|
||||
code: `function MinimalMetadata() {
|
||||
const items = [
|
||||
{ type: 'owner', createdBy: 'Admin', owners: ['Admin'], createdOn: 'yesterday' },
|
||||
{ type: 'editor', createdBy: 'Admin', editors: ['Admin'], createdOn: 'yesterday' },
|
||||
{ type: 'lastModified', value: '2 hours ago', modifiedBy: 'Admin' },
|
||||
];
|
||||
return <MetadataBar items={items} />;
|
||||
@@ -201,7 +201,7 @@ InteractiveMetadataBar.parameters = {
|
||||
code: `function FullMetadata() {
|
||||
const items = [
|
||||
{ type: 'sql', title: 'SELECT * FROM ...' },
|
||||
{ type: 'owner', createdBy: 'Jane Smith', owners: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
|
||||
{ type: 'editor', createdBy: 'Jane Smith', editors: ['Jane Smith', 'John Doe', 'Bob Wilson'], createdOn: '2 weeks ago' },
|
||||
{ type: 'lastModified', value: '3 days ago', modifiedBy: 'John Doe' },
|
||||
{ type: 'tags', values: ['production', 'finance', 'quarterly'] },
|
||||
{ type: 'dashboards', title: 'Used in 12 dashboards' },
|
||||
|
||||
@@ -36,7 +36,7 @@ const SQL_TITLE = 'Click to view query';
|
||||
const TABLE_TITLE = 'database.schema.table';
|
||||
const CREATED_BY = 'Jane Smith';
|
||||
const MODIFIED_BY = 'Jane Smith';
|
||||
const OWNERS = ['John Doe', 'Mary Wilson'];
|
||||
const EDITORS = ['John Doe', 'Mary Wilson'];
|
||||
const TAGS = ['management', 'research', 'poc'];
|
||||
const A_WEEK_AGO = 'a week ago';
|
||||
const TWO_DAYS_AGO = '2 days ago';
|
||||
@@ -71,9 +71,9 @@ const ITEMS: ContentType[] = [
|
||||
modifiedBy: MODIFIED_BY,
|
||||
},
|
||||
{
|
||||
type: MetadataType.Owner,
|
||||
type: MetadataType.Editor,
|
||||
createdBy: CREATED_BY,
|
||||
owners: OWNERS,
|
||||
editors: EDITORS,
|
||||
createdOn: A_WEEK_AGO,
|
||||
},
|
||||
{
|
||||
@@ -211,15 +211,15 @@ test('correctly renders the last modified tooltip', async () => {
|
||||
expect(within(tooltip).getByText(MODIFIED_BY)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('correctly renders the owner tooltip', async () => {
|
||||
test('correctly renders the editor tooltip', async () => {
|
||||
render(<MetadataBar items={ITEMS.slice(0, 4)} />);
|
||||
await userEvent.hover(screen.getByText(CREATED_BY));
|
||||
const tooltip = await screen.findByRole('tooltip');
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
expect(within(tooltip).getByText(CREATED_BY)).toBeInTheDocument();
|
||||
expect(within(tooltip).getByText(A_WEEK_AGO)).toBeInTheDocument();
|
||||
OWNERS.forEach(owner =>
|
||||
expect(within(tooltip).getByText(owner)).toBeInTheDocument(),
|
||||
EDITORS.forEach(editor =>
|
||||
expect(within(tooltip).getByText(editor)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ in the code and should be specific for each metadata object.
|
||||
Content types are predefined and consistent across the whole app. This means that
|
||||
they will be displayed and behave in a consistent manner, keeping the same ordering,
|
||||
information formatting, and interactions. For example, the Owner content type will always
|
||||
have the same icon and when hovered it will present who created the entity, its current owners, and when the entity was created.
|
||||
have the same icon and when hovered it will present who created the entity, its current editors, and when the entity was created.
|
||||
|
||||
To extend the list of content types, a developer needs to request the inclusion of the new type in the design system.
|
||||
This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
|
||||
@@ -125,7 +125,7 @@ export type LastModified = {
|
||||
export type Owner = {
|
||||
type: MetadataType.Owner;
|
||||
createdBy: string;
|
||||
owners: string[];
|
||||
editors: string[];
|
||||
createdOn: Date;
|
||||
onClick?: (type: string) => void;
|
||||
};
|
||||
|
||||
@@ -34,6 +34,6 @@ export const ORDER = {
|
||||
rows: 3,
|
||||
tags: 4,
|
||||
description: 5,
|
||||
owner: 6,
|
||||
editor: 6,
|
||||
lastModified: 7,
|
||||
};
|
||||
|
||||
@@ -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)`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -746,7 +746,10 @@ const AsyncSelect = forwardRef(
|
||||
) => number
|
||||
}
|
||||
getPopupContainer={
|
||||
getPopupContainer || (triggerNode => triggerNode.parentNode)
|
||||
getPopupContainer ||
|
||||
((triggerNode: HTMLElement) =>
|
||||
(triggerNode?.closest('.ant-modal-content') as HTMLElement) ||
|
||||
(triggerNode.parentNode as HTMLElement))
|
||||
}
|
||||
headerPosition={headerPosition}
|
||||
labelInValue
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { SelectOptionsType, SelectProps } from './types';
|
||||
import { Select } from '.';
|
||||
|
||||
@@ -109,7 +110,7 @@ export const InteractiveSelect = ({
|
||||
|
||||
InteractiveSelect.args = {
|
||||
mode: 'single',
|
||||
placeholder: 'Select ...',
|
||||
placeholder: t('Select ...'),
|
||||
showSearch: true,
|
||||
allowNewOptions: false,
|
||||
allowClear: false,
|
||||
@@ -414,7 +415,7 @@ AdvancedPlayground.args = {
|
||||
oneLine: false,
|
||||
optionsCount: options.length,
|
||||
optionFilterProps: ['value', 'label', 'custom'],
|
||||
placeholder: 'Select ...',
|
||||
placeholder: t('Select ...'),
|
||||
showSearch: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -811,7 +811,10 @@ const Select = forwardRef(
|
||||
) => number
|
||||
}
|
||||
getPopupContainer={
|
||||
getPopupContainer || (triggerNode => triggerNode.parentNode)
|
||||
getPopupContainer ||
|
||||
((triggerNode: HTMLElement) =>
|
||||
(triggerNode?.closest('.ant-modal-content') as HTMLElement) ||
|
||||
(triggerNode.parentNode as HTMLElement))
|
||||
}
|
||||
headerPosition={headerPosition}
|
||||
labelInValue={labelInValue}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -59,8 +59,7 @@ export default async function handleError(error: ErrorInput): Promise<never> {
|
||||
errorMessage = `${status} ${statusText}`;
|
||||
try {
|
||||
errorJson = (await error.json()) as
|
||||
| SupersetApiErrorPayload
|
||||
| SupersetApiMultiErrorsPayload;
|
||||
SupersetApiErrorPayload | SupersetApiMultiErrorsPayload;
|
||||
originalError = errorJson;
|
||||
} catch (error_) {
|
||||
originalError = error;
|
||||
|
||||
@@ -46,10 +46,7 @@ interface TimeoutError {
|
||||
}
|
||||
|
||||
type ErrorType =
|
||||
| SupersetClientResponse
|
||||
| TimeoutError
|
||||
| { response: Response }
|
||||
| string;
|
||||
SupersetClientResponse | TimeoutError | { response: Response } | string;
|
||||
|
||||
type ErrorTextSource = 'dashboard' | 'chart' | 'query' | 'dataset' | 'database';
|
||||
|
||||
@@ -162,10 +159,7 @@ export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
|
||||
|
||||
export function getClientErrorObject(
|
||||
response:
|
||||
| SupersetClientResponse
|
||||
| TimeoutError
|
||||
| { response: Response }
|
||||
| string,
|
||||
SupersetClientResponse | TimeoutError | { response: Response } | string,
|
||||
): Promise<ClientErrorObject> {
|
||||
// takes a SupersetClientResponse as input, attempts to read response as Json
|
||||
// if possible, and returns a Promise that resolves to a plain object with
|
||||
|
||||
@@ -186,10 +186,7 @@ export function isFilterDivider(
|
||||
|
||||
export function isChartCustomization(
|
||||
filterElement:
|
||||
| Filter
|
||||
| Divider
|
||||
| ChartCustomization
|
||||
| ChartCustomizationDivider,
|
||||
Filter | Divider | ChartCustomization | ChartCustomizationDivider,
|
||||
): filterElement is ChartCustomization {
|
||||
return filterElement.type === ChartCustomizationType.ChartCustomization;
|
||||
}
|
||||
@@ -204,10 +201,7 @@ export type FilterConfiguration = Array<Filter | Divider>;
|
||||
|
||||
export type Filters = {
|
||||
[filterId: string]:
|
||||
| Filter
|
||||
| Divider
|
||||
| ChartCustomization
|
||||
| ChartCustomizationDivider;
|
||||
Filter | Divider | ChartCustomization | ChartCustomizationDivider;
|
||||
};
|
||||
|
||||
export type PartialFilters = {
|
||||
@@ -226,8 +220,7 @@ export type ChartCustomizationConfiguration = Array<
|
||||
|
||||
export type ChartCustomizations = {
|
||||
[chartCustomizationId: string]:
|
||||
| ChartCustomization
|
||||
| ChartCustomizationDivider;
|
||||
ChartCustomization | ChartCustomizationDivider;
|
||||
};
|
||||
|
||||
export type PartialChartCustomizations = {
|
||||
|
||||
@@ -53,9 +53,7 @@ export type SetAdhocFilter = BaseSimpleAdhocFilter & {
|
||||
};
|
||||
|
||||
export type SimpleAdhocFilter =
|
||||
| UnaryAdhocFilter
|
||||
| BinaryAdhocFilter
|
||||
| SetAdhocFilter;
|
||||
UnaryAdhocFilter | BinaryAdhocFilter | SetAdhocFilter;
|
||||
|
||||
export interface FreeFormAdhocFilter extends BaseAdhocFilter {
|
||||
expressionType: 'SQL';
|
||||
|
||||
@@ -21,12 +21,7 @@ import { Currency, Maybe, QueryFormMetric } from '../../types';
|
||||
import { Column } from './Column';
|
||||
|
||||
export type Aggregate =
|
||||
| 'AVG'
|
||||
| 'COUNT'
|
||||
| 'COUNT_DISTINCT'
|
||||
| 'MAX'
|
||||
| 'MIN'
|
||||
| 'SUM';
|
||||
'AVG' | 'COUNT' | 'COUNT_DISTINCT' | 'MAX' | 'MIN' | 'SUM';
|
||||
|
||||
export interface AdhocMetricBase {
|
||||
hasCustomLabel?: boolean;
|
||||
|
||||
@@ -74,8 +74,7 @@ interface _PostProcessingAggregation {
|
||||
};
|
||||
}
|
||||
export type PostProcessingAggregation =
|
||||
| _PostProcessingAggregation
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingAggregation | DefaultPostProcessing;
|
||||
|
||||
export type BoxPlotQueryObjectWhiskerType = 'tukey' | 'min/max' | 'percentile';
|
||||
interface _PostProcessingBoxplot {
|
||||
@@ -88,8 +87,7 @@ interface _PostProcessingBoxplot {
|
||||
};
|
||||
}
|
||||
export type PostProcessingBoxplot =
|
||||
| _PostProcessingBoxplot
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingBoxplot | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingContribution {
|
||||
operation: 'contribution';
|
||||
@@ -101,8 +99,7 @@ interface _PostProcessingContribution {
|
||||
};
|
||||
}
|
||||
export type PostProcessingContribution =
|
||||
| _PostProcessingContribution
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingContribution | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingPivot {
|
||||
operation: 'pivot';
|
||||
@@ -132,8 +129,7 @@ interface _PostProcessingProphet {
|
||||
};
|
||||
}
|
||||
export type PostProcessingProphet =
|
||||
| _PostProcessingProphet
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingProphet | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingDiff {
|
||||
operation: 'diff';
|
||||
@@ -155,8 +151,7 @@ interface _PostProcessingRolling {
|
||||
};
|
||||
}
|
||||
export type PostProcessingRolling =
|
||||
| _PostProcessingRolling
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingRolling | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingCum {
|
||||
operation: 'cum';
|
||||
@@ -177,8 +172,7 @@ export interface _PostProcessingCompare {
|
||||
};
|
||||
}
|
||||
export type PostProcessingCompare =
|
||||
| _PostProcessingCompare
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingCompare | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingSort {
|
||||
operation: 'sort';
|
||||
@@ -199,8 +193,7 @@ interface _PostProcessingResample {
|
||||
};
|
||||
}
|
||||
export type PostProcessingResample =
|
||||
| _PostProcessingResample
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingResample | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingRename {
|
||||
operation: 'rename';
|
||||
@@ -211,8 +204,7 @@ interface _PostProcessingRename {
|
||||
};
|
||||
}
|
||||
export type PostProcessingRename =
|
||||
| _PostProcessingRename
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingRename | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingFlatten {
|
||||
operation: 'flatten';
|
||||
@@ -222,8 +214,7 @@ interface _PostProcessingFlatten {
|
||||
};
|
||||
}
|
||||
export type PostProcessingFlatten =
|
||||
| _PostProcessingFlatten
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingFlatten | DefaultPostProcessing;
|
||||
|
||||
interface _PostProcessingRank {
|
||||
operation: 'rank';
|
||||
@@ -245,8 +236,7 @@ interface _PostProcessingHistogram {
|
||||
};
|
||||
}
|
||||
export type PostProcessingHistogram =
|
||||
| _PostProcessingHistogram
|
||||
| DefaultPostProcessing;
|
||||
_PostProcessingHistogram | DefaultPostProcessing;
|
||||
|
||||
/**
|
||||
* Parameters for chart data postprocessing.
|
||||
|
||||
@@ -51,8 +51,7 @@ export type QueryFormColumn = PhysicalColumn | AdhocColumn;
|
||||
* Format: [metric/column, is_ascending].
|
||||
*/
|
||||
export type QueryFormOrderBy =
|
||||
| [QueryFormColumn | QueryFormMetric | {}, boolean]
|
||||
| [];
|
||||
[QueryFormColumn | QueryFormMetric | {}, boolean] | [];
|
||||
|
||||
export interface FormDataResidual {
|
||||
[key: string]: any;
|
||||
|
||||
@@ -30,14 +30,7 @@ export enum ComparisonTimeRangeType {
|
||||
}
|
||||
|
||||
export type DateTimeGrainType =
|
||||
| 'second'
|
||||
| 'minute'
|
||||
| 'hour'
|
||||
| 'day'
|
||||
| 'week'
|
||||
| 'month'
|
||||
| 'quarter'
|
||||
| 'year';
|
||||
'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||
|
||||
export type CustomRangeKey =
|
||||
| 'sinceMode'
|
||||
|
||||
@@ -29,13 +29,11 @@ export enum FeatureFlag {
|
||||
AlertReportWebhook = 'ALERT_REPORT_WEBHOOK',
|
||||
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
|
||||
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
|
||||
AvoidColorsCollision = 'AVOID_COLORS_COLLISION',
|
||||
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
|
||||
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
|
||||
CssTemplates = 'CSS_TEMPLATES',
|
||||
DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION',
|
||||
DashboardVirtualizationDeferData = 'DASHBOARD_VIRTUALIZATION_DEFER_DATA',
|
||||
DashboardRbac = 'DASHBOARD_RBAC',
|
||||
DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT',
|
||||
DatasetFolders = 'DATASET_FOLDERS',
|
||||
DateRangeTimeshiftsEnabled = 'DATE_RANGE_TIMESHIFTS_ENABLED',
|
||||
@@ -48,6 +46,7 @@ export enum FeatureFlag {
|
||||
EmbeddedSuperset = 'EMBEDDED_SUPERSET',
|
||||
EnableAdvancedDataTypes = 'ENABLE_ADVANCED_DATA_TYPES',
|
||||
EnableExtensions = 'ENABLE_EXTENSIONS',
|
||||
EnableViewers = 'ENABLE_VIEWERS',
|
||||
/** @deprecated */
|
||||
EnableJavascriptControls = 'ENABLE_JAVASCRIPT_CONTROLS',
|
||||
EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING',
|
||||
|
||||
@@ -95,6 +95,17 @@ describe('isProbablyHTML', () => {
|
||||
expect(isProbablyHTML('price < $100')).toBe(false);
|
||||
});
|
||||
|
||||
test('should return false for angle-bracketed data values (issue #34082)', () => {
|
||||
// MySQL column values wrapped in angle brackets must not be treated as
|
||||
// HTML, otherwise they get swallowed and appear truncated in SQL Lab
|
||||
// and Table chart results
|
||||
expect(isProbablyHTML('<Buddhist Blue Duck-No Giblets>')).toBe(false);
|
||||
expect(isProbablyHTML('<Roasted Chicken-Whole>')).toBe(false);
|
||||
expect(
|
||||
isProbablyHTML('prefix <Buddhist Blue Duck-No Giblets> suffix'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('should return true for all known HTML tags', () => {
|
||||
expect(isProbablyHTML('<section>Content</section>')).toBe(true);
|
||||
expect(isProbablyHTML('<article>Content</article>')).toBe(true);
|
||||
@@ -146,6 +157,12 @@ describe('safeHtmlSpan', () => {
|
||||
const result = safeHtmlSpan(plainText);
|
||||
expect(result).toEqual(plainText);
|
||||
});
|
||||
|
||||
test('should return angle-bracketed data values untouched (issue #34082)', () => {
|
||||
const dataValue = '<Buddhist Blue Duck-No Giblets>';
|
||||
expect(safeHtmlSpan(dataValue)).toBe(dataValue);
|
||||
expect(sanitizeHtmlIfNeeded(dataValue)).toBe(dataValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeHTMLTags', () => {
|
||||
|
||||