Compare commits

...

55 Commits

Author SHA1 Message Date
Mehmet Salih Yavuz
cd01d1d433 test(react18): drive reorder drag tests with react-dnd-test-backend
Replace the test.skip with a real fix. The two failing tests
(DndMetricSelect > "can drag metrics" and DndColumnMetricSelect > "can drag
and reorder items") drove drags via fireEvent.dragStart/dragOver/drop, which
under React 18 + react-dnd@^11 raced the redux dispatch and tripped
`Cannot call hover while not dragging`.

Switch the test wrapper to support `useDnd: 'test'` (TestBackend) and have
the two tests drive the drag via the backend's programmatic
simulateBeginDrag/simulateHover/simulateDrop API. Sidesteps the jsdom
HTML5-event/preventDefault/zero-rect gaps entirely; this is the pattern
react-dnd itself recommends for tests.

Expose `data-drag-source-id` / `data-drop-target-id` from OptionWrapper and
OptionControls (sourced from `monitor.getHandlerId()` in the collect fn) so
the tests can hand the backend the right handler IDs without hardcoding
"S1"/"T4". No runtime change otherwise.
2026-05-14 21:36:35 +03:00
Mehmet Salih Yavuz
84178bf4e3 docs(react18): expand TODO on skipped drag tests with three fix paths
The previous skip just said "upgrade react-dnd". After actually testing the
upgrade locally I confirmed two things:

  1. react-dnd@16 alone *does* fix the original "Cannot call hover while not
     dragging" Invariant — but v14+ requires `type` at the top level of every
     `useDrag` call (DatasourcePanelDragOption, OptionWrapper, OptionControls,
     and the dashboard legacy `DragSource` HOC sites), which is a bigger
     migration than belongs in a React-version bump.
  2. Even with the upgrade, these specific tests still fail at a *second*
     hurdle: OptionWrapper.hover compares `monitor.getClientOffset` against
     `getBoundingClientRect`, both of which jsdom returns as zeroes/null, so
     the in-place reorder never fires.

Replace the one-line "upgrade react-dnd" TODO with the three concrete paths
(library bump, react-dnd-test-backend in spec/helpers, or local DOM mocks)
so the follow-up is actionable without re-doing the investigation.
2026-05-14 20:28:10 +03:00
Mehmet Salih Yavuz
daf78c9374 test(react18): silence jest/no-disabled-tests on the skipped drag tests
oxlint's jest/no-disabled-tests fires on the two `test.skip` calls added in
the previous commit. Add a targeted eslint-disable-next-line so the skip
stays explicit (TODO points at the react-dnd upgrade) without breaking lint.
2026-05-14 19:28:37 +03:00
Mehmet Salih Yavuz
705653f3ba test(react18): skip 2 reorder-within-list drag tests pending react-dnd upgrade
react-dnd@^11.1.3 is 5 majors behind current. Under React 18 the dragStart
redux dispatch from react-dnd-html5-backend@11 doesn't reach
`monitor.isDragging()` before the next fireEvent, so drag tests that combine
useDrag + useDrop on the same element raise "Cannot call hover while not
dragging" at drop. Pure source-only drag tests in these same files still pass.

Skip the two affected tests with a TODO so the rest of the suite stays green;
the proper fix is upgrading react-dnd to v14+ (full React 18 support), which
is out of scope for this bump.
2026-05-14 19:09:43 +03:00
Mehmet Salih Yavuz
c6442adde1 fix(react18): resolve lint and findDOMNode/console-spy test failures
Lint / type errors:
- antd v5 doesn't re-export TooltipRef at the top level; import it from
  antd/es/tooltip in Tooltip and Popover wrappers.
- AntdAvatar.Group is a plain React.FC and rejects refs — drop forwardRef
  on the AvatarGroup wrapper.
- BaseIcon forwardRef target straddled HTMLSpanElement and SVGSVGElement;
  widen the ref type and cast the antd Icon branch to bypass the
  intersection.

Test failures from console / DOM behaviour change in React 18:
- ThemeController: re-spy console.warn/error in beforeEach so the
  jest-fail-on-console wrapper doesn't replace the spy each test.
- logging.test: restore window.console in a finally so the failOnConsole
  afterEach can read console[methodName].
- QueryAutoRefresh / ChartTable: import `act` from `react` instead of
  the deprecated `react-dom/test-utils`.
- CopyToClipboard: wrap the cloneElement copy node in a span so antd
  Tooltip has a real DOM ref target.
- GenericLink: convert to forwardRef so it can be a Tooltip trigger.
- DateFilterLabel: wrap the DateLabel trigger in a span — DateLabel
  forwards its ref to an inner span, which would otherwise become the
  popover's positioning anchor under React 18.
- getControlItemsMap: wrap StyledRowFormItem inside the Tooltip in a
  span so antd doesn't fall back to findDOMNode.
- SqlEditorLeftBar: wrap DatabaseSelector inside the Popover in a span
  for the same reason.
- IconTooltip: forwardRef so it can be used as a tooltip / popover
  trigger child.

Also pick up pre-commit auto-fixes (ruff PERF -> dict.fromkeys, prettier
formatting) so pre-commit passes cleanly on CI.
2026-05-14 18:27:13 +03:00
Mehmet Salih Yavuz
bf4840dea1 fix(react18): forwardRef Popover wrapper, wrap explore popover trigger children in span
- Popover (superset-ui-core): forward refs to antd Popover so consumers that
  pass it as a trigger child can attach a DOM ref without triggering findDOMNode.
- Explore popover triggers (ColorBreakpoint, Contour, DndColumnSelect, AdhocFilter):
  the popover children are arbitrary user nodes which are often function
  components without forwardRef. Wrapping them in a span gives antd's resize
  observer a real DOM ref to attach, eliminating the React 18 deprecation warning.
2026-05-14 16:23:33 +03:00
Mehmet Salih Yavuz
f80000102b fix(react18): forwardRef Avatar/AvatarGroup, wrap DropdownButton in span
- Avatar / AvatarGroup: antd Avatar.Group children pass through a resize
  observer, so the wrappers need to support refs to skip the findDOMNode
  fallback.
- DropdownButton: antd Dropdown.Button is a plain function component, so the
  Tooltip wrapping it cannot attach a ref. Wrap in a span so the resize
  observer attaches to a real DOM node.
2026-05-14 16:13:46 +03:00
Mehmet Salih Yavuz
f67bd7eadf fix(react18): silence findDOMNode / defaultProps deprecations from popovers and modal
- Modal: pass nodeRef to react-draggable so it stops falling back to
  ReactDOM.findDOMNode (deprecated in React 18).
- MetricsControl: replace the static MetricsControl.defaultProps assignment with
  destructured parameter defaults (React 18 deprecates defaultProps on function
  components).
- AdhocMetricPopoverTrigger: wrap the popover trigger child in a span so antd
  Popover's resize observer can attach a ref to a real DOM node, avoiding the
  findDOMNode fallback path.
2026-05-14 15:59:51 +03:00
Mehmet Salih Yavuz
bcea2c2668 fix(react18): forwardRef on superset-ui-core wrappers used as tooltip/dropdown triggers
React 18.3 promotes findDOMNode to a deprecation warning. rc-resize-observer
(inside antd Tooltip/Dropdown) falls back to findDOMNode when its child does not
support refs. Without forwardRef on Button, Label, Tooltip, BaseIconComponent,
AsyncIcon, the antd-generated Icons (antdEnhancedIcons + iconOverrides), and
the AsyncIcon jest mock, every Tooltip/Dropdown wrapping these would emit the
warning and fail jest-fail-on-console.

EditableTitle wraps antd Input.TextArea in a Tooltip. TextArea's imperative ref
returns { resizableTextArea, focus, blur } (not a DOM node), so the resize
observer's findDOMNode-fallback fires anyway; wrap in a span to attach a real
DOM ref.

Also drop the empty IconWithoutRef helper in RefreshLabel that was wrapping an
icon in forwardRef without forwarding the ref.
2026-05-14 15:48:24 +03:00
Mehmet Salih Yavuz
8cfbf24b81 chore(deps): bump react to ^18.3.0 2026-05-11 11:23:40 +03:00
Evan Rusackas
f81821086a chore(releasing): fix email parsing in verify_release.py (#39602)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 08:57:33 -07:00
dependabot[bot]
f67dd4a8f3 chore(deps): bump geostyler from 18.5.0 to 18.5.1 in /superset-frontend (#39702)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 16:17:31 -07:00
Maxime Beauchemin
68fa8e2733 fix(viz): flatten MultiIndex columns in Time-Series Table for multiple Group By (#37869)
Co-authored-by: Claude Opus 4 <noreply@anthropic.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-05-08 16:11:13 -07:00
Maxime Beauchemin
a60860c969 fix(table): fall back to datasource columns for conditional formatting when query results are empty (#39345)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-05-08 16:10:41 -07:00
Maxime Beauchemin
d023fe1703 fix(trino/presto): use equality for boolean filters to support computed columns (#39500) 2026-05-08 16:10:27 -07:00
Amin Ghadersohi
547660dcc4 fix(mcp): ASCII chart crashes with NaN when dataset contains null values (#39916) 2026-05-08 17:35:15 -04:00
Joe Li
e934f2af92 fix(tests): prevent jest hangs caused by MessageChannel-mocked React scheduler (#39957)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 14:27:03 -07:00
Amin Ghadersohi
cfb0b6e811 fix(mcp): clarify request wrapper in list_datasets, list_charts, list_dashboards (#39920) 2026-05-08 16:01:07 -04:00
Amin Ghadersohi
ff7dc53853 fix(mcp): get_chart_sql drops x_axis on echarts_timeseries_* and only renders one query for mixed_timeseries (#39865) 2026-05-08 15:29:28 -04:00
dependabot[bot]
dce3317bc9 chore(deps-dev): bump typescript-eslint from 8.59.1 to 8.59.2 in /docs (#39876)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 11:24:38 -07:00
dependabot[bot]
dc22b82d88 chore(deps-dev): bump @typescript-eslint/parser from 8.59.1 to 8.59.2 in /superset-websocket (#39874)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-08 11:24:22 -07:00
Evan Rusackas
0250092378 chore(frontend): TypeScript 6.0 readiness — declaration emit fixes (Phase A) (#39530)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:42:07 -07:00
Beto Dealmeida
4311a15eb2 feat(sqlglot): Vertica dialect (#39969) 2026-05-08 14:34:34 -03:00
Evan Rusackas
b899556130 docs: Superset 6.1 documentation catch-up (security, alerts/reports, theming, config) (#39440)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:11:09 -07:00
Evan Rusackas
2f82236b29 feat(docs): expand docs:screenshots generator with manifest and tutorial captures (#39444)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-05-08 10:02:19 -07:00
Evan Rusackas
5bde86785f fix(docs): read capability flags from engine specs in database docs generator (#39449)
Co-authored-by: Superset Dev <dev@superset.apache.org>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 09:13:08 -07:00
Mehmet Salih Yavuz
69fbbfd7ce fix(table): consolidate visual column options under Visual formatting section (#39856) 2026-05-08 10:43:38 +03:00
Enzo Martellucci
d3784879c2 fix(embedded-sdk): grant fullscreen and clipboard-write by default (#39943) 2026-05-08 09:28:55 +02:00
Vitor Avila
ad5e3170dd fix: OpenSearch dialect identifier delimiters (#39953) 2026-05-07 16:19:27 -03:00
Maxime Beauchemin
aa710672ed fix(ui): remove makeUrl() double-prefix bugs under subdirectory deployment (#39503)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com>
2026-05-07 15:39:38 -03:00
Richard Fogaca Nienkotter
8c80caefa3 fix(explore): preserve preview chart name on save (#39908) 2026-05-07 13:08:28 -03:00
Richard Fogaca Nienkotter
8088c5d1de fix(dashboard): match auto-refresh paused-dot outline to icon color (#39909) 2026-05-07 13:07:52 -03:00
Amin Ghadersohi
9b520312a1 fix(mcp): use tiktoken for response-size-guard token estimation (#39912) 2026-05-07 11:51:31 -04:00
Amin Ghadersohi
9ac4711ac8 fix(mcp): prevent DetachedInstanceError in get_chart_preview (#39921) 2026-05-07 11:44:11 -04:00
dependabot[bot]
7593d2a164 chore(deps): bump caniuse-lite from 1.0.30001791 to 1.0.30001792 in /docs (#39933)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 21:57:29 +07:00
dependabot[bot]
d3c44e311e chore(deps): bump aws-actions/amazon-ecr-login from 2.1.4 to 2.1.5 (#39931)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 21:54:59 +07:00
Enzo Martellucci
b5186d1c65 fix(reports): keep body sized so standalone screenshots don't time out (#39944) 2026-05-07 12:26:50 +02:00
bdonovan1
5b5dd01028 fix(sqla): parenthesize calculated column expressions in WHERE clause (#39793)
Co-authored-by: Brian Donovan <briand@netflix.com>
Co-authored-by: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com>
2026-05-06 19:45:27 -03:00
bialkou
4aa4415d8f fix(i18n): update Russian translations (#39589)
Co-authored-by: bito-code-review[bot] <188872107+bito-code-review[bot]@users.noreply.github.com>
2026-05-06 13:05:23 -04:00
Sebastian Mohr
e667ceb6cf feat(themes): expose active theme mode via data-theme-mode attribute (#39063) 2026-05-06 18:17:54 +03:00
Enzo Martellucci
9aaa12c7d4 fix(reports): preserve urlParams in multi-tab report fan-out (#39884) 2026-05-06 16:29:45 +02:00
Alexandru Soare
adfbbf1433 fix(sql): quote identifiers in transpile_to_dialect to fix case-sensitive column filters (#39521) 2026-05-06 10:53:09 +03:00
dependabot[bot]
d7663a9a1c chore(deps-dev): update denodo-sqlalchemy requirement from ~=1.0.6 to >=1.0.6,<2.1.0 (#39832)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:17:21 -07:00
dependabot[bot]
7290d3c452 chore(deps-dev): update pyathena requirement from <3,>=2 to >=2,<4 (#39830)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:17:00 -07:00
dependabot[bot]
d7beffcec1 chore(deps-dev): bump eslint-plugin-react-you-might-not-need-an-effect from 0.9.3 to 0.10.0 in /superset-frontend (#39853)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:15:10 -07:00
dependabot[bot]
f018b67895 chore(deps-dev): update sqlalchemy-vertica-python requirement from <0.6,>=0.5.9 to >=0.5.9,<0.7 (#39831)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:14:08 -07:00
dependabot[bot]
5e2c6d8c9e chore(deps): bump nanoid from 5.1.9 to 5.1.11 in /superset-frontend (#39820)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:13:52 -07:00
dependabot[bot]
b305c8681c chore(deps-dev): update impyla requirement from <0.17,>0.16.2 to >0.16.2,<0.23 (#39833)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:09:37 -07:00
dependabot[bot]
d578fa1949 chore(deps): bump @deck.gl/mapbox from 9.3.1 to 9.3.2 in /superset-frontend (#39814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-05 22:09:33 -07:00
dependabot[bot]
14d28c34fd chore(deps-dev): update cx-oracle requirement from <8.1,>8.0.0 to >8.0.0,<8.4 (#39753)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 22:05:54 -07:00
dependabot[bot]
c06aee8513 chore(deps-dev): bump jsdom from 29.1.0 to 29.1.1 in /superset-frontend (#39815)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-05-05 22:04:47 -07:00
dependabot[bot]
d0ef19953a chore(deps): bump memoize-one from 5.2.1 to 6.0.0 in /superset-frontend/plugins/plugin-chart-ag-grid-table (#37910)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-05-05 21:38:49 -07:00
Vitor Avila
3745e37182 fix(OAuth2): Support OAuth2 exception with legacy endpoint (#39897) 2026-05-05 21:21:48 -03:00
Joe Li
4b17ac2629 fix(explore): add matrixify_enable guard to prevent stale validators on pre-revamp charts (#38765)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-05 16:45:38 -07:00
Amin Ghadersohi
4a21a5365f fix(mcp): validate column refs in generate_explore_link, update_chart_preview, and update_chart (#39797) 2026-05-05 19:12:31 -04:00
203 changed files with 16382 additions and 5041 deletions

View File

@@ -58,7 +58,7 @@ jobs:
- name: Login to Amazon ECR
if: steps.describe-services.outputs.active == 'true'
id: login-ecr
uses: aws-actions/amazon-ecr-login@19d944daaa35f0fa1d3f7f8af1d3f2e5de25c5b7 # v2
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
- name: Delete ECR image tag
if: steps.describe-services.outputs.active == 'true'

View File

@@ -199,7 +199,7 @@ jobs:
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@19d944daaa35f0fa1d3f7f8af1d3f2e5de25c5b7 # v2
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
- name: Load, tag and push image to ECR
id: push-image
@@ -235,7 +235,7 @@ jobs:
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@19d944daaa35f0fa1d3f7f8af1d3f2e5de25c5b7 # v2
uses: aws-actions/amazon-ecr-login@fa648b43de3d4d023bcb3f89ed6940096949c419 # v2
- name: Check target image exists in ECR
id: check-image

View File

@@ -56,8 +56,33 @@ def verify_sha512(filename: str) -> str:
# Part 2: Verify RSA key - this is the same as running `gpg --verify {release}.asc {release}` and comparing the RSA key and email address against the KEYS file # noqa: E501
KEYS_URL = "https://downloads.apache.org/superset/KEYS"
def ensure_keys_imported() -> None:
"""Import the Apache Superset KEYS file into the local GPG keyring.
Without this, `gpg --verify` returns "No public key" and the signature
cannot actually be verified — only the key ID in the signature metadata
is visible.
"""
try:
keys = requests.get(KEYS_URL, timeout=30)
except requests.RequestException as exc:
print(f"Warning: could not fetch KEYS file for import: {exc}")
return
if keys.status_code != 200:
print(f"Warning: could not fetch KEYS file (HTTP {keys.status_code})")
return
subprocess.run( # noqa: S603
["gpg", "--import"], # noqa: S607
input=keys.content,
capture_output=True,
)
def get_gpg_info(filename: str) -> tuple[Optional[str], Optional[str]]:
"""Run the GPG verify command and extract RSA key and email address."""
"""Run the GPG verify command and extract RSA/EDDSA key and email address."""
asc_filename = filename + ".asc"
result = subprocess.run( # noqa: S603
["gpg", "--verify", asc_filename, filename], # noqa: S607
@@ -65,25 +90,50 @@ def get_gpg_info(filename: str) -> tuple[Optional[str], Optional[str]]:
)
output = result.stderr.decode()
# If no public key was available, import KEYS and retry so that
# `Good signature from "Name <email>"` appears in the output.
if "No public key" in output:
ensure_keys_imported()
result = subprocess.run( # noqa: S603
["gpg", "--verify", asc_filename, filename], # noqa: S607
capture_output=True, # noqa: S607
)
output = result.stderr.decode()
rsa_key = re.search(r"RSA key ([0-9A-F]+)", output)
eddsa_key = re.search(r"EDDSA key ([0-9A-F]+)", output)
email = re.search(r'issuer "([^"]+)"', output)
# Try multiple patterns — `Good signature from` is the most reliable
# source of the email; `issuer` is a fallback for older gpg output.
email_patterns = (
r'Good signature from ".*?<([^>]+)>"',
r'aka ".*?<([^>]+)>"',
r'issuer "([^"]+)"',
)
email_result: Optional[str] = None
for pattern in email_patterns:
match = re.search(pattern, output)
if match:
email_result = match.group(1)
break
rsa_key_result = rsa_key.group(1) if rsa_key else None
eddsa_key_result = eddsa_key.group(1) if eddsa_key else None
email_result = email.group(1) if email else None
key_result = rsa_key_result or eddsa_key_result
# Debugging:
if key_result:
print("RSA or EDDSA Key found")
else:
print("Warning: No RSA or EDDSA key found in GPG verification output.")
if email_result:
print("email found")
print(f"Email found: {email_result}")
else:
print("Warning: No email address found in GPG verification output.")
if "No public key" in output:
print(
"Hint: public key is not in your keyring. Import it with:\n"
f" curl -s {KEYS_URL} | gpg --import"
)
return key_result, email_result

View File

@@ -81,6 +81,87 @@ SLACK_CACHE_TIMEOUT = int(timedelta(days=2).total_seconds())
SLACK_API_RATE_LIMIT_RETRY_COUNT = 5
```
### Webhook integration
Superset can send alert and report notifications to any HTTP endpoint — useful for chat platforms, incident management tools, or custom automation.
#### Enabling Webhooks
Enable the feature flag in `superset_config.py`:
```python
FEATURE_FLAGS = {
"ALERT_REPORTS": True,
"ALERT_REPORT_WEBHOOK": True,
}
```
#### Configuring a Webhook Recipient
When creating or editing an alert or report, select **Webhook** as the notification method and enter your endpoint URL.
#### Payload Format
Superset sends an HTTP POST with `Content-Type: application/json`:
```json
{
"name": "My Alert",
"header": {
"notification_format": "JSON",
"notification_type": "Alert",
"notification_source": "Alert",
"chart_id": 42,
"dashboard_id": null
},
"text": "Alert condition met: value exceeded threshold",
"description": "Monthly revenue dropped below target",
"url": "https://your-superset-host/superset/dashboard/1/"
}
```
When a report includes file attachments (CSV, PDF, or PNG screenshots), the request is sent as `multipart/form-data` instead. In that case, each top-level payload field (`name`, `text`, `description`, `url`) becomes its own form field, and nested structures like `header` are serialized as a JSON-encoded string in their own field. Every attachment is added as a repeated form field named `files`:
```
POST /webhook HTTP/1.1
Content-Type: multipart/form-data; boundary=...
--...
Content-Disposition: form-data; name="name"
My Alert
--...
Content-Disposition: form-data; name="header"
{"notification_format": "JSON", "notification_type": "Alert", ...}
--...
Content-Disposition: form-data; name="text"
Alert condition met: value exceeded threshold
--...
Content-Disposition: form-data; name="files"; filename="report.csv"
Content-Type: text/csv
<file bytes>
--...
```
Webhook consumers should branch on `Content-Type`: parse the body as JSON when `application/json`, or read the individual form fields (decoding `header` as JSON) when `multipart/form-data`.
#### HTTPS Enforcement
To require HTTPS webhook URLs (recommended for production), set:
```python
ALERT_REPORTS_WEBHOOK_HTTPS_ONLY = True
```
When enabled, Superset rejects webhook configurations that use `http://` URLs.
#### Retry Behavior
Superset automatically retries webhook deliveries on `429 Too Many Requests` and `5xx` server errors using exponential backoff.
### Kubernetes-specific
- You must have a `celery beat` pod running. If you're using the chart included in the GitHub repository under [helm/superset](https://github.com/apache/superset/tree/master/helm/superset), you need to put `supersetCeleryBeat.enabled = true` in your values override.

View File

@@ -472,6 +472,38 @@ FEATURE_FLAGS = {
A current list of feature flags can be found in the [Feature Flags](/admin-docs/configuration/feature-flags) documentation.
## Security Configuration
### HASH_ALGORITHM
Controls the hashing algorithm used for internal checksums and cache keys (thumbnails, cache keys, etc.). The default is `sha256`, which satisfies environments with stricter compliance requirements (e.g., FedRAMP). Set it to `md5` to retain the legacy behavior from older Superset deployments:
```python
HASH_ALGORITHM = "sha256" # default; set to "md5" for legacy behavior
```
A companion `HASH_ALGORITHM_FALLBACKS` list (default: `["md5"]`) lets UUID lookups fall back to older algorithms, which enables gradual migration without breaking existing entries. Set it to `[]` for strict mode (use only `HASH_ALGORITHM`).
:::note
This setting affects internal Superset operations only, not user passwords or authentication tokens. Changing it in an existing deployment may invalidate cached values but does not require a database migration.
:::
## SQL Lab Query History Pruning
SQL Lab query history is stored in the metadata database and is **not** pruned by default. To trim older rows, enable the `prune_query` Celery beat task by uncommenting it in `CELERY_BEAT_SCHEDULE` and choosing a retention window:
```python
CELERY_BEAT_SCHEDULE = {
"prune_query": {
"task": "prune_query",
"schedule": crontab(minute=0, hour=0, day_of_month=1),
"kwargs": {"retention_period_days": 180},
},
}
```
Adjust `retention_period_days` to control how long query rows are kept. Companion opt-in tasks (`prune_logs`, `prune_tasks`) exist for pruning the logs and tasks tables; see the commented-out examples in `superset/config.py`. Without enabling these tasks, the metadata database will grow unbounded over time.
:::resources
- [Blog: Feature Flags in Apache Superset](https://preset.io/blog/feature-flags-in-apache-superset-and-preset/)
:::

View File

@@ -122,6 +122,17 @@ When `ENABLE_UI_THEME_ADMINISTRATION = True`:
3. Administrators can change system themes without restarting Superset
4. Configuration file themes serve as fallbacks when no UI themes are set
### Theme Validation and Fallback
Superset validates theme JSON when it is saved, either through the UI or via configuration. If a theme contains invalid tokens or an unrecognized structure, Superset logs a warning and falls back to the built-in default theme rather than applying a broken configuration. This prevents a bad theme from rendering the application unusable.
The fallback order is:
1. **UI-configured system theme** (highest priority, if `ENABLE_UI_THEME_ADMINISTRATION = True`)
2. **`THEME_DEFAULT` / `THEME_DARK`** from `superset_config.py`
3. **Built-in Superset default theme** (always present as a safety net)
If you see unexpected styling after a config change, check the Superset server logs for theme validation warnings.
### Copying Themes Between Systems
To export a theme for use in configuration files or another instance:
@@ -143,7 +154,11 @@ Superset supports custom fonts through the theme configuration, allowing you to
### Default Fonts
By default, Superset uses Inter and Fira Code fonts which are bundled with the application via `@fontsource` packages. These fonts work offline and require no external network calls.
By default, Superset uses **Inter** for UI text and **IBM Plex Mono** for code (SQL editors, JSON fields, and other monospace contexts). Both fonts are bundled with the application via `@fontsource` packages and work offline without any external network calls.
:::note
IBM Plex Mono replaced Fira Code as the default code font in Superset 6.1. If you have an existing theme that explicitly sets `fontFamilyCode: "Fira Code, ..."`, you may want to update it.
:::
### Configuring Custom Fonts

View File

@@ -205,6 +205,57 @@ FAB_ADD_SECURITY_API = True
Once configured, the documentation for additional "Security" endpoints will be visible in Swagger for you to explore.
### API Key Authentication
Superset supports long-lived API keys for service accounts, CI/CD pipelines, and programmatic integrations (including MCP clients).
#### Enabling API Key Authentication
API key authentication is **disabled by default**. To turn it on, set the Flask-AppBuilder config value in `superset_config.py` and also enable the matching feature flag so the management UI is exposed:
```python
FAB_API_KEY_ENABLED = True
FEATURE_FLAGS = {
"FAB_API_KEY_ENABLED": True,
}
```
The config value registers the `ApiKeyApi` blueprint on the backend; the feature flag controls whether the UI for managing keys appears for the user. See the [Feature Flags](/admin-docs/configuration/feature-flags) documentation for more on feature flag configuration.
#### Creating an API Key
Once enabled, each user manages their own keys from their profile page:
1. Open the user menu (top-right) and click **Info** to navigate to the User Info page
2. Expand the **API Keys** section
3. Click **+ API Key**
4. Enter a name and (optionally) an expiration date
5. Copy the generated token — it is shown only once
Only users with the `can_read` and `can_write` permissions on `ApiKey` (granted by default to Admins) can manage API keys.
#### Using an API Key
Pass the key as a Bearer token in the `Authorization` header:
```
Authorization: Bearer <your-api-key>
```
This works for all REST API endpoints and the MCP server. The request is executed with the permissions of the user who created the key.
#### Use Cases
- **CI/CD pipelines** — automated chart/dashboard exports and imports
- **MCP integrations** — connect AI assistants without interactive login
- **External services** — dashboards embedded in other applications
- **Service accounts** — long-lived credentials that don't expire with session cookies
:::caution
Store API keys securely. Anyone with a valid key can make requests on behalf of the creating user. Revoke keys promptly if they are compromised by deleting them from the **API Keys** section of your User Info page.
:::
### Customizing Permissions
The permissions exposed by FAB are very granular and allow for a great level of

View File

@@ -70,7 +70,7 @@
"@swc/core": "^1.15.33",
"antd": "^6.3.7",
"baseline-browser-mapping": "^2.10.27",
"caniuse-lite": "^1.0.30001791",
"caniuse-lite": "^1.0.30001792",
"docusaurus-plugin-openapi-docs": "^5.0.2",
"docusaurus-theme-openapi-docs": "^5.0.2",
"js-yaml": "^4.1.1",
@@ -106,7 +106,7 @@
"globals": "^17.6.0",
"prettier": "^3.8.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.59.1",
"typescript-eslint": "^8.59.2",
"webpack": "^5.106.2"
},
"browserslist": {

View File

@@ -141,6 +141,47 @@ def eval_node(node):
return "<f-string>"
return None
def static_return_bool(func_node):
"""
Statically resolve a method's return value to a bool when possible.
Returns True/False for functions whose body is (effectively) a single
\`return True\` / \`return False\` — allowing a leading docstring and
ignoring pure-comment/pass statements. Returns None for anything more
complex (conditional returns, computed values, no return, etc.).
Used by \`has_implicit_cancel\` handling: \`diagnose()\` in lib.py calls
the method and checks the return value, so an override that explicitly
returns False must NOT be treated as enabling query cancelation.
"""
returns = []
other_logic = False
docstring_skipped = False
for stmt in func_node.body:
# Skip docstring (only the FIRST expression statement that is a
# string constant — later bare string literals are not docstrings
# and should count as non-trivial logic).
if (not docstring_skipped
and isinstance(stmt, ast.Expr)
and isinstance(stmt.value, ast.Constant)
and isinstance(stmt.value.value, str)):
docstring_skipped = True
continue
if isinstance(stmt, ast.Pass):
continue
if isinstance(stmt, ast.Return):
returns.append(stmt)
continue
# Any other statement (if/for/assign/etc.) means control flow is
# non-trivial; bail out to be conservative.
other_logic = True
break
if other_logic or len(returns) != 1:
return None
val = eval_node(returns[0].value)
return val if isinstance(val, bool) else None
def deep_merge(base, override):
"""Deep merge two dictionaries. Override values take precedence."""
if base is None:
@@ -186,8 +227,55 @@ if not os.path.isdir(specs_dir):
print(json.dumps({"error": f"Directory not found: {specs_dir}", "cwd": os.getcwd()}))
sys.exit(1)
# First pass: collect all class info (name, bases, metadata)
class_info = {} # class_name -> {bases: [], metadata: {}, engine_name: str, filename: str}
# Capability flag attributes with their defaults from BaseEngineSpec
CAP_ATTR_DEFAULTS = {
'supports_dynamic_schema': False,
'supports_catalog': False,
'supports_dynamic_catalog': False,
'disable_ssh_tunneling': False,
'supports_file_upload': True,
'allows_joins': True,
'allows_subqueries': True,
}
# Maps source capability attribute -> output field name used in databases.json.
# When a cap attr is assigned an unevaluable expression (e.g.
# allows_joins = is_feature_enabled("DRUID_JOINS")), the JS layer uses this
# mapping to preserve the corresponding field from the previously-generated
# JSON rather than silently inheriting an incorrect parent default.
CAP_ATTR_TO_OUTPUT_FIELD = {
'allows_joins': 'joins',
'allows_subqueries': 'subqueries',
'supports_dynamic_schema': 'supports_dynamic_schema',
'supports_catalog': 'supports_catalog',
'supports_dynamic_catalog': 'supports_dynamic_catalog',
'disable_ssh_tunneling': 'ssh_tunneling',
'supports_file_upload': 'supports_file_upload',
}
# Methods that indicate a capability when overridden by a non-BaseEngineSpec class.
# Mirrors the has_custom_method checks in superset/db_engine_specs/lib.py.
# cancel_query / has_implicit_cancel -> query_cancelation
# (diagnose() checks cancel_query override OR has_implicit_cancel() == True;
# base has_implicit_cancel returns False, so overriding it is the static
# equivalent of that method returning True. get_cancel_query_id is NOT
# part of the diagnose() heuristic and is intentionally excluded.)
# estimate_statement_cost / estimate_query_cost -> query_cost_estimation
# impersonate_user / update_impersonation_config / get_url_for_impersonation -> user_impersonation
# validate_sql -> sql_validation (not used yet; validation is engine-based)
CAP_METHODS = {
'cancel_query', 'has_implicit_cancel',
'estimate_statement_cost', 'estimate_query_cost',
'impersonate_user', 'update_impersonation_config', 'get_url_for_impersonation',
'validate_sql',
}
# Only the literal BaseEngineSpec is excluded from method-override tracking.
# Intermediate base classes (e.g. PrestoBaseEngineSpec) do count as overrides.
TRUE_BASE_CLASS = 'BaseEngineSpec'
# First pass: collect all class info (name, bases, metadata, cap_attrs, direct_methods)
class_info = {} # class_name -> {bases: [], metadata: {}, engine_name: str, filename: str, ...}
for filename in sorted(os.listdir(specs_dir)):
if not filename.endswith('.py') or filename in ('__init__.py', 'lib.py', 'lint_metadata.py'):
@@ -218,30 +306,54 @@ for filename in sorted(os.listdir(specs_dir)):
# Extract class attributes
engine_name = None
engine_attr = None
metadata = None
cap_attrs = {} # capability flag attributes defined directly in this class
# Cap attrs assigned via expressions we can't statically resolve
# (e.g. is_feature_enabled("FLAG")). Tracked so the JS layer can
# fall back to the previously-generated databases.json value
# rather than inherit a parent default that would be wrong.
unresolved_cap_attrs = set()
direct_methods = set() # capability methods defined directly in this class
for item in node.body:
if isinstance(item, ast.Assign):
for target in item.targets:
if isinstance(target, ast.Name):
if target.id == 'engine_name':
val = eval_node(item.value)
if isinstance(val, str):
engine_name = val
elif target.id == 'metadata':
metadata = eval_node(item.value)
if not isinstance(target, ast.Name):
continue
if target.id == 'engine_name':
val = eval_node(item.value)
if isinstance(val, str):
engine_name = val
elif target.id == 'engine':
val = eval_node(item.value)
if isinstance(val, str):
engine_attr = val
elif target.id == 'metadata':
metadata = eval_node(item.value)
elif target.id in CAP_ATTR_DEFAULTS:
val = eval_node(item.value)
if isinstance(val, bool):
cap_attrs[target.id] = val
else:
# Unevaluable expression — defer to JS fallback.
unresolved_cap_attrs.add(target.id)
elif isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
if item.name in CAP_METHODS:
# has_implicit_cancel is special: diagnose() uses the
# method's RETURN VALUE, not just its presence. If the
# override statically returns False, treat it as if
# the method weren't overridden so query_cancelation
# matches diagnose(). Unresolvable / True / anything
# else falls through as an override (conservative).
if item.name == 'has_implicit_cancel':
if static_return_bool(item) is False:
continue
direct_methods.add(item.name)
# Check for engine attribute with non-empty value to distinguish
# true base classes from product classes like OceanBaseEngineSpec
has_non_empty_engine = False
for item in node.body:
if isinstance(item, ast.Assign):
for target in item.targets:
if isinstance(target, ast.Name) and target.id == 'engine':
# Check if engine value is non-empty string
if isinstance(item.value, ast.Constant):
has_non_empty_engine = bool(item.value.value)
break
has_non_empty_engine = engine_attr is not None and bool(engine_attr)
# True base classes: end with BaseEngineSpec AND don't define engine
# or have empty engine (like PostgresBaseEngineSpec with engine = "")
@@ -254,13 +366,18 @@ for filename in sorted(os.listdir(specs_dir)):
'bases': base_names,
'metadata': metadata,
'engine_name': engine_name,
'engine': engine_attr,
'filename': filename,
'is_base_or_mixin': is_true_base,
'cap_attrs': cap_attrs,
'unresolved_cap_attrs': unresolved_cap_attrs,
'direct_methods': direct_methods,
}
except Exception as e:
errors.append(f"{filename}: {str(e)}")
# Second pass: resolve inheritance and build final metadata
# Second pass: resolve inheritance and build final metadata + capability flags
def get_inherited_metadata(class_name, visited=None):
"""Recursively get metadata from parent classes."""
if visited is None:
@@ -286,6 +403,64 @@ def get_inherited_metadata(class_name, visited=None):
return inherited
def get_resolved_caps(class_name, visited=None):
"""
Resolve capability flags and method overrides with inheritance.
Returns (attr_values, unresolved, methods):
- attr_values: {attr: bool} for attrs where the nearest MRO assignment
was a literal bool. Defaults are applied at the call site.
- unresolved: attrs where the nearest MRO assignment was an unevaluable
expression (e.g. is_feature_enabled("FLAG")). The JS layer falls
back to the previously-generated JSON value for these.
- methods: capability methods defined directly in some non-base ancestor,
matching the has_custom_method() logic in db_engine_specs/lib.py.
attr_values and unresolved are disjoint — an attr is in at most one.
"""
if visited is None:
visited = set()
if class_name in visited:
return {}, set(), set()
visited.add(class_name)
info = class_info.get(class_name)
if not info:
return {}, set(), set()
attr_values = {}
unresolved = set()
resolved_methods = set()
# Collect from parents, iterating right-to-left so leftmost bases win
# (matches Python MRO: for class C(A, B), A's attributes take precedence).
for base_name in reversed(info['bases']):
p_vals, p_unres, p_meth = get_resolved_caps(base_name, visited.copy())
# A parent's literal assignments overwrite whatever we inherited so far.
for attr, val in p_vals.items():
attr_values[attr] = val
unresolved.discard(attr)
# A parent's unresolved assignments likewise take precedence.
for attr in p_unres:
unresolved.add(attr)
attr_values.pop(attr, None)
resolved_methods.update(p_meth)
# Apply this class's own assignments (override parents).
for attr, val in info['cap_attrs'].items():
attr_values[attr] = val
unresolved.discard(attr)
for attr in info['unresolved_cap_attrs']:
unresolved.add(attr)
attr_values.pop(attr, None)
# Accumulate method overrides, but skip the literal BaseEngineSpec
# (its implementations are stubs; only non-base overrides count).
if class_name != TRUE_BASE_CLASS:
resolved_methods.update(info['direct_methods'])
return attr_values, unresolved, resolved_methods
for class_name, info in class_info.items():
# Skip base classes and mixins
if info['is_base_or_mixin']:
@@ -310,7 +485,14 @@ for class_name, info in class_info.items():
if final_metadata and isinstance(final_metadata, dict) and display_name:
debug_info["classes_with_metadata"] += 1
databases[display_name] = {
# Resolve capability flags from Python source
attr_values, unresolved_caps, cap_methods = get_resolved_caps(class_name)
cap_attrs = dict(CAP_ATTR_DEFAULTS)
cap_attrs.update(attr_values)
engine_attr = info.get('engine') or ''
entry = {
'engine': display_name.lower().replace(' ', '_'),
'engine_name': display_name,
'module': info['filename'][:-3], # Remove .py extension
@@ -318,19 +500,40 @@ for class_name, info in class_info.items():
'time_grains': {},
'score': 0,
'max_score': 0,
'joins': True,
'subqueries': True,
'supports_dynamic_schema': False,
'supports_catalog': False,
'supports_dynamic_catalog': False,
'ssh_tunneling': False,
'query_cancelation': False,
'supports_file_upload': False,
'user_impersonation': False,
'query_cost_estimation': False,
'sql_validation': False,
# Capability flags read from engine spec class attributes/methods
'joins': cap_attrs['allows_joins'],
'subqueries': cap_attrs['allows_subqueries'],
'supports_dynamic_schema': cap_attrs['supports_dynamic_schema'],
'supports_catalog': cap_attrs['supports_catalog'],
'supports_dynamic_catalog': cap_attrs['supports_dynamic_catalog'],
'ssh_tunneling': not cap_attrs['disable_ssh_tunneling'],
'supports_file_upload': cap_attrs['supports_file_upload'],
# Method-based flags: True only when a non-base class overrides them.
# Matches diagnose() in lib.py: cancel_query override OR
# has_implicit_cancel() returning True (which, given the base
# returns False, is equivalent to overriding has_implicit_cancel).
'query_cancelation': bool({'cancel_query', 'has_implicit_cancel'} & cap_methods),
'query_cost_estimation': bool({'estimate_statement_cost', 'estimate_query_cost'} & cap_methods),
# SQL validation is implemented in external validator classes keyed by engine name
'sql_validation': engine_attr in {'presto', 'postgresql'},
'user_impersonation': bool(
{'impersonate_user', 'update_impersonation_config', 'get_url_for_impersonation'} & cap_methods
),
}
# Tell the JS layer which output fields were populated from the
# BaseEngineSpec default because the source assignment was an
# unevaluable expression; those get overridden from existing JSON.
unresolved_fields = sorted(
CAP_ATTR_TO_OUTPUT_FIELD[attr]
for attr in unresolved_caps
if attr in CAP_ATTR_TO_OUTPUT_FIELD
)
if unresolved_fields:
entry['_unresolved_cap_fields'] = unresolved_fields
databases[display_name] = entry
if errors and not databases:
print(json.dumps({"error": "Parse errors", "details": errors, "debug": debug_info}), file=sys.stderr)
@@ -851,24 +1054,52 @@ function loadExistingData() {
}
}
/**
* Fall back to the previously-generated databases.json for capability flags
* whose source assignment couldn't be statically resolved (e.g.
* `allows_joins = is_feature_enabled("DRUID_JOINS")`). The Python extractor
* flags these via the internal `_unresolved_cap_fields` marker; without this
* fallback those fields would silently inherit the BaseEngineSpec default
* and disagree with runtime behavior. The marker is stripped before output.
*/
function fallbackUnresolvedCaps(newDatabases, existingData) {
for (const [name, db] of Object.entries(newDatabases)) {
const unresolved = db._unresolved_cap_fields;
if (!unresolved || unresolved.length === 0) {
delete db._unresolved_cap_fields;
continue;
}
const existingDb = existingData?.databases?.[name];
if (existingDb) {
for (const field of unresolved) {
if (existingDb[field] !== undefined) {
db[field] = existingDb[field];
}
}
}
delete db._unresolved_cap_fields;
}
return newDatabases;
}
/**
* Merge new documentation with existing diagnostics
* Preserves score, time_grains, and feature flags from existing data
* Preserves score, max_score, and time_grains from existing data (these require
* Flask context to generate and cannot be derived from static source analysis).
* Capability flags (joins, supports_catalog, etc.) are NOT preserved here — they
* are read fresh from the Python engine spec source by extractEngineSpecMetadata(),
* with a separate fallback for expression-based assignments (see fallbackUnresolvedCaps).
*/
function mergeWithExistingDiagnostics(newDatabases, existingData) {
if (!existingData?.databases) return newDatabases;
const diagnosticFields = [
'score', 'max_score', 'time_grains', 'joins', 'subqueries',
'supports_dynamic_schema', 'supports_catalog', 'supports_dynamic_catalog',
'ssh_tunneling', 'query_cancelation', 'supports_file_upload',
'user_impersonation', 'query_cost_estimation', 'sql_validation'
];
// Only preserve fields that require Flask/runtime context to generate
const diagnosticFields = ['score', 'max_score', 'time_grains'];
for (const [name, db] of Object.entries(newDatabases)) {
const existingDb = existingData.databases[name];
if (existingDb && existingDb.score > 0) {
// Preserve diagnostics from existing data
// Preserve score/time_grain diagnostics from existing data
for (const field of diagnosticFields) {
if (existingDb[field] !== undefined) {
db[field] = existingDb[field];
@@ -879,7 +1110,7 @@ function mergeWithExistingDiagnostics(newDatabases, existingData) {
const preserved = Object.values(newDatabases).filter(d => d.score > 0).length;
if (preserved > 0) {
console.log(`Preserved diagnostics for ${preserved} databases from existing data`);
console.log(`Preserved score/time_grains for ${preserved} databases from existing data`);
}
return newDatabases;
@@ -927,6 +1158,12 @@ async function main() {
databases = mergeWithExistingDiagnostics(databases, existingData);
}
// For cap flags assigned via unevaluable expressions (e.g.
// `is_feature_enabled(...)`), prefer the value from a previously-generated
// JSON. Runs regardless of scores since it addresses static-analysis gaps,
// not missing Flask diagnostics. Always strips the internal marker.
databases = fallbackUnresolvedCaps(databases, existingData);
// Extract and merge custom_errors for troubleshooting documentation
const customErrors = extractCustomErrors();
mergeCustomErrors(databases, customErrors);

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -5040,100 +5040,100 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@8.59.1", "@typescript-eslint/eslint-plugin@^8.52.0":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz#781bc6f9002982cfaf75a185240e24ad7276628a"
integrity sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==
"@typescript-eslint/eslint-plugin@8.59.2", "@typescript-eslint/eslint-plugin@^8.52.0":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz#f37b2c189a0177141fe3de3b08f2a83991bfdbfa"
integrity sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.59.1"
"@typescript-eslint/type-utils" "8.59.1"
"@typescript-eslint/utils" "8.59.1"
"@typescript-eslint/visitor-keys" "8.59.1"
"@typescript-eslint/scope-manager" "8.59.2"
"@typescript-eslint/type-utils" "8.59.2"
"@typescript-eslint/utils" "8.59.2"
"@typescript-eslint/visitor-keys" "8.59.2"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.59.1", "@typescript-eslint/parser@^8.59.0":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.1.tgz#835d20a62350659a082a1ae2a60b822c40488905"
integrity sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==
"@typescript-eslint/parser@8.59.2", "@typescript-eslint/parser@^8.59.0":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.59.2.tgz#e2fd0084baa5dd0c24cd789af1c72cbc3a7a1c62"
integrity sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==
dependencies:
"@typescript-eslint/scope-manager" "8.59.1"
"@typescript-eslint/types" "8.59.1"
"@typescript-eslint/typescript-estree" "8.59.1"
"@typescript-eslint/visitor-keys" "8.59.1"
"@typescript-eslint/scope-manager" "8.59.2"
"@typescript-eslint/types" "8.59.2"
"@typescript-eslint/typescript-estree" "8.59.2"
"@typescript-eslint/visitor-keys" "8.59.2"
debug "^4.4.3"
"@typescript-eslint/project-service@8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.1.tgz#49efe87c37ef84262f23df8bf62fdc56698ca6fe"
integrity sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==
"@typescript-eslint/project-service@8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz#f8b8cbf8692e3a51c2c394acf8cf6900f7e755af"
integrity sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.59.1"
"@typescript-eslint/types" "^8.59.1"
"@typescript-eslint/tsconfig-utils" "^8.59.2"
"@typescript-eslint/types" "^8.59.2"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz#ed90d054fc3db2d0c81464db3a953a94fb85bb58"
integrity sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==
"@typescript-eslint/scope-manager@8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz#63cbd0af2e3180949d6be81122cc555bc71e736d"
integrity sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==
dependencies:
"@typescript-eslint/types" "8.59.1"
"@typescript-eslint/visitor-keys" "8.59.1"
"@typescript-eslint/types" "8.59.2"
"@typescript-eslint/visitor-keys" "8.59.2"
"@typescript-eslint/tsconfig-utils@8.59.1", "@typescript-eslint/tsconfig-utils@^8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz#ba2a779a444f1d5cb92a606f9b209d239fd4cab1"
integrity sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==
"@typescript-eslint/tsconfig-utils@8.59.2", "@typescript-eslint/tsconfig-utils@^8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz#6e92bc412083753185a79c9f1431e78169d9232f"
integrity sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==
"@typescript-eslint/type-utils@8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz#9c83d3f2ed9187a815e8120f72c08317e513e409"
integrity sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==
"@typescript-eslint/type-utils@8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz#a60a1192a804fa472a92c41656853ac6a9ba7176"
integrity sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==
dependencies:
"@typescript-eslint/types" "8.59.1"
"@typescript-eslint/typescript-estree" "8.59.1"
"@typescript-eslint/utils" "8.59.1"
"@typescript-eslint/types" "8.59.2"
"@typescript-eslint/typescript-estree" "8.59.2"
"@typescript-eslint/utils" "8.59.2"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.59.1", "@typescript-eslint/types@^8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.1.tgz#c1d014d3f03a97e0113a8899fc9d4e45a7fb0ca9"
integrity sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==
"@typescript-eslint/types@8.59.2", "@typescript-eslint/types@^8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.59.2.tgz#01caabcd7e4715c33ad5e11cab260829714d6b9c"
integrity sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==
"@typescript-eslint/typescript-estree@8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz#4391fadf98a22c869c5b6522dbf4e491e53e351a"
integrity sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==
"@typescript-eslint/typescript-estree@8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz#6a217ef65b18dbd12c718fc86a675d1d7a1414cc"
integrity sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==
dependencies:
"@typescript-eslint/project-service" "8.59.1"
"@typescript-eslint/tsconfig-utils" "8.59.1"
"@typescript-eslint/types" "8.59.1"
"@typescript-eslint/visitor-keys" "8.59.1"
"@typescript-eslint/project-service" "8.59.2"
"@typescript-eslint/tsconfig-utils" "8.59.2"
"@typescript-eslint/types" "8.59.2"
"@typescript-eslint/visitor-keys" "8.59.2"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.1.tgz#cf6204d69701bbbc5b150f98c18aeef0a42c10bd"
integrity sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==
"@typescript-eslint/utils@8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.59.2.tgz#ff619a6a3075f4017fa91b8610b752a8ca3366aa"
integrity sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.59.1"
"@typescript-eslint/types" "8.59.1"
"@typescript-eslint/typescript-estree" "8.59.1"
"@typescript-eslint/scope-manager" "8.59.2"
"@typescript-eslint/types" "8.59.2"
"@typescript-eslint/typescript-estree" "8.59.2"
"@typescript-eslint/visitor-keys@8.59.1":
version "8.59.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz#b5cba576287a3eeb0b400b62813189abcc3f976a"
integrity sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==
"@typescript-eslint/visitor-keys@8.59.2":
version "8.59.2"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz#5ccc486913cd347883d69158836b1189a660bfe6"
integrity sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==
dependencies:
"@typescript-eslint/types" "8.59.1"
"@typescript-eslint/types" "8.59.2"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -6035,10 +6035,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001791:
version "1.0.30001791"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz#dfb93d85c40ad380c57123e72e10f3c575786b51"
integrity sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001792:
version "1.0.30001792"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz#ca8bb9be244835a335e2018272ce7223691873c5"
integrity sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==
ccount@^2.0.0:
version "2.0.1"
@@ -14715,15 +14715,15 @@ types-ramda@^0.30.1:
dependencies:
ts-toolbelt "^9.6.0"
typescript-eslint@^8.59.1:
version "8.59.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.1.tgz#244a9fcbf27057ebbc2281d408239f1861b55b78"
integrity sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==
typescript-eslint@^8.59.2:
version "8.59.2"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz#e24b4f7232e20112e40572dba162a829a738ce98"
integrity sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==
dependencies:
"@typescript-eslint/eslint-plugin" "8.59.1"
"@typescript-eslint/parser" "8.59.1"
"@typescript-eslint/typescript-estree" "8.59.1"
"@typescript-eslint/utils" "8.59.1"
"@typescript-eslint/eslint-plugin" "8.59.2"
"@typescript-eslint/parser" "8.59.2"
"@typescript-eslint/typescript-estree" "8.59.2"
"@typescript-eslint/utils" "8.59.2"
typescript@~6.0.3:
version "6.0.3"

View File

@@ -114,7 +114,7 @@ dependencies = [
[project.optional-dependencies]
athena = ["pyathena[pandas]>=2, <3"]
athena = ["pyathena[pandas]>=2, <4"]
aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [
"pandas-gbq>=0.19.1",
@@ -135,17 +135,23 @@ databricks = [
"databricks-sqlalchemy==1.0.5",
]
db2 = ["ibm-db-sa>0.3.8, <=0.4.0"]
denodo = ["denodo-sqlalchemy~=1.0.6"]
denodo = ["denodo-sqlalchemy>=1.0.6,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.4, <2"]
druid = ["pydruid>=0.6.5,<0.7"]
duckdb = ["duckdb>=1.4.2,<2", "duckdb-engine>=0.17.0"]
dynamodb = ["pydynamodb>=0.4.2"]
solr = ["sqlalchemy-solr >= 0.2.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.12, <0.3.0"]
elasticsearch = ["elasticsearch-dbapi>=0.2.13, <0.3.0"]
exasol = ["sqlalchemy-exasol >= 2.4.0, <3.0"]
excel = ["xlrd>=1.2.0, <1.3"]
fastmcp = ["fastmcp>=3.2.4,<4.0"]
fastmcp = [
"fastmcp>=3.2.4,<4.0",
# tiktoken backs the response-size-guard token estimator. Without
# it, the middleware falls back to a coarser character-based
# heuristic that under-counts JSON-heavy MCP responses.
"tiktoken>=0.7.0,<1.0",
]
firebird = ["sqlalchemy-firebird>=0.7.0, <0.8"]
firebolt = ["firebolt-sqlalchemy>=1.0.0, <2"]
gevent = ["gevent>=23.9.1"]
@@ -158,7 +164,7 @@ hive = [
"thrift>=0.14.1, <1.0.0",
"thrift_sasl>=0.4.3, < 1.0.0",
]
impala = ["impyla>0.16.2, <0.17"]
impala = ["impyla>0.16.2, <0.23"]
kusto = ["sqlalchemy-kusto>=3.0.0, <4"]
kylin = ["kylinpy>=2.8.1, <2.9"]
mssql = ["pymssql>=2.2.8, <3"]
@@ -171,7 +177,7 @@ ocient = [
"shapely",
"geojson",
]
oracle = ["cx-Oracle>8.0.0, <8.1"]
oracle = ["cx-Oracle>8.0.0, <8.4"]
parseable = ["sqlalchemy-parseable>=0.1.3,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <6.0.0"]
playwright = ["playwright>=1.37.0, <2"]
@@ -197,7 +203,7 @@ tdengine = [
]
teradata = ["teradatasql>=16.20.0.23"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>=0.5.9, < 0.6"]
vertica = ["sqlalchemy-vertica-python>= 0.5.9, < 0.7"]
netezza = ["nzalchemy>=11.0.2"]
starrocks = ["starrocks>=1.0.0"]
doris = ["pydoris>=1.0.0, <2.0.0"]
@@ -377,6 +383,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.per-file-ignores]
"superset/mcp_service/app.py" = ["S608", "E501"] # LLM instruction text: SQL examples (S608) and long lines in multiline string (E501)
"superset/mcp_service/*/tool/list_*.py" = ["E501"] # LLM docstring examples show full request shapes which exceed line length
"scripts/*" = ["TID251"]
"setup.py" = ["TID251"]
"superset/config.py" = ["TID251"]

View File

@@ -183,7 +183,9 @@ idna==3.10
# trio
# url-normalize
isodate==0.7.2
# via apache-superset (pyproject.toml)
# via
# apache-superset (pyproject.toml)
# apache-superset-core
itsdangerous==2.2.0
# via
# flask
@@ -296,6 +298,7 @@ pyarrow==20.0.0
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
# apache-superset-core
pyasn1==0.6.3
# via
# pyasn1-modules

View File

@@ -442,6 +442,7 @@ isodate==0.7.2
# via
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
isort==6.0.1
# via pylint
itsdangerous==2.2.0
@@ -715,6 +716,7 @@ pyarrow==20.0.0
# via
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
# db-dtypes
# pandas-gbq
pyasn1==0.6.3
@@ -866,6 +868,8 @@ referencing==0.36.2
# jsonschema
# jsonschema-path
# jsonschema-specifications
regex==2026.4.4
# via tiktoken
requests==2.33.0
# via
# -c requirements/base-constraint.txt
@@ -878,6 +882,7 @@ requests==2.33.0
# requests-cache
# requests-oauthlib
# shillelagh
# tiktoken
# trino
requests-cache==1.2.1
# via
@@ -1003,6 +1008,8 @@ tabulate==0.9.0
# via
# -c requirements/base-constraint.txt
# apache-superset
tiktoken==0.12.0
# via apache-superset
tomli-w==1.2.0
# via apache-superset-extensions-cli
tomlkit==0.13.3

View File

@@ -66,7 +66,7 @@ export type EmbedDashboardParams = {
iframeTitle?: string;
/** additional iframe sandbox attributes ex (allow-top-navigation, allow-popups-to-escape-sandbox) **/
iframeSandboxExtras?: string[];
/** iframe allow attribute for Permissions Policy (e.g., ['clipboard-write', 'fullscreen']) **/
/** Additional Permissions Policy features for the iframe's `allow` attribute (e.g., ['camera', 'microphone']). `fullscreen` and `clipboard-write` are granted by default. **/
iframeAllowExtras?: string[];
/** force a specific refererPolicy to be used in the iframe request **/
referrerPolicy?: ReferrerPolicy;
@@ -233,9 +233,14 @@ export async function embedDashboard({
iframe.src = `${supersetDomain}/embedded/${id}${urlParamsString}`;
iframe.title = iframeTitle;
iframe.style.background = 'transparent';
if (iframeAllowExtras.length > 0) {
iframe.setAttribute('allow', iframeAllowExtras.join('; '));
}
// Permissions Policy features the embedded dashboard relies on. Modern
// browsers gate these APIs on the iframe's `allow` attribute regardless
// of sandbox flags, so we include them by default. Host apps can extend
// the list via `iframeAllowExtras`.
const allowFeatures = Array.from(
new Set(['fullscreen', 'clipboard-write', ...iframeAllowExtras]),
);
iframe.setAttribute('allow', allowFeatures.join('; '));
//@ts-ignore
mountPoint.replaceChildren(iframe);
log('placed the iframe');

View File

@@ -96,7 +96,7 @@
"fs-extra": "^11.3.4",
"fuse.js": "^7.3.0",
"geolib": "^3.3.14",
"geostyler": "^18.5.0",
"geostyler": "^18.5.1",
"geostyler-data": "^1.1.0",
"geostyler-openlayers-parser": "^5.7.0",
"geostyler-style": "11.0.2",
@@ -115,18 +115,18 @@
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.1.9",
"nanoid": "^5.1.11",
"ol": "^10.9.0",
"pretty-ms": "^9.3.0",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^18.2.0",
"react": "^18.3.0",
"react-arborist": "^3.5.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^18.2.0",
"react-dom": "^18.3.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^10.0.3",
"react-json-tree": "^0.20.0",
@@ -209,8 +209,8 @@
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.6.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
@@ -249,7 +249,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.3",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
@@ -261,10 +261,11 @@
"imports-loader": "^5.0.0",
"jest": "^30.3.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fail-on-console": "^3.3.4",
"jest-html-reporter": "^4.4.0",
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
"jsdom": "^29.1.0",
"jsdom": "^29.1.1",
"lerna": "^9.0.4",
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
@@ -274,6 +275,7 @@
"prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
"process": "^0.11.10",
"react-dnd-test-backend": "^11.1.3",
"react-refresh": "^0.18.0",
"react-resizable": "^3.1.3",
"redux-mock-store": "^1.5.4",
@@ -22872,9 +22874,9 @@
"license": "MIT"
},
"node_modules/eslint-plugin-react-you-might-not-need-an-effect": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-0.9.3.tgz",
"integrity": "sha512-44cce7LndBnpDRWBTQ8p7ircIdl2rJBP5+V9Ik64E935UB47uA9ZMU1Uv160lAMhtvoPYqXBjQ+tojr5JF3mFQ==",
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-0.10.0.tgz",
"integrity": "sha512-a4pugbQc2zLiE2NZGuXdTjtMNvlP2984QFPDv71eskUYDzigLFYfBL4QjK+RnRtcboHoXRKOcQqEZKxiK6KegA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -24760,9 +24762,9 @@
"license": "MIT"
},
"node_modules/geostyler": {
"version": "18.5.0",
"resolved": "https://registry.npmjs.org/geostyler/-/geostyler-18.5.0.tgz",
"integrity": "sha512-azjLMEhrTQot+pU3phfSrUZI7CdetyAl7JNAnxrGaPA/E/5mmyoPQugZso3CfIuIBwOtFLmfB36SLE/FeGFakA==",
"version": "18.5.1",
"resolved": "https://registry.npmjs.org/geostyler/-/geostyler-18.5.1.tgz",
"integrity": "sha512-5+vLuDo1oR4QQTnrfkccIQSe3qEn0ytV9dLiFFhnxhPdziv/Wp3vKNhJZ37MUF5yIj2ISWZ+q/VmSNH6ifvWpg==",
"license": "BSD-2-Clause",
"dependencies": {
"@ant-design/icons": "^5.5.1",
@@ -29633,6 +29635,19 @@
"node": ">=8"
}
},
"node_modules/jest-fail-on-console": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/jest-fail-on-console/-/jest-fail-on-console-3.3.4.tgz",
"integrity": "sha512-ckcABlg7ZLy83RRVOE1YvJMF1thgVPozVp+DjnE36Z1CaIbTrSeSR2AU4de/SEpfFgp2qK62EHXfm47N74zMuw==",
"dev": true,
"license": "MIT",
"engines": {
"yarn": "1.x"
},
"peerDependencies": {
"@jest/globals": ">=27.5.1"
}
},
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
@@ -31681,9 +31696,9 @@
}
},
"node_modules/jsdom": {
"version": "29.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.0.tgz",
"integrity": "sha512-YNUc7fB9QuvSSQWfrH0xF+TyABkxUwx8sswgIDaCrw4Hol8BghdZDkITtZheRJeMtzWlnTfsM3bBBusRvpO1wg==",
"version": "29.1.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -36703,9 +36718,9 @@
}
},
"node_modules/nanoid": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.9.tgz",
"integrity": "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==",
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
"integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
"funding": [
{
"type": "github",
@@ -41224,6 +41239,16 @@
"dnd-core": "^11.1.3"
}
},
"node_modules/react-dnd-test-backend": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/react-dnd-test-backend/-/react-dnd-test-backend-11.1.3.tgz",
"integrity": "sha512-5qFm+NI2GdWIUfiYun0A8Gv0xjbq0NGOPS+f6z3x/3nTuliApjmqcM1lfTgePoz1FDG47ZofF58N8y96If62+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"dnd-core": "^11.1.3"
}
},
"node_modules/react-docgen": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.1.1.tgz",
@@ -50003,8 +50028,8 @@
"jed": "^1.1.1",
"lodash": "^4.18.1",
"nanoid": "^5.0.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
}
@@ -50030,9 +50055,9 @@
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
"memoize-one": "^5.1.1",
"react": "^18.2.0",
"react": "^18.3.0",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0"
"react-dom": "^18.3.0"
}
},
"packages/superset-ui-core": {
@@ -50117,8 +50142,8 @@
"@types/tinycolor2": "*",
"antd": "^5.26.0",
"nanoid": "^5.0.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
}
@@ -50283,7 +50308,7 @@
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-calendar/node_modules/d3-array": {
@@ -50344,7 +50369,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-country-map/node_modules/d3-array": {
@@ -50372,7 +50397,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-horizon/node_modules/d3-array": {
@@ -50406,7 +50431,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-parallel-coordinates": {
@@ -50421,7 +50446,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-partition": {
@@ -50439,8 +50464,8 @@
"@superset-ui/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-rose": {
@@ -50457,7 +50482,7 @@
"@emotion/react": "^11.4.1",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-world-map": {
@@ -50475,7 +50500,7 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/legacy-plugin-chart-world-map/node_modules/d3-array": {
@@ -50562,7 +50587,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"dayjs": "^1.11.19",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/plugin-chart-ag-grid-table": {
@@ -50575,7 +50600,7 @@
"classnames": "^2.5.1",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"memoize-one": "^5.2.1",
"memoize-one": "^6.0.0",
"react-table": "^7.8.0",
"regenerator-runtime": "^0.14.1",
"xss": "^1.0.15"
@@ -50590,8 +50615,8 @@
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
},
"plugins/plugin-chart-ag-grid-table/node_modules/d3-array": {
@@ -50606,6 +50631,12 @@
"node": ">=12"
}
},
"plugins/plugin-chart-ag-grid-table/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"plugins/plugin-chart-cartodiagram": {
"name": "@superset-ui/plugin-chart-cartodiagram",
"version": "0.0.1",
@@ -50629,8 +50660,8 @@
"geostyler-wfs-parser": "^3.0.1",
"ol": "^10.8.0",
"polished": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
},
"plugins/plugin-chart-echarts": {
@@ -50652,7 +50683,7 @@
"dayjs": "^1.11.19",
"echarts": "*",
"memoize-one": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/plugin-chart-echarts/node_modules/acorn": {
@@ -50710,9 +50741,9 @@
"dayjs": "^1.11.19",
"handlebars": "^4.7.8",
"lodash": "^4.18.1",
"react": "^18.2.0",
"react": "^18.3.0",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0"
"react-dom": "^18.3.0"
}
},
"plugins/plugin-chart-handlebars/node_modules/just-handlebars-helpers": {
@@ -50743,8 +50774,8 @@
"@superset-ui/core": "*",
"lodash": "^4.18.1",
"prop-types": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
},
"plugins/plugin-chart-point-cluster-map": {
@@ -50762,8 +50793,8 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
},
"plugins/plugin-chart-point-cluster-map/node_modules/react-map-gl": {
@@ -50816,8 +50847,8 @@
"@testing-library/user-event": "*",
"@types/react": "*",
"match-sorter": "^8.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
},
"plugins/plugin-chart-table/node_modules/d3-array": {
@@ -50856,7 +50887,7 @@
"@superset-ui/core": "*",
"@types/lodash": "*",
"@types/react": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
},
"plugins/plugin-chart-word-cloud/node_modules/@types/d3-scale": {
@@ -50887,7 +50918,7 @@
"@deck.gl/extensions": "~9.2.9",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mapbox": "~9.3.1",
"@deck.gl/mapbox": "~9.3.2",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
@@ -50925,8 +50956,8 @@
"@superset-ui/core": "*",
"dayjs": "^1.11.19",
"mapbox-gl": ">=1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"peerDependenciesMeta": {
"mapbox-gl": {
@@ -50935,16 +50966,16 @@
}
},
"plugins/preset-chart-deckgl/node_modules/@deck.gl/mapbox": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.1.tgz",
"integrity": "sha512-4SgpWMeZiqiZEiz9yPdr89cVRL8HFcvXLxXUA0ExhMreUdNuK/j2OIQHPhw6vp1xCFbJEEqRelQ0pJYkhGDkYw==",
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.2.tgz",
"integrity": "sha512-+T9pJwsOXwjUxyGN6oiBMfIs28VtDIG1V1Rqz4qqn4TjjNEFFw+xO0olJIg8FO5IAqw2OtePdsrMj0tX8tHdGQ==",
"license": "MIT",
"dependencies": {
"@math.gl/web-mercator": "^4.1.0"
},
"peerDependencies": {
"@deck.gl/core": "~9.3.0",
"@luma.gl/core": "~9.3.2",
"@luma.gl/core": "~9.3.3",
"@math.gl/web-mercator": "^4.1.0"
}
},

View File

@@ -109,20 +109,20 @@
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@googleapis/sheets": "^13.0.1",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
"@luma.gl/engine": "~9.2.5",
"@luma.gl/gltf": "~9.2.5",
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@fontsource/fira-code": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
"@jsonforms/core": "^3.7.0",
"@jsonforms/react": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/antd": "^5.24.13",
"@rjsf/core": "^5.24.13",
@@ -140,16 +140,16 @@
"@superset-ui/legacy-plugin-chart-partition": "file:./plugins/legacy-plugin-chart-partition",
"@superset-ui/legacy-plugin-chart-rose": "file:./plugins/legacy-plugin-chart-rose",
"@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map",
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
"@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3",
"@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table",
"@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram",
"@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
"@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map",
"@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars",
"@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table",
"@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map",
"@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
"@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud",
"@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
"@types/d3-selection": "^3.0.11",
@@ -177,7 +177,7 @@
"fs-extra": "^11.3.4",
"fuse.js": "^7.3.0",
"geolib": "^3.3.14",
"geostyler": "^18.5.0",
"geostyler": "^18.5.1",
"geostyler-data": "^1.1.0",
"geostyler-openlayers-parser": "^5.7.0",
"geostyler-style": "11.0.2",
@@ -196,18 +196,18 @@
"memoize-one": "^5.2.1",
"mousetrap": "^1.6.5",
"mustache": "^4.2.0",
"nanoid": "^5.1.9",
"nanoid": "^5.1.11",
"ol": "^10.9.0",
"pretty-ms": "^9.3.0",
"query-string": "9.3.1",
"re-resizable": "^6.11.2",
"react": "^18.2.0",
"react": "^18.3.0",
"react-arborist": "^3.5.0",
"react-checkbox-tree": "^1.8.0",
"react-diff-viewer-continued": "^4.2.2",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^18.2.0",
"react-dom": "^18.3.0",
"react-google-recaptcha": "^3.1.0",
"react-intersection-observer": "^10.0.3",
"react-json-tree": "^0.20.0",
@@ -290,8 +290,8 @@
"@types/json-bigint": "^1.0.4",
"@types/mousetrap": "^1.6.15",
"@types/node": "^25.6.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
"@types/react-redux": "^7.1.10",
"@types/react-resizable": "^3.0.8",
@@ -330,7 +330,7 @@
"eslint-plugin-no-only-tests": "^3.4.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react-prefer-function-component": "^5.0.0",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.9.3",
"eslint-plugin-react-you-might-not-need-an-effect": "^0.10.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^7.16.2",
"eslint-plugin-theme-colors": "file:eslint-rules/eslint-plugin-theme-colors",
@@ -342,10 +342,11 @@
"imports-loader": "^5.0.0",
"jest": "^30.3.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fail-on-console": "^3.3.4",
"jest-html-reporter": "^4.4.0",
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
"jsdom": "^29.1.0",
"jsdom": "^29.1.1",
"lerna": "^9.0.4",
"lightningcss": "^1.32.0",
"mini-css-extract-plugin": "^2.10.2",
@@ -355,6 +356,7 @@
"prettier": "3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
"process": "^0.11.10",
"react-dnd-test-backend": "^11.1.3",
"react-refresh": "^0.18.0",
"react-resizable": "^3.1.3",
"redux-mock-store": "^1.5.4",

View File

@@ -97,8 +97,8 @@
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.2.6",
"nanoid": "^5.0.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*",
"lodash": "^4.18.1",

View File

@@ -38,9 +38,12 @@ import {
import { normalizeThemeConfig, serializeThemeConfig } from './utils';
export class Theme {
theme: SupersetTheme;
// Assigned via setConfig() in the constructor; TypeScript 6.0's
// strictPropertyInitialization can't trace that call chain, so we use
// a definite-assignment assertion.
theme!: SupersetTheme;
private antdConfig: AntdThemeConfig;
private antdConfig!: AntdThemeConfig;
private constructor({ config }: { config?: AnyThemeConfig }) {
this.SupersetThemeProvider = this.SupersetThemeProvider.bind(this);

View File

@@ -50,20 +50,24 @@ test('should pipe to `console` methods', () => {
});
test('should use noop functions when console unavailable', () => {
const originalConsole = window.console;
Object.assign(window, { console: undefined });
const { logging } = require('@apache-superset/core/utils');
try {
const { logging } = require('@apache-superset/core/utils');
expect(() => {
logging.debug();
logging.log();
logging.info();
logging.warn('warn');
logging.error('error');
logging.trace();
logging.table([
[1, 2],
[3, 4],
]);
}).not.toThrow();
Object.assign(window, { console });
expect(() => {
logging.debug();
logging.log();
logging.info();
logging.warn('warn');
logging.error('error');
logging.trace();
logging.table([
[1, 2],
[3, 4],
]);
}).not.toThrow();
} finally {
Object.assign(window, { console: originalConsole });
}
});

View File

@@ -20,3 +20,10 @@
* Stub for the untyped jed module.
*/
declare module 'jed';
/**
* CSS side-effect imports from @fontsource packages. These are bundler-only
* artifacts and carry no type information at runtime; declaring them here
* silences TS2882 under TypeScript 6.0's stricter module-resolution rules.
*/
declare module '@fontsource/*';

View File

@@ -40,9 +40,9 @@
"ace-builds": "^1.4.14",
"brace": "^0.11.1",
"memoize-one": "^5.1.1",
"react": "^18.2.0",
"react": "^18.3.0",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0"
"react-dom": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -18,6 +18,7 @@
*/
import { isMatrixifyVisible } from './matrixifyControls';
import type { ControlStateMapping } from '../types';
/**
* Helper to build a controls object matching the shape used by
@@ -25,7 +26,7 @@ import { isMatrixifyVisible } from './matrixifyControls';
*/
function makeControls(
overrides: Record<string, unknown> = {},
): Record<string, { value: unknown }> {
): ControlStateMapping {
const defaults: Record<string, unknown> = {
matrixify_enable: false,
matrixify_mode_rows: 'disabled',
@@ -36,7 +37,7 @@ function makeControls(
const merged = { ...defaults, ...overrides };
return Object.fromEntries(
Object.entries(merged).map(([k, v]) => [k, { value: v }]),
);
) as ControlStateMapping;
}
// ── matrixify_enable guard ──────────────────────────────────────────

View File

@@ -20,7 +20,7 @@
import { t } from '@apache-superset/core/translation';
import { validateNonEmpty } from '@superset-ui/core';
import { SharedControlConfig } from '../types';
import { ControlStateMapping, SharedControlConfig } from '../types';
import { dndAdhocMetricControl } from './dndControls';
import { defineSavedMetrics } from '../utils';
@@ -29,9 +29,12 @@ import { defineSavedMetrics } from '../utils';
* Controls for transforming charts into matrix/grid layouts
*/
// Utility function to check if matrixify controls should be visible
// Utility function to check if matrixify controls should be visible.
// Controls both visibility callbacks and validator injection via mapStateToProps.
// The matrixify_enable guard prevents hidden validators from firing on
// pre-revamp charts with stale matrixify_mode defaults (fix for #38519).
const isMatrixifyVisible = (
controls: any,
controls: ControlStateMapping | undefined,
axis: 'rows' | 'columns',
mode?: 'metrics' | 'dimensions',
selectionMode?: 'members' | 'topn' | 'all',

View File

@@ -0,0 +1,238 @@
/**
* 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.
*/
/**
* Tests for the matrixify_enable guard in isMatrixifyVisible() and
* validator injection via mapStateToProps on real matrixify control definitions.
*
* These are TDD tests for the fix to apache/superset#38519 regression:
* isMatrixifyVisible() must check matrixify_enable before evaluating mode,
* otherwise pre-revamp charts with stale matrixify_mode defaults trigger
* hidden validators that block save.
*/
import {
matrixifyControls,
isMatrixifyVisible,
} from '../../src/shared-controls/matrixifyControls';
import type { ControlPanelState, ControlStateMapping } from '../../src/types';
// Helper: build a minimal controls object for ControlPanelState
const buildControls = (
overrides: Record<string, any> = {},
): ControlStateMapping => {
const controls: Record<string, { value: any }> = {};
Object.entries(overrides).forEach(([key, value]) => {
controls[key] = { value };
});
return controls as ControlStateMapping;
};
// Helper: build a minimal ControlPanelState for mapStateToProps.
// Only provides fields that isMatrixifyVisible and mapStateToProps actually read.
const buildState = (
controlValues: Record<string, any> = {},
formData: Record<string, any> = {},
) =>
({
controls: buildControls(controlValues),
datasource: { columns: [], type: 'table' },
form_data: formData,
common: {},
metadata: {},
slice: { slice_id: 0 },
}) as unknown as ControlPanelState;
// ============================================================
// Validator injection tests via real mapStateToProps (rows)
// ============================================================
// --- matrixify_dimension_rows ---
test('matrixify_dimension_rows: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_dimension_rows;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'members',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_dimension_rows: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_dimension_rows;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'members',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// --- matrixify_topn_value_rows ---
test('matrixify_topn_value_rows: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_topn_value_rows;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_topn_value_rows: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_topn_value_rows;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// --- matrixify_topn_metric_rows ---
test('matrixify_topn_metric_rows: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_topn_metric_rows;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_topn_metric_rows: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_topn_metric_rows;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_selection_mode_rows: 'topn',
},
{ matrixify_mode_rows: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// ============================================================
// Validator injection tests via real mapStateToProps (columns)
// ============================================================
test('matrixify_dimension_columns: validators empty when matrixify_enable is falsy', () => {
const control = matrixifyControls.matrixify_dimension_columns;
const state = buildState(
{
matrixify_enable: undefined,
matrixify_mode_columns: 'dimensions',
matrixify_dimension_selection_mode_columns: 'members',
},
{ matrixify_mode_columns: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators).toEqual([]);
});
test('matrixify_dimension_columns: validators present when matrixify_enable is true', () => {
const control = matrixifyControls.matrixify_dimension_columns;
const state = buildState(
{
matrixify_enable: true,
matrixify_mode_columns: 'dimensions',
matrixify_dimension_selection_mode_columns: 'members',
},
{ matrixify_mode_columns: 'dimensions' },
);
const result = control.mapStateToProps!(state, {} as any);
expect(result.validators.length).toBeGreaterThan(0);
});
// ============================================================
// Direct isMatrixifyVisible guard tests
// ============================================================
test.each([
['undefined', undefined],
['null', null],
['false', false],
['0', 0],
])(
'isMatrixifyVisible returns false when matrixify_enable is %s',
(_, value) => {
const controls = buildControls({
matrixify_enable: value,
matrixify_mode_rows: 'dimensions',
});
expect(isMatrixifyVisible(controls, 'rows')).toBe(false);
},
);
test('isMatrixifyVisible returns true when matrixify_enable is true and mode matches', () => {
const controls = buildControls({
matrixify_enable: true,
matrixify_mode_rows: 'dimensions',
});
expect(isMatrixifyVisible(controls, 'rows', 'dimensions')).toBe(true);
});
test('isMatrixifyVisible returns false when matrixify_enable is true but mode is disabled', () => {
const controls = buildControls({
matrixify_enable: true,
matrixify_mode_rows: 'disabled',
});
expect(isMatrixifyVisible(controls, 'rows')).toBe(false);
});
test('isMatrixifyVisible returns true when matrixify_enable is true and any non-disabled mode (no mode filter)', () => {
const controls = buildControls({
matrixify_enable: true,
matrixify_mode_columns: 'metrics',
});
expect(isMatrixifyVisible(controls, 'columns')).toBe(true);
});

View File

@@ -101,8 +101,8 @@
"@types/tinycolor2": "*",
"antd": "^5.26.0",
"nanoid": "^5.0.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"react-loadable": "^5.5.0",
"tinycolor2": "*"
},

View File

@@ -17,15 +17,16 @@
* under the License.
*/
import { forwardRef } from 'react';
import { Avatar as AntdAvatar } from 'antd';
import type { AvatarProps, GroupProps as AvatarGroupProps } from './types';
export function Avatar(props: AvatarProps) {
return <AntdAvatar {...props} />;
}
export const Avatar = forwardRef<HTMLSpanElement, AvatarProps>((props, ref) => (
<AntdAvatar ref={ref} {...props} />
));
export function AvatarGroup(props: AvatarGroupProps) {
return <AntdAvatar.Group {...props} />;
}
export const AvatarGroup = (props: AvatarGroupProps) => (
<AntdAvatar.Group {...props} />
);
export type { AvatarProps, AvatarGroupProps };

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Children, ReactElement, Fragment } from 'react';
import { Children, ReactElement, Fragment, forwardRef, Ref } from 'react';
import cx from 'classnames';
import { Button as AntdButton } from 'antd';
import { useTheme } from '@apache-superset/core/theme';
@@ -100,7 +100,7 @@ const BUTTON_STYLE_MAP: Record<
link: { type: 'link' },
};
export function Button(props: ButtonProps) {
function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
const {
tooltip,
placement,
@@ -160,6 +160,7 @@ export function Button(props: ButtonProps) {
const button = (
<AntdButton
ref={ref as Ref<HTMLButtonElement & HTMLAnchorElement>}
href={disabled ? undefined : href}
disabled={disabled}
type={antdType}
@@ -235,4 +236,6 @@ export function Button(props: ButtonProps) {
return button;
}
export const Button = forwardRef<HTMLElement, ButtonProps>(ButtonInner);
export type { ButtonProps, OnClickHandler };

View File

@@ -75,7 +75,10 @@ export const DropdownButton = ({
id={`${kebabCase(tooltip)}-tooltip`}
title={tooltip}
>
{button}
{/* antd Dropdown.Button is a plain function component without
forwardRef; wrap in a span so the Tooltip can attach a ref to a
real DOM node and skip the findDOMNode fallback. */}
<span>{button}</span>
</Tooltip>
);
}

View File

@@ -240,7 +240,10 @@ export function EditableTitle({
t("You don't have the rights to alter this title.")
}
>
{titleComponent}
{/* Wrap in span so the Tooltip can attach a ref to a DOM element.
antd's Input.TextArea forwards a non-DOM imperative handle, which
triggers a React 18 findDOMNode deprecation warning. */}
<span>{titleComponent}</span>
</Tooltip>
);
}

View File

@@ -16,47 +16,54 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Tooltip } from '../Tooltip';
import { Button } from '../Button';
import type { IconTooltipProps } from './types';
export const IconTooltip = ({
children = null,
className = '',
onClick = () => undefined,
placement = 'top',
style = {},
tooltip = null,
mouseEnterDelay = 0.3,
mouseLeaveDelay = 0.15,
}: IconTooltipProps) => {
const iconTooltip = (
<Button
onClick={onClick}
style={{
padding: 0,
...style,
}}
buttonStyle="link"
className={`IconTooltip ${className}`}
>
{children}
</Button>
);
if (tooltip) {
return (
<Tooltip
id="tooltip"
title={tooltip}
placement={placement}
mouseEnterDelay={mouseEnterDelay}
mouseLeaveDelay={mouseLeaveDelay}
export const IconTooltip = forwardRef<HTMLElement, IconTooltipProps>(
(
{
children = null,
className = '',
onClick = () => undefined,
placement = 'top',
style = {},
tooltip = null,
mouseEnterDelay = 0.3,
mouseLeaveDelay = 0.15,
},
ref,
) => {
const iconTooltip = (
<Button
ref={ref}
onClick={onClick}
style={{
padding: 0,
...style,
}}
buttonStyle="link"
className={`IconTooltip ${className}`}
>
{iconTooltip}
</Tooltip>
{children}
</Button>
);
}
return iconTooltip;
};
if (tooltip) {
return (
<Tooltip
id="tooltip"
title={tooltip}
placement={placement}
mouseEnterDelay={mouseEnterDelay}
mouseLeaveDelay={mouseLeaveDelay}
>
{iconTooltip}
</Tooltip>
);
}
return iconTooltip;
},
);
export type { IconTooltipProps };

View File

@@ -165,7 +165,7 @@ import {
SlackOutlined,
ApiOutlined,
} from '@ant-design/icons';
import { FC } from 'react';
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
import { IconType } from './types';
import { BaseIconComponent } from './BaseIcon';
@@ -323,19 +323,25 @@ type AntdIconNames = keyof typeof AntdIcons;
export const antdEnhancedIcons: Record<
AntdIconNames,
FC<IconType>
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
> = Object.keys(AntdIcons)
.filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded)))
.reduce(
(acc, key) => {
acc[key as AntdIconNames] = (props: IconType) => (
<BaseIconComponent
component={AntdIcons[key as AntdIconNames]}
fileName={key}
{...props}
/>
acc[key as AntdIconNames] = forwardRef<HTMLSpanElement, IconType>(
(props, ref) => (
<BaseIconComponent
ref={ref}
component={AntdIcons[key as AntdIconNames]}
fileName={key}
{...props}
/>
),
);
return acc;
},
{} as Record<AntdIconNames, FC<IconType>>,
{} as Record<
AntdIconNames,
ForwardRefExoticComponent<IconType & RefAttributes<HTMLSpanElement>>
>,
);

View File

@@ -17,12 +17,12 @@
* under the License.
*/
import { FC, SVGProps, useEffect, useRef, useState } from 'react';
import { FC, SVGProps, forwardRef, useEffect, useRef, useState } from 'react';
import TransparentIcon from './svgs/transparent.svg';
import { IconType } from './types';
import { BaseIconComponent } from './BaseIcon';
const AsyncIcon = (props: IconType) => {
const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
const [, setLoaded] = useState(false);
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
const { fileName, customIcons, iconSize, iconColor, viewBox, ...restProps } =
@@ -46,6 +46,7 @@ const AsyncIcon = (props: IconType) => {
return (
<BaseIconComponent
ref={ref}
component={ImportedSVG.current || TransparentIcon}
fileName={fileName}
customIcons={customIcons}
@@ -55,6 +56,6 @@ const AsyncIcon = (props: IconType) => {
{...restProps}
/>
);
};
});
export default AsyncIcon;

View File

@@ -17,6 +17,7 @@
* under the License.
*/
import { forwardRef, type ComponentType } from 'react';
import { css, useTheme, getFontSize } from '@apache-superset/core/theme';
import { AntdIconType, BaseIconProps, CustomIconType, IconType } from './types';
@@ -35,65 +36,78 @@ const genAriaLabel = (fileName: string) => {
return name.toLowerCase();
};
export const BaseIconComponent: React.FC<
export const BaseIconComponent = forwardRef<
HTMLSpanElement | SVGSVGElement,
BaseIconProps & Omit<IconType, 'component'>
> = ({
component: Component,
iconColor,
iconSize,
viewBox,
customIcons,
fileName,
...rest
}) => {
const theme = useTheme();
const whatRole = rest?.onClick ? 'button' : 'img';
const ariaLabel = genAriaLabel(fileName || '');
const style = {
color: iconColor,
fontSize: iconSize
? `${getFontSize(theme, iconSize)}px`
: `${theme.fontSize}px`,
cursor: rest?.onClick ? 'pointer' : undefined,
};
>(
(
{
component: Component,
iconColor,
iconSize,
viewBox,
customIcons,
fileName,
...rest
},
ref,
) => {
const theme = useTheme();
const whatRole = rest?.onClick ? 'button' : 'img';
const ariaLabel = genAriaLabel(fileName || '');
const style = {
color: iconColor,
fontSize: iconSize
? `${getFontSize(theme, iconSize)}px`
: `${theme.fontSize}px`,
cursor: rest?.onClick ? 'pointer' : undefined,
};
return customIcons ? (
<span
role={whatRole}
aria-label={ariaLabel}
data-test={ariaLabel}
css={[
css`
display: inline-flex;
align-items: center;
line-height: 0;
vertical-align: middle;
`,
]}
>
<Component
viewBox={viewBox || '0 0 24 24'}
const AntdComponent = Component as ComponentType<
Record<string, unknown> & {
ref?: React.Ref<HTMLSpanElement | SVGSVGElement>;
}
>;
return customIcons ? (
<span
ref={ref as React.Ref<HTMLSpanElement>}
role={whatRole}
aria-label={ariaLabel}
data-test={ariaLabel}
css={[
css`
display: inline-flex;
align-items: center;
line-height: 0;
vertical-align: middle;
`,
]}
>
<Component
viewBox={viewBox || '0 0 24 24'}
style={style}
width={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
height={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
{...(rest as CustomIconType)}
/>
</span>
) : (
<AntdComponent
ref={ref}
role={whatRole}
style={style}
width={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
height={
iconSize
? `${getFontSize(theme, iconSize) || theme.fontSize}px`
: `${theme.fontSize}px`
}
{...(rest as CustomIconType)}
aria-label={ariaLabel}
data-test={ariaLabel}
{...(rest as AntdIconType)}
/>
</span>
) : (
<Component
role={whatRole}
style={style}
aria-label={ariaLabel}
data-test={ariaLabel}
{...(rest as AntdIconType)}
/>
);
};
);
},
);

View File

@@ -17,12 +17,16 @@
* under the License.
*/
import { FC } from 'react';
import { ForwardRefExoticComponent, RefAttributes, forwardRef } from 'react';
import { antdEnhancedIcons } from './AntdEnhanced';
import AsyncIcon from './AsyncIcon';
import type { IconType } from './types';
type IconComponent = ForwardRefExoticComponent<
IconType & RefAttributes<HTMLSpanElement>
>;
/**
* Filename is going to be inferred from the icon name.
* i.e. BigNumberChartTile => assets/images/icons/big_number_chart_tile
@@ -58,15 +62,17 @@ const customIcons = [
'Undo',
] as const;
type CustomIconType = Record<(typeof customIcons)[number], FC<IconType>>;
type CustomIconType = Record<(typeof customIcons)[number], IconComponent>;
const iconOverrides: CustomIconType = {} as CustomIconType;
customIcons.forEach(customIcon => {
const fileName = customIcon
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.toLowerCase();
iconOverrides[customIcon] = (props: IconType) => (
<AsyncIcon customIcons fileName={fileName} {...props} />
iconOverrides[customIcon] = forwardRef<HTMLSpanElement, IconType>(
(props, ref) => (
<AsyncIcon ref={ref} customIcons fileName={fileName} {...props} />
),
);
});
@@ -74,7 +80,7 @@ export type IconNameType =
| keyof typeof antdEnhancedIcons
| keyof typeof iconOverrides;
type IconComponentType = Record<IconNameType, FC<IconType>>;
type IconComponentType = Record<IconNameType, IconComponent>;
export const Icons: IconComponentType = {
...antdEnhancedIcons,

View File

@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Tag } from '@superset-ui/core/components/Tag';
import { css } from '@emotion/react';
import { useTheme, getColorVariants } from '@apache-superset/core/theme';
@@ -23,7 +24,7 @@ import { DatasetTypeLabel } from './reusable/DatasetTypeLabel';
import { PublishedLabel } from './reusable/PublishedLabel';
import type { LabelProps } from './types';
export function Label(props: LabelProps) {
export const Label = forwardRef<HTMLSpanElement, LabelProps>((props, ref) => {
const theme = useTheme();
// Use Ant Design's motion duration instead of deprecated transitionTiming
const {
@@ -71,6 +72,7 @@ export function Label(props: LabelProps) {
return (
<Tag
ref={ref}
onClick={onClick}
role={onClick ? 'button' : undefined}
style={style}
@@ -81,6 +83,6 @@ export function Label(props: LabelProps) {
{children}
</Tag>
);
}
});
export { DatasetTypeLabel, PublishedLabel };
export type { LabelType } from './types';

View File

@@ -357,6 +357,9 @@ const CustomModal = ({
disabled={!draggable || dragDisabled}
bounds={bounds}
onStart={(event, uiData) => onDragStart(event, uiData)}
// Pass nodeRef so react-draggable does not fall back to
// ReactDOM.findDOMNode (deprecated in React 18+ Strict Mode).
nodeRef={draggableRef}
{...draggableConfig}
>
{resizable ? (

View File

@@ -16,11 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Popover as AntdPopover } from 'antd';
import { PopoverProps as AntdPopoverProps } from 'antd/es/popover';
import type { TooltipRef } from 'antd/es/tooltip';
export interface PopoverProps extends AntdPopoverProps {
forceRender?: boolean;
}
export const Popover = (props: PopoverProps) => <AntdPopover {...props} />;
export const Popover = forwardRef<TooltipRef, PopoverProps>((props, ref) => (
<AntdPopover ref={ref} {...props} />
));

View File

@@ -16,10 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { MouseEventHandler, forwardRef } from 'react';
import { MouseEventHandler } from 'react';
import { SupersetTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import type { IconType } from '@superset-ui/core/components/Icons/types';
import { Tooltip } from '../Tooltip';
export interface RefreshLabelProps {
@@ -32,25 +31,19 @@ const RefreshLabel = ({
onClick,
tooltipContent,
disabled,
}: RefreshLabelProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const IconWithoutRef = forwardRef((props: IconType, ref: any) => (
<Icons.SyncOutlined iconSize="l" {...props} />
));
return (
<Tooltip title={tooltipContent}>
<IconWithoutRef
role="button"
onClick={disabled ? undefined : onClick}
css={(theme: SupersetTheme) => ({
cursor: 'pointer',
color: theme.colorIcon,
'&:hover': { color: theme.colorPrimary },
})}
/>
</Tooltip>
);
};
}: RefreshLabelProps) => (
<Tooltip title={tooltipContent}>
<Icons.SyncOutlined
iconSize="l"
role="button"
onClick={disabled ? undefined : onClick}
css={(theme: SupersetTheme) => ({
cursor: 'pointer',
color: theme.colorIcon,
'&:hover': { color: theme.colorPrimary },
})}
/>
</Tooltip>
);
export default RefreshLabel;

View File

@@ -1160,7 +1160,7 @@ test('does not fire onChange if the same value is selected in single mode', asyn
// Reference for the bug this tests: https://github.com/apache/superset/pull/33043#issuecomment-2809419640
test('typing and deleting the last character for a new option displays correctly', async () => {
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
render(<Select {...defaultProps} allowNewOptions />);
await open();

View File

@@ -16,17 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef } from 'react';
import { Tooltip as AntdTooltip } from 'antd';
import type { TooltipRef } from 'antd/es/tooltip';
import type { TooltipProps, TooltipPlacement } from './types';
export const Tooltip = ({ overlayStyle, ...props }: TooltipProps) => (
<AntdTooltip
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}
/>
export const Tooltip = forwardRef<TooltipRef, TooltipProps>(
({ overlayStyle, ...props }, ref) => (
<AntdTooltip
ref={ref}
styles={{
body: { overflow: 'hidden', textOverflow: 'ellipsis' },
root: overlayStyle ?? {},
}}
{...props}
/>
),
);
export type { TooltipProps, TooltipPlacement };

View File

@@ -25,22 +25,108 @@
*
* Run locally:
* cd superset-frontend
* npm run docs:screenshots
* PLAYWRIGHT_BASE_URL=http://localhost:8088 PLAYWRIGHT_ADMIN_PASSWORD=admin npm run docs:screenshots
*
* Or directly:
* npx playwright test --config=playwright/generators/playwright.config.ts docs/
*
* Screenshots are saved to docs/static/img/screenshots/.
* Screenshots are saved under docs/static/img/.
* As new screenshots are scripted, entries are removed from screenshot-manifest.yaml
* and the output path moves from that manifest into the test below.
*/
import path from 'path';
import { Page } from '@playwright/test';
import { test, expect } from '@playwright/test';
import { URL } from '../../utils/urls';
import { apiDelete, apiGet } from '../../helpers/api/requests';
const SCREENSHOTS_DIR = path.resolve(
__dirname,
'../../../../docs/static/img/screenshots',
);
const DOCS_STATIC = path.resolve(__dirname, '../../../../docs/static/img');
const SCREENSHOTS_DIR = path.join(DOCS_STATIC, 'screenshots');
const TUTORIAL_DIR = path.join(DOCS_STATIC, 'tutorial');
/**
* Waits for animations and async renders to settle before taking a screenshot.
* ECharts entry animations, image lazy-loading, and other async UI updates
* require a short pause that can't be expressed as a deterministic wait condition.
*/
async function settle(page: Page, ms = 1000): Promise<void> {
await page.waitForTimeout(ms);
}
/**
* Navigates to the Sales Dashboard (from example data) and waits for charts
* to finish rendering. Used by several tutorial screenshots that show the
* dashboard in view or edit mode.
*/
async function openSalesDashboard(page: Page): Promise<void> {
await page.goto(URL.DASHBOARD_LIST);
const searchInput = page.getByPlaceholder('Type a value');
await expect(searchInput).toBeVisible({ timeout: 15000 });
await searchInput.fill('Sales Dashboard');
await searchInput.press('Enter');
const dashboardLink = page.getByRole('link', { name: /sales dashboard/i });
await expect(dashboardLink).toBeVisible({ timeout: 10000 });
await dashboardLink.click();
const dashboardWrapper = page.locator(
'[data-test="dashboard-content-wrapper"]',
);
await expect(dashboardWrapper).toBeVisible({ timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder').first(),
).toBeVisible({ timeout: 15000 });
await expect(
dashboardWrapper.locator('[data-test="loading-indicator"]'),
).toHaveCount(0, { timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder canvas').first(),
).toBeVisible({ timeout: 15000 });
}
/**
* Delete all dashboards matching the given exact title, along with the
* charts attached to them. Used by the save-flow test to clean up after
* itself and to recover from prior failed runs (idempotent pre-cleanup).
*
* Only safe because the title is unique to the test ("Superset Duper
* Sales Dashboard"); don't reuse this against titles that could match
* example-data dashboards.
*/
async function deleteDashboardByTitle(
page: Page,
title: string,
): Promise<void> {
const filter = `(filters:!((col:dashboard_title,opr:eq,value:'${title}')))`;
const resp = await apiGet(page, 'api/v1/dashboard/', {
params: { q: filter },
failOnStatusCode: false,
});
if (!resp.ok()) return;
const body = await resp.json();
const dashboards: { id: number }[] = body.result || [];
for (const dash of dashboards) {
const chartsResp = await apiGet(
page,
`api/v1/dashboard/${dash.id}/charts`,
{ failOnStatusCode: false },
);
const chartIds: number[] = chartsResp.ok()
? ((await chartsResp.json()).result || [])
.map((c: { id?: number }) => c.id)
.filter((id: unknown): id is number => typeof id === 'number')
: [];
await apiDelete(page, `api/v1/dashboard/${dash.id}`, {
failOnStatusCode: false,
});
for (const id of chartIds) {
await apiDelete(page, `api/v1/chart/${id}`, { failOnStatusCode: false });
}
}
}
test('chart gallery screenshot', async ({ page }) => {
await page.goto(URL.CHART_ADD);
@@ -58,6 +144,7 @@ test('chart gallery screenshot', async ({ page }) => {
vizGallery.locator('[data-test="viztype-selector-container"]').first(),
).toBeVisible();
await settle(page);
await vizGallery.screenshot({
path: path.join(SCREENSHOTS_DIR, 'gallery.jpg'),
type: 'jpeg',
@@ -65,36 +152,7 @@ test('chart gallery screenshot', async ({ page }) => {
});
test('dashboard screenshot', async ({ page }) => {
// Navigate to Sales Dashboard via the dashboard list (slug is null)
await page.goto(URL.DASHBOARD_LIST);
const searchInput = page.getByPlaceholder('Type a value');
await expect(searchInput).toBeVisible({ timeout: 15000 });
await searchInput.fill('Sales Dashboard');
await searchInput.press('Enter');
// Click the Sales Dashboard link
const dashboardLink = page.getByRole('link', { name: /sales dashboard/i });
await expect(dashboardLink).toBeVisible({ timeout: 10000 });
await dashboardLink.click();
// Wait for dashboard to fully render
const dashboardWrapper = page.locator(
'[data-test="dashboard-content-wrapper"]',
);
await expect(dashboardWrapper).toBeVisible({ timeout: 30000 });
// Wait for chart holders to appear, then wait for all loading spinners to clear
await expect(
page.locator('.dashboard-component-chart-holder').first(),
).toBeVisible({ timeout: 15000 });
await expect(
dashboardWrapper.locator('[data-test="loading-indicator"]'),
).toHaveCount(0, { timeout: 30000 });
// Wait for at least one chart to finish rendering (ECharts renders to canvas)
await expect(
page.locator('.dashboard-component-chart-holder canvas').first(),
).toBeVisible({ timeout: 15000 });
await openSalesDashboard(page);
// Open the filter bar (collapsed by default)
const expandButton = page.locator('[data-test="filter-bar__expand-button"]');
@@ -109,6 +167,8 @@ test('dashboard screenshot', async ({ page }) => {
).toBeVisible({ timeout: 5000 });
}
// Allow ECharts entry animations to finish before capturing
await settle(page);
await page.screenshot({
path: path.join(SCREENSHOTS_DIR, 'dashboard.jpg'),
type: 'jpeg',
@@ -143,6 +203,7 @@ test('chart editor screenshot', async ({ page }) => {
timeout: 15000,
});
await settle(page);
await page.screenshot({
path: path.join(SCREENSHOTS_DIR, 'explore.jpg'),
type: 'jpeg',
@@ -151,7 +212,7 @@ test('chart editor screenshot', async ({ page }) => {
});
test('SQL Lab screenshot', async ({ page }) => {
// SQL Lab has many interactive steps (schema, table, query, results) — allow extra time
// SQL Lab has many interactive steps — allow extra time
test.setTimeout(90000);
await page.goto(URL.SQLLAB);
@@ -168,34 +229,7 @@ test('SQL Lab screenshot', async ({ page }) => {
}
await expect(aceEditor).toBeVisible({ timeout: 15000 });
// Select the "public" schema so we can pick a table from the left panel
const schemaSelect = page.locator('#select-schema');
await expect(schemaSelect).toBeEnabled({ timeout: 10000 });
await schemaSelect.click({ force: true });
await schemaSelect.fill('public');
await page.getByRole('option', { name: 'public' }).click();
// Wait for table list to load after schema change, then select birth_names
const tableSelectWrapper = page
.locator('.ant-select')
.filter({ has: page.locator('#select-table') });
await expect(tableSelectWrapper).toBeVisible({ timeout: 10000 });
await tableSelectWrapper.click();
await page.keyboard.type('birth_names');
// Wait for the filtered option to appear in the DOM, then select it
const tableOption = page
.locator('.ant-select-dropdown [role="option"]')
.filter({ hasText: 'birth_names' });
await expect(tableOption).toBeAttached({ timeout: 10000 });
await page.keyboard.press('Enter');
// Wait for table schema to load and show columns in the left panel
await expect(page.locator('[data-test="col-name"]').first()).toBeVisible({
timeout: 15000,
});
// Close the table dropdown by clicking elsewhere, then switch to the query tab
await page.locator('[data-test="sql-editor-tabs"]').first().click();
// Click the active query tab to ensure focus is on the editor pane
await page.getByText('Untitled Query').first().click();
// Write a multi-line SELECT with explicit columns to fill the editor
@@ -205,8 +239,8 @@ test('SQL Lab screenshot', async ({ page }) => {
'SELECT\n ds,\n name,\n gender,\n state,\n num\nFROM birth_names\nLIMIT 100',
);
// Run the query
const runButton = page.getByText('Run', { exact: true });
// Run the query — use the stable data-test attribute on the action button
const runButton = page.locator('[data-test="run-query-action"]');
await expect(runButton).toBeVisible();
await runButton.click();
@@ -222,9 +256,352 @@ test('SQL Lab screenshot', async ({ page }) => {
await page.mouse.move(0, 0);
await expect(page.getByRole('tooltip')).toHaveCount(0, { timeout: 2000 });
await settle(page);
await page.screenshot({
path: path.join(SCREENSHOTS_DIR, 'sql_lab.jpg'),
type: 'jpeg',
fullPage: true,
});
});
// ---------------------------------------------------------------------------
// Tutorial screenshots
// ---------------------------------------------------------------------------
test('datasets list screenshot', async ({ page }) => {
await page.goto(URL.DATASET_LIST);
const table = page.locator('[data-test="listview-table"]');
await expect(table).toBeVisible({ timeout: 15000 });
// Wait for at least one visible data row (skip ant-table-measure-row which is always hidden)
await expect(
table.locator('tbody tr:not(.ant-table-measure-row)').first(),
).toBeVisible({ timeout: 10000 });
// Viewport screenshot (not fullPage) captures the SubMenu — showing the
// "Datasets" nav item, Bulk Select button, and + Dataset button — plus the
// top of the table. This is more informative than screenshotting the table alone.
await settle(page);
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_08_sources_tables.png'),
type: 'png',
});
});
test('chart type picker screenshot', async ({ page }) => {
await page.goto(URL.CHART_ADD);
// Wait for the dataset step to appear (step title is first match; placeholder is second)
await expect(page.getByText('Choose a dataset').first()).toBeVisible({
timeout: 15000,
});
// Open the dataset selector and choose birth_names
await page.getByTestId('Dataset').click();
await page.keyboard.type('birth_names');
// The dataset select uses a hidden ARIA listbox — the visible popup is a portal.
// Wait for the first option to appear in the DOM, then select it via keyboard.
await expect(
page.locator('[role="listbox"] [role="option"]').first(),
).toBeAttached({ timeout: 10000 });
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
// Open the chart gallery and wait for thumbnails to render
await expect(page.getByText('Choose chart type')).toBeVisible({
timeout: 10000,
});
await page.getByRole('tab', { name: 'All charts' }).click();
const vizGallery = page.locator('.viz-gallery');
await expect(vizGallery).toBeVisible();
await expect(
vizGallery.locator('[data-test="viztype-selector-container"]').first(),
).toBeVisible();
// Select the Pivot Table chart type
await vizGallery
.locator('[data-test="viztype-selector-container"]')
.filter({ hasText: 'Pivot Table' })
.first()
.click();
// Allow thumbnails to finish loading and selection state to render
await settle(page);
// Viewport screenshot shows the dataset step (birth_names selected) and
// the chart type gallery (Pivot Table highlighted)
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'create_pivot.png'),
type: 'png',
});
});
test('publish button dashboard screenshot', async ({ page }) => {
// Toggle Sales Dashboard to Draft, hover the label so the tooltip renders,
// then capture the header area plus enough room below for the tooltip.
// Always restores the dashboard to Published at the end.
await openSalesDashboard(page);
const publishedLabel = page.getByText('Published', { exact: true }).first();
await expect(publishedLabel).toBeVisible({ timeout: 10000 });
await publishedLabel.click();
const draftLabel = page.getByText('Draft', { exact: true }).first();
await expect(draftLabel).toBeVisible({ timeout: 10000 });
try {
await draftLabel.hover();
await expect(page.getByRole('tooltip')).toBeVisible({ timeout: 5000 });
await settle(page, 500);
const headerBox = await page
.locator('[data-test="dashboard-header-container"]')
.boundingBox();
if (!headerBox) {
throw new Error('Could not locate dashboard header container');
}
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'publish_button_dashboard.png'),
type: 'png',
clip: {
x: headerBox.x,
y: headerBox.y,
width: headerBox.width,
height: headerBox.height + 140,
},
});
} finally {
// Restore: click Draft to re-publish so other runs start from a clean state
await page.mouse.move(0, 0);
await draftLabel.click();
await expect(
page.getByText('Published', { exact: true }).first(),
).toBeVisible({ timeout: 10000 });
}
});
test('edit button screenshot', async ({ page }) => {
// Capture the right-side action buttons (Edit dashboard + "..." menu)
// rather than the edit button in isolation.
await openSalesDashboard(page);
await settle(page);
const rightPanel = page.locator('.right-button-panel');
await expect(rightPanel).toBeVisible({ timeout: 5000 });
await rightPanel.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_edit_button.png'),
type: 'png',
});
});
test('chart resize screenshot', async ({ page }) => {
// Enter edit mode, start a resize drag on the right-edge handle, then
// screenshot the chart mid-drag. While `DashboardGrid` is in the resizing
// state it renders vertical `grid-column-guide` overlays across the grid
// and the chart gets a blue `--resizing` outline — that's the state the
// original tutorial screenshot was capturing.
await openSalesDashboard(page);
const editButton = page.locator('[data-test="edit-dashboard-button"]');
await expect(editButton).toBeVisible();
await editButton.click();
await expect(
page.locator('[data-test="dashboard-builder-sidepane"]'),
).toBeVisible({ timeout: 10000 });
const chart = page.locator('.dashboard-component-chart-holder').first();
await expect(chart).toBeVisible();
const chartBox = await chart.boundingBox();
if (!chartBox) {
throw new Error('Could not locate chart bounding box');
}
// Hover over the chart so the on-hover action buttons (drag/trash/settings)
// and resize handles become visible.
await page.mouse.move(
chartBox.x + chartBox.width / 2,
chartBox.y + chartBox.height / 2,
);
await settle(page, 200);
// The right-edge handle is a `<span>` added by re-resizable with our
// custom class. Locating it by class is more reliable than computing
// coordinates from the chart-holder (which isn't the full resizable box).
const rightHandle = page
.locator('.resizable-container-handle--right')
.first();
await expect(rightHandle).toBeVisible();
const handleBox = await rightHandle.boundingBox();
if (!handleBox) {
throw new Error('Could not locate right-edge resize handle');
}
const handleX = handleBox.x + handleBox.width / 2;
const handleY = handleBox.y + handleBox.height / 2;
await page.mouse.move(handleX, handleY);
await page.mouse.down();
// Move far enough to snap at least one grid column, which puts
// DashboardGrid into isResizing=true so the column guides render.
await page.mouse.move(handleX + 80, handleY, { steps: 10 });
await settle(page, 500);
// Clip to the chart area plus a left gutter for the hover action rail
// and right padding that reaches past the dragged handle position.
const leftGutter = 32;
const rightPadding = 100;
const topPadding = 16;
const bottomPadding = 24;
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_chart_resize.png'),
type: 'png',
clip: {
x: Math.max(0, chartBox.x - leftGutter),
y: Math.max(0, chartBox.y - topPadding),
width: chartBox.width + leftGutter + rightPadding,
height: chartBox.height + topPadding + bottomPadding,
},
});
// Release back at the start to avoid persisting a size change. Edit-mode
// changes aren't saved (we never click the dashboard Save button).
await page.mouse.move(handleX, handleY, { steps: 6 });
await page.mouse.up();
});
test('save flow and first dashboard screenshots', async ({ page }) => {
// Captures two linked tutorial screenshots in a single flow so the second
// faithfully shows the dashboard the user just created:
// 1. tutorial_save_slice.png — Save modal with the "Add to dashboard"
// dropdown surfacing a creatable option for a new dashboard.
// 2. tutorial_first_dashboard.png — the freshly-created dashboard with
// the single saved chart (matches the tutorial narrative).
//
// Creates and then deletes a "Superset Duper Sales Dashboard" dashboard
// plus the duplicate chart it owns. Pre-cleans in case a prior run failed.
const NEW_DASHBOARD_NAME = 'Superset Duper Sales Dashboard';
await deleteDashboardByTitle(page, NEW_DASHBOARD_NAME);
// 1100px is wide enough to show the full "Superset Duper Sales Dashboard"
// title alongside the header actions without truncation.
await page.setViewportSize({ width: 1100, height: 800 });
await page.goto(URL.CHART_LIST);
const searchInput = page.getByPlaceholder('Type a value');
await expect(searchInput).toBeVisible({ timeout: 15000 });
await searchInput.fill('Scatter Plot');
await searchInput.press('Enter');
const chartLink = page.getByRole('link', { name: /scatter plot/i });
await expect(chartLink).toBeVisible({ timeout: 10000 });
await chartLink.click();
await page.waitForURL('**/explore/**', { timeout: 15000 });
const sliceContainer = page.locator('[data-test="slice-container"]');
await expect(sliceContainer).toBeVisible({ timeout: 15000 });
await expect(
sliceContainer.locator('[data-test="loading-indicator"]'),
).toHaveCount(0, { timeout: 15000 });
const saveButton = page.locator('[data-test="query-save-button"]');
await expect(saveButton).toBeVisible({ timeout: 10000 });
await saveButton.click();
const modal = page.locator('.ant-modal-content').filter({
has: page.locator('[data-test="save-modal-body"]'),
});
await expect(modal).toBeVisible({ timeout: 10000 });
// Open the "Add to dashboard" select and type a new dashboard name so
// the dropdown surfaces the creatable option.
const dashboardSelect = page.getByRole('combobox', {
name: /select a dashboard/i,
});
await dashboardSelect.click();
await page.keyboard.type(NEW_DASHBOARD_NAME);
// Ant Design portals the visible dropdown with the class
// `.ant-select-item-option` on each option (distinct from the hidden
// ARIA listbox options rendered inside the combobox itself).
const createOption = page
.locator('.ant-select-item-option')
.filter({ hasText: NEW_DASHBOARD_NAME });
await expect(createOption).toBeVisible({ timeout: 10000 });
await settle(page);
try {
// Screenshot 1: save modal + portaled dropdown.
const modalBox = await modal.boundingBox();
const optionBox = await createOption.boundingBox();
if (!modalBox || !optionBox) {
throw new Error('Could not locate save modal or create-option');
}
const padding = 16;
const top = Math.max(0, modalBox.y - padding);
const bottom = optionBox.y + optionBox.height + padding;
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_save_slice.png'),
type: 'png',
clip: {
x: Math.max(0, modalBox.x - padding),
y: top,
width: modalBox.width + padding * 2,
height: bottom - top,
},
});
// Pick the creatable option, then click "Save & go to dashboard" so the
// backend creates the dashboard + slice and redirects us to the new one.
await createOption.click();
const saveAndGotoBtn = page.locator('#btn_modal_save_goto_dash');
await expect(saveAndGotoBtn).toBeEnabled({ timeout: 5000 });
await saveAndGotoBtn.click();
await page.waitForURL(/\/dashboard\/[^/]+\/?/, { timeout: 30000 });
await expect(
page.locator('[data-test="dashboard-content-wrapper"]'),
).toBeVisible({ timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder').first(),
).toBeVisible({ timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder canvas').first(),
).toBeVisible({ timeout: 15000 });
// Dismiss the "Chart [X] has been saved" toast so it doesn't appear in
// the screenshot. The close button is inside the toast container.
const toast = page.locator('[data-test="toast-container"]').first();
if (await toast.isVisible().catch(() => false)) {
await toast.locator('.toast__close').click();
await expect(toast).toBeHidden({ timeout: 5000 });
}
await settle(page);
// Screenshot 2: the newly-created single-chart dashboard (title + chart).
const headerBox = await page
.locator('[data-test="dashboard-header-wrapper"]')
.boundingBox();
const chartBox = await page
.locator('.dashboard-component-chart-holder')
.first()
.boundingBox();
if (!headerBox || !chartBox) {
throw new Error('Could not locate dashboard header or chart');
}
// Trim right edge to just past the chart so the screenshot isn't padded
// with empty grid space.
const rightPadding = 16;
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_first_dashboard.png'),
type: 'png',
clip: {
x: 0,
y: headerBox.y,
width: Math.min(1100, chartBox.x + chartBox.width + rightPadding),
height: chartBox.y + chartBox.height - headerBox.y + 16,
},
});
} finally {
await deleteDashboardByTitle(page, NEW_DASHBOARD_NAME);
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,7 @@ export default defineConfig({
name: 'docs-generators',
use: {
browserName: 'chromium',
baseURL, // explicit here so globalSetup can read it from config.projects[0].use.baseURL
testIdAttribute: 'data-test',
storageState: path.resolve(__dirname, '../.auth/user.json'),
},

View File

@@ -33,7 +33,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -34,6 +34,6 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
}

View File

@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -31,7 +31,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -36,6 +36,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
}

View File

@@ -33,8 +33,8 @@
"@apache-superset/core": "*",
"@testing-library/jest-dom": "*",
"@testing-library/react": "^14.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -32,7 +32,7 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -39,6 +39,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"@apache-superset/core": "*",
"react": "^18.2.0"
"react": "^18.3.0"
}
}

View File

@@ -43,6 +43,6 @@
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"dayjs": "^1.11.19",
"react": "^18.2.0"
"react": "^18.3.0"
}
}

View File

@@ -29,7 +29,7 @@
"classnames": "^2.5.1",
"d3-array": "^3.2.4",
"lodash": "^4.18.1",
"memoize-one": "^5.2.1",
"memoize-one": "^6.0.0",
"react-table": "^7.8.0",
"regenerator-runtime": "^0.14.1",
"xss": "^1.0.15"
@@ -44,8 +44,8 @@
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "*",
"@types/react": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -494,6 +494,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'column_config',
@@ -587,18 +593,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'show_cell_bars',
config: {
type: 'CheckboxControl',
label: t('Show cell bars'),
label: t('Show cell bars for all columns'),
renderTrigger: true,
default: true,
description: t(
@@ -612,7 +612,7 @@ const config: ControlPanelConfig = {
name: 'align_pn',
config: {
type: 'CheckboxControl',
label: t('Align +/-'),
label: t('Align +/- for all columns'),
renderTrigger: true,
default: false,
description: t(
@@ -626,7 +626,7 @@ const config: ControlPanelConfig = {
name: 'color_pn',
config: {
type: 'CheckboxControl',
label: t('Add colors to cell bars for +/-'),
label: t('Add colors to cell bars for +/- for all columns'),
renderTrigger: true,
default: true,
description: t(

View File

@@ -0,0 +1,99 @@
/**
* 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.
*/
/**
* Regression coverage for memoize-one v6 adoption.
*
* memoize-one v6 changed the signature of the (optional) custom `isEqual`
* callback from per-argument `(a, b) => bool` to arg-array
* `(newArgs, lastArgs) => bool`. Of the four memoizeOne callsites in
* `src/transformProps.ts` (`processComparisonDataRecords`,
* `processDataRecords`, `processColumns`, `getBasicColorFormatter`), only
* `processColumns` passes a custom comparator (`isEqualColumns`); its
* signature already takes arg-arrays and is compatible with v6. The other
* three rely on memoize-one's default referential-equality comparator, which
* is unchanged between v5 and v6.
*
* These tests lock those assumptions in by observing the memoization
* behavior through the public `transformProps` API: identical chart-props
* input references should produce referentially-equal `data` and `columns`
* arrays (cache hit), while inputs that differ on the sub-fields each
* memoizer actually compares should produce fresh arrays (cache miss).
*/
import transformProps from '../src/transformProps';
import testData from '../../plugin-chart-table/test/testData';
test('transformProps returns referentially-equal data/columns on identical input (cache hit)', () => {
// processColumns and processDataRecords are both wrapped by memoizeOne at
// module scope. Two consecutive calls with the same chartProps reference
// should hit both caches and yield the same output references.
const first = transformProps(testData.basic);
const second = transformProps(testData.basic);
expect(second.columns).toBe(first.columns);
expect(second.data).toBe(first.data);
});
test('transformProps busts its memoization caches when sub-field inputs change (cache miss)', () => {
const first = transformProps(testData.basic);
// `processColumns` is wrapped with a custom equality (`isEqualColumns`) that
// compares specific chartProps sub-fields by identity — mutating only the
// top-level props reference is NOT enough to bust it. Here we supply a fresh
// `datasource.columnFormats` reference, which `isEqualColumns` compares with
// `===`, forcing `processColumns` to recompute and return a new `columns`
// array.
//
// `processDataRecords` uses memoize-one's default referential equality on
// `(data, columns)`. We also hand it a fresh `queriesData[0].data` array, so
// together with the recomputed `columns` reference it too cache-misses.
const freshProps = {
...testData.basic,
datasource: {
...testData.basic.datasource,
columnFormats: {},
},
queriesData: [
{
...testData.basic.queriesData[0],
data: [...(testData.basic.queriesData[0].data || [])],
},
],
};
const second = transformProps(freshProps);
expect(second.columns).not.toBe(first.columns);
expect(second.data).not.toBe(first.data);
});
test('transformProps memoizes the comparison-mode data pipeline on identical input', () => {
// Exercises `processComparisonDataRecords` (the third of four memoizeOne
// callsites in transformProps.ts) via the `comparison` fixture, which has
// `time_compare` set and therefore flows through the comparison branch
// where `passedData = comparisonData`.
//
// Note: we don't assert reference equality on `columns` here because the
// comparison branch runs `comparisonColumns` through the non-memoized
// `processComparisonColumns` helper, which returns a fresh array on each
// call by design.
const first = transformProps(testData.comparison);
const second = transformProps(testData.comparison);
expect(second.data).toBe(first.data);
});

View File

@@ -47,7 +47,7 @@
"geostyler-wfs-parser": "^3.0.1",
"ol": "^10.8.0",
"polished": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
}
}

View File

@@ -38,7 +38,7 @@
"dayjs": "^1.11.19",
"echarts": "*",
"memoize-one": "*",
"react": "^18.2.0"
"react": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -39,9 +39,9 @@
"handlebars": "^4.7.8",
"lodash": "^4.18.1",
"dayjs": "^1.11.19",
"react": "^18.2.0",
"react": "^18.3.0",
"react-ace": "^10.1.0",
"react-dom": "^18.2.0"
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/jest": "^30.0.0",

View File

@@ -33,8 +33,8 @@
"@superset-ui/core": "*",
"lodash": "^4.18.1",
"prop-types": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@babel/types": "^7.29.0",

View File

@@ -36,8 +36,8 @@
"@apache-superset/core": "*",
"@superset-ui/chart-controls": "*",
"@superset-ui/core": "*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -45,8 +45,8 @@
"@testing-library/user-event": "*",
"@types/react": "*",
"match-sorter": "^8.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"publishConfig": {
"access": "public"

View File

@@ -552,6 +552,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'column_config',
@@ -648,18 +654,12 @@ const config: ControlPanelConfig = {
},
},
],
],
},
{
label: t('Visual formatting'),
expanded: true,
controlSetRows: [
[
{
name: 'show_cell_bars',
config: {
type: 'CheckboxControl',
label: t('Show cell bars'),
label: t('Show cell bars for all columns'),
renderTrigger: true,
default: true,
description: t(
@@ -673,7 +673,7 @@ const config: ControlPanelConfig = {
name: 'align_pn',
config: {
type: 'CheckboxControl',
label: t('Align +/-'),
label: t('Align +/- for all columns'),
renderTrigger: true,
default: false,
description: t(
@@ -687,7 +687,7 @@ const config: ControlPanelConfig = {
name: 'color_pn',
config: {
type: 'CheckboxControl',
label: t('Add colors to cell bars for +/-'),
label: t('Add colors to cell bars for +/- for all columns'),
renderTrigger: true,
default: true,
description: t(
@@ -796,45 +796,63 @@ const config: ControlPanelConfig = {
},
);
}
const { colnames, coltypes } =
const { colnames: queryColnames, coltypes: queryColtypes } =
chart?.queriesResponse?.[0] ?? {};
const allColumns =
Array.isArray(colnames) && Array.isArray(coltypes)
? [
{
value: ObjectFormattingEnum.ENTIRE_ROW,
label: t('entire row'),
dataType: GenericDataType.String,
},
...colnames.map((colname: string, index: number) => ({
const hasQueryColumns =
Array.isArray(queryColnames) &&
Array.isArray(queryColtypes) &&
queryColnames.length > 0;
// Fall back to datasource columns when query results are empty
const datasourceColumns = ensureIsArray(
(explore?.datasource as Dataset)?.columns,
);
const colnames = hasQueryColumns
? queryColnames
: datasourceColumns.map((col: ColumnMeta) => col.column_name);
const coltypes = hasQueryColumns
? queryColtypes
: datasourceColumns.map(
(col: ColumnMeta) =>
col.type_generic ?? GenericDataType.String,
);
const hasColumns = colnames.length > 0;
const allColumns = hasColumns
? [
{
value: ObjectFormattingEnum.ENTIRE_ROW,
label: t('entire row'),
dataType: GenericDataType.String,
},
...colnames.map((colname: string, index: number) => ({
value: colname,
label: Array.isArray(verboseMap)
? colname
: (verboseMap[colname] ?? colname),
dataType: coltypes[index],
})),
]
: [];
const numericColumns = hasColumns
? colnames.reduce((acc, colname, index) => {
if (
coltypes[index] === GenericDataType.Numeric ||
(!hasTimeComparison &&
(coltypes[index] === GenericDataType.String ||
coltypes[index] === GenericDataType.Boolean))
) {
acc.push({
value: colname,
label: Array.isArray(verboseMap)
? colname
: (verboseMap[colname] ?? colname),
dataType: coltypes[index],
})),
]
: [];
const numericColumns =
Array.isArray(colnames) && Array.isArray(coltypes)
? colnames.reduce((acc, colname, index) => {
if (
coltypes[index] === GenericDataType.Numeric ||
(!hasTimeComparison &&
(coltypes[index] === GenericDataType.String ||
coltypes[index] === GenericDataType.Boolean))
) {
acc.push({
value: colname,
label: Array.isArray(verboseMap)
? colname
: (verboseMap[colname] ?? colname),
dataType: coltypes[index],
});
}
return acc;
}, [])
: [];
});
}
return acc;
}, [])
: [];
const columnOptions = hasTimeComparison
? processComparisonColumns(
numericColumns || [],

View File

@@ -25,6 +25,7 @@ import {
ControlPanelState,
ControlState,
ColorSchemeEnum,
ObjectFormattingEnum,
} from '@superset-ui/chart-controls';
import config from '../src/controlPanel';
@@ -55,11 +56,12 @@ const createMockControlState = (value: string[] | undefined): ControlState => ({
const createMockExplore = (
timeCompareValue: string[] | undefined,
datasourceColumns: Partial<Dataset>['columns'] = [],
): ControlPanelState => ({
slice: { slice_id: 123 },
datasource: {
verbose_map: { col1: 'Column 1', col2: 'Column 2' },
columns: [],
columns: datasourceColumns,
} as Partial<Dataset> as Dataset,
controls: {
time_compare: createMockControlState(timeCompareValue),
@@ -206,3 +208,144 @@ test('static extraColorChoices removed from config', () => {
expect(controlConfig?.extraColorChoices).toBeUndefined();
});
test('columnOptions falls back to datasource columns when queriesResponse is empty', () => {
const controlConfig = findConditionalFormattingControl();
expect(controlConfig).toBeTruthy();
const datasourceColumns = [
{ column_name: 'revenue', type_generic: GenericDataType.Numeric },
{ column_name: 'name', type_generic: GenericDataType.String },
];
const explore = createMockExplore(undefined, datasourceColumns);
const chart = { chartStatus: 'success' as const, queriesResponse: null };
const result = controlConfig!.mapStateToProps!(
explore,
createMockControlStateForConditionalFormatting(),
chart,
);
expect(result.columnOptions).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: 'revenue' }),
expect.objectContaining({ value: 'name' }),
]),
);
expect(result.allColumns).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: 'revenue' }),
expect.objectContaining({ value: 'name' }),
]),
);
});
test('columnOptions prefers queriesResponse over datasource columns', () => {
const controlConfig = findConditionalFormattingControl();
expect(controlConfig).toBeTruthy();
const datasourceColumns = [
{ column_name: 'revenue', type_generic: GenericDataType.Numeric },
{ column_name: 'extra_col', type_generic: GenericDataType.String },
];
const explore = createMockExplore(undefined, datasourceColumns);
const chart = createMockChart();
const result = controlConfig!.mapStateToProps!(
explore,
createMockControlStateForConditionalFormatting(),
chart,
);
expect(result.columnOptions).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: 'col1' }),
expect.objectContaining({ value: 'col2' }),
]),
);
expect(result.columnOptions).not.toEqual(
expect.arrayContaining([expect.objectContaining({ value: 'extra_col' })]),
);
});
test('columnOptions falls back to datasource when queriesResponse has empty colnames', () => {
const controlConfig = findConditionalFormattingControl();
expect(controlConfig).toBeTruthy();
const datasourceColumns = [
{ column_name: 'revenue', type_generic: GenericDataType.Numeric },
];
const explore = createMockExplore(undefined, datasourceColumns);
const chart = {
chartStatus: 'success' as const,
queriesResponse: [{ colnames: [], coltypes: [] }],
};
const result = controlConfig!.mapStateToProps!(
explore,
createMockControlStateForConditionalFormatting(),
chart,
);
expect(result.columnOptions).toEqual(
expect.arrayContaining([expect.objectContaining({ value: 'revenue' })]),
);
});
test('columnOptions returns empty when both queriesResponse and datasource have no columns', () => {
const controlConfig = findConditionalFormattingControl();
expect(controlConfig).toBeTruthy();
const explore = createMockExplore(undefined, []);
const chart = { chartStatus: 'success' as const, queriesResponse: null };
const result = controlConfig!.mapStateToProps!(
explore,
createMockControlStateForConditionalFormatting(),
chart,
);
expect(result.columnOptions).toEqual([]);
expect(result.allColumns).toEqual([]);
});
test('allColumns includes ENTIRE_ROW when falling back to datasource columns', () => {
const controlConfig = findConditionalFormattingControl();
expect(controlConfig).toBeTruthy();
const datasourceColumns = [
{ column_name: 'revenue', type_generic: GenericDataType.Numeric },
];
const explore = createMockExplore(undefined, datasourceColumns);
const chart = { chartStatus: 'success' as const, queriesResponse: null };
const result = controlConfig!.mapStateToProps!(
explore,
createMockControlStateForConditionalFormatting(),
chart,
);
expect(result.allColumns).toEqual(
expect.arrayContaining([
expect.objectContaining({ value: ObjectFormattingEnum.ENTIRE_ROW }),
]),
);
});
test('columnOptions defaults type_generic to String when missing from datasource columns', () => {
const controlConfig = findConditionalFormattingControl();
expect(controlConfig).toBeTruthy();
const datasourceColumns = [{ column_name: 'untyped_col' }];
const explore = createMockExplore(undefined, datasourceColumns);
const chart = { chartStatus: 'success' as const, queriesResponse: null };
const result = controlConfig!.mapStateToProps!(
explore,
createMockControlStateForConditionalFormatting(),
chart,
);
expect(result.columnOptions).toEqual(
expect.arrayContaining([
expect.objectContaining({
value: 'untyped_col',
dataType: GenericDataType.String,
}),
]),
);
});

View File

@@ -39,7 +39,7 @@
"@superset-ui/core": "*",
"@types/lodash": "*",
"@types/react": "*",
"react": "^18.2.0"
"react": "^18.3.0"
},
"devDependencies": {
"@types/d3-cloud": "^1.2.9"

View File

@@ -29,7 +29,7 @@
"@deck.gl/extensions": "~9.2.9",
"@deck.gl/geo-layers": "~9.2.5",
"@deck.gl/layers": "~9.2.5",
"@deck.gl/mapbox": "~9.3.1",
"@deck.gl/mapbox": "~9.3.2",
"@deck.gl/mesh-layers": "~9.2.5",
"@luma.gl/constants": "~9.2.5",
"@luma.gl/core": "~9.2.5",
@@ -67,8 +67,8 @@
"@superset-ui/core": "*",
"dayjs": "^1.11.19",
"mapbox-gl": ">=1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"peerDependenciesMeta": {
"mapbox-gl": {

View File

@@ -22,6 +22,28 @@ import React from 'react';
// eslint-disable-next-line no-restricted-imports
import { configure as configureTestingLibrary } from '@testing-library/react';
import { matchers } from '@emotion/jest';
import failOnConsole from 'jest-fail-on-console';
// Fail tests only on React 19 deprecation/removal warnings surfaced by React 18.3
// (other console.error/warn output is left alone during the migration).
const REACT_19_DEPRECATION_PATTERNS = [
/Support for defaultProps will be removed/,
/ReactDOM\.render is no longer supported/,
/ReactDOM\.hydrate is no longer supported/,
/unmountComponentAtNode is deprecated/,
/findDOMNode is deprecated/,
/String refs are deprecated/,
/Legacy context API has been detected/,
/`ReactDOMTestUtils\.act` is deprecated/,
/will be removed in (a future major release|React 19)/,
];
failOnConsole({
shouldFailOnError: true,
shouldFailOnWarn: true,
silenceMessage: message =>
!REACT_19_DEPRECATION_PATTERNS.some(pattern => pattern.test(message)),
});
configureTestingLibrary({
testIdAttribute: 'data-test',

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { AriaAttributes } from 'react';
import { AriaAttributes, Ref } from 'react';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import jQuery from 'jquery';
@@ -98,31 +98,39 @@ jest.mock('rehype-raw', () => () => jest.fn());
// Tests should override this when needed
jest.mock('@superset-ui/core/components/Icons/AsyncIcon', () => ({
__esModule: true,
default: ({
fileName,
role,
'aria-label': ariaLabel,
onClick,
...rest
}: {
fileName: string;
role?: string;
'aria-label'?: AriaAttributes['aria-label'];
onClick?: () => void;
}) => {
// Simple mock that provides the essential attributes for testing
const label = ariaLabel || fileName?.replace(/_/g, '-').toLowerCase() || '';
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span
role={role || (onClick ? 'button' : 'img')}
aria-label={label}
data-test={label}
onClick={onClick}
{...rest}
/>
);
},
// eslint-disable-next-line global-require
default: require('react').forwardRef(
(
{
fileName,
role,
'aria-label': ariaLabel,
onClick,
...rest
}: {
fileName: string;
role?: string;
'aria-label'?: AriaAttributes['aria-label'];
onClick?: () => void;
},
ref: Ref<HTMLSpanElement>,
) => {
// Simple mock that provides the essential attributes for testing
const label =
ariaLabel || fileName?.replace(/_/g, '-').toLowerCase() || '';
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<span
ref={ref}
role={role || (onClick ? 'button' : 'img')}
aria-label={label}
data-test={label}
onClick={onClick}
{...rest}
/>
);
},
),
StyledIcon: ({
component: Component,
role,

View File

@@ -37,6 +37,7 @@ import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { TestBackend } from 'react-dnd-test-backend';
import reducerIndex from 'spec/helpers/reducerIndex';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5';
@@ -46,7 +47,11 @@ import userEvent from '@testing-library/user-event';
type Options = Omit<RenderOptions, 'queries'> & {
useRedux?: boolean;
useDnd?: boolean;
// `true` -> HTML5Backend (default; matches browser).
// `'test'` -> TestBackend, drive drags programmatically via
// `react-dnd-test-backend` `getInstance()` — avoids the jsdom
// HTML5-drag-event / preventDefault / zero-rect gaps.
useDnd?: boolean | 'test';
useQueryParams?: boolean;
useRouter?: boolean;
useTheme?: boolean;
@@ -97,8 +102,9 @@ export function createWrapper(options?: Options) {
}
if (useDnd) {
const backend = useDnd === 'test' ? TestBackend : HTML5Backend;
// @ts-expect-error react-dnd types not updated for React 18
result = <DndProvider backend={HTML5Backend}>{result}</DndProvider>;
result = <DndProvider backend={backend}>{result}</DndProvider>;
}
if (useRedux || store) {

View File

@@ -856,7 +856,7 @@ export function loadQueryEditor(queryEditor: QueryEditor): SqlLabAction {
return { type: LOAD_QUERY_EDITOR, queryEditor };
}
interface TableSchema {
export interface TableSchema {
description: {
columns: unknown[];
selectStar: string;
@@ -1284,7 +1284,7 @@ export function addTable(
};
}
interface NewTable {
export interface NewTable {
id?: string;
dbId: number | string;
catalog?: string | null;
@@ -1346,7 +1346,7 @@ export function runTablePreviewQuery(
};
}
interface TableMetaData {
export interface TableMetaData {
columns?: unknown[];
selectStar?: string;
primaryKey?: unknown;
@@ -1660,7 +1660,7 @@ export function createDatasourceFailed(err: string): SqlLabAction {
return { type: CREATE_DATASOURCE_FAILED, err };
}
interface VizOptions {
export interface VizOptions {
dbId: number;
catalog?: string | null;
schema: string;

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { act } from 'react-dom/test-utils';
import { act } from 'react';
import { QueryState } from '@superset-ui/core';
import fetchMock from 'fetch-mock';
import configureStore from 'redux-mock-store';
@@ -53,7 +53,7 @@ describe('QueryAutoRefresh', () => {
const refreshApi = 'glob:*/api/v1/query/updated_since?*';
beforeEach(() => {
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
});
afterEach(() => {

View File

@@ -45,7 +45,7 @@ fetchMock.get('glob:*/api/v1/dataset/?*', {
dataset_count: 3,
});
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
// Mock the user
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');

View File

@@ -189,15 +189,19 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
placement="bottomLeft"
trigger="click"
>
<DatabaseSelector
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
schema ?? 'no-schema'
}`}
{...dbSelectorProps}
emptyState={<EmptyState />}
sqlLabMode
onOpenModal={openSelectorModal}
/>
{/* Wrap in a span so the Popover can attach a ref without relying
on findDOMNode (deprecated in React 18+). */}
<span>
<DatabaseSelector
key={`db-selector-${db ? db.id : 'no-db'}:${catalog ?? 'no-catalog'}:${
schema ?? 'no-schema'
}`}
{...dbSelectorProps}
emptyState={<EmptyState />}
sqlLabMode
onOpenModal={openSelectorModal}
/>
</span>
</Popover>
<StyledDivider />
<TableExploreTree queryEditorId={activeQEId} />

View File

@@ -98,7 +98,10 @@ class CopyToClip extends Component<CopyToClipboardProps> {
trigger={['hover']}
arrow={{ pointAtCenter: true }}
>
{this.getDecoratedCopyNode()}
{/* Wrap in a span so antd Tooltip has a real DOM ref target;
avoids findDOMNode fallback when copyNode is a function
component without forwardRef. */}
<span>{this.getDecoratedCopyNode()}</span>
</Tooltip>
) : (
this.getDecoratedCopyNode()

View File

@@ -17,21 +17,20 @@
* under the License.
*/
import { PropsWithoutRef, RefAttributes } from 'react';
import { forwardRef, PropsWithoutRef, Ref, RefAttributes } from 'react';
import { Link, LinkProps } from 'react-router-dom';
import { isUrlExternal, parseUrl } from 'src/utils/urlUtils';
export const GenericLink = <S,>({
to,
component,
replace,
innerRef,
children,
...rest
}: PropsWithoutRef<LinkProps<S>> & RefAttributes<HTMLAnchorElement>) => {
type GenericLinkProps<S> = PropsWithoutRef<LinkProps<S>> &
RefAttributes<HTMLAnchorElement>;
const GenericLinkInner = <S,>(
{ to, component, replace, innerRef, children, ...rest }: GenericLinkProps<S>,
ref: Ref<HTMLAnchorElement>,
) => {
if (typeof to === 'string' && isUrlExternal(to)) {
return (
<a data-test="external-link" href={parseUrl(to)} {...rest}>
<a ref={ref} data-test="external-link" href={parseUrl(to)} {...rest}>
{children}
</a>
);
@@ -42,10 +41,14 @@ export const GenericLink = <S,>({
to={to}
component={component}
replace={replace}
innerRef={innerRef}
innerRef={innerRef ?? ref}
{...rest}
>
{children}
</Link>
);
};
export const GenericLink = forwardRef(GenericLinkInner) as <S>(
props: GenericLinkProps<S> & { ref?: Ref<HTMLAnchorElement> },
) => ReturnType<typeof GenericLinkInner>;

View File

@@ -17,7 +17,8 @@
* under the License.
*/
import { render, screen, act } from 'spec/helpers/testing-library';
import { StatusIndicatorDot } from './StatusIndicatorDot';
import { supersetTheme } from '@apache-superset/core/theme';
import { getStatusConfig, StatusIndicatorDot } from './StatusIndicatorDot';
import { AutoRefreshStatus } from '../../types/autoRefresh';
afterEach(() => {
@@ -62,6 +63,15 @@ test('renders with paused status', () => {
expect(dot).toHaveAttribute('data-status', AutoRefreshStatus.Paused);
});
test('uses the icon color for the paused status outline', () => {
expect(
getStatusConfig(supersetTheme, AutoRefreshStatus.Paused),
).toMatchObject({
needsBorder: true,
outlineColor: 'currentColor',
});
});
test('has correct accessibility attributes', () => {
render(<StatusIndicatorDot status={AutoRefreshStatus.Success} />);
const dot = screen.getByTestId('status-indicator-dot');

View File

@@ -39,9 +39,10 @@ export interface StatusIndicatorDotProps {
interface StatusConfig {
color: string;
needsBorder: boolean;
outlineColor?: string;
}
const getStatusConfig = (
export const getStatusConfig = (
theme: ReturnType<typeof useTheme>,
status: AutoRefreshStatus,
): StatusConfig => {
@@ -75,6 +76,7 @@ const getStatusConfig = (
return {
color: theme.colorBgContainer,
needsBorder: true,
outlineColor: 'currentColor',
};
default:
return {
@@ -136,13 +138,15 @@ export const StatusIndicatorDot: FC<StatusIndicatorDotProps> = ({
width: ${size}px;
height: ${size}px;
border-radius: 50%;
color: ${theme.colorTextSecondary};
background-color: ${statusConfig.color};
transition:
background-color ${theme.motionDurationMid} ease-in-out,
border-color ${theme.motionDurationMid} ease-in-out;
border: ${statusConfig.needsBorder
? `1px solid ${theme.colorBorder}`
: 'none'};
border: ${statusConfig.needsBorder ? '1px solid' : 'none'};
border-color: ${statusConfig.needsBorder
? statusConfig.outlineColor
: 'transparent'};
box-shadow: ${statusConfig.needsBorder
? 'none'
: `0 0 0 2px ${theme.colorBgContainer}`};

View File

@@ -67,7 +67,7 @@ interface DashboardActions {
setDatasources: (datasources: unknown) => void;
}
interface DashboardProps {
export interface DashboardProps {
actions: DashboardActions;
dashboardId: number;
editMode?: boolean;

View File

@@ -32,7 +32,7 @@ import { Droppable } from './dnd/DragDroppable';
import { GRID_GUTTER_SIZE, GRID_COLUMN_COUNT } from '../util/constants';
import { TAB_TYPE } from '../util/componentTypes';
interface DashboardGridProps {
export interface DashboardGridProps {
depth: number;
editMode?: boolean;
canEdit?: boolean;

View File

@@ -611,7 +611,7 @@ test('should refresh the charts', async () => {
});
test('auto-refresh uses onRefresh with skipped filters and toggles refresh state', async () => {
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
onRefresh.mockResolvedValue(undefined);
const originalRequestAnimationFrame = window.requestAnimationFrame;

View File

@@ -80,7 +80,7 @@ interface FilterScopeMap {
[key: string]: FilterScopeMapEntry;
}
interface FilterScopeSelectorProps {
export interface FilterScopeSelectorProps {
dashboardFilters: Record<number, DashboardFilter>;
layout: DashboardLayout;
updateDashboardFiltersScope: (

View File

@@ -43,7 +43,7 @@ import {
export const CHART_MARGIN = 32;
interface ChartHolderProps {
export interface ChartHolderProps {
id: string;
parentId: string;
dashboardId: number;

View File

@@ -39,7 +39,7 @@ import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
import { BACKGROUND_TRANSPARENT } from 'src/dashboard/util/constants';
import { EMPTY_CONTAINER_Z_INDEX } from 'src/dashboard/constants';
interface ColumnProps {
export interface ColumnProps {
id: string;
parentId: string;
component: LayoutItem;

View File

@@ -43,13 +43,13 @@ import {
GRID_BASE_UNIT,
} from 'src/dashboard/util/constants';
interface EditorInstance {
export interface EditorInstance {
resize?: (force: boolean) => void;
getSession?: () => { setUseWrapMode: (wrap: boolean) => void };
focus?: () => void;
}
interface MarkdownOwnProps {
export interface MarkdownOwnProps {
id: string;
parentId: string;
component: LayoutItem;
@@ -71,7 +71,7 @@ interface MarkdownOwnProps {
updateComponents: (components: Record<string, LayoutItem>) => void;
}
interface MarkdownStateProps {
export interface MarkdownStateProps {
logEvent: (eventName: string, eventData: JsonObject) => void;
addDangerToast: (msg: string) => void;
undoLength: number;
@@ -80,9 +80,9 @@ interface MarkdownStateProps {
htmlSchemaOverrides?: JsonObject;
}
type MarkdownProps = MarkdownOwnProps & MarkdownStateProps;
export type MarkdownProps = MarkdownOwnProps & MarkdownStateProps;
interface MarkdownState {
export interface MarkdownState {
isFocused: boolean;
markdownSource: string;
editor: EditorInstance | null;

View File

@@ -57,7 +57,7 @@ export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT';
// Delay before refreshing charts to ensure they are fully mounted
const CHART_MOUNT_DELAY = 100;
interface TabProps {
export interface TabProps {
dashboardId: number;
id: string;
parentId: string;

View File

@@ -44,7 +44,7 @@ import TabsRenderer from '../TabsRenderer';
import type { LayoutItem, RootState } from 'src/dashboard/types';
import type { DropResult } from 'src/dashboard/components/dnd/dragDroppableConfig';
interface TabsProps {
export interface TabsProps {
id: string;
parentId: string;
component: LayoutItem;

View File

@@ -39,7 +39,7 @@ import FilterBar from '.';
import { FILTERS_CONFIG_MODAL_TEST_ID } from '../FiltersConfigModal/FiltersConfigModal';
import * as dataMaskActions from 'src/dataMask/actions';
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),

View File

@@ -31,7 +31,7 @@ describe('FilterScope TreeInitialization', () => {
let formRef: { current: FormInstance | null };
beforeEach(() => {
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
formRef = { current: null };
});

View File

@@ -31,7 +31,7 @@ describe('FilterScope TreeSelection', () => {
let formRef: { current: FormInstance | null };
beforeEach(() => {
jest.useFakeTimers();
jest.useFakeTimers({ advanceTimers: true });
formRef = { current: null };
});

Some files were not shown because too many files have changed in this diff Show More