Compare commits

...
Author SHA1 Message Date
amaannawab923 d59eb79cf9 Merge branch 'master' into feat/embedded-chart 2026-09-09 21:44:02 +05:30
dependabot[bot] a78591be60 chore(deps-dev): bump oxfmt from 0.65.0 to 0.66.0 in /superset-websocket (#44049)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 22:34:46 +07:00
Evan RusackasandClaude Sonnet 5 4dcc800b58 fix(security): use self-repository syntax for change-detector in integration tests (#44065)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:33:42 +07:00
Evan RusackasandClaude Sonnet 5 9267d8b666 fix(security): suppress zizmor self-repository false positive for playwright experimental step (#44064)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:33:33 +07:00
Evan RusackasandClaude Sonnet 5 b1c6401e82 fix(security): suppress zizmor self-repository false positive for mysql setup step (#44067)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:33:23 +07:00
Evan RusackasandClaude Sonnet 5 196fe87c54 fix(security): use self-repository syntax for setup-backend in mysql job (#44066)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:33:21 +07:00
Evan RusackasandClaude Sonnet 5 09522cc4b8 fix(security): suppress zizmor self-repository false positive for playwright test-data step (#44075)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:32:40 +07:00
Evan RusackasandClaude Sonnet 5 4aca0d7da4 fix(security): suppress zizmor self-repository false positive for playwright install step (#44074)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:32:29 +07:00
Evan RusackasandClaude Sonnet 5 ce700f389c fix(security): suppress zizmor self-repository false positive for postgres setup step (#44084)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:31:57 +07:00
Evan RusackasandClaude Sonnet 5 072a29ddb5 fix(security): suppress zizmor self-repository false positive for playwright build step (#44073)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:31:05 +07:00
ShaitanandClaude Opus 4.8 f73ccee782 fix(couchbase): remove debug prints that emit the connection URI to stdout (#44070)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 22:30:45 +07:00
Evan RusackasandClaude Sonnet 5 0fe58044cb fix(security): use self-repository syntax for setup-backend in Playwright workflow (#44085)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:29:56 +07:00
Evan RusackasandClaude Sonnet 5 4686d13379 fix(security): use self-repository syntax for change-detector in Playwright workflow (#44086)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:28:38 +07:00
dependabot[bot] 68528e3308 chore(deps-dev): bump jest-environment-jsdom from 30.5.0 to 30.5.1 in /superset-frontend (#44055)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 22:26:26 +07:00
Evan RusackasandClaude Sonnet 5 c442180328 fix(security): use self-repository syntax for chart-testing action ref (#44083)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:25:42 +07:00
Evan RusackasandClaude Sonnet 5 80ebfad094 fix(security): suppress zizmor self-repository false positive for npm install step (#44076)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 22:25:21 +07:00
Joe LiandClaude Fable 5.1 d1ad6aeda6 chore(docker): speed up image builds and the compose dev loop (#43916)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 22:24:37 +07:00
Michael S. MolinaandClaude Sonnet 5 7f06e4eb60 feat(table): add opt-in migration from Table V1 to Table V2 (#42088)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 12:08:05 -03:00
dependabot[bot] 07a8f8b984 chore(deps-dev): bump oxlint from 1.80.0 to 1.81.0 in /docs (#44052)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 07:50:48 -07:00
Amin Ghadersohi 08e686b535 fix(mcp): complete Gauge chart support follow-up (#43943) 2026-09-09 10:34:50 -04:00
Mafi 97329bb1cf fix(reports): detect perceptually blank screenshots (#44034) 2026-09-09 15:18:20 +02:00
4d8dfdd152 fix(sqllab): restrict TabStateView updates to owned columns and queries (#43992)
Co-authored-by: sha174n <pedro.sousa@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 14:03:30 +01:00
57a92170bd fix(chart): apply the query-context datasource check on create and update (#43997)
Co-authored-by: sha174n <pedro.sousa@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 14:03:14 +01:00
954dbc9ff6 fix(dataset): validate SQL table access on virtual dataset import (#43999)
Co-authored-by: sha174n <pedro.sousa@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 14:02:52 +01:00
ShaitanandClaude Opus 4.8 7b83ed8374 fix(mcp): align execute_sql table-access checks with the SQL Lab path (#43998)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 14:02:35 +01:00
amaannawab923 f559837f1c Merge branch 'master' into feat/embedded-chart 2026-09-09 18:03:56 +05:30
Enzo MartellucciandClaude Opus 5 49b9347974 fix(charts): preserve dataset conversion when saving SQL Lab charts (#43988)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 14:07:50 +02:00
dependabot[bot] df95ab23ec chore(deps-dev): bump @types/node from 26.4.0 to 26.4.1 in /superset-websocket (#44046)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:47:29 -07:00
dependabot[bot] e43f44e91e chore(deps-dev): bump @types/node from 26.4.0 to 26.4.1 in /superset-embedded-sdk (#44048)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:47:24 -07:00
dependabot[bot] d38f20e5ad chore(deps): bump query-string from 9.5.0 to 9.5.1 in /superset-frontend (#44061)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:46:59 -07:00
dependabot[bot] 0a412b07cb chore(deps-dev): bump webpack from 5.110.2 to 5.110.3 in /superset-embedded-sdk (#44050)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:46:28 -07:00
dependabot[bot] 5e99f14ac9 chore(deps-dev): bump oxlint from 1.80.0 to 1.81.0 in /superset-websocket (#44051)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:46:23 -07:00
dependabot[bot] ea287c1ab0 chore(deps-dev): bump webpack from 5.110.2 to 5.110.3 in /docs (#44053)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:46:16 -07:00
dependabot[bot] e170822a28 chore(deps-dev): bump oxfmt from 0.65.0 to 0.66.0 in /docs (#44054)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:46:13 -07:00
dependabot[bot] 677e9189f1 chore(deps-dev): bump oxfmt from 0.65.0 to 0.66.0 in /superset-frontend (#44056)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:46:02 -07:00
dependabot[bot] 638f87de5f chore(deps-dev): bump jest from 30.5.0 to 30.5.1 in /superset-frontend (#44058)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:45:51 -07:00
dependabot[bot] cb2188aec3 chore(deps-dev): bump babel-jest from 30.5.0 to 30.5.1 in /superset-frontend (#44059)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:45:43 -07:00
dependabot[bot] 176a50d254 chore(deps-dev): bump webpack from 5.110.2 to 5.110.3 in /superset-frontend (#44060)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:45:37 -07:00
dependabot[bot] 010c7a12ed chore(deps): bump query-string from 9.5.0 to 9.5.1 in /superset-frontend (#44061)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:45:27 -07:00
dependabot[bot] de08dc207c chore(deps-dev): bump @types/node from 26.4.0 to 26.4.1 in /superset-frontend (#44062)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-09 04:45:17 -07:00
amaannawab923 4c8fb70e5c docs(embedded): regenerate the API spec for the chart embed endpoints
The chart embed routes and their schemas were never reflected in
openapi.json, and the guest-token resource enum still listed dashboard
alone. Regenerated on the pinned requirements.
2026-09-09 17:07:48 +05:30
amaannawab923 4c9e4f3d17 refactor(embedded): name the hook after what it fetches
The hook no longer reads from the explore endpoint, so useExploreData named a
detail that is no longer true. Rename it to useEmbeddedChartData.
2026-09-09 16:23:20 +05:30
amaannawab923 e36912aa41 fix(embedded): chain the migration onto the current master head
The migration and b3e9c1a75d24 both claimed 8f31c5d726ab as their parent, which
left the chain with two heads. Point it at the current head and correct the
docstring, which still named an older revision.
2026-09-09 16:23:20 +05:30
amaannawab923 a733b1fd84 fix(dashboard): refetch the embedded config when the resource type changes
The fetch effect depended on dashboardId alone while the endpoint it calls is
derived from resourceType too, so a change to the resource type without a change
of id would have shown the previous resource's settings. Both call sites pass a
fixed resource type today, so this is a latent hazard rather than a reachable
bug, but the dependency was wrong either way.
2026-09-09 16:23:20 +05:30
amaannawab923 e5187b7b91 fix(embedded): rename the chart description for the chart stack
The API spells the field description_markeddown and the chart stack reads
description_markdown, a rename that hydrate.ts and sliceEntities.ts both
perform. The fabricated payload passed the API spelling straight through, so an
expanded description never appeared on an embedded chart.
2026-09-09 16:23:20 +05:30
amaannawab923 6b30778d81 perf(embedded): load the chart stack lazily
A static import pulled the dashboard chart stack into the initial embedded
bundle for every consumer, including deployments that only ever embed
dashboards and never render this path. Import it lazily instead, following the
LazyDashboardPage pattern already in this file; it renders inside the Suspense
boundary that component already sits in, so no new boundary is needed.
2026-09-09 16:23:20 +05:30
amaannawab923 3337218ea7 fix(embedded): honour guest token revocation for charts
embedded_charts carries guest_token_revoked_before, but nothing enforced it:
the revocation check skipped every resource that was not a dashboard, and
revoke_guest_token_access resolved embedded dashboards only. Setting a cutoff on
a chart therefore did nothing, and the only way to cut access was to delete the
embed config, which destroys the uuid that revocation exists to preserve.

Dispatch on resource type in the check, and fall back to the chart DAO when a
uuid does not resolve to an embedded dashboard. The two uuid spaces are
distinct, so the fallthrough is unambiguous, and it matches the order the
embedded view already uses to decide which resource a uuid names. Charts are
only ever addressed by their embed uuid, so the chart branch needs no equivalent
of the dashboard branch's legacy raw-id lookup.
2026-09-09 16:23:20 +05:30
amaannawab923 7ae1f859c0 fix(embedded): load an embedded chart under the chart permission
The standalone chart page read its slice and dataset from /api/v1/explore/,
which sits under the Explore permission. An embedded guest role does not hold
it, so a chart guest token was refused there and the page stopped at its error
state. Explore also assembles its payload from request-supplied form data and
resolves its datasource from that, so granting it to the guest role would widen
a guest well past the one chart its token names.

Serve the pair from a chart-scoped endpoint instead. It sits under the Chart
permission the guest already needs for /api/v1/chart/data and resolves one fixed
chart. Dataset narrowing follows the dashboard's rule, treating a guest holding
a token for that chart as entitled to it so the chart keeps the columns and
metrics it needs to render; params stays withheld either way, being
operator-authored configuration the renderer never reads.

ChartFilter's guest branch now resolves chart resources as well as dashboard
ones. A chart-only token previously produced an empty dashboard scope, which the
branch turned into a deny-all clause, so every Chart-API read was refused for
exactly the tokens this feature issues. A token granting neither kind still
denies all charts.

Document the guest-role grants, which were not written down anywhere.
2026-09-09 16:23:20 +05:30
amaannawab923 7bf63bdd8c fix(embedded): dump the chart id on embedded chart responses
EmbeddedChartResponseSchema declared chart_id, but EmbeddedChart stores the
identifier as slice_id, so the field never resolved and every embedded-chart
response omitted it. Bind the field to the model attribute and keep the public
name, which matches how the embedded-dashboard schema names its own.

Point changed_by at a schema dedicated to this response so the two embedded
response shapes are identical, rather than changing the shared UserSchema that
other chart endpoints dump through.
2026-09-09 16:23:19 +05:30
amaannawab923 2a3cca40d8 fix(embedded): chain the migration onto the current master head
Master gained migrations while this branch was open, so down_revision pointed
at a revision that is no longer the tip and the chain had two heads.
2026-09-09 16:22:48 +05:30
amaannawab923 976707dc8e fix(embedded): register the embedded chart response schema
The embedded endpoints reference EmbeddedChartResponseSchema by $ref, but it
was never added to openapi_spec_component_schemas, so the generated spec
pointed at a component that did not exist and OpenAPI validation failed. The
dashboard API registers its equivalent the same way. The config schema needs no
entry, since it is inlined rather than referenced.
2026-09-09 16:22:48 +05:30
amaannawab923 29cc378010 fix(embedded): resolve the guest user before the chart lookup
Two unit test failures, one real and one a fixture gap.

has_embedded_chart_access queried the slice before establishing that a guest
user exists, so it issued a database lookup on every datasource check that
reached it, including paths where the grant could never hold. Resolving the
guest user first skips the query entirely in that case.

The embedded guest chart test builds its security manager as a mock specced
against the real class, so has_guest_access_to_chart returned a truthy mock and
the denial assertion no longer held. It is closed by default alongside the other
paths the helper already closes, and the chart-direct grant now has a test of
its own rather than only being exercised implicitly.
2026-09-09 16:22:48 +05:30
amaannawab923 31ad989238 fix(embedded): document the embedded PUT and expect the new chart permission
set_embedded is exposed for POST and PUT but only documented post, so the
generated spec carried a put operation with no responses and failed OpenAPI
validation. The chart info permission set also needed can_set_embedded, which
the dashboard equivalent already lists.
2026-09-09 16:22:48 +05:30
amaannawab923 3b68d0897e fix(embedded): declare embedded_charts in the chart purge policy
The purge registry requires every inbound foreign key to a supported model to
be declared, so adding embedded_charts.slice_id left Slice with an incomplete
policy. Declared as OWNED with its three outbound keys, mirroring how
embedded_dashboards is declared against Dashboard, which matches the cascade
already on the relationship and the migration.
2026-09-09 16:22:48 +05:30
amaannawab923 f573fa0c14 fix(embedded): type the chart's datasource as Datasource
tsc rejected passing the loosely typed dataset to setDatasources, which stores
it as a Datasource. The explore endpoint returns the full datasource, so the
field is declared as one rather than cast at the call site. Also applies oxfmt
formatting the frontend hook expects.
2026-09-09 16:22:48 +05:30
amaannawab923 dad9b3e113 chore(embedded): apply lint and formatting fixes
Import ordering and formatting from ruff, and dropping an unused alembic op
import from the migration, which builds its table through the shared helpers.
2026-09-09 16:22:48 +05:30
amaannawab923 b4d2b470f5 fix(embedded): pad the embedded chart and size it to its container
The chart title and the header menu rendered flush against the iframe edge,
because on a dashboard that breathing room comes from the grid gutter and an
embed has no grid. The holder supplies it instead.

Sizing moved from the viewport to the holder's content box, so the chart lays
out inside that padding rather than overflowing it, and the holder also carries
dashboard-component-chart-holder, which is the class the fullscreen styles
select on.
2026-09-09 16:22:47 +05:30
amaannawab923 116815a77f test(embedded): cover the fabricated hydrate payload
Runs the real dashboard reducers against the real payload, so a slice that a
HYDRATE_DASHBOARD handler dereferences without optional chaining cannot go
missing again. That failure only surfaces at runtime and only in the embedded
path, which is why it went unnoticed. Also fills in dashboardLayout and
nativeFilters on HydrateEmbeddedAction, which had gone stale.
2026-09-09 16:22:47 +05:30
amaannawab923 4b223c1851 fix(embedded): don't serve the mobile experience inside an iframe
useIsMobile matches a media query against the current viewport, which inside an
iframe is the size the host chose for the embed rather than the size of the
device. A narrow embed on a desktop was therefore treated as a phone and lost
its chart controls entirely, since SliceHeader hides them when isMobile. Mobile
consumption mode is a whole-app experience (route guarding, drawer navigation)
that an embed does not have to begin with.
2026-09-09 16:22:47 +05:30
amaannawab923 223cf0e432 fix(embedded): restore fullscreen and image exports for embedded charts
The dashboard gives every chart a holder element that owns two things the
header controls reach for: the node handed to requestFullscreen, and the
dashboard-chart-id-<id> class the jpeg and PDF exports select on. Rendering
Chart directly meant neither existed, so fullscreen always reported
'not supported in this browser' and the image exports silently produced
nothing. The embed now supplies its own holder rather than adopting
ChartHolder, which would drag in drag-and-drop, resize and edit-mode logic an
iframe has no use for.

Popups are portaled into the fullscreen element for the same reason the
dashboard does it: only that subtree is painted, so the header menu would
otherwise be unreachable while fullscreen.
2026-09-09 16:22:47 +05:30
amaannawab923 e5a85d89a0 fix(embedded): don't 500 on a malformed Referer
same_origin parses the referrer eagerly, so an attacker-controlled value whose
authority looks like it carries a non-numeric port (for example
http://localhost:3007.evil.com/) raised ValueError instead of returning False.
Access already failed closed, but any anonymous client could turn one header
into a stack trace. This is not specific to embedded charts: the same path
serves embedded dashboards today.
2026-09-09 16:22:47 +05:30
amaannawab923 287698785f fix(embedded): say 'chart' in the embed modal when embedding a chart
The modal is shared with the dashboard flow and its copy was hardcoded to
'dashboard', so a chart embed read 'This dashboard is ready to embed'.
2026-09-09 16:22:47 +05:30
amaannawab923 e36787ea80 feat(embedded): complete the standalone chart render path
Two gaps surfaced once a chart was actually rendered on its own:

- The nativeFilters and dashboardLayout reducers dereference their slice
  unconditionally on HYDRATE_DASHBOARD, so the fabricated state has to carry
  both even though a lone chart has no layout tree and no native filters.

- raise_for_access gates guest datasource access entirely on a dashboardId in
  the form data, which a standalone chart never has. Added a chart leg that
  authorizes the datasource when the guest token was issued for that chart and
  the request targets the chart's own datasource.
2026-09-09 16:22:47 +05:30
amaannawab923 5e5e37e521 feat(embedded): allow guest tokens scoped to a chart
Adds a CHART guest token resource type, validates it against the embedded
chart uuid, and lets a directly-embedded chart satisfy the guest branch of
raise_for_access without belonging to a dashboard.
2026-09-09 16:22:47 +05:30
amaannawab923 76a87ee241 fix(embedded): correct import paths for the 6.x module layout
t moved to @apache-superset/core/translation, css/styled to
@apache-superset/core/theme, and ErrorBoundary is a named export.
2026-09-09 16:22:47 +05:30
amaannawab923 6cfef05967 feat(embedded): expose Embed chart from the chart menu
Generalises the embed modal with an optional resourceType (defaulting to
dashboard, so existing call sites are unchanged) and adds an Embed chart
item to the chart header menu, gated on EMBEDDED_SUPERSET and the
can_set_embedded permission on Chart.
2026-09-09 16:22:47 +05:30
amaannawab923 1d6768523a feat(embedded): render an embedded chart via fabricated dashboard state
Builds the minimum slice of dashboard state a single chart needs and renders
the existing dashboard Chart component against it, so cross-filtering, drill
and the header controls work without reimplementation.

Reuses HYDRATE_DASHBOARD rather than adding a parallel action, so no
dashboard reducer changes are required; datasources is populated through its
own setDatasources action.
2026-09-09 16:22:47 +05:30
amaannawab923 b62e0e5d2d feat(embedded): resolve a uuid as either an embedded dashboard or chart
The embed view now falls back to EmbeddedChartDAO and passes resource_type
plus chart_id into the bootstrap payload.
2026-09-09 16:22:47 +05:30
amaannawab923 0775f0dacb feat(embedded): add EmbeddedChartDAO, schemas and chart embedded endpoints
Adds GET/POST/PUT/DELETE on /api/v1/chart/<pk>/embedded, mirroring the
dashboard embedded endpoints, with a set_embedded permission for writes.
2026-09-09 16:22:47 +05:30
amaannawab923 7a230c0090 feat(embedded): add EmbeddedChart model and migration
Mirrors EmbeddedDashboard so both embeddable resource types share the same
guest-token and allowed-domain semantics, including
guest_token_revoked_before.
2026-09-09 16:22:47 +05:30
9079f002ee fix(dashboard): scope tab-diff report lookup to the edited dashboard (#43989)
Co-authored-by: sha174n <pedro.sousa@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 11:03:32 +02:00
Enzo MartellucciandClaude Sonnet 5 1ab6077e06 fix(filters): clamp filter bar row while remeasuring after items change (#43966)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 10:52:28 +02:00
Sepuri Sai KrishnaandEvan Rusackas c76e4d98f2 fix(api): advertise only the time grains Prophet can resolve (#43710)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
2026-09-08 22:39:15 -07:00
fc110d8428 fix(versioning): enforce is_managed_externally on version restore (#44013)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 21:55:08 -07:00
KunalandJoe Li 4f26627a6a fix(native-filters): stop Date Range calendar popup from clipping in overflow dropdown (#43978)
Co-authored-by: Joe Li <joe@preset.io>
2026-09-08 21:52:33 -07:00
KunalandClaude Sonnet 5 613871ed36 fix(native-filters): stabilize DefaultValue's filterState object identity (#43859)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 21:51:20 -07:00
Francesco.CastaldiandFrancescoCastaldi c503f92642 fix(charts): restrict Prophet time grain schema validation to supported Prophet grains (#43585)
Co-authored-by: FrancescoCastaldi <francesco.castaldi@mapsgroup.it>
2026-09-08 21:43:33 -07:00
nacretion 50c1504cac fix(db_engine_specs): cast VARCHAR before Postgres DATE_TRUNC (#43167) 2026-09-08 21:38:37 -07:00
ShaitanandClaude Opus 4.8 5d771e9f46 fix(dataset): apply the import overwrite permission check to identity-matched datasets (#43058)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 21:37:14 -07:00
Grégoire Gailly c9965c7bf4 feat(folders): drag and drop visible elements (columns/metrics) of folders in explore view (#42483) 2026-09-08 21:36:55 -07:00
Đỗ Trọng Hải 9669e41471 chore(build): remove legacy re-export shim used for development (#42190)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-09-08 21:34:40 -07:00
Evan RusackasandClaude Sonnet 5 daf41bdf04 fix(security): suppress zizmor self-repository false positive for sqlite install-deps step (#44002)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 18:25:00 -07:00
986b648f1c fix(versioning): resolve dashboard M2M membership by attach/detach windows, not end_transaction_id (#44010)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 17:59:04 -07:00
dependabot[bot]andClaude Code acb29937ae chore(deps): bump the security group across 1 directory with 3 updates (#44030)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-08 17:52:32 -07:00
dependabot[bot]andClaude Code 2875c76d55 chore(deps): bump react-ace from 14.0.1 to 15.0.0 in /superset-frontend (#42833)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-08 17:52:09 -07:00
d0f139c210 feat(deps): bump flask-caching from 2.4.1 to 2.5.0 + bump flask from v2 to v3 (#43875)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
2026-09-08 17:20:53 -07:00
dependabot[bot] 9de50b85e5 chore(deps): bump the security group across 1 directory with 3 updates (#44029)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 17:04:39 -07:00
Joe LiandVitor Avila c510946df5 fix: BigQuery pivot-table Custom SQL row label with a space drops from GROUPING SETS (#43720)
Co-authored-by: Vitor Avila <vitor.avila@preset.io>
2026-09-08 16:58:49 -07:00
Joe LiandClaude Sonnet 5 91f4d01ca2 fix(chart-creation): stop antd v6 Steps from doubling the icon-content gap (#43730)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:57:55 -07:00
Evan Rusackas 3828dcb922 fix(security): suppress zizmor self-repository false positive for cached-dependencies in presto-hive workflow (#43976) 2026-09-08 16:53:30 -07:00
Evan Rusackas 362d3509cb fix(security): suppress zizmor self-repository false positive for cached-dependencies in presto-hive workflow (#43984) 2026-09-08 16:53:23 -07:00
Evan RusackasandClaude Sonnet 5 fa31e9c1c3 fix(security): suppress zizmor self-repository false positive for cached-dependencies (#43972)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:52:42 -07:00
Evan RusackasandClaude Sonnet 5 335a2a78ea fix(dashboard): expand clipped scrollable content before screenshot/PDF export (#43979)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:46:34 -07:00
Evan RusackasandClaude Sonnet 5 3564baabf6 fix(security): suppress zizmor self-repository false positive for setup-backend step (#44018)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:41:46 -07:00
Evan RusackasandClaude 380675662d fix(ci): use self-repository syntax for local actions in presto-hive workflow (#43975)
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-08 16:33:28 -07:00
Evan Rusackas 0e6170aaa3 fix(security): use self-repository syntax for setup-backend ref in unit-test workflow (#43970) 2026-09-08 16:31:39 -07:00
Evan Rusackas 9d89dbddc7 fix(security): use self-repository syntax for change-detector action ref (#43971) 2026-09-08 16:31:26 -07:00
Evan Rusackas 02ea5775c1 fix(ci): use self-repository syntax for local supersetbot action ref (#43963) 2026-09-08 16:30:33 -07:00
Evan RusackasandClaude Sonnet 5 83d5d1ccda fix(security): use GitHub self-repository syntax for local action ref (#43961)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 16:30:08 -07:00
Sam Firkeandrusackas 7b65d22205 chore(tags): TAGGING_SYSTEM to True by default (#39888)
Co-authored-by: rusackas <evan@rusackas.com>
2026-09-08 16:27:37 -07:00
Evan RusackasandClaude Code 1339bdc6ce ci: bump ASF allowlist-check to v1.0.1 for self-repository syntax support (#44014)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-08 15:49:12 -07:00
Evan RusackasandClaude 10866337c5 docs(helm): document Gateway API HTTPRoute support (#43635)
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-08 15:33:45 -07:00
Amin Ghadersohi e6d088d6b1 fix(mcp): surface rejected filter columns in get_chart_sql (#43680) 2026-09-08 18:05:37 -04:00
Elizabeth ThompsonandClaude Opus 4.6 5986fe92c2 fix(rls): roll back db.session after RLS failure in get_from_clause (#43883)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-08 15:04:32 -07:00
dependabot[bot] 04ab089962 chore(deps): bump @swc/html from 1.15.43 to 1.16.2 in /docs in the security group across 1 directory (#44023)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 14:29:15 -07:00
rlei bc35151350 docs(handlebars): correct helper names in the 6.1.0 versioned docs (#44024) 2026-09-08 16:40:44 -04:00
7029c468c4 fix(reports): bound CSV transport retries and sanitize failures (#43977)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-09-08 16:37:25 -04:00
Evan RusackasandClaude 60553b4326 docs(ag-grid): document Show summary support for Raw Records mode (#43905)
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-08 13:30:07 -07:00
Evan RusackasandClaude Sonnet 5 85ea6e900a docs(mcp): document MCP_STATELESS_HTTP and its multi-pod tradeoff (#43902)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 13:29:51 -07:00
a114b4cc46 fix(import): skip redundant has_table check when dataset import has no data URI (#43808)
Co-authored-by: Kunal8954 <your-github-email@example.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:27:40 -07:00
Evan RusackasandClaude Fable 5.1 3ccab2ea7a fix(time-format): share DateWithFormatter and stop reading four-digit years as epoch offsets (#43909)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 13:24:43 -07:00
Evan RusackasandClaude Opus 4.8 067a356b6f fix(chart): include echart_options in Matrixify render-trigger allowlist (#43946)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 13:24:13 -07:00
rlei 57eafd75e0 docs(handlebars): correct helper names that are not registered (#44020) 2026-09-08 16:10:40 -04:00
c45fc6a31a feat(embedded): diagnose guest-token header size budgets (#43944)
Co-authored-by: Matt Fitzgerald <matt.fitzgerald@preset.io>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
Co-authored-by: Joe Li <joe@preset.io>
2026-09-08 14:55:04 -04:00
33c6df77c9 fix(database): don't reattach stored connection details when the effective destination changes (#44003)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shaitan <105581038+sha174n@users.noreply.github.com>
2026-09-08 11:02:42 -07:00
Rafael BenitezandClaude Opus 4.8 204ccc7ce7 fix(datasets): apply Schema filter on the combined datasource list (#43829)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-08 14:24:27 -03:00
Chandan P 08694d64cb fix(plugin-chart-table): declare DataTable hooks before the early return (#43150) 2026-09-08 10:13:17 -07:00
bouhalimedaziz 056fd218d4 fix: handle missing table element in downloadAsPivotExcel (#39386) 2026-09-08 10:11:03 -07:00
Evan RusackasandClaude Code ad319bd8a3 fix(docs): unbreak Netlify docs preview build (#44001)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-08 10:06:17 -07:00
Joe LiandClaude Sonnet 5 7382ee0bfe fix(dashboard): prevent header metadata bar from overlapping action buttons at narrow widths (#43845)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-08 09:34:09 -07:00
Rutger Meuzelaar 7d0cf1acfc docs(scarf-404): replace dead links by the Scarf homepage (#43973) 2026-09-08 21:24:25 +07:00
Enzo Martellucci c4e145b071 fix(sql_lab): raise 400 not 500 on malformed Jinja during CSV export access check (#43866) 2026-09-08 12:27:29 +02:00
dependabot[bot] cdb6c8e0c9 chore(deps-dev): bump the typescript-eslint group in /superset-frontend with 2 updates (#43980)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 01:48:13 -07:00
dependabot[bot] 89786160f9 chore(deps): bump @ant-design/icons from 6.3.3 to 6.3.4 in /docs (#43981)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 01:48:07 -07:00
dependabot[bot] bd3ab30907 chore(deps): bump @ant-design/icons from 6.3.3 to 6.3.4 in /superset-frontend (#43982)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 01:48:03 -07:00
Evan RusackasandClaude Fable 5.1 1b3758d2a4 ci: add ASF GitHub Actions allowlist check (#43844)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 22:04:10 -07:00
Elizabeth Thompson 1fb6b98941 fix: downgrade md5 namespace-fallback deprecation log to info (#43884) 2026-09-07 15:03:54 -07:00
Elizabeth ThompsonandClaude Opus 4.8 8d6044a147 fix(sqllab): wrap cost-estimate JSON parse to prevent raw JSONDecodeError leak (#43888)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-07 15:03:50 -07:00
Evan Rusackas 574d121cd8 fix(security): use self-repository uses syntax in supersetbot workflow (#43962) 2026-09-07 12:27:17 -07:00
Evan RusackasandClaude d22069a7af fix(security): use GitHub self-repository syntax for local action refs (#43969)
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-07 12:26:41 -07:00
Evan RusackasandClaude Sonnet 5 233f6379ae fix(ci): use self-repository syntax for supersetbot setup in tag-release (#43965)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:26:22 -07:00
Evan RusackasandClaude Sonnet 5 4440887dc2 fix(security): use self-repository syntax for setup-backend action ref (#43968)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:25:42 -07:00
GeidōandClaude Opus 5 53070fa4c0 fix(dashboard): resolve metric Label in Chart Data (View as table) results grid (#43719)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 15:13:03 +03:00
92bd2ba057 chore(deps): bump @ant-design/icons from 6.3.2 to 6.3.3 in /superset-frontend (#43955)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: rusackas <evan@rusackas.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-07 17:34:47 +07:00
dependabot[bot] 4eff13b81e chore(deps-dev): bump webpack from 5.110.0 to 5.110.2 in /superset-embedded-sdk (#43953)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 17:34:11 +07:00
dependabot[bot] 06913244de chore(deps-dev): bump webpack from 5.110.1 to 5.110.2 in /docs (#43952)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:38:21 -07:00
dependabot[bot] a5377541c9 chore(deps): bump zizmorcore/zizmor-action from 0.6.2 to 0.6.3 (#43954)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:38:16 -07:00
dependabot[bot] e5e6d74819 chore(deps-dev): bump webpack from 5.110.1 to 5.110.2 in /superset-frontend (#43956)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:38:12 -07:00
dependabot[bot] b8fb5a268c chore(deps): bump zod from 4.4.3 to 4.5.4 in /superset-frontend (#43957)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:38:08 -07:00
dependabot[bot] 07052752b6 chore(deps-dev): bump tsx from 4.23.12 to 4.23.13 in /superset-frontend (#43958)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:38:05 -07:00
dependabot[bot] 7c9c32651c chore(deps): bump react-error-boundary from 6.1.3 to 6.1.4 in /superset-frontend (#43959)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:38:01 -07:00
dependabot[bot] 0db9ca7401 chore(deps): bump @ant-design/icons from 6.3.2 to 6.3.3 in /docs (#43951)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 02:36:03 -07:00
Evan RusackasandClaude Code 7d09490bb8 fix(security): grant SQL Lab query authors an explore access bypass (#39296) (#42590)
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-06 23:18:34 -07:00
Evan RusackasandClaude Sonnet 5 df3abe474c docs(theming): document new component sizing tokens and HIDE_NAVBAR_LOGO (#43639)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 23:17:12 -07:00
Evan RusackasandClaude 87f138cc80 docs(mcp): add theme management tools to AI/MCP tools reference (#43899)
Co-authored-by: Claude <noreply@anthropic.com>
2026-09-06 18:03:15 -07:00
c03f3441bd feat(mcp): gauge chart type plugin (#43568)
Co-authored-by: Greg Neighbors <gregneighbors@Gregs-Air-2.lan>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-06 12:55:35 -04:00
f3896ccf20 fix(explore): say when filter-value suggestions could not be loaded (#43779)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-09-06 10:26:48 -04:00
Joe LiandClaude Sonnet 5 18d277c096 ci(playwright): make embedded and app-root E2E coverage required (#43919)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 10:17:59 +07:00
fe1b368bcd feat: add dashed line support for derived series (timeseries comparison) on MixedTimeseries chart (#34794)
Co-authored-by: Evan <evan@preset.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-05 14:17:10 -07:00
Nguyen Van VietandClaude Fable 5 1358543827 fix(sqllab): preserve ClickHouse LIMIT BY when applying the row limit (#43578)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-05 09:51:00 -07:00
dependabot[bot]andgithub-actions[bot] 1315f5ff6b chore(deps): bump pydantic from 2.13.4 to 2.13.5 (#43924)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-05 07:40:30 -07:00
dependabot[bot]andgithub-actions[bot] 974f36f94f chore(deps-dev): bump google-cloud-bigquery from 3.43.0 to 3.45.0 (#43926)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-05 07:36:47 -07:00
dependabot[bot]andgithub-actions[bot] 9e9a3d06ad chore(deps-dev): bump grpcio from 1.83.0 to 1.83.1 (#43922)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-05 07:36:18 -07:00
dependabot[bot]andgithub-actions[bot] 5c4c8b831f chore(deps): bump cryptography from 50.0.0 to 50.0.1 (#43923)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-05 07:36:14 -07:00
dependabot[bot] 1e76fb6098 chore(deps-dev): update teradatasql requirement from >=20.0.0.66 to >=20.0.0.67 (#43925)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-05 07:36:11 -07:00
364 changed files with 23411 additions and 4043 deletions
@@ -1,23 +0,0 @@
name: Label Draft PRs
on:
pull_request:
types:
- opened
- converted_to_draft
jobs:
label-draft:
runs-on: ubuntu-latest
steps:
- name: Check if the PR is a draft
id: check-draft
uses: actions/github-script@v8
with:
script: |
const isDraft = context.payload.pull_request.draft;
core.setOutput('isDraft', isDraft);
- name: Add `review:draft` Label
if: steps.check-draft.outputs.isDraft == 'true'
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: "review:draft"
+3 -3
View File
@@ -26,7 +26,7 @@ runs:
- name: Set up QEMU
if: ${{ inputs.build == 'true' }}
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
with:
# Pin the binfmt image to a specific QEMU release. The default
# (`tonistiigi/binfmt:latest`) is a moving target, and drift across
@@ -39,12 +39,12 @@ runs:
- name: Set up Docker Buildx
if: ${{ inputs.build == 'true' }}
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Try to login to DockerHub
if: ${{ inputs.login-to-dockerhub == 'true' }}
continue-on-error: true
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ inputs.dockerhub-user }}
password: ${{ inputs.dockerhub-token }}
+46
View File
@@ -0,0 +1,46 @@
# Verifies that every `uses:` ref under .github/ is on the ASF Infrastructure
# GitHub Actions allowlist (apache/infrastructure-actions). An action that is
# not allowlisted fails at "Set up job" with no logs and no notification, so
# this check surfaces the problem at PR time instead. It also warns (without
# failing) when a pinned SHA's allowlist entry is about to expire.
name: ASF Allowlist Check
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- ".github/**"
push:
branches:
- "master"
- "[0-9].[0-9]*"
paths:
- ".github/**"
schedule:
# Weekly, so allowlist expirations are surfaced even when nothing under
# .github/ has changed.
- cron: "0 6 * * 1"
permissions:
contents: read
# cancel previous workflow jobs for PRs
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: true
jobs:
asf-allowlist-check:
runs-on: ubuntu-26.04
steps:
- name: Checkout Repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check action refs against the ASF allowlist
uses: apache/infrastructure-actions/allowlist-check@df54e48ff76152790f317934c691cfa7fd7a1a46 # allowlist-check/v1.0.1
with:
# Default scan-glob is .github/**/*.yml, which misses .yaml files.
scan-glob: ".github/**/*.y*ml"
@@ -49,4 +49,4 @@ jobs:
run: bash .github/workflows/scripts/check-docs-deploy-freshness.test.sh
- name: Check for security issues on GHA workflows
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3
+24 -7
View File
@@ -168,7 +168,10 @@ jobs:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-26.04
timeout-minutes: 30
# Embedded Tests below adds its own gunicorn boot + spec run on top of
# Required and Soft-delete; 30m was tight even for the two-step shadow
# job this replaced.
timeout-minutes: 40
permissions:
contents: read
pull-requests: read
@@ -176,7 +179,10 @@ jobs:
fail-fast: false
matrix:
browser: ["chromium"]
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
# Subdirectory deployment (APPLICATION_ROOT) is a required-to-pass
# dimension, not an optional one, so it runs on every event —
# unlike cypress-matrix above, which only widens on push.
app_root: ["", "/app/prefix"]
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
@@ -266,14 +272,25 @@ jobs:
# Scoped to this step: each playwright-run boots its own gunicorn
# with the step's env, so the Required Tests server above keeps
# master's Flask configuration while this one runs with SOFT_DELETE
# on — the same isolation pattern as the Embedded step in
# superset-playwright.yml. Without a flag-on server the
# recently-archived specs skip themselves everywhere and ship zero
# executed E2E coverage; in the Required run above they are
# collected and skipped, which is expected.
# on — the same isolation pattern as the Embedded Tests step below.
# Without a flag-on server the recently-archived specs skip
# themselves everywhere and ship zero executed E2E coverage; in the
# Required run above they are collected and skipped, which is
# expected.
SUPERSET_FEATURE_SOFT_DELETE: "true"
with:
run: playwright-run "${{ matrix.app_root }}" recently-archived/
- name: Run Playwright (Embedded Tests)
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"
# Scoped to this step for the same reason as Soft-delete above:
# embedding is a real, required feature, so its Playwright coverage
# now gates merges instead of running only in shadow mode.
SUPERSET_FEATURE_EMBEDDED_SUPERSET: "true"
INCLUDE_EMBEDDED: "true"
with:
run: playwright-run "${{ matrix.app_root }}" embedded
- name: Set safe app root
if: failure()
id: set-safe-app-root
@@ -49,7 +49,7 @@ jobs:
# allowlist (only v8.1.0+ are, at apache/infrastructure-actions'
# actions.yml). Needs an INFRA request before this can de-vendor too.
- name: Set up chart-testing
uses: ./.github/actions/chart-testing-action
uses: $/.github/actions/chart-testing-action
- name: Run chart-testing (list-changed)
id: list-changed
+50 -31
View File
@@ -42,12 +42,14 @@ jobs:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
uses: $/.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
# NOTE: Required Playwright tests are in superset-e2e.yml (E2E / playwright-tests)
# This workflow contains only experimental tests that run in shadow mode
# NOTE: Required Playwright tests are in superset-e2e.yml (E2E / playwright-tests),
# including Embedded — it moved out of this workflow because embedding is a
# required feature, not an experimental one. This workflow now contains
# only experimental and mobile tests, which run in shadow mode.
playwright-tests-experimental:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
@@ -107,13 +109,25 @@ jobs:
submodules: recursive
# -------------------------------------------------------
- name: Setup Python
uses: ./.github/actions/setup-backend/
uses: $/.github/actions/setup-backend/
- name: Setup postgres
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: setup-postgres
- name: Import test data
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: playwright_testdata
- name: Setup Node.js
@@ -123,48 +137,53 @@ jobs:
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install npm dependencies
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: npm-install
- name: Build javascript packages
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: build-instrumented-assets
- name: Build embedded SDK
uses: ./.github/actions/cached-dependencies
with:
run: build-embedded-sdk
- name: Install Playwright
uses: ./.github/actions/cached-dependencies
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's gitlink. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: playwright-install
- name: Run Playwright (Experimental Tests)
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
env:
NODE_OPTIONS: "--max-old-space-size=4096"
with:
run: playwright-run "${{ matrix.app_root }}" experimental/
- name: Run Playwright (Embedded Tests)
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"
# Scope embedded-only env vars to this step. Setting them at the job
# level enabled the EMBEDDED_SUPERSET feature flag inside Flask for
# the preceding "Required Tests" and "Experimental Tests" steps too,
# which loads extra handlers and destabilizes the werkzeug dev
# server under the 2-worker Playwright load. Required Tests should
# match master's Flask configuration.
SUPERSET_FEATURE_EMBEDDED_SUPERSET: "true"
INCLUDE_EMBEDDED: "true"
with:
run: playwright-run "${{ matrix.app_root }}" embedded
- name: Run Playwright (Mobile Tests)
uses: ./.github/actions/cached-dependencies
env:
NODE_OPTIONS: "--max-old-space-size=4096"
# Scoped to this step for the same reason as the embedded flags
# above: the mobile consumption mode should not alter Flask's
# configuration for the required desktop test steps.
# Scoped to this step: setting feature flags at the job level would
# alter Flask's configuration for the preceding Experimental step
# too — the mobile consumption mode should not do that.
SUPERSET_FEATURE_MOBILE_CONSUMPTION_MODE: "true"
INCLUDE_MOBILE: "true"
with:
@@ -34,7 +34,7 @@ jobs:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
uses: $/.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -75,9 +75,15 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Python
uses: ./.github/actions/setup-backend/
uses: $/.github/actions/setup-backend/
- name: Setup MySQL
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a git submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's gitlink. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: setup-mysql
- name: Start Celery worker
@@ -210,9 +216,15 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Python
uses: ./.github/actions/setup-backend/
uses: $/.github/actions/setup-backend/
- name: Install dependencies
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a git submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's gitlink. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: |
# sqlite needs this working directory
@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
uses: $/.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -80,9 +80,15 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Python
uses: ./.github/actions/setup-backend/
uses: $/.github/actions/setup-backend/
- name: Setup Postgres
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: setup-postgres
- name: Start Celery worker
@@ -141,13 +147,19 @@ jobs:
- name: Start hadoop and hive
run: docker compose -f scripts/databases/hive/docker-compose.yml up -d
- name: Setup Python
uses: ./.github/actions/setup-backend/
uses: $/.github/actions/setup-backend/
- name: Setup Postgres
uses: ./.github/actions/cached-dependencies
with:
run: setup-postgres
- name: Start Celery worker
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: celery-worker
- name: Python unit tests (PostgreSQL)
@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
uses: $/.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -60,7 +60,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Python
uses: ./.github/actions/setup-backend/
uses: $/.github/actions/setup-backend/
with:
python-version: ${{ matrix.python-version }}
- name: Python unit tests
+10 -4
View File
@@ -32,7 +32,7 @@ jobs:
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
uses: $/.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
@@ -45,7 +45,13 @@ jobs:
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install dependencies
if: steps.check.outputs.frontend
uses: ./.github/actions/cached-dependencies
# cached-dependencies is a git submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's gitlink. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: npm-install
- name: lint
@@ -68,13 +74,13 @@ jobs:
- name: Check for file changes
id: check
uses: ./.github/actions/change-detector/
uses: $/.github/actions/change-detector/
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Python
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
uses: ./.github/actions/setup-backend/
uses: $/.github/actions/setup-backend/
- name: Install gettext tools
if: steps.check.outputs.python == 'true' || steps.check.outputs.frontend == 'true'
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
persist-credentials: false
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
uses: $/.github/actions/setup-supersetbot/
- name: Execute custom Node.js script
env:
+3 -3
View File
@@ -66,7 +66,7 @@ jobs:
fetch-depth: 0
- name: Setup Docker Environment
uses: ./.github/actions/setup-docker
uses: $/.github/actions/setup-docker
with:
dockerhub-user: ${{ secrets.DOCKERHUB_USER }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -81,7 +81,7 @@ jobs:
package-manager-cache: false
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
uses: $/.github/actions/setup-supersetbot
- name: Execute custom Node.js script
env:
@@ -139,7 +139,7 @@ jobs:
package-manager-cache: false
- name: Setup supersetbot
uses: ./.github/actions/setup-supersetbot/
uses: $/.github/actions/setup-supersetbot/
- name: Label the PRs with the right release-related labels
env:
+1 -1
View File
@@ -178,7 +178,7 @@ repos:
name: zizmor (GHA security audit)
entry: zizmor
language: python
additional_dependencies: [zizmor==1.25.2]
additional_dependencies: [zizmor==1.30.0]
files: ^\.github/
types: [yaml]
pass_filenames: false
+56 -25
View File
@@ -231,20 +231,6 @@ RUN /app/docker/apt-install.sh \
# The database file will be created at runtime when examples are loaded from Parquet files
RUN mkdir -p /app/data && chown -R superset:superset /app/data
# Copy compiled things from previous stages
COPY --from=superset-node /app/superset/static/assets superset/static/assets
# Copy service.worker.js optionall as it doesn't exist when DEV_MODE=true
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
# TODO, when the next version comes out, use --exclude superset/translations
COPY superset superset
# TODO in the meantime, remove the .po files
RUN rm superset/translations/*/*/*.po
# Merging translations from backend and frontend stages
COPY --from=superset-node /app/superset/translations superset/translations
COPY --from=python-translation-compiler /app/translations_mo superset/translations
# --- Realtime WebSocket server (part of the official image) ---------------
# The realtime transport (superset-websocket) is a Node service, bundled by
# esbuild into a single self-contained file. Copy the Node runtime plus that
@@ -267,7 +253,10 @@ EXPOSE ${SUPERSET_PORT}
######################################################################
FROM python-common AS lean
# Install Python dependencies using docker/pip-install.sh
# Install Python dependencies using docker/pip-install.sh.
# Requirements are installed *before* the application source is copied
# below so that source-only changes don't bust this (slow, network-bound)
# cache layer or defeat --cache-from.
COPY requirements/base.txt requirements/
# Copy superset-core package needed for editable install in base.txt
@@ -275,9 +264,27 @@ COPY superset-core superset-core
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
/app/docker/pip-install.sh --requires-build-essential -r requirements/base.txt
# Install the superset package
# Copy compiled frontend assets and application source now that
# dependencies have been resolved and cached above.
COPY --from=superset-node /app/superset/static/assets superset/static/assets
# Copy service.worker.js optionally as it doesn't exist when DEV_MODE=true
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
# TODO, when the next version comes out, use --exclude superset/translations
COPY superset superset
# TODO in the meantime, remove the .po files
RUN rm superset/translations/*/*/*.po
# Merging translations from backend and frontend stages
COPY --from=superset-node /app/superset/translations superset/translations
COPY --from=python-translation-compiler /app/translations_mo superset/translations
# Install the superset package itself. --no-deps because its dependencies
# were already installed from requirements/base.txt above, so this layer
# stays fast even though the source copy above changes on every edit.
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
uv pip install -e .
uv pip install -e . --no-deps
RUN python -m compileall /app/superset
USER superset
@@ -293,22 +300,46 @@ RUN /app/docker/apt-install.sh \
pkg-config \
default-libmysqlclient-dev
# Copy development requirements and install them
# Copy development requirements and install them *before* the application
# source is copied below, so source-only edits don't bust this cache layer.
COPY requirements/*.txt requirements/
# Copy local packages needed for editable installs in development.txt
COPY superset-core superset-core
COPY superset-extensions-cli superset-extensions-cli
# Install Python dependencies using docker/pip-install.sh
# requirements/development.txt is generated by `uv pip compile` and embeds
# `-e .` (an editable install of this same package) as its first line. That
# self-reference needs the full superset/ source tree, which hasn't been
# copied in yet at this point, so it's stripped here; the real editable
# install of `.` runs below, once the source is present.
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
/app/docker/pip-install.sh --requires-build-essential -r requirements/development.txt
# Install the superset package
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
uv pip install -e .
grep -vxF -- "-e ." requirements/development.txt > requirements/development-deps.txt && \
/app/docker/pip-install.sh --requires-build-essential -r requirements/development-deps.txt
RUN uv pip install .[postgres]
RUN python -m compileall /app/superset
# Copy compiled frontend assets and application source now that
# dependencies have been resolved and cached above.
COPY --from=superset-node /app/superset/static/assets superset/static/assets
# Copy service.worker.js optionally as it doesn't exist when DEV_MODE=true
COPY --from=superset-node /app/superset/static/service-worker.j[s] superset/static/service-worker.js
# TODO, when the next version comes out, use --exclude superset/translations
COPY superset superset
# TODO in the meantime, remove the .po files
RUN rm superset/translations/*/*/*.po
# Merging translations from backend and frontend stages
COPY --from=superset-node /app/superset/translations superset/translations
COPY --from=python-translation-compiler /app/translations_mo superset/translations
# Install the superset package together with its postgres extra, using the
# same uv cache mount as the requirements install above. --no-deps because
# all dependencies (including the postgres extra's psycopg2-binary) are
# already installed from requirements/development.txt above.
# NOTE: source is bind-mounted over /app/superset in DEV_MODE, so a
# compileall pass here would be wasted work; unlike `lean`, `dev` skips it.
RUN --mount=type=cache,target=${SUPERSET_HOME}/.cache/uv \
uv pip install -e .[postgres] --no-deps
USER superset
+31
View File
@@ -24,6 +24,34 @@ assists people when migrating to a new version.
## Next
### Tagging is on by default
`TAGGING_SYSTEM` now ships **on**. The Tags menu entry, the tag columns and
filters on the chart, dashboard and saved-query lists, and the Tags field in the
chart and dashboard property modals are all visible without configuration, and
tags are included in asset export and import.
**What operators should expect:**
- **Implicit tags accrue.** Saving a chart, dashboard, dataset or saved query,
and favoriting an asset, write rows to `tag` and `tagged_object` (`type:chart`,
`editor:<user id>`, `favorited_by:<user id>`). These have always been created
when the flag was on; they are simply no longer opt-in.
- **Exports gain a `tags` key and a `tags.yaml` file.** Chart and dashboard
export bundles carry custom tags. Importers on 6.0 and later understand both;
older importers skip the unrecognized `tags.yaml` file but reject chart and
dashboard YAML that contains a `tags` key, so strip that key before importing
a bundle into Superset 5.x or earlier.
- **The flag is honored at write time.** The tagging SQLA event listeners are
always attached at startup; the ones that create tags check `TAGGING_SYSTEM`
when they fire, so the flag, including a runtime override through
`GET_FEATURE_FLAGS_FUNC` or `IS_FEATURE_ENABLED_FUNC`, takes effect without a
restart. The cleanup listeners run regardless of the flag, so deleting an
asset never leaves orphaned `tagged_object` rows behind.
Set `FEATURE_FLAGS = {"TAGGING_SYSTEM": False}` to restore the previous
behavior. Existing tag rows are left untouched.
### Global Async Queries re-platformed onto the Global Task Framework (breaking)
Global Async Queries (GAQ) no longer runs on its own bespoke async-events
@@ -202,6 +230,7 @@ before retrying. Preview or recheck failures fail closed rather than treating
unknown impact as zero. Chart and dashboard purge endpoints are unchanged.
- The dashboard datasource-based visibility fallback now fails closed: a dashboard whose member charts datasources cannot be resolved (deleted datasource rows, missing `datasource_id`, or unsupported datasource types) is no longer accessible to users without explicit editor/viewer rights, and a dashboard composed of semantic-view charts now requires `datasource_access` on (at least one of) its semantic views or their parent semantic layer — previously any authenticated user could open such a dashboards shell. Because the fallback now considers every member chart rather than only table-backed ones, a user holding `datasource_access` on any single member datasource — including a semantic view or its parent layer — can open a mixed dashboard that previously denied them. Dashboards with no charts remain accessible, and dashboards with explicit viewers are unaffected. Conversely, holders of `all_datasource_access` now see every published no-viewer dashboard in the dashboard list — including chart-less ones previously hidden by the inner joins — matching what the object-level gate already allowed them to open.
- Version restore (`POST /api/v1/{chart,dashboard,dataset}/<uuid>/versions/<version_uuid>/restore`) now refuses an **externally managed** entity (`is_managed_externally = True`) with HTTP 403, enforcing server-side what the docs already promised. Previously the refusal existed only in the browser, so an otherwise-authorized editor could restore such an entity by calling the endpoint directly and have the restore overwritten on the next external sync. Soft-delete recovery is deliberately unaffected — it changes visibility, not content.
- `SAMPLES_ROW_LIMIT` is now the default for `/datasource/samples` requests without a valid explicit `per_page`, rather than a hard per-request ceiling; explicit limits are honored up to the existing global row-limit ceiling, matching `/chart/data` SAMPLES requests.
- The `cockroachdb` extra (`pip install apache-superset[cockroachdb]`) now installs `sqlalchemy-cockroachdb` instead of the abandoned `cockroachdb` package, whose SQLAlchemy dialect could not be imported under SQLAlchemy 2.0. Existing environments with the old package installed should `pip uninstall cockroachdb && pip install sqlalchemy-cockroachdb` (or simply reinstall the extra) to restore CockroachDB connectivity.
@@ -1204,6 +1233,8 @@ Custom time ranges that use the "Now" or "Today" anchor (for the Start, End, or
Charts and dashboards using these anchors will compute a different (correct) timestamp after upgrading; if a chart's filters or drill-downs were tuned to compensate for the old offset, review them after upgrading.
- [43916](https://github.com/apache/superset/pull/43916): The `docker-compose` dev loop now skips re-running `superset load_examples` on every `docker compose up` once the example data and dashboards are present in the databases (set `SUPERSET_FORCE_LOAD_EXAMPLES=yes` to reload them anyway), and the `superset-node` service now defaults `DISABLE_TS_CHECKER=true` like `docker-compose-light.yml` already did, skipping webpack's TypeScript type-checking pass in dev by default.
## 6.1.0
### ClickHouse minimum driver version bump
+1
View File
@@ -138,6 +138,7 @@ services:
condition: service_started
volumes: *superset-volumes
environment:
SUPERSET_FORCE_LOAD_EXAMPLES: "${SUPERSET_FORCE_LOAD_EXAMPLES:-}"
DATABASE_HOST: db-light
DATABASE_DB: superset_light
POSTGRES_DB: superset_light
+3
View File
@@ -183,6 +183,8 @@ services:
condition: service_started
user: *superset-user
volumes: *superset-volumes
environment:
SUPERSET_FORCE_LOAD_EXAMPLES: "${SUPERSET_FORCE_LOAD_EXAMPLES:-}"
healthcheck:
disable: true
@@ -202,6 +204,7 @@ services:
BUILD_SUPERSET_FRONTEND_IN_DOCKER: true
NPM_RUN_PRUNE: false
SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}"
DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}"
# configuring the dev-server to use the host.docker.internal to connect to the backend
superset: "http://superset:8088"
# Webpack dev server must bind to 0.0.0.0 to be accessible from outside the container
+6
View File
@@ -73,6 +73,12 @@ SUPERSET_ENV=development
# Swagger UI is opt-in (off by default); enable it for local development.
SUPERSET_ENABLE_SWAGGER_UI=true
SUPERSET_LOAD_EXAMPLES=yes
# Once the example data and dashboards are present in the databases,
# `docker-init.sh` skips `superset load_examples` on later runs. Set to "yes"
# (or run `SUPERSET_FORCE_LOAD_EXAMPLES=yes docker compose up`) to reload the
# examples anyway, e.g. after changing the example datasets or after a partial
# load.
#SUPERSET_FORCE_LOAD_EXAMPLES=no
CYPRESS_CONFIG=false
SUPERSET_PORT=8088
MAPBOX_API_KEY=''
+33 -3
View File
@@ -66,14 +66,44 @@ echo_step "3" "Starting" "Setting up roles and perms"
superset init
echo_step "3" "Complete" "Setting up roles and perms"
# Loading examples parses and inserts every example dataset, chart and
# dashboard and is one of the slowest steps of `docker compose up`. Rather
# than trusting a marker file (which goes stale as soon as the database volume
# is recreated), ask the databases themselves: when both the example data and
# the dashboards imported from it are present, the previous load completed and
# there is nothing left to redo. Any failure here (missing tables, unreachable
# database, import error) simply reports "not loaded" so the full load runs.
examples_already_loaded() {
python - <<'PY' 2>/dev/null
import sys
from superset.app import create_app
from superset.sql.parse import Table
app = create_app()
with app.app_context():
from superset import db
from superset.models.dashboard import Dashboard
from superset.utils.database import get_example_database
has_dashboard = (
db.session.query(Dashboard).filter_by(slug="world_health").first() is not None
)
has_data = get_example_database().has_table(Table("wb_health_population"))
sys.exit(0 if has_dashboard and has_data else 1)
PY
}
if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then
# Load some data to play with
echo_step "4" "Starting" "Loading examples"
# If Cypress run which consumes superset_test_config load required data for tests
# Cypress runs always load, since they need a distinct set of test data
# (`--load-test-data`) in a separate database. Set
# SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
if [ "$CYPRESS_CONFIG" == "true" ]; then
superset load_examples --load-test-data
elif [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
echo "Examples already loaded, skipping (set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload them)"
else
superset load_examples
fi
@@ -476,6 +476,16 @@ to determine which hosts are internal.
There are many reasons that reports might not be working. Try these steps to check for specific issues.
### Blank PDF or PNG captures
Playwright report captures are checked for blank image content as well as chart
readiness. Captures detected as blank where chart content is expected are retried
up to three total attempts, within the execution deadline. If they remain blank,
the report fails instead of delivering the blank or partial attachment. This also
applies to alerts that attach reports. Check worker logs for
`ScreenshotBlankCaptureError` and `report_capture_validation` when investigating
these failures.
### Confirm feature flag is enabled and you have sufficient permissions
If you don't see "Alerts & Reports" under the _Manage_ section of the Settings dropdown in the Superset UI, you need to enable the `ALERT_REPORTS` feature flag (see above). Enable another feature flag and check to see that it took effect, to verify that your config file is getting loaded.
@@ -486,6 +496,39 @@ Log in as an admin user to ensure you have adequate permissions.
This is the best source of information about the problem. In a docker compose deployment, you can do this with a command like `docker logs superset_worker --since 1h`.
### CSV and Excel chart-data request failures
The worker uses the saved query context to POST to the chart-data export endpoint,
falling back to the legacy GET export when a query context cannot be generated.
`ALERT_REPORTS_CSV_REQUEST_TIMEOUT` (60 seconds by default) limits socket operations;
the report execution budget and its delivery/cleanup reserves also cap the request.
Connection and read timeouts are reported as CSV/Excel generation timeouts.
These attachment timeouts are logged at error level and explicitly mark the report
task as failed, while the report execution retains its ERROR state and separate
error-notification history. Other HTTP 408 exception handling is unchanged.
To tolerate short-lived transport failures, operators can opt in with
`ALERT_REPORTS_CSV_REQUEST_RETRY = True` (default: `False`). This permits **one** retry
for transient connection/read failures and HTTP 429, 500, 502, 503, or 504. Other
HTTP statuses are not retried. Backoff is 0.5 seconds, extended to at most 2 seconds
for a numeric `Retry-After`; longer, invalid, or date-based delays are not retried
inline. Both attempts and backoff share the initial request timeout allowance and
respect the remaining execution budget. Unbounded requests are not retried.
A request that consumes its entire timeout does **not** get another full timeout.
Socket timeouts are not wall-clock cancellation: existing report task limits still
interrupt in-flight work. A timed-out server query can continue running, so enabling
retries can increase database load. Leave retries disabled unless appropriate for
your deployment; disable the setting to roll back retry behavior.
Worker diagnostics include schedule/chart identifiers, a fixed endpoint path (no
query string), error category, HTTP status, timeout, elapsed duration, and attempt.
For HTTP errors, at most 4097 response bytes are read to enforce a 4096-byte limit.
Only recognized Superset error types from up to four JSON errors are retained;
free-form messages, extra fields, and non-JSON or oversized bodies are redacted or
omitted. Cookies, authentication headers, URLs, SQL, and query payloads are not
included in these transport diagnostics. HTTP 400 therefore remains a failure to
investigate, not a reason to repeat the same request.
### Check web browser and webdriver installation
To take a screenshot, the worker visits the dashboard or chart using a headless browser, then takes a screenshot. If you are able to send a chart as CSV, XLSX, or text but can't send as PNG, your problem may lie with the browser.
+8 -1
View File
@@ -540,6 +540,8 @@ MCP_STORE_CONFIG = {
When `CACHE_REDIS_URL` is set, the MCP server uses a Redis-backed EventStore for session management, allowing replicas to share state. Without Redis, each pod manages its own in-memory sessions and stateful MCP interactions may fail when requests hit different replicas.
`MCP_STATELESS_HTTP` (default `True`) controls whether requests get a fresh, ephemeral transport per HTTP round trip or a transport that stays alive for the session's lifetime. The default suits multi-pod deployments because it doesn't require session affinity -- any pod can handle any request. Its tradeoff: a client disconnecting mid-tool-call can crash not just its own session but other concurrent sessions on the same worker. Setting it to `False` avoids that, but it requires session-affinity (sticky session) routing on `Mcp-Session-Id` at the mesh/ingress layer, since a session's follow-up requests must land on the same pod that created it. See [`MCP_STATELESS_HTTP`](#core) below.
---
## Configuration Reference
@@ -555,6 +557,7 @@ All MCP settings go in `superset_config.py`. Defaults are defined in `superset/m
| `MCP_SERVICE_URL` | `None` | Public base URL for MCP-generated links (set this when behind a reverse proxy) |
| `MCP_DEBUG` | `False` | Enable debug logging |
| `MCP_DEV_USERNAME` | -- | Superset username for development mode (no auth) |
| `MCP_STATELESS_HTTP` | `True` | Streamable-HTTP session mode. `True` gives each request a fresh, ephemeral transport, torn down as soon as that request completes; a client disconnecting mid-tool-call can crash not just its own session but other concurrent sessions on the same worker. `False` keeps the transport alive for the session's lifetime, avoiding that crash, but requires session-affinity routing on `Mcp-Session-Id` for multi-pod deployments (see [Multi-Pod (Kubernetes)](#multi-pod-kubernetes)). |
| `MCP_RBAC_ENABLED` | `True` | Enforce Superset's role-based access control on MCP tool calls. When `True`, each tool checks that the authenticated user has the required FAB permission before executing. Disable only for testing or trusted-network deployments. |
| `MCP_DISABLED_TOOLS` | `set()` | Set of tool names to remove from the MCP server at startup. Disabled tools are never advertised to AI clients during tool discovery. Useful when a custom extension tool should replace a built-in Superset tool. See [Disabling built-in tools](#disabling-built-in-tools). |
| `MCP_DISABLED_CHART_PLUGINS` | `frozenset()` | Set of chart type plugin names (e.g. `"handlebars"`) to hide from `generate_chart`. Does not affect `get_chart_type_schema`. See [Disabling chart type plugins](#disabling-chart-type-plugins). |
@@ -951,7 +954,11 @@ Specifying a tool name that does not exist logs a warning at startup and is othe
## Disabling chart type plugins
The `generate_chart` tool dispatches per chart type (`xy`, `table`, `pie`, `pivot_table`, `mixed_timeseries`, `handlebars`, `big_number`, `histogram`, `box_plot`, `waterfall`) to a registered chart type plugin. Two settings let operators enable or disable individual chart type plugins at runtime, without a code deploy.
The `generate_chart` tool dispatches per chart type (`xy`, `table`, `pie`, `gauge`, `pivot_table`, `interactive_pivot`, `mixed_timeseries`, `handlebars`, `big_number`, `histogram`, `box_plot`, `waterfall`) to a registered chart type plugin. Gauge requests use the public MCP discriminator `chart_type: "gauge"`; Superset stores the corresponding native Explore visualization as `viz_type: "gauge_chart"`. Two settings let operators enable or disable individual chart type plugins at runtime, without a code deploy.
Gauge data inspection and JSON/CSV/XLSX exports preserve source groups, including NULL and non-finite metric values, using the same row-count and completeness semantics as other chart types. The default XLSX exporter writes non-finite floats as `nan`, `inf`, or `-inf` text, matching CSV and distinguishing them from blank NULL cells. Only previews and compile checks skip NULL, NaN, non-finite, and other nonnumeric dial values while preserving finite groups. In preview/compile paths, a nonempty result containing no finite dial returns an error; genuinely empty results retain the no-data behavior. Compile checks inspect the configured dial limit (at most 10), rather than only two groups. Grouped Vega previews center each needle within its own facet and extend it to the midpoint of the dial band. Automatic all-zero ranges span 01. Empty or whitespace-only temporal ranges, like `No filter`, are neutral sentinels; missing or non-string native temporal comparators are invalid.
Gauge previews retain data-derived automatic bounds even when configured interval thresholds extend beyond the visible range; Vega previews clip those thresholds into adjacent, non-overlapping bands. Explicit min/max bounds still constrain intervals. All-zero data with both bounds omitted uses a nondegenerate preview range of 01. On same-dataset Gauge updates, clearing or changing the dashboard temporal binding preserves user-authored temporal ranges; dataset rebinds discard old query roles and filters. The native `"No filter"` sentinel is a neutral dashboard temporal binding, not a conflicting restriction. Duplicate generated neutral bindings self-heal on updates without removing user-authored ranges. Restating the existing dataset ID without a config is allowed; changing the Gauge dataset still requires a complete config. Native restrictive temporal filters that conflict with the top-level range or another restrictive temporal filter are rejected rather than silently dropped. Unsupported native filter operators are rejected with an indexed error. Native `TEMPORAL_RANGE` adhoc filters require an explicit string comparator; `"No filter"`, empty strings, and whitespace represent an unrestricted range.
### Static deny-list
+124
View File
@@ -138,6 +138,18 @@ The existing `APP_NAME` Python config key continues to work for backward compati
Email and alert/report notification subjects are driven by backend settings such as
`EMAIL_REPORTS_SUBJECT_PREFIX` and `APP_NAME`, not by this theme token.
To hide the entire brand area in the navbar (both the logo image and the
brand text), set `HIDE_NAVBAR_LOGO` in `superset_config.py`:
```python
# Hide the entire brand area in the navbar, including the logo image and the
# brand text (brandAppName / APP_NAME). Defaults to False.
HIDE_NAVBAR_LOGO = True
```
`HIDE_NAVBAR_LOGO` is a Python config flag rather than a theme token, so it
cannot be set through the theme CRUD UI or `THEME_DEFAULT`/`THEME_DARK`.
### Migration from Configuration to UI
When `ENABLE_UI_THEME_ADMINISTRATION = True`:
@@ -487,6 +499,118 @@ THEME_DEFAULT = {
This feature provides powerful theming capabilities while maintaining the flexibility of ECharts' extensive configuration options.
## Component Sizing & Style Tokens
:::note
These tokens landed after the Superset 6.1 release and are only available on
`master`; they are not present in any tagged release yet.
:::
Beyond colors and fonts, a handful of Superset-specific tokens let you tune the
sizing, radius, and outline behavior of individual UI components. All of these
tokens are optional — omit them and components fall back to their existing
defaults, so applying them is a zero-visual-change operation until you opt in.
### Button & DropdownButton Sizing
```python
THEME_DEFAULT = {
"token": {
# ... other tokens
"buttonControlHeight": 32, # default button height, in px
"buttonControlHeightSM": 30, # small/dropdown button height, in px
"buttonControlHeightXS": 22, # xsmall button height, in px
"buttonPaddingInline": 18, # horizontal padding, in px
"buttonPaddingInlineSM": 10, # horizontal padding for small buttons, in px
"buttonFontSize": 14,
"buttonBorderRadius": 4,
}
}
```
`buttonControlHeight` and `buttonBorderRadius` also drive the sizing of the
menu-trigger button used by `PageHeaderWithActions`, so a single pair of tokens
keeps page-header icon buttons visually consistent with regular buttons.
For one-off overrides that shouldn't apply to every button in the app, pass a
`styleConfig` prop directly to `Button` or `DropdownButton` instead of setting
a theme token:
```tsx
<Button
styleConfig={{
controlHeight: 40,
paddingInline: 20,
fontSize: 16,
fontWeight: 700,
borderRadius: 8,
ctaMinWidth: 120,
ctaMinHeight: 40,
iconGap: 8,
}}
>
Click me
</Button>
<DropdownButton
styleConfig={{
controlHeight: 32,
fontSize: 14,
fontWeight: 500,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
}}
menu={menuProps}
>
Options
</DropdownButton>
```
`styleConfig` values take precedence over the equivalent theme tokens, which in
turn take precedence over the built-in defaults.
### Label Border Radius
```python
THEME_DEFAULT = {
"token": {
"labelBorderRadius": 4, # defaults to 8px
}
}
```
### Select Option Outline
By default, hovering or navigating to an option in a `Select` dropdown draws a
2px outline in `colorPrimary`. Set `selectOptionActiveOutline` to `False` for a
more subtle hover style with no outline:
```python
THEME_DEFAULT = {
"token": {
"selectOptionActiveOutline": False,
}
}
```
### Dashboard Tile Appearance
Chart tiles on a dashboard (not text/markdown tiles) can be restyled via
`dashboardTile*` tokens. All fall back to the existing look — a
`colorBgContainer` background, a `1px solid colorBorder` border, and a
hairline `box-shadow` while the tile is fading out (e.g. when a filter
makes it irrelevant):
```python
THEME_DEFAULT = {
"token": {
"dashboardTileBg": "#ffffff",
"dashboardTileBorder": "1px solid #e0e0e0",
"dashboardTileBorderRadius": 8,
"dashboardTileBoxShadow": "0 1px 2px rgba(0, 0, 0, 0.08)",
}
}
```
## Advanced Features
- **System Themes**: Manage system-wide default and dark themes via UI or configuration
@@ -196,7 +196,9 @@ One important variable is `SUPERSET_LOAD_EXAMPLES` which determines whether the
container will populate example data and visualizations into the metadata database. These examples
are helpful for learning and testing out Superset but unnecessary for experienced users and
production deployments. The loading process can sometimes take a few minutes and a good amount of
CPU, so you may want to disable it on a resource-constrained device.
CPU, so you may want to disable it on a resource-constrained device. Once the example data and
dashboards are present in the databases, later `superset_init` runs skip loading them; run
`SUPERSET_FORCE_LOAD_EXAMPLES=yes docker compose up` to reload the examples anyway.
For more advanced or dynamic configurations that are typically managed in a `superset_config.py` file
located in your `PYTHONPATH`, note that it can be done by providing a
@@ -215,7 +217,7 @@ If you have a good solution for this, let us know!
:::
:::note
Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry
Superset uses [Scarf Gateway](https://about.scarf.sh/) to collect telemetry
data. Knowing the installation counts for different Superset versions informs the project's
decisions about patching and long-term support. Scarf purges personally identifiable information
(PII) and provides only aggregated statistics.
+49 -1
View File
@@ -87,6 +87,7 @@ The chart will publish appropriate services to expose the Superset UI internally
- Configure the Service as a `LoadBalancer` or `NodePort`
- Set up an `Ingress` for it - the chart includes a definition, but will need to be tuned to your needs (hostname, tls, annotations etc...)
- Set up a Gateway API `HTTPRoute` for it - see [Exposing Superset via Gateway API (HTTPRoute)](#exposing-superset-via-gateway-api-httproute) below
- Run `kubectl port-forward superset-xxxx-yyyy :8088` to directly tunnel one pod's port into your localhost
Depending how you configured external access, the URL will vary. Once you've identified the appropriate URL you can log in with:
@@ -135,7 +136,7 @@ init:
```
:::note
Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
Superset uses [Scarf Gateway](https://about.scarf.sh/) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
There are two independent telemetry channels:
@@ -319,6 +320,53 @@ configOverrides:
AUTH_USER_REGISTRATION_ROLE = "Admin"
```
### Exposing Superset via Gateway API (HTTPRoute)
As an alternative to `Ingress`, the chart can create a [Gateway API](https://gateway-api.sigs.k8s.io/)
`HTTPRoute` that attaches to a Gateway already running in your cluster. This requires the Gateway
API CRDs serving the configured `httproute.apiVersion` (`gateway.networking.k8s.io/v1` by default)
to be installed, along with a Gateway resource for the route to attach to. If the Gateway lives in
a different namespace than the `HTTPRoute` (as in the
example below), its listener's `allowedRoutes` must explicitly permit routes from this release's
namespace, or the `HTTPRoute` will install successfully but never attach.
```yaml
httproute:
enabled: true
parentRefs:
- name: my-gateway
namespace: gateway-system
hostnames:
- superset.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
```
- `httproute.parentRefs` lists the Gateway(s) the route attaches to.
- `httproute.hostnames` matches against the HTTP `Host` header; it's templated, so values like
`{{ .Release.Name }}` can be used.
- `httproute.rules` are routing rules backed by the Superset service; each rule accepts standard
`matches`, `filters`, and `timeouts` fields, and an optional `weight` (defaults to `1`) applied to
its single backend reference. Since each rule maps to one backend, `weight` has no traffic-splitting
effect here; it only matters if you fork the template to add multiple `backendRefs` to a rule.
`timeouts` only joined the Gateway API Standard channel in v1.2, so it requires both v1.2+ CRDs
and a supporting controller; drop it if either predates that.
- If `supersetWebsockets.enabled` is set, an extra rule routing `supersetWebsockets.ingress.path`
(default `/ws`) to the `-ws` service is appended automatically, mirroring the `Ingress` behavior.
WebSocket upgrade support is controller-dependent under Gateway API; check your Gateway
implementation's docs in case it needs an explicit protocol opt-in for global async queries to
keep working behind a Gateway.
- If `supersetMcp.enabled` and `supersetMcp.httproute.enabled` are both set, an extra rule routing
`supersetMcp.httproute.path` to the `-mcp` service is appended as well. Don't expose this route
without first enabling MCP authentication — see the
[MCP Server Deployment & Authentication](/admin-docs/configuration/mcp-server#authentication) doc;
by default the MCP server runs in dev mode with auth disabled.
- Set `httproute.apiVersion` to `gateway.networking.k8s.io/v1beta1` if your cluster's Gateway API
installation hasn't promoted `HTTPRoute` to `v1` yet.
### Enable Alerts and Reports
For this, as per the [Alerts and Reports doc](/admin-docs/configuration/alerts-reports), you will need to:
@@ -215,7 +215,7 @@ If you have a good solution for this, let us know!
:::
:::note
Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry
Superset uses [Scarf Gateway](https://about.scarf.sh/) to collect telemetry
data. Knowing the installation counts for different Superset versions informs the project's
decisions about patching and long-term support. Scarf purges personally identifiable information
(PII) and provides only aggregated statistics.
@@ -135,7 +135,7 @@ init:
```
:::note
Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
Superset uses [Scarf Gateway](https://about.scarf.sh/) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
To opt-out of this data collection in your Helm-based installation, edit the `repository:` line in your `helm/superset/values.yaml` file, replacing `apachesuperset.docker.scarf.sh/apache/superset` with `apache/superset` to pull the image directly from Docker Hub.
:::
@@ -99,11 +99,16 @@ Affecting the Docker build process:
- **INCLUDE_CHROMIUM (default=false):** whether to include the Chromium headless browser in the build
- **BUILD_TRANSLATIONS(default=false):** whether to compile the translations from the .po files available
- **SUPERSET_LOAD_EXAMPLES (default=yes):** whether to load the examples into the database upon startup,
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`
save some precious time on startup by `SUPERSET_LOAD_EXAMPLES=no docker compose up`. Once the example
data and dashboards are present in the databases, later `docker compose up` runs skip loading
them; run `SUPERSET_FORCE_LOAD_EXAMPLES=yes docker compose up` to reload the examples anyway.
- **SUPERSET_LOG_LEVEL (default=info)**: Can be set to debug, info, warning, error, critical
for more verbose logging
- **SUPERSET_DEBUG_ENABLED (default=false)**: Enable Werkzeug debugger with interactive console.
Set to `true` for debugging: `SUPERSET_DEBUG_ENABLED=true docker compose up`
- **DISABLE_TS_CHECKER (default=true)**: whether the `superset-node` webpack dev server skips
TypeScript type-checking, which speeds up rebuilds and saves several GB of memory. Set to
`false` to have webpack surface type errors during development.
For more env vars that affect your configuration, see this
[superset_config.py](https://github.com/apache/superset/blob/master/docker/pythonpath_dev/superset_config.py)
@@ -379,6 +379,15 @@ AG Grid supports server-side column filters that query the full dataset — not
AG Grid Interactive Table supports **Time Shift** (time comparison), matching the behavior of the standard Table chart. In the **Advanced Analytics** → **Time Comparison** section of the chart configuration, enter a shift expression (e.g., `1 year ago`, `minus 7 days`) to add comparison columns showing values from the offset period. Dashboard-level time range overrides apply to both the base and comparison periods.
#### Show Summary
The **Show summary** checkbox lives at the top of the **Visual formatting** section in the **Customize** tab, for both **Aggregate** and **Raw Records** query modes. Enabling it pins a summary row to the bottom of the grid whenever there is something to summarize: at least one metric in **Aggregate** mode, or at least one eligible numeric column in **Raw Records** mode. Otherwise no summary row is added.
- In **Aggregate** mode, the summary row applies each metric's own aggregation (or the **Summary aggregation** override, where available) across the full filtered dataset.
- In **Raw Records** mode, the summary row defaults to a server-side `SUM` for each numeric column that's backed by a physical or calculated dataset column; the **Summary aggregation** control can override this to `AVG` as well. Non-numeric cells and columns built from free-form SQL expressions stay blank.
In both modes, the summary is computed across the full result set, independent of the chart's row limit and pagination, and it reflects dashboard and chart-level filters. It does not reflect AG Grid's own server-side column filters (the per-column filter UI in the grid header), which are excluded from the summary query.
### Dynamic Currency Formatting
Chart metric values can display currencies dynamically rather than using a fixed currency code. To enable:
+69
View File
@@ -61,6 +61,27 @@ embedDashboard({
---
## Guest role permissions
Embedded viewers authenticate with a guest token rather than a session, and Superset resolves them onto the role named by `GUEST_ROLE_NAME` (default: `Public`). What that role grants therefore decides whether an embed loads.
Set `PUBLIC_ROLE_LIKE = "Public"` and run `superset init` to sync the built-in defaults, which already cover both embeddable resource types. This is the supported path, and an embedded chart needs nothing added to it.
If you maintain the guest role by hand, an embedded chart needs at least:
| Grant | Needed for |
| ------------------------------ | ---------------------------------------------------------------- |
| `can read on Chart` | The chart definition, its dataset, and `/api/v1/chart/data` |
| `can read on CurrentUserRestApi` | `/api/v1/me/roles/`, which the embedded page calls before it renders anything |
Omitting the second one is easy to miss: the embed fails during authentication rather than while loading the chart, so the browser console reports an authentication failure with no mention of a missing grant.
An embedded chart needs **no** grant on Explore. It reads its definition and dataset through the same `Chart` permission it already needs to fetch data, so a guest role that can open an embedded dashboard can open an embedded chart.
The guest token itself, not the role, decides *which* dashboards and charts are in scope — a role grant alone never widens a guest past the resources its token names.
---
## Callbacks
### `resolvePermalinkUrl`
@@ -145,3 +166,51 @@ The following URL parameters can be passed through the `urlParams` option in `da
- **Row-level security** — pass `rls` rules in the guest token request to restrict which rows are visible to the embedded user.
- **Allowed domains** — restrict which host origins can embed a dashboard by setting **Allowed Domains** per-dashboard in the _Embed_ settings modal. Superset checks the request's `Referer` header against this list before serving the embedded view; an empty list allows any origin, so configure this explicitly for production.
- **Redacted errors** — API responses to a guest token report a generic `An error occurred while fetching the data.` instead of the underlying error, since engine errors quote catalog, schema, table and column names. Errors Superset raises itself — access denials, timeouts, payload validation — keep their message, and the full error is always available in the server logs.
## Guest-token request-header size diagnostics
A successful guest-token mint does not guarantee the token can pass through your
deployment's proxies. Limits apply to the **encoded JWT bytes plus header
overhead**, not the number of RLS rules or identifiers. A proxy can reject the
subsequent authentication request before it reaches Superset, including an HTTP
400 HTML response instead of JSON. A 400 alone does not establish a size problem.
Operators can set a deployment-specific diagnostic budget in `superset_config.py`:
```python
# Example only: choose a budget for your complete proxy path.
GUEST_TOKEN_HEADER_MAX_BYTES = 16 * 1024
```
The default is `None` (no budget warnings). Positive integer budgets count UTF-8
bytes of `GUEST_TOKEN_HEADER_NAME`, `: `, the encoded token, and `\r\n`
(four framing bytes). Only sizes **strictly greater** than the budget warn;
equality does not. This is consistent diagnostic accounting, not a prediction of
every proxy's wire-level accounting, HTTP/2 compression, or total-header limits.
Leave a safety margin and validate your actual deployment, including custom
header names. Zero, negative, non-integral, or non-numeric values (including strings and
booleans) disable budget warnings, as do values above JavaScript's maximum safe
integer (2^53 1). Whole-number floats are accepted. Convert environment-variable
strings to integers in deployment configuration to enable the budget.
Issuance audit metadata includes `token_bytes`, `header_bytes`,
`header_budget_bytes`, and `header_budget_exceeded`. Issuance remains HTTP 200
with the same token and response shape. The embedded bootstrap exposes the budget
and configured header name; reload the iframe after changing deployment config.
The embedded client measures initial and refreshed tokens and warns in the
developer console with sizes only. Initial authentication failures get a targeted
suggestion only when the request's token exceeds the budget and the failure has
no status or HTTP 400/431/494; other statuses and ambiguous in-flight
refreshes use the generic error. Refresh warnings do not restart authentication.
These diagnostics do not record JWTs, decoded claims, RLS SQL, or request headers.
[AWS Application Load Balancer quotas](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-limits.html)
list a non-adjustable 16 K single-header limit. Increasing a Superset diagnostic
budget does not increase that limit or add large-token support.
To reduce payload size, replace large inline RLS ID lists with a compact
entitlements-table subquery where supported by your database. Keep the same
tenant/user restrictions, derive identity from your trusted token-issuing
backend, and verify equivalent row access and query performance before rollout.
Do not remove RLS or broaden entitlements to make a token smaller.
+56 -20
View File
@@ -71,17 +71,17 @@ Parses a JSON string into an object that can be used in your template.
---
#### `groupBy`
#### `group`
Groups an array of objects by a key, powered by [handlebars-group-by](https://github.com/nicktindall/handlebars-group-by).
Groups an array of objects by a key, powered by [handlebars-group-by](https://github.com/nicktindall/handlebars-group-by). The key is passed as a `by` hash argument.
```handlebars
{{#groupBy data 'department'}}
{{#group data by="department"}}
<h3>{{value}}</h3>
{{#each items}}
<p>{{this.name}}</p>
{{/each}}
{{/groupBy}}
{{/group}}
```
---
@@ -90,6 +90,14 @@ Groups an array of objects by a key, powered by [handlebars-group-by](https://gi
Superset also registers all helpers from the [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) library. These include a wide range of comparison, math, string, and conditional helpers. Commonly used ones include:
:::note
These names are specific to `just-handlebars-helpers` and differ from other
Handlebars helper libraries — notably `handlebars-helpers`, which spells the
math helpers `add`, `subtract`, `multiply` and `divide`. Calling a helper that
is not registered raises `Missing helper: "..."`, which renders the chart blank,
so it is worth checking a name against the tables below before using it.
:::
#### Comparison
| Helper | Description | Example |
@@ -97,6 +105,7 @@ Superset also registers all helpers from the [just-handlebars-helpers](https://g
| `eq` | Strict equality | `{{#if (eq status "active")}}` |
| `eqw` | Weak equality | `{{#if (eqw count "5")}}` |
| `neq` | Strict inequality | `{{#if (neq role "admin")}}` |
| `neqw` | Weak inequality | `{{#if (neqw count "5")}}` |
| `lt` | Less than | `{{#if (lt score 50)}}` |
| `lte` | Less than or equal | `{{#if (lte score 100)}}` |
| `gt` | Greater than | `{{#if (gt price 0)}}` |
@@ -114,25 +123,52 @@ Superset also registers all helpers from the [just-handlebars-helpers](https://g
#### String
| Helper | Description | Example |
| ------------ | ----------------------------------- | --------------------------------- |
| `capitalize` | Capitalizes first letter | `{{capitalize name}}` |
| `uppercase` | Converts to uppercase | `{{uppercase status}}` |
| `lowercase` | Converts to lowercase | `{{lowercase email}}` |
| `truncate` | Truncates a string | `{{truncate description 100}}` |
| `contains` | Checks if string contains substring | `{{#if (contains tag "urgent")}}` |
| Helper | Description | Example |
| ----------------- | ----------------------------------------------- | ------------------------------ |
| `capitalizeFirst` | Capitalizes the first letter | `{{capitalizeFirst name}}` |
| `capitalizeEach` | Capitalizes the first letter of each word | `{{capitalizeEach title}}` |
| `uppercase` | Converts to uppercase | `{{uppercase status}}` |
| `lowercase` | Converts to lowercase | `{{lowercase email}}` |
| `excerpt` | Truncates to a length and appends an ellipsis | `{{excerpt description 100}}` |
| `sprintf` | printf-style formatting | `{{sprintf "%.1f" score}}` |
| `concat` | Concatenates values | `{{concat first " " last}}` |
| `join` | Joins an array with a separator | `{{join tags ", "}}` |
| `first` / `last` | First or last element of an array | `{{first items}}` |
| `newLineToBr` | Converts newlines to `<br>` (needs `{{{ }}}`) | `{{{newLineToBr notes}}}` |
#### Math
| Helper | Description | Example |
| ---------- | -------------- | ----------------------------- |
| `add` | Addition | `{{add a b}}` |
| `subtract` | Subtraction | `{{subtract total discount}}` |
| `multiply` | Multiplication | `{{multiply price quantity}}` |
| `divide` | Division | `{{divide total count}}` |
| `ceil` | Ceiling | `{{ceil value}}` |
| `floor` | Floor | `{{floor value}}` |
| `round` | Round | `{{round value}}` |
| Helper | Description | Example |
| ---------------- | ----------------------- | ------------------------------------ |
| `sum` | Addition | `{{sum a b}}` |
| `difference` | Subtraction | `{{difference total discount}}` |
| `multiplication` | Multiplication | `{{multiplication price quantity}}` |
| `division` | Division | `{{division total count}}` |
| `remainder` | Modulo | `{{remainder index 2}}` |
| `abs` | Absolute value | `{{abs delta}}` |
| `ceil` | Ceiling | `{{ceil value}}` |
| `floor` | Floor | `{{floor value}}` |
`sum` takes exactly two arguments — it adds a pair of numbers and does not total
an array. There is no `round` helper; use `{{sprintf "%.0f" value}}` to round to
a given number of decimal places.
#### Arrays
| Helper | Description | Example |
| ---------- | ---------------------------------- | --------------------------------- |
| `includes` | Whether an array contains a value | `{{#if (includes tags "urgent")}}` |
| `empty` | Whether an array is empty | `{{#if (empty rows)}}` |
| `count` | Number of items in an array | `{{count rows}}` |
`includes` tests array membership. It returns `false` for a string, so it cannot
be used to check for a substring.
#### Formatting
| Helper | Description | Example |
| ---------------- | ---------------------------- | -------------------------------- |
| `formatCurrency` | Formats a number as currency | `{{formatCurrency revenue "$"}}` |
For the full list of available helpers, see the [just-handlebars-helpers documentation](https://github.com/leapfrogtechnology/just-handlebars-helpers).
@@ -305,6 +305,14 @@ Ask your admin for the MCP server URL and any authentication tokens you need.
| `list_databases` | List configured database connections |
| `get_database_info` | Get details about a specific database connection |
### Themes
| Tool | Description |
| ---------------- | ------------------------------------------------------------------------- |
| `list_themes` | Discover themes (antd design-token configurations) with filters |
| `get_theme_info` | Get a theme's tokens (`json_data`) by ID or UUID |
| `create_theme` | Create a reusable theme from antd design tokens (requires write access) |
---
## Troubleshooting
+5 -2
View File
@@ -43,8 +43,11 @@ publish = "build"
ignore = 'if [ -n "$CACHED_COMMIT_REF" ]; then git diff --quiet "$CACHED_COMMIT_REF" HEAD -- . ../README.md; else git fetch --no-tags origin master >/dev/null 2>&1 || true; i=0; while [ "$i" -lt 10 ] && ! git merge-base origin/master HEAD >/dev/null 2>&1; do git fetch --deepen=200 origin master >/dev/null 2>&1 || break; i=$((i+1)); done; BASE="$(git merge-base origin/master HEAD 2>/dev/null || true)"; if [ -z "$BASE" ]; then exit 1; fi; git diff --quiet "$BASE" HEAD -- . ../README.md; fi'
[build.environment]
# Node version matching docs/.nvmrc
NODE_VERSION = "20"
# Node version is intentionally not pinned here: Netlify auto-detects it
# from docs/.nvmrc, which is a symlink to the repo's single source of truth
# at superset-frontend/.nvmrc. Duplicating the version here previously let
# it drift out of sync (stuck on Node 20 after the repo moved to Node 24),
# breaking installs once a dependency required a newer Node engine.
# Yarn version
YARN_VERSION = "1.22.22"
# Increase heap size for webpack bundling of Superset UI components
+4 -4
View File
@@ -43,7 +43,7 @@
"version:remove:components": "node scripts/manage-versions.mjs remove components"
},
"dependencies": {
"@ant-design/icons": "^6.2.5",
"@ant-design/icons": "^6.3.4",
"@docusaurus/core": "^3.10.2",
"@docusaurus/faster": "^3.10.2",
"@docusaurus/plugin-client-redirects": "^3.10.2",
@@ -87,11 +87,11 @@
"@docusaurus/module-type-aliases": "^3.10.2",
"@types/js-yaml": "^4.0.9",
"@types/react": "^19.1.8",
"oxfmt": "^0.65.0",
"oxlint": "^1.80.0",
"oxfmt": "^0.66.0",
"oxlint": "^1.81.0",
"oxlint-tsgolint": "^7.0.2001",
"typescript": "7.0.2",
"webpack": "^5.110.1"
"webpack": "^5.110.3"
},
"browserslist": {
"production": [
+6 -6
View File
@@ -98,12 +98,6 @@
"default": false,
"lifecycle": "development",
"description": "Enable Table V2 time comparison feature"
},
{
"name": "TAGGING_SYSTEM",
"default": false,
"lifecycle": "development",
"description": "Enables the tagging system for organizing assets"
}
],
"testing": [
@@ -240,6 +234,12 @@
"description": "Allow users to enable SSH tunneling when creating a DB connection. DB engine must support SSH Tunnels.",
"docs": "https://superset.apache.org/docs/configuration/setup-ssh-tunneling"
},
{
"name": "TAGGING_SYSTEM",
"default": true,
"lifecycle": "testing",
"description": "Enables the tagging system for organizing assets"
},
{
"name": "USE_ANALOGOUS_COLORS",
"default": false,
+352 -23
View File
@@ -1203,28 +1203,6 @@
},
"time_grain_sqla": {
"description": "To what level of granularity should the temporal column be aggregated. Supports [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.",
"enum": [
"PT1S",
"PT5S",
"PT30S",
"PT1M",
"PT5M",
"PT10M",
"PT15M",
"PT30M",
"PT1H",
"PT6H",
"P1D",
"P1W",
"P1M",
"P3M",
"P1Y",
"1969-12-28T00:00:00Z/P1W",
"1969-12-29T00:00:00Z/P1W",
"P1W/1970-01-03T00:00:00Z",
"P1W/1970-01-04T00:00:00Z",
null
],
"example": "P1D",
"nullable": true,
"type": "string"
@@ -1485,12 +1463,14 @@
"PT10M",
"PT15M",
"PT30M",
"PT0.5H",
"PT1H",
"PT6H",
"P1D",
"P1W",
"P1M",
"P3M",
"P0.25Y",
"P1Y",
"1969-12-28T00:00:00Z/P1W",
"1969-12-29T00:00:00Z/P1W",
@@ -7039,6 +7019,44 @@
},
"type": "object"
},
"EmbeddedChartConfig": {
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"allowed_domains"
],
"type": "object"
},
"EmbeddedChartResponseSchema": {
"properties": {
"allowed_domains": {
"items": {
"type": "string"
},
"type": "array"
},
"changed_by": {
"$ref": "#/components/schemas/EmbeddedResponseUser"
},
"changed_on": {
"format": "date-time",
"type": "string"
},
"chart_id": {
"type": "string"
},
"uuid": {
"type": "string"
}
},
"type": "object"
},
"EmbeddedDashboardConfig": {
"properties": {
"allowed_domains": {
@@ -7113,6 +7131,23 @@
},
"type": "object"
},
"EmbeddedResponseUser": {
"properties": {
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_name": {
"type": "string"
},
"username": {
"type": "string"
}
},
"type": "object"
},
"EngineInformation": {
"properties": {
"disable_ssh_tunneling": {
@@ -10562,7 +10597,8 @@
},
"type": {
"enum": [
"dashboard"
"dashboard",
"chart"
]
}
},
@@ -16702,6 +16738,299 @@
]
}
},
"/api/v1/chart/{pk}/embedded": {
"delete": {
"parameters": [
{
"description": "The chart id",
"in": "path",
"name": "pk",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"message": {
"type": "string"
}
},
"type": "object"
}
}
},
"description": "Successfully removed the configuration"
},
"401": {
"$ref": "#/components/responses/401"
},
"404": {
"$ref": "#/components/responses/404"
},
"500": {
"$ref": "#/components/responses/500"
}
},
"security": [
{
"jwt": []
},
{
"api_key": []
}
],
"summary": "Delete a chart's embedded configuration",
"tags": [
"Charts"
]
},
"get": {
"parameters": [
{
"description": "The chart id",
"in": "path",
"name": "pk",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"result": {
"$ref": "#/components/schemas/EmbeddedChartResponseSchema"
}
},
"type": "object"
}
}
},
"description": "Result contains the embedded chart config"
},
"401": {
"$ref": "#/components/responses/401"
},
"404": {
"$ref": "#/components/responses/404"
},
"500": {
"$ref": "#/components/responses/500"
}
},
"security": [
{
"jwt": []
},
{
"api_key": []
}
],
"summary": "Get the chart's embedded configuration",
"tags": [
"Charts"
]
},
"post": {
"parameters": [
{
"description": "The chart id",
"in": "path",
"name": "pk",
"required": true,
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EmbeddedChartConfig"
}
}
},
"description": "The embedded configuration to set",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"result": {
"$ref": "#/components/schemas/EmbeddedChartResponseSchema"
}
},
"type": "object"
}
}
},
"description": "Successfully set the configuration"
},
"401": {
"$ref": "#/components/responses/401"
},
"404": {
"$ref": "#/components/responses/404"
},
"500": {
"$ref": "#/components/responses/500"
}
},
"security": [
{
"jwt": []
},
{
"api_key": []
}
],
"summary": "Set a chart's embedded configuration",
"tags": [
"Charts"
]
},
"put": {
"parameters": [
{
"description": "The chart id",
"in": "path",
"name": "pk",
"required": true,
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EmbeddedChartConfig"
}
}
},
"description": "The embedded configuration to set",
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"result": {
"$ref": "#/components/schemas/EmbeddedChartResponseSchema"
}
},
"type": "object"
}
}
},
"description": "Successfully set the configuration"
},
"401": {
"$ref": "#/components/responses/401"
},
"404": {
"$ref": "#/components/responses/404"
},
"500": {
"$ref": "#/components/responses/500"
}
},
"security": [
{
"jwt": []
},
{
"api_key": []
}
],
"summary": "Update a chart's embedded configuration",
"tags": [
"Charts"
]
}
},
"/api/v1/chart/{pk}/embedded_context": {
"get": {
"description": "The chart analogue of a dashboard's ``/charts`` and ``/datasets`` sub-resources, collapsed into one call because a chart has exactly one of each. Sits under the ``Chart`` read permission, so a standalone embedded chart loads with the same grant its guest token already needs to fetch that chart's data.",
"parameters": [
{
"description": "The chart id",
"in": "path",
"name": "pk",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"properties": {
"result": {
"properties": {
"dataset": {
"type": "object"
},
"slice": {
"$ref": "#/components/schemas/ChartEntityResponseSchema"
}
},
"type": "object"
}
},
"type": "object"
}
}
},
"description": "The chart and its dataset"
},
"401": {
"$ref": "#/components/responses/401"
},
"403": {
"$ref": "#/components/responses/403"
},
"404": {
"$ref": "#/components/responses/404"
},
"500": {
"$ref": "#/components/responses/500"
}
},
"security": [
{
"jwt": []
},
{
"api_key": []
}
],
"summary": "Get a chart and its dataset in one payload",
"tags": [
"Charts"
]
}
},
"/api/v1/chart/{pk}/favorites/": {
"delete": {
"parameters": [
@@ -215,7 +215,7 @@ If you have a good solution for this, let us know!
:::
:::note
Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry
Superset uses [Scarf Gateway](https://about.scarf.sh/) to collect telemetry
data. Knowing the installation counts for different Superset versions informs the project's
decisions about patching and long-term support. Scarf purges personally identifiable information
(PII) and provides only aggregated statistics.
@@ -135,7 +135,7 @@ init:
```
:::note
Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
Superset uses [Scarf Gateway](https://about.scarf.sh/) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics.
To opt-out of this data collection in your Helm-based installation, edit the `repository:` line in your `helm/superset/values.yaml` file, replacing `apachesuperset.docker.scarf.sh/apache/superset` with `apache/superset` to pull the image directly from Docker Hub.
:::
@@ -71,17 +71,17 @@ Parses a JSON string into an object that can be used in your template.
---
#### `groupBy`
#### `group`
Groups an array of objects by a key, powered by [handlebars-group-by](https://github.com/nicktindall/handlebars-group-by).
Groups an array of objects by a key, powered by [handlebars-group-by](https://github.com/nicktindall/handlebars-group-by). The key is passed as a `by` hash argument.
```handlebars
{{#groupBy data 'department'}}
{{#group data by="department"}}
<h3>{{value}}</h3>
{{#each items}}
<p>{{this.name}}</p>
{{/each}}
{{/groupBy}}
{{/group}}
```
---
@@ -90,6 +90,14 @@ Groups an array of objects by a key, powered by [handlebars-group-by](https://gi
Superset also registers all helpers from the [just-handlebars-helpers](https://github.com/leapfrogtechnology/just-handlebars-helpers) library. These include a wide range of comparison, math, string, and conditional helpers. Commonly used ones include:
:::note
These names are specific to `just-handlebars-helpers` and differ from other
Handlebars helper libraries — notably `handlebars-helpers`, which spells the
math helpers `add`, `subtract`, `multiply` and `divide`. Calling a helper that
is not registered raises `Missing helper: "..."`, which renders the chart blank,
so it is worth checking a name against the tables below before using it.
:::
#### Comparison
| Helper | Description | Example |
@@ -97,6 +105,7 @@ Superset also registers all helpers from the [just-handlebars-helpers](https://g
| `eq` | Strict equality | `{{#if (eq status "active")}}` |
| `eqw` | Weak equality | `{{#if (eqw count "5")}}` |
| `neq` | Strict inequality | `{{#if (neq role "admin")}}` |
| `neqw` | Weak inequality | `{{#if (neqw count "5")}}` |
| `lt` | Less than | `{{#if (lt score 50)}}` |
| `lte` | Less than or equal | `{{#if (lte score 100)}}` |
| `gt` | Greater than | `{{#if (gt price 0)}}` |
@@ -114,25 +123,52 @@ Superset also registers all helpers from the [just-handlebars-helpers](https://g
#### String
| Helper | Description | Example |
| ------------ | ----------------------------------- | --------------------------------- |
| `capitalize` | Capitalizes first letter | `{{capitalize name}}` |
| `uppercase` | Converts to uppercase | `{{uppercase status}}` |
| `lowercase` | Converts to lowercase | `{{lowercase email}}` |
| `truncate` | Truncates a string | `{{truncate description 100}}` |
| `contains` | Checks if string contains substring | `{{#if (contains tag "urgent")}}` |
| Helper | Description | Example |
| ----------------- | ----------------------------------------------- | ------------------------------ |
| `capitalizeFirst` | Capitalizes the first letter | `{{capitalizeFirst name}}` |
| `capitalizeEach` | Capitalizes the first letter of each word | `{{capitalizeEach title}}` |
| `uppercase` | Converts to uppercase | `{{uppercase status}}` |
| `lowercase` | Converts to lowercase | `{{lowercase email}}` |
| `excerpt` | Truncates to a length and appends an ellipsis | `{{excerpt description 100}}` |
| `sprintf` | printf-style formatting | `{{sprintf "%.1f" score}}` |
| `concat` | Concatenates values | `{{concat first " " last}}` |
| `join` | Joins an array with a separator | `{{join tags ", "}}` |
| `first` / `last` | First or last element of an array | `{{first items}}` |
| `newLineToBr` | Converts newlines to `<br>` (needs `{{{ }}}`) | `{{{newLineToBr notes}}}` |
#### Math
| Helper | Description | Example |
| ---------- | -------------- | ----------------------------- |
| `add` | Addition | `{{add a b}}` |
| `subtract` | Subtraction | `{{subtract total discount}}` |
| `multiply` | Multiplication | `{{multiply price quantity}}` |
| `divide` | Division | `{{divide total count}}` |
| `ceil` | Ceiling | `{{ceil value}}` |
| `floor` | Floor | `{{floor value}}` |
| `round` | Round | `{{round value}}` |
| Helper | Description | Example |
| ---------------- | ----------------------- | ------------------------------------ |
| `sum` | Addition | `{{sum a b}}` |
| `difference` | Subtraction | `{{difference total discount}}` |
| `multiplication` | Multiplication | `{{multiplication price quantity}}` |
| `division` | Division | `{{division total count}}` |
| `remainder` | Modulo | `{{remainder index 2}}` |
| `abs` | Absolute value | `{{abs delta}}` |
| `ceil` | Ceiling | `{{ceil value}}` |
| `floor` | Floor | `{{floor value}}` |
`sum` takes exactly two arguments — it adds a pair of numbers and does not total
an array. There is no `round` helper; use `{{sprintf "%.0f" value}}` to round to
a given number of decimal places.
#### Arrays
| Helper | Description | Example |
| ---------- | ---------------------------------- | --------------------------------- |
| `includes` | Whether an array contains a value | `{{#if (includes tags "urgent")}}` |
| `empty` | Whether an array is empty | `{{#if (empty rows)}}` |
| `count` | Number of items in an array | `{{count rows}}` |
`includes` tests array membership. It returns `false` for a string, so it cannot
be used to check for a substring.
#### Formatting
| Helper | Description | Example |
| ---------------- | ---------------------------- | -------------------------------- |
| `formatCurrency` | Formats a number as currency | `{{formatCurrency revenue "$"}}` |
For the full list of available helpers, see the [just-handlebars-helpers documentation](https://github.com/leapfrogtechnology/just-handlebars-helpers).
+283 -344
View File
@@ -222,18 +222,18 @@
resolved "https://registry.yarnpkg.com/@ant-design/fast-color/-/fast-color-3.0.1.tgz#fee56b95427c0b55b216c93d9a7f3473f31615b5"
integrity sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==
"@ant-design/icons-svg@^4.5.0":
version "4.5.0"
resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz#7b1c567e489840d747f211d3688949bcba363ad2"
integrity sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==
"@ant-design/icons-svg@^4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.6.0.tgz#5f7ebfe2a6b7c871920f73db095bd4ab50d7764d"
integrity sha512-PRomU725ABMf/lnQp5HiB7my1kjEbFY0D10N4lXYxK6TIB1gKjIVD5MRThpDaezLgw1D774J8eOeeDB0M6wHrQ==
"@ant-design/icons@^6.2.5", "@ant-design/icons@^6.3.2":
version "6.3.2"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.3.2.tgz#8291dffc53003db9a5df59f80ed758473cd5c8df"
integrity sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==
"@ant-design/icons@^6.3.2", "@ant-design/icons@^6.3.4":
version "6.3.4"
resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.3.4.tgz#9a76f7b6b4554a65f25e3e5c439405b96c4ebd71"
integrity sha512-kDoUlppczVyCUTFthF6cHmPzMBYqNZQhu9j6EyIX/YIdM9j9CZNZoJUjzVw7/1v1VO8WwVMmPp15r/uwIAS0AQ==
dependencies:
"@ant-design/colors" "^8.0.1"
"@ant-design/icons-svg" "^4.5.0"
"@ant-design/icons-svg" "^4.6.0"
"@rc-component/util" "^1.11.0"
clsx "^2.1.1"
@@ -3078,100 +3078,100 @@
resolved "https://registry.yarnpkg.com/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz#dbdf12721396ef4c31899d0963b002df32f57050"
integrity sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==
"@oxfmt/binding-android-arm-eabi@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.65.0.tgz#1d9be45cec55c86b17e1feffbffaeadd45fd2c9a"
integrity sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==
"@oxfmt/binding-android-arm-eabi@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.66.0.tgz#81935e074d88a7da5afc731d4cc74bb1763aa3f1"
integrity sha512-2Me9eoptv6ERdEuI2P8AOlYdHHraXebJaM6SC0kc2Dfb+mLrep2db+fedBPKaYn673h/vBgvP4tkOdAbaudX6w==
"@oxfmt/binding-android-arm64@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.65.0.tgz#7a6dbd301c1198d2a16439e005ca33fb3ebd2f62"
integrity sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==
"@oxfmt/binding-android-arm64@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.66.0.tgz#ea52efcaff8f7d708a61119124b36e656b0d74ca"
integrity sha512-u7O+bSSF0HGsDKkQQxBqvLGVepu93RA+JKu+ONqvfh4sCnCEbj31wZj4iG5gk3XfRwrmYj0/8catkO2LcblQKQ==
"@oxfmt/binding-darwin-arm64@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.65.0.tgz#7fa69acf25aefb7ff832aaa207ece9f55c48398e"
integrity sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==
"@oxfmt/binding-darwin-arm64@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.66.0.tgz#620dfd8993f71e557b169eeda792203d15698061"
integrity sha512-/ikyMIVjX/sdo7KtjxoEsSUosfPzveVhT9RWMx9yGqFDKFJ89JAEKuEeLBmurDjrkb4w8tOnAdSO3SBaplY3bw==
"@oxfmt/binding-darwin-x64@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.65.0.tgz#c315ca1fb9fb606477dc76fc4154d92c32a244a9"
integrity sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==
"@oxfmt/binding-darwin-x64@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.66.0.tgz#9f45ff27d347283fdc2a79b87cd67cc40204a46e"
integrity sha512-q5xUsKeFqawa9NXa6ZGXWimFV19m8MogKPdTaSVDAAk2EQKBmBZRDeluwcl1p8ty/OFc9s9888OKEh3xfPVH0g==
"@oxfmt/binding-freebsd-x64@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.65.0.tgz#4815e6ac50a9f38cd93dda3d649c6988bded481d"
integrity sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==
"@oxfmt/binding-freebsd-x64@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.66.0.tgz#5c04bc6fea57c0829d06698c480331019acf57f3"
integrity sha512-CR+x4VzMY0pRXLK/xFQ/RzsSFkP5t2Z2mef0QY6OP/rTRcMUoMLCOM62/3Fp/t0K+UDoBKxvMyeb6D0zPMjleA==
"@oxfmt/binding-linux-arm-gnueabihf@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.65.0.tgz#36c1c82b18186293c62c48fbcf1e3e4bdc9cd72d"
integrity sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==
"@oxfmt/binding-linux-arm-gnueabihf@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.66.0.tgz#c343c923fb1acab9b22bf411ad7feb12ee9c415a"
integrity sha512-ZEYmO/LbH9tTQCADILHGZE4GeOXOAj2VzedHkASNwjmwlwtutJCLpCJbIs37wRGTFgWRoEcD72jpMX+IBJUGjQ==
"@oxfmt/binding-linux-arm-musleabihf@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.65.0.tgz#623880e16cd16fb36abf02aa050e5e33d1acbf67"
integrity sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==
"@oxfmt/binding-linux-arm-musleabihf@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.66.0.tgz#ec038e354a0b4fa903eaaf29b7e70b3ea0ce4956"
integrity sha512-hNtR9/oU0CeTkq7JnRkmBQwqe17v2ZaAMLC4VcN7IIOWeRyWDk0knSPWS9iiLmtbZ2RRBBtsG01jQgkZmKCJeQ==
"@oxfmt/binding-linux-arm64-gnu@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.65.0.tgz#1570f61b877385b9c1b56800663f68e8f988c533"
integrity sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==
"@oxfmt/binding-linux-arm64-gnu@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.66.0.tgz#11b3a1d966acbb65cb76238594fa62b68d226109"
integrity sha512-uwOVQ8i6I1LT/+eDzfsgrrcZp8Fn6NPVUPn8fF5gdFGekFf0PddF+LEuwsD0/pbNUcKZhDj2rQ5UpITh9gF4iQ==
"@oxfmt/binding-linux-arm64-musl@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.65.0.tgz#e7494ed26c0cd52a1ac4b38fe5f0a2a88231cd2e"
integrity sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==
"@oxfmt/binding-linux-arm64-musl@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.66.0.tgz#b441c3255ff3cecf393a15e4393e2bc506c59dba"
integrity sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg==
"@oxfmt/binding-linux-ppc64-gnu@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.65.0.tgz#e87bc27e8d9fba5eaba59b11a1f00ad1e116f803"
integrity sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==
"@oxfmt/binding-linux-ppc64-gnu@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.66.0.tgz#8300b1420ffab047d07527ae78776f331f5aeef6"
integrity sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw==
"@oxfmt/binding-linux-riscv64-gnu@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.65.0.tgz#dd27e2b20b27817938815a2b47f87fcf41ca1aa0"
integrity sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==
"@oxfmt/binding-linux-riscv64-gnu@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.66.0.tgz#58718cadbd2e876414802ffd2445b61063f496fc"
integrity sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q==
"@oxfmt/binding-linux-riscv64-musl@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.65.0.tgz#dc37b95c1f07579eeedcf869d7cf3b47ca695ac9"
integrity sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==
"@oxfmt/binding-linux-riscv64-musl@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.66.0.tgz#f397a34335356dacaefc5e3e9fb4a58de7d9c0c8"
integrity sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA==
"@oxfmt/binding-linux-s390x-gnu@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.65.0.tgz#c898674718ce8da7a7c625f3b3bcda0b0ff55bc1"
integrity sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==
"@oxfmt/binding-linux-s390x-gnu@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.66.0.tgz#4d0d9f6c18136836d9e366f366d1cd5afff0f1c0"
integrity sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A==
"@oxfmt/binding-linux-x64-gnu@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.65.0.tgz#720a641158804551480d79cdc85cfed21b0882c8"
integrity sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==
"@oxfmt/binding-linux-x64-gnu@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.66.0.tgz#cd02a84ea2ac21d5267592d947a0df7234ad5a7a"
integrity sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA==
"@oxfmt/binding-linux-x64-musl@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.65.0.tgz#6cb3e1f25c85c619ce634b2f7e3238c33caafe86"
integrity sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==
"@oxfmt/binding-linux-x64-musl@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.66.0.tgz#9b3066ef25ee4c011dbda21cd96479b645997c58"
integrity sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw==
"@oxfmt/binding-openharmony-arm64@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.65.0.tgz#c620f9f383c544428f363bf4418b94b81b5b9346"
integrity sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==
"@oxfmt/binding-openharmony-arm64@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.66.0.tgz#7d0e493670a2e6b8a15d03698be7eabb40234976"
integrity sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ==
"@oxfmt/binding-win32-arm64-msvc@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.65.0.tgz#79c59706cc5450854d57d582116b13d6611e875c"
integrity sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==
"@oxfmt/binding-win32-arm64-msvc@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.66.0.tgz#3d20c363517caea2ddc6621641216d1f099ce5d8"
integrity sha512-soV+0vESv7e5ntCHWC61x4gg8OSak6IHHnWsZmHrJFlvMj2AK+kmldErCNkVkrvc1Ts2/++rJXn+IuAb2WMXhw==
"@oxfmt/binding-win32-ia32-msvc@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.65.0.tgz#6a52067c7fc666ee46a1b95366c08a9a1c9b26a1"
integrity sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==
"@oxfmt/binding-win32-ia32-msvc@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.66.0.tgz#19f58a4532b4499ea14bf1a740d5fe1be7640e36"
integrity sha512-YCPi23uRIEYuIKTZohAkKbPFpujQ5QBuUM5iDv+UqbCmTPAkaFsxjsSuB8xlBpRT0G7eP/4HMF+cPDSqHtOD9A==
"@oxfmt/binding-win32-x64-msvc@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.65.0.tgz#f63624f39bbfddcd679d471c15879176581bb32b"
integrity sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==
"@oxfmt/binding-win32-x64-msvc@0.66.0":
version "0.66.0"
resolved "https://registry.yarnpkg.com/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.66.0.tgz#6a427d9080a54713e59dcef338b13a870df888ea"
integrity sha512-bwTQcv/JVRPkOqQtMF0X7vpvpncDQiBcXHxZ9S2hR12Hlo8bvBdUR5x5XnxzDZ3kM0qoZw1rv7KaD66Ly+pFWA==
"@oxlint-tsgolint/darwin-arm64@7.0.2001":
version "7.0.2001"
@@ -3203,100 +3203,100 @@
resolved "https://registry.yarnpkg.com/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2001.tgz#814bcdd2707fa8ab1ae0f0b51a7243b034d2833a"
integrity sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==
"@oxlint/binding-android-arm-eabi@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz#924b041cbcea4e934fd9ef66a2d2b7d7463c0180"
integrity sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==
"@oxlint/binding-android-arm-eabi@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.81.0.tgz#86a0305480e680431429c8259c5701ac5592e4a7"
integrity sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==
"@oxlint/binding-android-arm64@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz#d1716d2be903de06b4fdb2fdaa121a2943695c35"
integrity sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==
"@oxlint/binding-android-arm64@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-android-arm64/-/binding-android-arm64-1.81.0.tgz#dba51cb2a1258f37eaca15a4e74d5d6ee19f238c"
integrity sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==
"@oxlint/binding-darwin-arm64@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz#475eb4061db4d4fe9349e92f15962c0203e8ebec"
integrity sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==
"@oxlint/binding-darwin-arm64@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.81.0.tgz#f384ae1eddd399429281f88a0073729e287ec0b1"
integrity sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==
"@oxlint/binding-darwin-x64@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz#602fa5681dd746c0fb5ca7a0e4a81d0dd07b7f90"
integrity sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==
"@oxlint/binding-darwin-x64@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.81.0.tgz#7167e3b4c18852fcd1ddade536d232d73d55d30c"
integrity sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==
"@oxlint/binding-freebsd-x64@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz#4e0490c344726fd0b1a027afc129fb9c78f069ec"
integrity sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==
"@oxlint/binding-freebsd-x64@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.81.0.tgz#64f230765343bfe7e2280b071bb191bb666303ac"
integrity sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==
"@oxlint/binding-linux-arm-gnueabihf@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz#82d215fc05e046a0ec085821c1da8ab99d35fbdc"
integrity sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==
"@oxlint/binding-linux-arm-gnueabihf@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.81.0.tgz#5095f021a28009146f7536a7ccd518cdfcbfc3bf"
integrity sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==
"@oxlint/binding-linux-arm-musleabihf@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz#92068e3b51cd50fc5a83a3ebfba7925313d6cd11"
integrity sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==
"@oxlint/binding-linux-arm-musleabihf@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.81.0.tgz#d894dcfb6fb1b8b224b79e6ce07b17403f413749"
integrity sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==
"@oxlint/binding-linux-arm64-gnu@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz#6c81ddc85dd5b79070f87401d61813172068666d"
integrity sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==
"@oxlint/binding-linux-arm64-gnu@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.81.0.tgz#bf8e362c87e7828ba28c2ec36bae73ad82989ef1"
integrity sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==
"@oxlint/binding-linux-arm64-musl@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz#11216704b606d67e946a868850243dc2739ec92e"
integrity sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==
"@oxlint/binding-linux-arm64-musl@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.81.0.tgz#2aa5214eb5b032e8f77d94a27c9bd3f6dbc446f3"
integrity sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==
"@oxlint/binding-linux-ppc64-gnu@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz#125302a0e6732e32e4a7d53f0212d58f1b13af01"
integrity sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==
"@oxlint/binding-linux-ppc64-gnu@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.81.0.tgz#a0af1af6fd309214555f33b4fa0432f3ec45f242"
integrity sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==
"@oxlint/binding-linux-riscv64-gnu@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz#03846614f184ed3dcbab4bffad7417fd54cd0967"
integrity sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==
"@oxlint/binding-linux-riscv64-gnu@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.81.0.tgz#60413a335a63e0911a625d047690e41c03f2a833"
integrity sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==
"@oxlint/binding-linux-riscv64-musl@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz#49e0fb90e1c8358429b9989a407f4020c56d8ea9"
integrity sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==
"@oxlint/binding-linux-riscv64-musl@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.81.0.tgz#5f1dd7bc2bdbe92fa5454df081ee91fa2c499948"
integrity sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==
"@oxlint/binding-linux-s390x-gnu@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz#7aee2ae2426f7bdcae77969073a015a6b3f8373e"
integrity sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==
"@oxlint/binding-linux-s390x-gnu@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.81.0.tgz#830c5bdd145ad39c9b5371e3214d0a9f9c1f46b1"
integrity sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==
"@oxlint/binding-linux-x64-gnu@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz#0c850b00faed2f884cf8665fe9c391f9f653d6ca"
integrity sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==
"@oxlint/binding-linux-x64-gnu@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.81.0.tgz#7f72ea3c9ae70a76c1a92e56a5755869f335e756"
integrity sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==
"@oxlint/binding-linux-x64-musl@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz#91f54f1b0cc93a7e75ce4118aa85f9fd112f25c0"
integrity sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==
"@oxlint/binding-linux-x64-musl@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.81.0.tgz#758fd6ea69123c6f4c1ba160bfc28f6b84548c11"
integrity sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==
"@oxlint/binding-openharmony-arm64@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz#7ab37c21e547812177bffdbd97c8a34493eedd7b"
integrity sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==
"@oxlint/binding-openharmony-arm64@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.81.0.tgz#e492b7a1875b70ea55ffe13f685d4302e6b1900e"
integrity sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==
"@oxlint/binding-win32-arm64-msvc@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz#5a470fa82339044ad9cceef2bb8c62d88695e394"
integrity sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==
"@oxlint/binding-win32-arm64-msvc@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.81.0.tgz#9247fdc1b4c86ecc718747c5f8c68902969677d1"
integrity sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==
"@oxlint/binding-win32-ia32-msvc@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz#06f6559998f1a53a8f5ace78d91f217a54f6a963"
integrity sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==
"@oxlint/binding-win32-ia32-msvc@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.81.0.tgz#dfa4eed78612c5f4df788eb056fd5d1b05a75bb1"
integrity sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==
"@oxlint/binding-win32-x64-msvc@1.80.0":
version "1.80.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz#d3abbf1a7a09b9039ca5ca570c9689908850efc4"
integrity sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==
"@oxlint/binding-win32-x64-msvc@1.81.0":
version "1.81.0"
resolved "https://registry.yarnpkg.com/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.81.0.tgz#27a474cc288e47a0e5c2d9c922bee64de0cfd614"
integrity sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==
"@parcel/watcher-android-arm64@2.5.6":
version "2.5.6"
@@ -4969,85 +4969,85 @@
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/html-darwin-arm64@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.15.43.tgz#c88069f140ead901724018f96ad709779526a368"
integrity sha512-+PFbHbeeN+zB0zfvR1V1NmvPriuWPI+sijQXpI+wq/nLIujxvtENWjOKVHgouC9TIN/uKmL2zu9HAq6L6YxnPA==
"@swc/html-darwin-arm64@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-darwin-arm64/-/html-darwin-arm64-1.16.2.tgz#dfe45d70266262a59aaaa0d93740b6161803f192"
integrity sha512-SNBUxkxLBXD0ATwnOG1rF8mpSrRtFDfqWnEUmbm/g4KwmCt7NuHHv9YYqA3lqfq90Ucc+Xlk7afx8KAW/utz4A==
"@swc/html-darwin-x64@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.15.43.tgz#9bc8171494d3b98eac017b5b73f0f63054558626"
integrity sha512-LQJ2U8Oxcx4T1rRF25y4h+/p05nn58FugTe/uGxC5OT3K83c2MftcSZLYaahOu4GVHRZeS1NI94CkSvQV++TVw==
"@swc/html-darwin-x64@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-darwin-x64/-/html-darwin-x64-1.16.2.tgz#72066bda0d843c024dd5230aa013445d10037cde"
integrity sha512-WVBgn6yrBPMZu+DL95/XGAXYcgd1nhd67Ml1UjMtFoFMVKY+VRpCq8JpTZTMXhWbVoRENUHk+3PHu0nNjlE/Fg==
"@swc/html-linux-arm-gnueabihf@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.15.43.tgz#271a5d345719fa2c381df45355f70870faaaf35c"
integrity sha512-DKIen6DuIRO7Xc5gAbgBT5QyRHJGEGXreIdM1VBosYWTGnnrQ//Hwd7bLD6UbT8X8eU1vqvpXwQ1E24QRqRaBQ==
"@swc/html-linux-arm-gnueabihf@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.16.2.tgz#efa7dd85b03b941ac173ae2870355f9721993905"
integrity sha512-V9F/Akd2TXrf5nUhdLgdy3FoVFxQbw8pA2AOyqnEOa2Mbm1R7DZJJ0GdShEMcoyMyMDB9r/4pWuWfxNtP4mFHA==
"@swc/html-linux-arm64-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.15.43.tgz#93cd3202f204351e7279efd07abc280fe0b2d193"
integrity sha512-0AuHiyfcE86CZ/CajFIszLzZVzbM2wn5p01oet8Q9RikflCGwyH79Nv9TrAKD1Cx7juUrONzDk+f2b/x73wLTg==
"@swc/html-linux-arm64-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.16.2.tgz#4b6a02b42a40463e9ed9a96e0c42b7095b244f48"
integrity sha512-jonZVtHc6BesMjC/muUEJGzE1L2kVdgiPVuHc7CL79MrUm0Hjf8LS4Wmtjqe2bLTfRcaMfaYl/60ZcRXHCaYSQ==
"@swc/html-linux-arm64-musl@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.15.43.tgz#a6c0c2a1646755b1ee53a6bddbee68674f8edaea"
integrity sha512-TweIdl/g9ugkoiYvcL/qbu+gbglDY3TqNxfXH84WXc4rSqEP20owVlxLya2NjVct8LIP2wDrtutpOwAXWC+Eew==
"@swc/html-linux-arm64-musl@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.16.2.tgz#0334e071cb8a01e0423fe7da27204b71f1afed66"
integrity sha512-dvki9/sgacHk9ouORmnIok5FbpeE9zUE8yqGGhL1kitNJi6/TKzfnMOpRxSxeDk1/ccvJTAdjRGDIGkT45+b3Q==
"@swc/html-linux-ppc64-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-linux-ppc64-gnu/-/html-linux-ppc64-gnu-1.15.43.tgz#51319cc1a4184b788613e0e556fc33ff77af0c52"
integrity sha512-4oue1pB38/W6mbudp+w0q1jbwxuwdbdbaOj85ay0pisCs213WkgP+MPN8Zqa5VVPjQnVk2CTY9kmEc74XQI/sA==
"@swc/html-linux-ppc64-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-linux-ppc64-gnu/-/html-linux-ppc64-gnu-1.16.2.tgz#6c0d7293a1f7f7631e975c767755eb5236769193"
integrity sha512-6m0vVWHl9MW7cmWKVgKlFW6yhRv0uahMEaDxNIvXrPC3LdbbiiYZui+ryhyQGIYeVps3OMujzUjc0GihNz/afQ==
"@swc/html-linux-s390x-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-linux-s390x-gnu/-/html-linux-s390x-gnu-1.15.43.tgz#a612115a5f2f6c9df438a52ab2cf8bc4fd3865b1"
integrity sha512-/tceMNvAxK70SKUZtcn3X+K0vcElMGk3i8Sz0CmPdtooso8MZ7WfAvVP1qi3TWgh1rpQ3cC+Al3433AHlET6+w==
"@swc/html-linux-s390x-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-linux-s390x-gnu/-/html-linux-s390x-gnu-1.16.2.tgz#853018d7b57377e2f3b23d369a1571862c34d2b9"
integrity sha512-TOlz6wgKyZjg4THJsNZfDz/rAMO+rBa0s2eewTeHEfuJhI+jGu7H6Co6bdbMpN3oyDvTMG7N1f1ktSbkE0erAg==
"@swc/html-linux-x64-gnu@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.15.43.tgz#028980da812e1797316f0a06a758a49aca699318"
integrity sha512-YE7ltlTt5ZFl59GsoHTDrIHnCBY8EDBio66CVj4bqkElFXbE/28xmpVE5ksdGoI5c5aQ/8byUCfHxqzCzQQSVg==
"@swc/html-linux-x64-gnu@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.16.2.tgz#a347921e256f6ecb8847956bbacd4a23e1476adb"
integrity sha512-5EduoVpsnuAAkG9BW8COxcIKAe5swgNAEo+BVkAJCOy1ZMZm0krQYBdvlaDCsGGE9yLDKVPm7rpYIi7vTTZTbA==
"@swc/html-linux-x64-musl@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.15.43.tgz#363ac7ce3866b664db0c35d78a6a90636f280139"
integrity sha512-nS20HmbOk+dEEzdosJqqxAeyjMIiS5yrCAti8LUf0+dgr4eRmjkH4MlkjfPjf49aayR8o+eMJ1jsDZ7whx4zog==
"@swc/html-linux-x64-musl@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.16.2.tgz#ebfbbdb05f2f991a34bc500dce0ca10bfbcdfc3b"
integrity sha512-c0Z84dvBd0oh1ZcBHnM18itmvJFLbCZBKFF2lEDHsGBSLQ/1sPbggEKsVO4KgWkkhwQV2l9AB4jnsw1HrwZJCg==
"@swc/html-win32-arm64-msvc@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.15.43.tgz#d0aa3f99c091577aaaa1aaf51a3695c98278564d"
integrity sha512-Yz7aQQhXT/Yc6QcuMDQDZP9jqf2phkVyU+qSu8ZRWEcJgIorrPL6q7YLqMk+MB5PpZyu5XJEODvc1/UVDE1Kyg==
"@swc/html-win32-arm64-msvc@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.16.2.tgz#69207d99cd80e55fe1ea722c8c8d5174b3d3cd2a"
integrity sha512-Aq7V2B5gS23X59DzV2z892c4NBHYtJbwhvsCjJN1MBMx723htjgNE9KVIJp9dQaJBr2PrNfb/u3QFwnWV2tAoQ==
"@swc/html-win32-ia32-msvc@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.15.43.tgz#e2cfe4dd26ce8b8c5787ca1fdd69ef6b5dc94822"
integrity sha512-muUgfsSQRZk6YBRuhaGKSLvXy0bV9BW6/mHLI0N/06btWuf0hekoHhIzR7dUmS98NXKCA7Hv+buBPE/0vXUwyA==
"@swc/html-win32-ia32-msvc@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.16.2.tgz#9473a17f22c65ec20059533bd8cc78b32c82a711"
integrity sha512-9gslPcsfXxKvAZtOvDkxGuEbM7lqBrONzLAyRsyUtw8KxFcSYkGIO48RDTstGWOkgTgKjjAq/WWqt9qr/NcE3A==
"@swc/html-win32-x64-msvc@1.15.43":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.15.43.tgz#ee1a8a7fe4d928595268c214cf17c72867ee8f0f"
integrity sha512-tuLDy4MxPXsLi6jW+ozCdFWO61AoMMnlhePWJxMafefC2Ojm+iILxP2zI2Hgfu6F16y1q7ITdXdpEuqptu5fHw==
"@swc/html-win32-x64-msvc@1.16.2":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.16.2.tgz#7c7aeaa21b8848a765be2d4f7ee2ee3fecfd0f52"
integrity sha512-Kdb4VdC8FyF5s1MQaFUNeASLckHECrb/oYy/6OCtU+hbgxQ/o/JCgE4uCe8YAg0LCWSOjhx73PCZDGwPf1TpKw==
"@swc/html@^1.15.40":
version "1.15.43"
resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.15.43.tgz#421da1ffc3226d149fd57c73f73755c6e6148427"
integrity sha512-SKbkbdGi9SDO9cTdV+6H0/AYifnb2nDOlz5BlWxlWMXACV3kmX6WwZDo0bBdyGlO/G4jCVWdR5r84qfotU2now==
version "1.16.2"
resolved "https://registry.yarnpkg.com/@swc/html/-/html-1.16.2.tgz#91ee34374e4c926c6c8a8f43561688192b8a1725"
integrity sha512-RmWH8m5dePWDFpHpmFKquZCRe5SyD/Sb0FBPxWcWv/tsjtlJl6oHeaxBsTL2edvaHuW385Fy5nPuTjDD/a+GEA==
dependencies:
"@swc/counter" "^0.1.3"
optionalDependencies:
"@swc/html-darwin-arm64" "1.15.43"
"@swc/html-darwin-x64" "1.15.43"
"@swc/html-linux-arm-gnueabihf" "1.15.43"
"@swc/html-linux-arm64-gnu" "1.15.43"
"@swc/html-linux-arm64-musl" "1.15.43"
"@swc/html-linux-ppc64-gnu" "1.15.43"
"@swc/html-linux-s390x-gnu" "1.15.43"
"@swc/html-linux-x64-gnu" "1.15.43"
"@swc/html-linux-x64-musl" "1.15.43"
"@swc/html-win32-arm64-msvc" "1.15.43"
"@swc/html-win32-ia32-msvc" "1.15.43"
"@swc/html-win32-x64-msvc" "1.15.43"
"@swc/html-darwin-arm64" "1.16.2"
"@swc/html-darwin-x64" "1.16.2"
"@swc/html-linux-arm-gnueabihf" "1.16.2"
"@swc/html-linux-arm64-gnu" "1.16.2"
"@swc/html-linux-arm64-musl" "1.16.2"
"@swc/html-linux-ppc64-gnu" "1.16.2"
"@swc/html-linux-s390x-gnu" "1.16.2"
"@swc/html-linux-x64-gnu" "1.16.2"
"@swc/html-linux-x64-musl" "1.16.2"
"@swc/html-win32-arm64-msvc" "1.16.2"
"@swc/html-win32-ia32-msvc" "1.16.2"
"@swc/html-win32-x64-msvc" "1.16.2"
"@swc/types@^0.1.28":
version "0.1.28"
@@ -6924,9 +6924,9 @@ color-name@~1.1.4:
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
colord@^2.9.3:
version "2.9.3"
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
version "2.10.0"
resolved "https://registry.yarnpkg.com/colord/-/colord-2.10.0.tgz#56c9050e6b06b4b6c62ddec366a48d65ef57e860"
integrity sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==
colorette@^1.2.0:
version "1.4.0"
@@ -8284,36 +8284,11 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
eslint-scope@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
dependencies:
esrecurse "^4.3.0"
estraverse "^4.1.1"
esprima@~4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
dependencies:
estraverse "^5.2.0"
estraverse@^4.1.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-util-attach-comments@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz#344bde6a64c8a31d15231e5ee9e297566a691c2d"
@@ -9671,9 +9646,9 @@ jiti@^1.20.0:
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
joi@^17.9.2:
version "17.13.4"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.4.tgz#ad6153d97ce558eb3a3b593e0d43eab51df1c474"
integrity sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==
version "17.13.7"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.7.tgz#92e212c50dbbbcb1a1592424f84083eb265cc778"
integrity sha512-MF80Dm5Y2veNy8QWVx9Bj3ui4mo7+VPSPsR1M+oaHXV0Gx6zGX9a2F+OZG3Blby9tOlzU9Rs5FUimlEhbKtfnQ==
dependencies:
"@hapi/hoek" "^9.3.0"
"@hapi/topo" "^5.1.0"
@@ -11137,16 +11112,6 @@ minimist@^1.2.0:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
minimizer-webpack-plugin@^5.6.1:
version "5.6.1"
resolved "https://registry.yarnpkg.com/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz#289922a4c96c4ed1ddb76b8a00bd8074e89a2f7f"
integrity sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
jest-worker "^27.4.5"
schema-utils "^4.3.0"
terser "^5.31.1"
minimizer-webpack-plugin@^5.7.0:
version "5.8.0"
resolved "https://registry.yarnpkg.com/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.8.0.tgz#744f0e28da888aa1708e2be32b4ddcf2eeb09e58"
@@ -11576,32 +11541,32 @@ oxc-resolver@11.21.2:
"@oxc-resolver/binding-win32-arm64-msvc" "11.21.2"
"@oxc-resolver/binding-win32-x64-msvc" "11.21.2"
oxfmt@^0.65.0:
version "0.65.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.65.0.tgz#43f38114a6d9ad0952741c13c49f70ff1a4208d6"
integrity sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==
oxfmt@^0.66.0:
version "0.66.0"
resolved "https://registry.yarnpkg.com/oxfmt/-/oxfmt-0.66.0.tgz#4b1e831d31676a2a8fc9e85551115ffa778515a7"
integrity sha512-FfvqR8RFtV6JJpRrpkfqyVCQ7HDvZ/VriWFx7veftCgL1B5ZO9qNr+1rvPieycMQnNfVG0PWyJQiy7p0hq1I5w==
dependencies:
tinypool "2.1.0"
optionalDependencies:
"@oxfmt/binding-android-arm-eabi" "0.65.0"
"@oxfmt/binding-android-arm64" "0.65.0"
"@oxfmt/binding-darwin-arm64" "0.65.0"
"@oxfmt/binding-darwin-x64" "0.65.0"
"@oxfmt/binding-freebsd-x64" "0.65.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.65.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.65.0"
"@oxfmt/binding-linux-arm64-gnu" "0.65.0"
"@oxfmt/binding-linux-arm64-musl" "0.65.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.65.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.65.0"
"@oxfmt/binding-linux-riscv64-musl" "0.65.0"
"@oxfmt/binding-linux-s390x-gnu" "0.65.0"
"@oxfmt/binding-linux-x64-gnu" "0.65.0"
"@oxfmt/binding-linux-x64-musl" "0.65.0"
"@oxfmt/binding-openharmony-arm64" "0.65.0"
"@oxfmt/binding-win32-arm64-msvc" "0.65.0"
"@oxfmt/binding-win32-ia32-msvc" "0.65.0"
"@oxfmt/binding-win32-x64-msvc" "0.65.0"
"@oxfmt/binding-android-arm-eabi" "0.66.0"
"@oxfmt/binding-android-arm64" "0.66.0"
"@oxfmt/binding-darwin-arm64" "0.66.0"
"@oxfmt/binding-darwin-x64" "0.66.0"
"@oxfmt/binding-freebsd-x64" "0.66.0"
"@oxfmt/binding-linux-arm-gnueabihf" "0.66.0"
"@oxfmt/binding-linux-arm-musleabihf" "0.66.0"
"@oxfmt/binding-linux-arm64-gnu" "0.66.0"
"@oxfmt/binding-linux-arm64-musl" "0.66.0"
"@oxfmt/binding-linux-ppc64-gnu" "0.66.0"
"@oxfmt/binding-linux-riscv64-gnu" "0.66.0"
"@oxfmt/binding-linux-riscv64-musl" "0.66.0"
"@oxfmt/binding-linux-s390x-gnu" "0.66.0"
"@oxfmt/binding-linux-x64-gnu" "0.66.0"
"@oxfmt/binding-linux-x64-musl" "0.66.0"
"@oxfmt/binding-openharmony-arm64" "0.66.0"
"@oxfmt/binding-win32-arm64-msvc" "0.66.0"
"@oxfmt/binding-win32-ia32-msvc" "0.66.0"
"@oxfmt/binding-win32-x64-msvc" "0.66.0"
oxlint-tsgolint@^7.0.2001:
version "7.0.2001"
@@ -11615,30 +11580,30 @@ oxlint-tsgolint@^7.0.2001:
"@oxlint-tsgolint/win32-arm64" "7.0.2001"
"@oxlint-tsgolint/win32-x64" "7.0.2001"
oxlint@^1.80.0:
version "1.80.0"
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.80.0.tgz#228271087d3f04e391e383ccdc0e840458d8b653"
integrity sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==
oxlint@^1.81.0:
version "1.81.0"
resolved "https://registry.yarnpkg.com/oxlint/-/oxlint-1.81.0.tgz#7b20ada29a171883de4517d041ea5b057fb48ab5"
integrity sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==
optionalDependencies:
"@oxlint/binding-android-arm-eabi" "1.80.0"
"@oxlint/binding-android-arm64" "1.80.0"
"@oxlint/binding-darwin-arm64" "1.80.0"
"@oxlint/binding-darwin-x64" "1.80.0"
"@oxlint/binding-freebsd-x64" "1.80.0"
"@oxlint/binding-linux-arm-gnueabihf" "1.80.0"
"@oxlint/binding-linux-arm-musleabihf" "1.80.0"
"@oxlint/binding-linux-arm64-gnu" "1.80.0"
"@oxlint/binding-linux-arm64-musl" "1.80.0"
"@oxlint/binding-linux-ppc64-gnu" "1.80.0"
"@oxlint/binding-linux-riscv64-gnu" "1.80.0"
"@oxlint/binding-linux-riscv64-musl" "1.80.0"
"@oxlint/binding-linux-s390x-gnu" "1.80.0"
"@oxlint/binding-linux-x64-gnu" "1.80.0"
"@oxlint/binding-linux-x64-musl" "1.80.0"
"@oxlint/binding-openharmony-arm64" "1.80.0"
"@oxlint/binding-win32-arm64-msvc" "1.80.0"
"@oxlint/binding-win32-ia32-msvc" "1.80.0"
"@oxlint/binding-win32-x64-msvc" "1.80.0"
"@oxlint/binding-android-arm-eabi" "1.81.0"
"@oxlint/binding-android-arm64" "1.81.0"
"@oxlint/binding-darwin-arm64" "1.81.0"
"@oxlint/binding-darwin-x64" "1.81.0"
"@oxlint/binding-freebsd-x64" "1.81.0"
"@oxlint/binding-linux-arm-gnueabihf" "1.81.0"
"@oxlint/binding-linux-arm-musleabihf" "1.81.0"
"@oxlint/binding-linux-arm64-gnu" "1.81.0"
"@oxlint/binding-linux-arm64-musl" "1.81.0"
"@oxlint/binding-linux-ppc64-gnu" "1.81.0"
"@oxlint/binding-linux-riscv64-gnu" "1.81.0"
"@oxlint/binding-linux-riscv64-musl" "1.81.0"
"@oxlint/binding-linux-s390x-gnu" "1.81.0"
"@oxlint/binding-linux-x64-gnu" "1.81.0"
"@oxlint/binding-linux-x64-musl" "1.81.0"
"@oxlint/binding-openharmony-arm64" "1.81.0"
"@oxlint/binding-win32-arm64-msvc" "1.81.0"
"@oxlint/binding-win32-ia32-msvc" "1.81.0"
"@oxlint/binding-win32-x64-msvc" "1.81.0"
p-cancelable@^3.0.0:
version "3.0.0"
@@ -14219,9 +14184,9 @@ svg-parser@^2.0.4:
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
svgo@^3.0.2, svgo@^3.2.0:
version "3.3.4"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.4.tgz#fd2aa10ff585b3bd2b83ce3602f5582bc0718bb5"
integrity sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==
version "3.3.5"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.5.tgz#8a3d9557ab2f386eca7e24760385849554985a1c"
integrity sha512-8SQMzdrvWaD8deUmrnYB+ASyxBVgWUOilg+A75nE/76WdLpj6LopCwiAVvkzkcqy/9b7t2Mg7faFLjg0ZRcZ3w==
dependencies:
commander "^7.2.0"
css-select "^5.1.0"
@@ -15163,10 +15128,10 @@ webpack-virtual-modules@^0.6.2:
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webpack@^5.110.1:
version "5.110.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.110.1.tgz#d662d8ff1866fcb58a6b8af09c86bc6f1b9c5c1d"
integrity sha512-gInQB+jxXxgnZyvPwuzT5NGQmECDqeu85oxcrjinrYHqPoBex0hCAN2SFTJVyPVrK0Pq9E44VFP+e89fAc10/w==
webpack@^5.110.3, webpack@^5.88.1, webpack@^5.95.0:
version "5.110.3"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.110.3.tgz#e122b66f6226b7af8f209b6102cd13238c4813a8"
integrity sha512-GuizBzRvo9YPpyoNMf3ag7AzxbaW85qrRSqTha345KyJbAFPt3/cMzBM0h+RWg7SK/7DdzRLINP3LvQ0hvr4hg==
dependencies:
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
@@ -15188,32 +15153,6 @@ webpack@^5.110.1:
watchpack "^2.5.2"
webpack-sources "^3.5.1"
webpack@^5.88.1, webpack@^5.95.0:
version "5.109.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.109.2.tgz#b58dc289561c3282db35c210a99379a836a4c28d"
integrity sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==
dependencies:
"@types/estree" "^1.0.8"
"@types/json-schema" "^7.0.15"
"@webassemblyjs/ast" "^1.14.1"
"@webassemblyjs/wasm-edit" "^1.14.1"
"@webassemblyjs/wasm-parser" "^1.14.1"
acorn "^8.16.0"
browserslist "^4.28.1"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.24.4"
es-module-lexer "^2.1.0"
eslint-scope "5.1.1"
events "^3.2.0"
graceful-fs "^4.2.11"
mime-db "^1.54.0"
minimizer-webpack-plugin "^5.6.1"
neo-async "^2.6.2"
schema-utils "^4.3.3"
tapable "^2.3.0"
watchpack "^2.5.2"
webpack-sources "^3.5.1"
webpackbar@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-7.0.0.tgz#7228d32881af2392381b6514499ddea73cdf218a"
+6 -6
View File
@@ -50,11 +50,11 @@ dependencies = [
"flask-cors>=6.0.5, <7.0",
"croniter>=6.2.4",
"cron-descriptor",
"cryptography>=50.0.0, <51.0.0",
"cryptography>=50.0.1, <51.0.0",
"deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <4.0.0",
"flask-appbuilder>=5.2.2, <6.0.0",
"flask-caching>=2.4.1, <3",
"flask-caching>=2.5.0, <3",
"flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0",
"flask-login>=0.6.0, < 1.0",
@@ -96,7 +96,7 @@ dependencies = [
"pgsanity",
"Pillow>=12.3.0, <13", # raise floor to match resolved pin; closes SCA false-positive on 11.x-range CVEs already fixed in 12.3.0
"polyline>=2.0.4, <3.0",
"pydantic>=2.8.0",
"pydantic>=2.13.5",
"pyparsing>=3.3.2, <4",
"python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
@@ -142,7 +142,7 @@ bigquery = [
# 1.17.1 is likely the final release: googleapis/python-bigquery-sqlalchemy
# was archived 2026-05-16. Both 1.17.0 and 1.17.1 support SQLAlchemy 1.4/2.0.
"sqlalchemy-bigquery>=1.17.2",
"google-cloud-bigquery>=3.42.3",
"google-cloud-bigquery>=3.44.0",
]
clickhouse = ["clickhouse-connect>=1.7.2, <2.0"]
# The `cockroachdb` PyPI package (last released 2021) is abandoned and its
@@ -273,7 +273,7 @@ tdengine = [
"taospy>=2.8.10",
"taos-ws-py>=0.7.0"
]
teradata = ["teradatasql>=20.0.0.66"]
teradata = ["teradatasql>=20.0.0.67"]
thumbnails = [] # deprecated, will be removed in 7.0
vertica = ["sqlalchemy-vertica-python>= 0.6.3, < 0.7"]
netezza = ["nzalchemy>= 11.1.2, < 11.2"]
@@ -288,7 +288,7 @@ development = [
"docker",
"flask-testing",
"freezegun",
"grpcio>=1.82.1",
"grpcio>=1.83.1",
"openapi-spec-validator",
"parameterized",
"pip",
+1 -1
View File
@@ -26,7 +26,7 @@ filelock>=3.20.3,<4.0.0
brotli>=1.2.0,<2.0.0
numexpr>=2.9.0
# Security: CVE-2026-34073 (MEDIUM) - Improper Certificate Validation
cryptography>=50.0.0,<51.0.0
cryptography>=50.0.1,<51.0.0
# Security: Snyk - XSS vulnerability in Mako templates
mako>=1.4.1,<2.0.0
# Security: CVE-2024-52338 (CRITICAL) - Deserialization of untrusted data in IPC/Parquet readers
+8 -7
View File
@@ -40,7 +40,7 @@ brotli==1.2.0
# via
# -r requirements/base.in
# flask-compress
cachelib==0.13.0
cachelib==0.17.0
# via
# flask-caching
# flask-session
@@ -84,7 +84,7 @@ cron-descriptor==1.4.5
# via apache-superset (pyproject.toml)
croniter==6.2.4
# via apache-superset (pyproject.toml)
cryptography==50.0.0
cryptography==50.0.1
# via
# -r requirements/base.in
# apache-superset (pyproject.toml)
@@ -105,7 +105,7 @@ et-xmlfile==2.0.0
# via openpyxl
filelock==3.20.3
# via -r requirements/base.in
flask==2.3.3
flask==3.1.3
# via
# apache-superset (pyproject.toml)
# flask-appbuilder
@@ -124,9 +124,9 @@ flask-appbuilder==5.2.2
# via
# apache-superset (pyproject.toml)
# apache-superset-core
flask-babel==3.1.0
flask-babel==4.0.0
# via flask-appbuilder
flask-caching==2.4.1
flask-caching==2.5.0
# via apache-superset (pyproject.toml)
flask-compress==1.24
# via apache-superset (pyproject.toml)
@@ -218,6 +218,7 @@ markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via
# flask
# jinja2
# mako
# werkzeug
@@ -297,11 +298,11 @@ pyasn1-modules==0.4.2
# via google-auth
pycparser==2.22
# via cffi
pydantic==2.13.4
pydantic==2.13.5
# via
# apache-superset (pyproject.toml)
# apache-superset-core
pydantic-core==2.46.4
pydantic-core==2.46.5
# via pydantic
pygeohash==3.2.2
# via apache-superset (pyproject.toml)
+10 -9
View File
@@ -94,7 +94,7 @@ brotli==1.2.0
# via
# -c requirements/base-constraint.txt
# flask-compress
cachelib==0.13.0
cachelib==0.17.0
# via
# -c requirements/base-constraint.txt
# flask-caching
@@ -179,7 +179,7 @@ croniter==6.2.4
# via
# -c requirements/base-constraint.txt
# apache-superset
cryptography==50.0.0
cryptography==50.0.1
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -247,7 +247,7 @@ filelock==3.20.3
# via
# -c requirements/base-constraint.txt
# virtualenv
flask==2.3.3
flask==3.1.3
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -269,11 +269,11 @@ flask-appbuilder==5.2.2
# -c requirements/base-constraint.txt
# apache-superset
# apache-superset-core
flask-babel==3.1.0
flask-babel==4.0.0
# via
# -c requirements/base-constraint.txt
# flask-appbuilder
flask-caching==2.4.1
flask-caching==2.5.0
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -360,7 +360,7 @@ google-auth-oauthlib==1.2.1
# via
# pandas-gbq
# pydata-google-auth
google-cloud-bigquery==3.43.0
google-cloud-bigquery==3.45.0
# via
# apache-superset
# pandas-gbq
@@ -384,7 +384,7 @@ greenlet==3.5.5
# sqlalchemy
griffelib==2.0.2
# via fastmcp-slim
grpcio==1.83.0
grpcio==1.83.1
# via
# apache-superset
# google-api-core
@@ -524,6 +524,7 @@ markdown-it-py==3.0.0
markupsafe==3.0.2
# via
# -c requirements/base-constraint.txt
# flask
# jinja2
# mako
# werkzeug
@@ -731,7 +732,7 @@ pycparser==2.22
# via
# -c requirements/base-constraint.txt
# cffi
pydantic==2.13.4
pydantic==2.13.5
# via
# -c requirements/base-constraint.txt
# apache-superset
@@ -740,7 +741,7 @@ pydantic==2.13.4
# mcp
# openapi-pydantic
# pydantic-settings
pydantic-core==2.46.4
pydantic-core==2.46.5
# via
# -c requirements/base-constraint.txt
# pydantic
+8 -8
View File
@@ -17,13 +17,13 @@
"@babel/core": "^8.0.1",
"@babel/preset-env": "^8.0.1",
"@babel/preset-typescript": "^8.0.1",
"@types/node": "^26.4.0",
"@types/node": "^26.4.1",
"babel-loader": "^10.1.1",
"jsdom": "^30.0.1",
"tscw-config": "^1.1.2",
"typescript": "^7.0.2",
"vitest": "^4.0.18",
"webpack": "^5.110.0",
"webpack": "^5.110.3",
"webpack-cli": "^7.2.3"
}
},
@@ -2184,9 +2184,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "26.4.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz",
"integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==",
"version": "26.4.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz",
"integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5260,9 +5260,9 @@
}
},
"node_modules/webpack": {
"version": "5.110.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.1.tgz",
"integrity": "sha512-gInQB+jxXxgnZyvPwuzT5NGQmECDqeu85oxcrjinrYHqPoBex0hCAN2SFTJVyPVrK0Pq9E44VFP+e89fAc10/w==",
"version": "5.110.3",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.3.tgz",
"integrity": "sha512-GuizBzRvo9YPpyoNMf3ag7AzxbaW85qrRSqTha345KyJbAFPt3/cMzBM0h+RWg7SK/7DdzRLINP3LvQ0hvr4hg==",
"dev": true,
"license": "MIT",
"dependencies": {
+2 -2
View File
@@ -41,13 +41,13 @@
"@babel/core": "^8.0.1",
"@babel/preset-env": "^8.0.1",
"@babel/preset-typescript": "^8.0.1",
"@types/node": "^26.4.0",
"@types/node": "^26.4.1",
"babel-loader": "^10.1.1",
"jsdom": "^30.0.1",
"tscw-config": "^1.1.2",
"typescript": "^7.0.2",
"vitest": "^4.0.18",
"webpack": "^5.110.0",
"webpack": "^5.110.3",
"webpack-cli": "^7.2.3"
},
"repository": {
+785 -817
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -193,7 +193,7 @@
"nanoid": "^6.0.1",
"ol": "^10.10.0",
"postcss": "^8.5.26",
"query-string": "9.5.0",
"query-string": "9.5.1",
"re-resizable": "^6.11.2",
"react": "^18.3.0",
"react-arborist": "^3.16.0",
@@ -280,7 +280,7 @@
"@types/json-bigint": "^1.0.4",
"@types/lodash-es": "^4.17.12",
"@types/mousetrap": "^1.6.15",
"@types/node": "^26.4.0",
"@types/node": "^26.4.1",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react-loadable": "^5.5.11",
@@ -292,9 +292,9 @@
"@types/rison": "0.1.0",
"@types/tinycolor2": "^1.4.3",
"@types/unzipper": "^0.10.11",
"@typescript-eslint/eslint-plugin": "^8.68.0",
"@typescript-eslint/eslint-plugin": "^8.69.0",
"@typescript-eslint/parser": "^8.63.0",
"babel-jest": "^30.5.0",
"babel-jest": "^30.5.1",
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
@@ -320,8 +320,8 @@
"history": "^5.3.0",
"html-webpack-plugin": "^5.6.8",
"imports-loader": "^5.0.0",
"jest": "^30.5.0",
"jest-environment-jsdom": "^30.5.0",
"jest": "^30.5.1",
"jest-environment-jsdom": "^30.5.1",
"jest-html-reporter": "^4.4.0",
"jest-websocket-mock": "^2.5.0",
"js-yaml-loader": "^1.2.2",
@@ -331,7 +331,7 @@
"mini-css-extract-plugin": "^2.10.2",
"minimizer-webpack-plugin": "^5.8.0",
"open-cli": "^9.0.0",
"oxfmt": "^0.65.0",
"oxfmt": "^0.66.0",
"oxlint": "^1.80.0",
"po2json": "^0.4.5",
"postcss-styled-syntax": "^0.7.2",
@@ -349,11 +349,11 @@
"swc-loader": "^0.2.7",
"ts-jest": "^29.4.12",
"tscw-config": "^1.1.2",
"tsx": "^4.23.12",
"tsx": "^4.23.13",
"typescript": "5.4.5",
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
"webpack": "^5.110.1",
"webpack": "^5.110.3",
"webpack-bundle-analyzer": "^5.3.2",
"webpack-cli": "^7.2.3",
"webpack-dev-server": "^6.0.0",
@@ -39,7 +39,7 @@
"devDependencies": {
"cross-env": "^10.1.0",
"fs-extra": "^11.4.0",
"jest": "^30.5.0",
"jest": "^30.5.1",
"yeoman-test": "^11.6.0"
},
"engines": {
@@ -20,7 +20,3 @@
export * from './TranslatorSingleton';
export * from './types';
export { default as Translator } from './Translator';
export default {};
export { default as __hack_reexport_translation } from './types';
@@ -18,7 +18,7 @@
*/
import { Jed as BaseJed, JedOptions, DomainData, Translations } from './jed';
export { Translations, DomainConfig, DomainData, JedOptions } from './jed';
export type { Translations, DomainConfig, DomainData, JedOptions } from './jed';
/**
* Superset supported languages.
@@ -86,5 +86,3 @@ export interface TranslatorConfig {
* Key-value mapping of translation key and the translations.
*/
export type LocaleData = Partial<Record<Locale, Translations>>;
export default {};
@@ -52,5 +52,3 @@ export interface Jed {
options: JedOptions;
}
export default {};
@@ -34,7 +34,7 @@
"tinycolor2": "*"
},
"peerDependencies": {
"@ant-design/icons": "^5.6.1 || ^6.0.0",
"@ant-design/icons": "^5.6.1",
"@emotion/react": "^11.4.1",
"@superset-ui/core": "*",
"@testing-library/dom": "^10.4.1",
@@ -537,8 +537,6 @@ export type ResolvedColorFormatterResult = {
color?: string;
};
export default {};
export function isColumnMeta(column: AnyDict): column is ColumnMeta {
return !!column && 'column_name' in column;
}
@@ -49,7 +49,7 @@
"access": "public"
},
"dependencies": {
"@ant-design/icons": "^6.3.2",
"@ant-design/icons": "^6.3.4",
"@apache-superset/core": "*",
"@babel/runtime": "^7.29.7",
"@braintree/sanitize-url": "^7.1.2",
@@ -78,9 +78,9 @@
"math-expression-evaluator": "^2.0.7",
"parse-ms": "^4.0.0",
"re-resizable": "^6.11.2",
"react-ace": "^14.0.1",
"react-ace": "^15.0.0",
"react-draggable": "^4.7.1",
"react-error-boundary": "^6.1.3",
"react-error-boundary": "^6.1.4",
"react-js-cron": "^6.0.2",
"react-markdown": "^10.1.0",
"react-resize-detector": "^7.1.2",
@@ -103,7 +103,7 @@
"@types/d3-time-format": "^4.0.3",
"@types/jquery": "^4.0.1",
"@types/lodash": "^4.17.25",
"@types/node": "^26.4.0",
"@types/node": "^26.4.1",
"@types/prop-types": "^15.7.15",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-table": "^7.7.20",
@@ -566,6 +566,104 @@ test('should NOT refetch data when other string-based renderTrigger controls cha
});
});
test('should NOT refetch data when echart_options (string-based renderTrigger control) changes', async () => {
// Matches how the Timeseries/MixedTimeseries control panels reference this
// shared control: a bare string, e.g. ['echart_options'].
const controlPanelConfig = {
controlPanelSections: [
{
controlSetRows: [['echart_options']],
},
],
};
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const formDataWithEchartOptions = {
...mockFormData,
echart_options: '{}',
};
const { rerender, getByTestId } = render(
<StatefulChart
formData={formDataWithEchartOptions}
chartType="test_chart"
/>,
);
await waitFor(() => {
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
});
// Edit the ECharts Options field (e.g. from the Customize tab while the
// chart is part of a Matrixify grid cell).
const updatedFormData = {
...formDataWithEchartOptions,
echart_options: '{"title": {"text": "My Chart"}}',
};
rerender(<StatefulChart formData={updatedFormData} chartType="test_chart" />);
await waitFor(() => {
// Should NOT refetch data - echart_options is a renderTrigger control
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
// But should re-render with the new formData
expect(getByTestId('super-chart')).toHaveTextContent(
JSON.stringify(updatedFormData),
);
});
});
test('should refetch when a chart overrides a shared renderTrigger control to renderTrigger: false', async () => {
// Matches Country Map's controlPanel.controlOverrides, which sets
// linear_color_scheme to renderTrigger: false because it drives the
// choropleth data query rather than just styling.
const controlPanelConfig = {
controlPanelSections: [
{
controlSetRows: [['linear_color_scheme']],
},
],
controlOverrides: {
linear_color_scheme: {
renderTrigger: false,
},
},
};
jest.mocked(getChartControlPanelRegistry).mockReturnValue({
get: jest.fn().mockReturnValue(controlPanelConfig),
} as unknown as ReturnType<typeof getChartControlPanelRegistry>);
const formDataWithColorScheme = {
...mockFormData,
linear_color_scheme: 'schemeA',
};
const { rerender } = render(
<StatefulChart formData={formDataWithColorScheme} chartType="test_chart" />,
);
await waitFor(() => {
expect(mockChartClient.client.post).toHaveBeenCalledTimes(1);
});
const updatedFormData = {
...formDataWithColorScheme,
linear_color_scheme: 'schemeB',
};
rerender(<StatefulChart formData={updatedFormData} chartType="test_chart" />);
await waitFor(() => {
// Should refetch because this chart's controlOverrides mark the control
// as data-affecting, overriding the shared-control fallback.
expect(mockChartClient.client.post).toHaveBeenCalledTimes(2);
});
});
test('should refetch when string control is NOT in RENDER_TRIGGER_SHARED_CONTROLS', async () => {
// Control panel with a string control that is NOT in the renderTrigger set
const controlPanelConfig = {
@@ -50,6 +50,9 @@ type LoadingState = 'uninitialized' | 'loading' | 'loaded' | 'error';
* This list is needed because string-based control references (e.g., ['zoomable'])
* cannot be introspected for their renderTrigger property without importing
* sharedControls, which would create a circular dependency.
*
* Keep this list in sync with the `renderTrigger: true` entries in
* @superset-ui/chart-controls's sharedControls.tsx.
*/
const RENDER_TRIGGER_SHARED_CONTROLS = new Set([
'zoomable',
@@ -57,6 +60,11 @@ const RENDER_TRIGGER_SHARED_CONTROLS = new Set([
'time_shift_color',
'y_axis_format',
'currency_format',
'color_picker',
'linear_color_scheme',
'x_axis_time_format',
'x_axis_number_format',
'echart_options',
]);
/**
@@ -111,6 +119,28 @@ function shouldRefetchData(
}
});
// Individual chart types can override a shared control's renderTrigger
// behavior (e.g., Country Map sets `linear_color_scheme` to
// renderTrigger: false because it drives the choropleth query, not just
// styling). Apply those overrides on top of the shared-control fallback
// so such controls still trigger a refetch for that chart type.
const { controlOverrides } = controlPanel;
if (controlOverrides) {
Object.entries(controlOverrides).forEach(([controlName, override]) => {
if (
override &&
typeof override === 'object' &&
'renderTrigger' in override
) {
if ((override as { renderTrigger?: boolean }).renderTrigger) {
renderTriggerControls.add(controlName);
} else {
renderTriggerControls.delete(controlName);
}
}
});
}
// Check which fields changed
const changedFields = Object.keys(nextFormData).filter(
key =>
@@ -44,7 +44,3 @@ export * from './types/TransformFunction';
export * from './types/QueryResponse';
export * from './types/VizType';
export * from './types/matrixify';
export { default as __hack_reexport_chart_Base } from './types/Base';
export { default as __hack_reexport_chart_TransformFunction } from './types/TransformFunction';
export { default as __hack_reexport_chart_QueryResponse } from './types/QueryResponse';
@@ -113,8 +113,19 @@ export interface BackendOwnState {
* Each chart plugin can implement this to convert its internal state representation
* to the standardized backend format.
*/
export interface ChartStateConverterOptions {
// Set when converting for a download/export query rather than the chart's
// live (re-)query. Some chart-specific state (e.g. AG Grid's client-side
// sort/filter) is normally excluded from the live query's ownState to
// avoid triggering an unnecessary requery, but a downloaded file has no
// client-side pass to apply that state, so it still needs to be converted
// for exports to reproduce the displayed view.
forExport?: boolean;
}
export type ChartStateConverter<TChartState = JsonObject> = (
chartState: TChartState,
options?: ChartStateConverterOptions,
) => Partial<BackendOwnState>;
export interface PlainObject {
@@ -153,5 +164,3 @@ export enum AxisType {
export interface LegendState {
[key: string]: boolean;
}
export default {};
@@ -48,5 +48,3 @@ export type LegacyQueryData = PlainObject;
* Don't use this for a specific chart (since you know which API it uses already).
*/
export type QueryData = LegacyQueryData | ChartDataResponseResult;
export default {};
@@ -50,5 +50,3 @@ export type BuildQueryFunction<T extends QueryFormData> = (
};
},
) => QueryContext;
export default {};
@@ -604,6 +604,61 @@ test('cleans up event listeners on unmount', async () => {
offSpy.mockRestore();
});
test('re-applies annotations only when their content actually changes across renders (react-ace 15 fast-equals regression guard)', async () => {
// react-ace's componentDidUpdate decides whether to call
// session.setAnnotations() by deep-comparing the new/old `annotations`
// prop (lib/ace.js, using an internal deep-equality helper -- lodash's
// isEqual through react-ace 14.x, fast-equals's deepEqual from 15.0.0
// onward). Superset's own AceEditorProvider/EditorWrapper always pass a
// freshly `.map()`-derived annotations array on every render, so this
// guards the actual behavior Superset relies on: a same-content-but-
// different-reference array must NOT re-trigger setAnnotations (or the
// editor would thrash on every keystroke-driven re-render), while a
// genuinely different array must still update the editor.
const ref = createRef<AceEditor>();
const annotationsV1 = [{ row: 0, column: 0, type: 'error', text: 'oops' }];
const { rerender, container } = render(
<SQLEditor ref={ref as React.Ref<never>} annotations={annotationsV1} />,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
const session = ref.current?.editor?.getSession();
expect(session).toBeDefined();
if (!session) return;
// The initial mount already applies annotations via componentDidMount,
// not componentDidUpdate, so start observing only from the first update.
const setAnnotationsSpy = jest.spyOn(session, 'setAnnotations');
// Same content, new array/object references -- must be a no-op.
const annotationsV1SameContent = [
{ row: 0, column: 0, type: 'error', text: 'oops' },
];
rerender(
<SQLEditor
ref={ref as React.Ref<never>}
annotations={annotationsV1SameContent}
/>,
);
expect(setAnnotationsSpy).not.toHaveBeenCalled();
// Genuinely different content -- must update, with the new value.
const annotationsV2 = [
{ row: 1, column: 2, type: 'warning', text: 'different' },
];
rerender(
<SQLEditor ref={ref as React.Ref<never>} annotations={annotationsV2} />,
);
expect(setAnnotationsSpy).toHaveBeenCalledTimes(1);
expect(setAnnotationsSpy).toHaveBeenCalledWith(annotationsV2);
setAnnotationsSpy.mockRestore();
});
test('does not move autocomplete popup if target container is document.body', async () => {
const ref = createRef<AceEditor>();
const { container } = render(<SQLEditor ref={ref as React.Ref<never>} />);
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { CSSProperties } from 'react';
import { screen, render } from '@superset-ui/core/spec';
import { Button, DropdownContainer, Icons } from '..';
@@ -178,3 +179,73 @@ test('component renders and functions without throwing errors', () => {
// Basic functionality test
expect(screen.getByText('Element 1')).toBeInTheDocument();
});
const ITEM_WIDTH = 100;
/* Width the flex layout leaves the row once the trigger button is laid out. */
const ROW_WIDTH = 250;
/* Item count of the transient row that holds every item while remeasuring. */
const ALL_ITEMS = 4;
/**
* Lays items out at ITEM_WIDTH each. In the steady state the row is reported at
* the flex-bounded ROW_WIDTH, so a three-item row overflows and the trigger
* shows. In the all-items frame the row is reported at its own content width
* instead, which is the frame Edge can paint before the flex layout bounds the
* row and its children spill out. `onRowMeasure` receives the row during that
* frame so a test can assert how it is styled.
*/
const mockBoundingRects = (onRowMeasure: (row: HTMLElement) => void) => {
const getBoundingClientRect: (this: HTMLElement) => DOMRect = function () {
let right: number;
if (this.dataset.test === 'container') {
const allItemsFrame = this.children.length === ALL_ITEMS;
right = allItemsFrame ? this.children.length * ITEM_WIDTH : ROW_WIDTH;
if (allItemsFrame) {
onRowMeasure(this);
}
} else {
const itemNumber = Number(this.textContent?.match(/Element (\d+)/)?.[1]);
right = itemNumber ? itemNumber * ITEM_WIDTH : ROW_WIDTH;
}
return {
bottom: 0,
height: 0,
left: right - ITEM_WIDTH,
right,
top: 0,
width: ITEM_WIDTH,
x: right - ITEM_WIDTH,
y: 0,
toJSON: () => ({}),
};
};
jest
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(getBoundingClientRect);
};
/* Grows an overflowing row by one item, which mounts every item for a frame
* while the new overflow index is calculated. */
const remeasureWithExtraItem = (style?: CSSProperties) => {
const { rerender } = render(<DropdownContainer items={generateItems(3)} />);
rerender(<DropdownContainer items={generateItems(3)} />);
expect(screen.getByTestId('dropdown-container-btn')).toBeInTheDocument();
rerender(
<DropdownContainer items={generateItems(ALL_ITEMS)} style={style} />,
);
};
test('clips the item row while remeasuring, then restores it', () => {
const measured: string[] = [];
mockBoundingRects(row => {
measured.push(row.style.overflow);
});
remeasureWithExtraItem({ overflow: 'visible' });
/* While the row holds every item its children can spill past its own box, so
* clipping wins over the consumer's `overflow: visible` for that frame. Once
* the new overflow index is applied the consumer's value comes back. */
expect(measured[0]).toBe('hidden');
expect(measured.at(-1)).toBe('visible');
});
@@ -330,7 +330,18 @@ export const DropdownContainer = forwardRef(
min-width: 0px;
`}
data-test="container"
style={style}
style={
recalculating
? {
...style,
/* The flex layout already bounds the row's own box, but its
* children can spill past that box in the frame Edge paints
* before the new overflow index is applied. Clipping keeps
* them inside the filter bar. */
overflow: 'hidden',
}
: style
}
>
{notOverflowedItems.map(item => item.element)}
</div>
@@ -0,0 +1,102 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Icons } from '../Icons';
import { Button } from '../Button';
import MetadataBar, { MetadataType } from '../MetadataBar';
import { Menu } from '../Menu';
import { PageHeaderWithActions, PageHeaderWithActionsProps } from '.';
export default {
title: 'Design System/Components/PageHeaderWithActions',
component: PageHeaderWithActions,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component:
'Header used on entity pages (e.g. the dashboard page) combining an editable title with badges, a metadata bar, and page-level actions.',
},
},
},
};
// Mirrors src/dashboard/components/Header's real composition: an editable
// title, the certified badge, and a titlePanelAdditionalItems cluster of a
// refresh button, an auto-refresh indicator, a published-status toggle, and
// a MetadataBar (Last Modified + Editor) -- the same items the real
// dashboard header packs into that space -- so this story reproduces the
// header's real narrow-viewport layout behavior, not just the isolated
// MetadataBar. The real RefreshButton/AutoRefreshIndicator/PublishedStatus
// components live in src/dashboard and depend on this package, so they
// can't be imported here without inverting that dependency; these are
// same-sized stand-ins built from core components instead.
export const DashboardHeader = (args: PageHeaderWithActionsProps) => (
<PageHeaderWithActions {...args} />
);
DashboardHeader.args = {
editableTitleProps: {
title: 'Q3 Executive Revenue and Growth Overview Dashboard',
placeholder: 'Add the name of the dashboard',
onSave: () => {},
canEdit: true,
label: 'Dashboard title',
},
showTitlePanelItems: true,
certificatiedBadgeProps: {
certifiedBy: 'Jane Doe',
details: 'Certified by the BI team',
},
showFaveStar: true,
faveStarProps: { itemId: 1, saveFaveStar: () => {}, isStarred: false },
titlePanelAdditionalItems: [
<Button key="refresh-button" buttonStyle="link" tooltip="Refresh dashboard">
<Icons.ReloadOutlined iconSize="l" />
</Button>,
<Icons.SyncOutlined key="auto-refresh-indicator" iconSize="l" />,
<Button key="published-status" buttonStyle="link">
Published
</Button>,
<MetadataBar
key="metadata-bar"
tooltipPlacement="bottom"
items={[
{
type: MetadataType.LastModified,
value: '2 hours ago',
modifiedBy: 'Jane Doe',
},
{
type: MetadataType.Editor,
createdBy: 'Jane Doe',
editors: ['Jane Doe', 'John Smith'],
createdOn: 'a week ago',
},
]}
/>,
],
rightPanelAdditionalItems: <button type="button">Edit dashboard</button>,
additionalActionsMenu: (
<Menu
items={[{ label: 'Edit properties', key: '1' }]}
data-test="additional-actions-menu"
/>
),
menuDropdownProps: {},
};
@@ -18,7 +18,12 @@
*/
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { PageHeaderWithActions, PageHeaderWithActionsProps } from './index';
import { supersetTheme } from '@apache-superset/core/theme';
import {
buttonsStyles,
PageHeaderWithActions,
PageHeaderWithActionsProps,
} from './index';
import { Menu } from '../Menu';
const defaultProps: PageHeaderWithActionsProps = {
@@ -54,3 +59,15 @@ test('Renders', async () => {
await userEvent.click(screen.getByLabelText('Menu actions trigger'));
expect(defaultProps.menuDropdownProps.onOpenChange).toHaveBeenCalled();
});
test('clips the title panel buttons/metadata cluster instead of letting it overflow into the actions menu', () => {
// jsdom doesn't compute real flexbox layout, so it can't verify the
// overlap itself is fixed; this guards the underlying CSS from
// regressing instead. Without `overflow: hidden`, this wrapper's
// automatic flex minimum size is based on its content rather than 0, so
// it refuses to shrink -- forcing the title to absorb all the space
// pressure until the cluster's content renders outside its box and
// overlaps the actions menu once the title has fully collapsed.
const { styles } = buttonsStyles(supersetTheme);
expect(styles).toMatch(/overflow:\s*hidden/);
});
@@ -99,9 +99,13 @@ const headerStyles = (theme: SupersetTheme) => css`
}
`;
const buttonsStyles = (theme: SupersetTheme) => css`
// Exported only so PageHeaderWithActions.test.tsx can assert on the
// `overflow: hidden` declaration directly; not part of the component's
// public API.
export const buttonsStyles = (theme: SupersetTheme) => css`
display: flex;
align-items: center;
overflow: hidden;
padding-left: ${theme.sizeUnit * 2}px;
& .anticon-star {
@@ -222,6 +222,7 @@ export type {
GridState,
GridReadyEvent,
CellClickedEvent,
CellContextMenuEvent,
CellKeyDownEvent,
CellClassParams,
IMenuActionParams,
@@ -26,4 +26,3 @@ export type { NormalizeOptions } from './normalizeBackendUrls';
export * from './types';
export * from './constants';
export { default as __hack_reexport_connection } from './types';
@@ -176,5 +176,3 @@ export interface SupersetClientInterface extends Pick<
}
export type SupersetClientResponse = Response | JsonResponse | TextResponse;
export default {};
@@ -24,4 +24,3 @@ export { default as mergeMargin } from './mergeMargin';
export { default as parseLength } from './parseLength';
export * from './types';
export { default as __hack_reexport_dimension } from './types';
@@ -37,5 +37,3 @@ export interface Dimension {
width: number;
height: number;
}
export default {};
@@ -21,4 +21,3 @@ export { default as getFormData } from './getFormData';
export { default as getDatasourceMetadata } from './getDatasourceMetadata';
export * from './types';
export { default as __hack_reexport_query_api_legacy } from './types';
@@ -24,5 +24,3 @@ export interface LegacyChartDataResponse extends Omit<
> {
data: Record<string, unknown>[] | Record<string, unknown>;
}
export default {};
@@ -39,5 +39,3 @@ export interface TimeCompare {
time_compare?: string;
comparison_type?: ComparisonType;
}
export default {};
@@ -82,5 +82,3 @@ export function isAdhocColumnReference(column?: any): column is AdhocColumn {
export function isQueryFormColumn(column: any): column is QueryFormColumn {
return isPhysicalColumn(column) || isAdhocColumn(column);
}
export default {};
@@ -271,5 +271,3 @@ export interface LegacyChartCustomizationItem {
chartId?: number;
customization: LegacyChartCustomizationConfig;
}
export default {};
@@ -71,5 +71,3 @@ export const DEFAULT_METRICS: Metric[] = [
uuid: nanoid(),
},
];
export default {};
@@ -97,5 +97,3 @@ export function isQueryFormMetric(metric: any): metric is QueryFormMetric {
isAdhocMetricSQL(metric)
);
}
export default {};
@@ -494,5 +494,3 @@ export enum ContributionType {
export type DatasourceSamplesQuery = {
filters?: QueryObjectFilterClause[];
};
export default {};
@@ -214,5 +214,3 @@ export type LatestQueryFormData = Partial<QueryFormData>;
//---------------------------------------------------
// Type guards
//---------------------------------------------------
export default {};
@@ -114,5 +114,3 @@ export interface TimeseriesChartDataResponseResult extends ChartDataResponseResu
export interface ChartDataResponse {
queries: ChartDataResponseResult[];
}
export default {};
@@ -36,5 +36,3 @@ export type TimeColumnConfigKey =
export type AppliedTimeExtras = Partial<
Record<TimeColumnConfigKey, keyof QueryObject>
>;
export default {};
@@ -28,15 +28,3 @@ export * from './Time';
export * from './AdvancedAnalytics';
export * from './PostProcessing';
export * from './Dashboard';
export { default as __hack_reexport_Datasource } from './Datasource';
export { default as __hack_reexport_Column } from './Column';
export { default as __hack_reexport_Metric } from './Metric';
export { default as __hack_reexport_Query } from './Query';
export { default as __hack_reexport_QueryResponse } from './QueryResponse';
export { default as __hack_reexport_QueryFormData } from './QueryFormData';
export { default as __hack_reexport_Time } from './Time';
export { default as __hack_reexport_AdvancedAnalytics } from './AdvancedAnalytics';
export { default as __hack_reexport_Dashboard } from './Dashboard';
export default {};
@@ -16,15 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
DataRecordValue,
normalizeTimestamp,
TimeFormatFunction,
} from '@superset-ui/core';
import type { DataRecordValue } from '../query/types/QueryResponse';
import type { TimeFormatFunction } from './types';
import normalizeTimestamp from './utils/normalizeTimestamp';
/**
* A missing date can arrive as either `null`/`undefined` or an empty string
* (e.g. a blank cell in an otherwise-numeric epoch column, which also has the
* side effect of degrading the whole column's formatter to `String` - see
* `isNumeric` in transformProps.ts). Both should be treated as "no value".
*/
export const isEmptyDateInput = (input: DataRecordValue): boolean =>
input === null || input === undefined || input === '';
/**
* Extended Date object with a custom formatter, and retains the original input
* when the formatter is simple `String(..)`.
*
* `toString()` never formats an Invalid Date: it returns the original input
* instead. `stringifyTimeInput` relies on that when it falls back to
* `${value}` for an unparseable input, otherwise the two would call each other
* forever.
*/
export default class DateWithFormatter extends Date {
formatter: TimeFormatFunction;
@@ -19,6 +19,10 @@
export { default as TimeFormats, LOCAL_PREFIX } from './TimeFormats';
export { default as TimeFormatter, PREVIEW_TIME } from './TimeFormatter';
export {
default as DateWithFormatter,
isEmptyDateInput,
} from './DateWithFormatter';
export { DEFAULT_D3_TIME_FORMAT } from './D3FormatConfig';
export {
@@ -28,8 +28,17 @@ export default function stringifyTimeInput(
let time: Date;
if (typeof value === 'string') {
const trimmed = value.trim();
// A bare four-digit string is the ISO 8601 year-only form ("2017"), which
// every engine parses as January 1st of that year. Any other integer
// string is an epoch timestamp in milliseconds that was stringified on
// its way here, e.g. by the pivot table, and is not a valid Date input.
const isYear = /^\d{4}$/.test(trimmed);
const isIntegerString = /^-?\d+$/.test(trimmed);
time = new Date(isIntegerString ? Number(trimmed) : value);
if (isYear) {
time = new Date(trimmed);
} else {
time = new Date(isIntegerString ? Number(trimmed) : value);
}
} else {
time = value instanceof Date ? value : new Date(value);
}
@@ -37,7 +46,9 @@ export default function stringifyTimeInput(
// An input that does not resolve to a valid date - a duration such as
// "00:01:54", for instance - would otherwise be formatted from an Invalid
// Date and render as "NaN:NaN:NaN". Fall back to its own representation,
// as is already done for null and undefined above.
// as is already done for null and undefined above. For a `DateWithFormatter`
// this calls its `toString()`, which returns the original input rather than
// re-entering the formatter; that guard is what keeps the fallback finite.
if (Number.isNaN(time.getTime())) {
return `${value}`;
}
@@ -79,4 +79,5 @@ export interface AgGridChartState {
columnOrder?: string[];
pageSize?: number;
currentPage?: number;
serverPagination?: boolean;
}
@@ -1,4 +1,4 @@
/*
/**
* 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
@@ -16,11 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { getTimeFormatter } from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import DateWithFormatter from '../../src/utils/DateWithFormatter';
import { formatColumnValue } from '../../src/utils/formatValue';
import { DataColumnMeta } from '../../src/types';
import { DateWithFormatter, getTimeFormatter } from '@superset-ui/core';
const formatter = getTimeFormatter('%H:%M:%S');
@@ -43,18 +39,3 @@ test('retains the original input when the formatter is String', () => {
const value = new DateWithFormatter('00:01:54');
expect(String(value)).toBe('00:01:54');
});
test('renders a duration cell through the column formatter without producing NaN', () => {
// The cell text is produced by formatColumnValue, which hands the wrapped
// value straight to the formatter rather than going through toString().
const column: DataColumnMeta = {
key: 'call_period',
label: 'call_period',
dataType: GenericDataType.Temporal,
formatter,
isNumeric: false,
};
const value = new DateWithFormatter('00:01:54', { formatter });
expect(formatColumnValue(column, value)).toEqual([false, '00:01:54']);
});
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { DateWithFormatter, getTimeFormatter } from '@superset-ui/core';
import stringifyTimeInput from '../../../src/time-format/utils/stringifyTimeInput';
const format = (time: Date) => time.toISOString();
@@ -56,3 +57,25 @@ test('returns unparseable strings unchanged instead of formatting an Invalid Dat
test('returns the representation of a Date that could not be resolved', () => {
expect(stringifyTimeInput(new Date('00:01:54'), format)).toBe('Invalid Date');
});
test('treats a four-digit integer string as a year, not as milliseconds', () => {
// "2017" is the ISO 8601 year-only form. Reading it as an epoch offset
// would silently turn it into two seconds past 1970.
expect(stringifyTimeInput('2017', format)).toBe('2017-01-01T00:00:00.000Z');
expect(stringifyTimeInput(' 1987 ', format)).toBe('1987-01-01T00:00:00.000Z');
// Longer digit strings stay epoch milliseconds.
expect(stringifyTimeInput('1704067200000', format)).toBe(
'2024-01-01T00:00:00.000Z',
);
});
test('returns the original input of an unparseable DateWithFormatter without re-entering the formatter', () => {
// The `${value}` fallback calls `DateWithFormatter.toString()`, which must
// return the input rather than call the formatter again, or the two would
// recurse until the stack overflows.
const formatter = getTimeFormatter('%H:%M:%S');
const value = new DateWithFormatter('00:01:54', { formatter });
expect(stringifyTimeInput(value, time => formatter(time))).toBe('00:01:54');
expect(formatter(value)).toBe('00:01:54');
});
@@ -40,6 +40,7 @@ import {
GridReadyEvent,
GridState,
CellClickedEvent,
CellContextMenuEvent,
CellKeyDownEvent,
SelectionChangedEvent,
} from '@superset-ui/core/components/ThemedAgGridReact';
@@ -59,9 +60,13 @@ import getInitialSortState, { shouldSort } from '../utils/getInitialSortState';
import getInitialFilterModel from '../utils/getInitialFilterModel';
import reconcileColumnState from '../utils/reconcileColumnState';
import getColumnStateSignature from '../utils/getColumnStateSignature';
import { PAGE_SIZE_OPTIONS } from '../consts';
import { getCompleteFilterState } from '../utils/filterStateManager';
import { PAGE_SIZE_OPTIONS, ROW_NUMBER_COL_ID } from '../consts';
import {
getCompleteFilterState,
type FilterState,
} from '../utils/filterStateManager';
import { copyCellValueOnKeyDown } from '../utils/copyCellValue';
import type { ClientViewSnapshot } from '../utils/externalAPIs';
export interface AgGridState extends Partial<GridState> {
timestamp?: number;
@@ -77,7 +82,6 @@ export type AgGridChartStateWithMetadata = Partial<AgGridChartState> & {
export interface AgGridTableProps {
gridTheme?: string;
isDarkMode?: boolean;
gridHeight?: number;
updateInterval?: number;
data?: any[];
onGridReady?: (params: GridReadyEvent) => void;
@@ -100,17 +104,20 @@ export interface AgGridTableProps {
serverPageLength: number;
hasServerPageLengthChanged: boolean;
handleCellClicked: (event: CellClickedEvent) => void;
handleCellContextMenu?: (event: CellContextMenuEvent) => void;
handleSelectionChanged: (event: SelectionChangedEvent) => void;
filters?: Record<string, DataRecordValue[]> | null;
isActiveFilterValue?: (key: string, val: DataRecordValue) => boolean;
renderTimeComparisonDropdown: () => JSX.Element | null;
cleanedTotals: DataRecord;
showTotals: boolean;
width: number;
onColumnStateChange?: (state: AgGridChartStateWithMetadata) => void;
onFilterChanged?: (filterModel: Record<string, any>) => void;
onFilterChanged?: (completeFilterState: FilterState) => void;
metricColumns?: string[];
gridRef?: RefObject<AgGridReact>;
chartState?: AgGridChartState;
onClientViewChange?: (snapshot: ClientViewSnapshot) => void;
}
ModuleRegistry.registerModules([AllCommunityModule, ClientSideRowModelModule]);
@@ -119,7 +126,6 @@ const isSearchFocused = new Map<string, boolean>();
const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
({
gridHeight,
data = [],
colDefsFromProps,
includeSearch,
@@ -140,8 +146,10 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
serverPageLength,
hasServerPageLengthChanged,
handleCellClicked,
handleCellContextMenu,
handleSelectionChanged,
filters,
isActiveFilterValue,
renderTimeComparisonDropdown,
cleanedTotals,
showTotals,
@@ -150,12 +158,14 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
onFilterChanged,
metricColumns = [],
chartState,
onClientViewChange,
}) => {
const gridRef = useRef<AgGridReact>(null);
const inputRef = useRef<HTMLInputElement>(null);
const rowData = useMemo(() => data, [data]);
const containerRef = useRef<HTMLDivElement>(null);
const lastCapturedStateRef = useRef<string | null>(null);
const hasCapturedInitialGridStateRef = useRef(false);
const filterOperationVersionRef = useRef(0);
const searchId = `search-${id}`;
@@ -189,13 +199,26 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
[],
);
// Memoize container style
// Fills the full height allotted by the chart container (StyledChartContainer);
// the search/time-comparison controls and pagination bar take their natural
// height and the grid flexes into whatever space remains (see gridFlexStyles),
// instead of a hardcoded pixel height that drifts from the actual chrome height.
const containerStyles = useMemo(
() => ({
height: gridHeight,
height: '100%',
width,
display: 'flex',
flexDirection: 'column' as const,
}),
[gridHeight, width],
[width],
);
const gridFlexStyles = useMemo(
() => ({
flex: '1 1 auto',
minHeight: 0,
}),
[],
);
const [quickFilterText, setQuickFilterText] = useState<string>();
@@ -293,6 +316,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
sortModel,
filterModel,
timestamp: Date.now(),
serverPagination: true,
});
}
@@ -321,49 +345,85 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
[serverPagination, gridInitialState, percentMetrics, onSortChange],
);
const handleGridStateChange = useCallback(
const captureGridState = useCallback(() => {
const { api } = gridRef.current ?? {};
if (!api) return null;
const columnState = api.getColumnState ? api.getColumnState() : [];
const filterModel = api.getFilterModel ? api.getFilterModel() : {};
const sortModel = columnState
.filter(col => col.sort)
.map(col => ({
colId: col.colId,
sort: col.sort as 'asc' | 'desc',
sortIndex: col.sortIndex || 0,
}))
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
return {
stateToSave: {
columnState,
sortModel,
filterModel,
timestamp: Date.now(),
serverPagination: !!serverPagination,
},
stateHash: getColumnStateSignature(columnState, sortModel, filterModel),
};
}, [serverPagination]);
const persistGridStateChange = useCallback(
debounce(() => {
if (onColumnStateChange && gridRef.current?.api) {
try {
const { api } = gridRef.current;
if (!onColumnStateChange) return;
try {
const captured = captureGridState();
if (!captured) return;
const { stateToSave, stateHash } = captured;
const columnState = api.getColumnState ? api.getColumnState() : [];
if (stateHash !== lastCapturedStateRef.current) {
lastCapturedStateRef.current = stateHash;
const filterModel = api.getFilterModel ? api.getFilterModel() : {};
const sortModel = columnState
.filter(col => col.sort)
.map(col => ({
colId: col.colId,
sort: col.sort as 'asc' | 'desc',
sortIndex: col.sortIndex || 0,
}))
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0));
const stateToSave = {
columnState,
sortModel,
filterModel,
timestamp: Date.now(),
};
const stateHash = getColumnStateSignature(
columnState,
sortModel,
filterModel,
);
if (stateHash !== lastCapturedStateRef.current) {
lastCapturedStateRef.current = stateHash;
onColumnStateChange(stateToSave);
}
} catch (error) {
console.warn('Error capturing AG Grid state:', error);
onColumnStateChange(stateToSave);
}
} catch (error) {
console.warn('Error capturing AG Grid state:', error);
}
}, Constants.SLOW_DEBOUNCE),
[onColumnStateChange],
[onColumnStateChange, captureGridState],
);
const handleGridStateChange = useCallback(() => {
// AG Grid fires onStateUpdated once as it applies the initial
// column/sort/filter state on mount, before any user interaction.
// That first event just reflects the state the grid was initialized
// with (chartState/gridInitialState) - not a user-driven change - so
// it's captured synchronously as the baseline rather than persisted.
// This check runs on every raw call, before debouncing, so a real
// user action that lands inside the same debounce window as this
// first call is never coalesced into it and dropped.
if (!hasCapturedInitialGridStateRef.current) {
hasCapturedInitialGridStateRef.current = true;
try {
const captured = captureGridState();
if (captured) {
lastCapturedStateRef.current = captured.stateHash;
}
} catch (error) {
console.warn('Error capturing AG Grid state:', error);
}
return;
}
persistGridStateChange();
}, [captureGridState, persistGridStateChange]);
useEffect(
() =>
// Cleanup debounced grid-state capture
() => {
persistGridStateChange.cancel();
},
[persistGridStateChange],
);
const handleFilterChanged = useCallback(async () => {
@@ -416,6 +476,81 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
serverPaginationData?.agGridFilterModel,
]);
// Captures the "current view" (post-filter/sort, all rows across all
// pages) for the "Export Current View" menu, mirroring Table V1's
// clientView snapshot. Client-side mode only: in server pagination mode
// the grid only ever holds a single page's rows, so a client-derived
// snapshot can't represent the full filtered/sorted result and export
// falls back to a fresh backend query instead (see useExploreAdditionalActionsMenu).
const lastClientViewSignatureRef = useRef<string | null>(null);
// Unlike handleGridStateChange's columnState/sortModel/filterModel,
// clientView is excluded from ownState re-query comparisons on both the
// Explore (ExploreViewContainer) and dashboard (activeAllDashboardFilters)
// paths, so publishing it - including the very first snapshot right
// after mount - can't trigger a requery/remount loop. It's therefore
// always persisted below rather than having its initial value skipped;
// skipping it would leave "Export Current View" without a snapshot to
// export until some later grid event changes the signature.
// Debounced (like handleGridStateChange below) because the full
// filtered+sorted traversal is O(n) and onModelUpdated can fire rapidly
// in succession (e.g. while typing into a quick filter); only the
// trailing update needs to recompute the snapshot.
const handleModelUpdated = useCallback(
debounce(() => {
if (serverPagination || !onClientViewChange || !gridRef.current?.api) {
return;
}
const { api } = gridRef.current;
const displayedColumns = api
.getAllDisplayedColumns()
.filter(column => column.getColId() !== ROW_NUMBER_COL_ID);
const columns = displayedColumns.map(column => {
const colDef = column.getColDef();
// For comparison columns, colId has "Main " stripped for display,
// but row data is still keyed by the unstripped original field
// (colDef.context.dataKey, set in useColDefs) -- use that to read
// row values so exported rows aren't blank for the main metric.
const dataKey = colDef.context?.dataKey ?? column.getColId();
return {
key: dataKey,
label: colDef.headerName || column.getColId(),
};
});
const rows: Record<string, unknown>[] = [];
api.forEachNodeAfterFilterAndSort(node => {
if (node.data) {
rows.push(node.data);
}
});
// Without a getRowId callback, AG Grid's node ids are purely
// positional and reset to 0..n-1 on every setRowData call, so they
// don't identify a row's content across a data refresh — hashing
// the actual filtered+sorted row content (which this function
// already has to visit to build `rows`) is what actually detects
// both value changes (e.g. a refresh with the same row count) and
// order changes (e.g. a pure sort), not just count/column changes.
const signature = `${JSON.stringify(rows)}|${columns.map(c => c.key).join(',')}`;
if (signature === lastClientViewSignatureRef.current) {
return;
}
lastClientViewSignatureRef.current = signature;
onClientViewChange({ rows, columns, count: rows.length });
}, Constants.SLOW_DEBOUNCE),
[serverPagination, onClientViewChange],
);
useEffect(
() =>
// Cleanup debounced client-view snapshot capture
() => {
handleModelUpdated.cancel();
},
[handleModelUpdated],
);
useEffect(() => {
if (
hasServerPageLengthChanged &&
@@ -436,14 +571,32 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
}
}, [width]);
// Row highlighting must reflect the active cross filter regardless of how
// it was applied (cell click, context menu, or an external dashboard
// filter), so it survives re-renders and server-side re-queries rather
// than only reflecting whichever handler last called setSelected.
useEffect(() => {
if (
(!filters || Object.keys(filters).length === 0) &&
gridRef.current?.api?.getSelectedRows().length
) {
gridRef.current.api.deselectAll();
const api = gridRef.current?.api;
if (!api) return;
if (!filters || Object.keys(filters).length === 0) {
if (api.getSelectedRows().length) {
api.deselectAll();
}
return;
}
}, [filters]);
if (!isActiveFilterValue) return;
api.forEachNode(node => {
const matches = Object.keys(filters).some(key =>
isActiveFilterValue(key, node.data?.[key] as DataRecordValue),
);
if (node.isSelected() !== matches) {
node.setSelected(matches, false, 'api');
}
});
}, [filters, isActiveFilterValue, rowData]);
const onGridReady = (params: GridReadyEvent) => {
// This will make columns fill the grid width
@@ -511,126 +664,130 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
)}
</div>
<ThemedAgGridReact
ref={gridRef}
onGridReady={onGridReady}
className="ag-container"
rowData={rowData}
headerHeight={36}
rowHeight={30}
columnDefs={colDefsFromProps}
defaultColDef={defaultColDef}
onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
rowSelection="multiple"
animateRows
onCellClicked={handleCellClicked}
onCellKeyDown={handleCellKeyDown}
onSelectionChanged={handleSelectionChanged}
onFilterChanged={handleFilterChanged}
onStateUpdated={handleGridStateChange}
initialState={gridInitialState}
maintainColumnOrder
suppressAggFuncInHeader
// Clicking a cell should select (focus) the cell rather than select
// its text content (#106389). enableCellTextSelection forces browser
// text selection on click, which suppresses the cell-focus behavior.
// Because the Enterprise clipboard module isn't registered, native
// text selection was the only way to copy a value, so onCellKeyDown
// (above) restores Ctrl/Cmd+C copy for the focused cell. Full
// multi-cell range selection still requires AG Grid Enterprise, which
// is not available in the Community build used here.
enableCellTextSelection={false}
quickFilterText={serverPagination ? '' : quickFilterText}
suppressMovableColumns={!allowRearrangeColumns}
pagination={pagination}
paginationPageSize={pageSize}
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
suppressDragLeaveHidesColumns
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
tooltipShowDelay={500}
localeText={{
// Pagination controls
next: t('Next'),
previous: t('Previous'),
page: t('Page'),
more: t('More'),
to: t('to'),
of: t('of'),
first: t('First'),
last: t('Last'),
loadingOoo: t('Loading...'),
// Set Filter
selectAll: t('Select All'),
searchOoo: t('Search...'),
blanks: t('Blanks'),
// Filter operations
filterOoo: t('Filter'),
applyFilter: t('Apply Filter'),
equals: t('Equals'),
notEqual: t('Not Equal'),
lessThan: t('Less Than'),
greaterThan: t('Greater Than'),
lessThanOrEqual: t('Less Than or Equal'),
greaterThanOrEqual: t('Greater Than or Equal'),
inRange: t('In Range'),
contains: t('Contains'),
notContains: t('Not Contains'),
startsWith: t('Starts With'),
endsWith: t('Ends With'),
// Logical conditions
andCondition: t('AND'),
orCondition: t('OR'),
// Panel and group labels
group: t('Group'),
columns: t('Columns'),
filters: t('Filters'),
valueColumns: t('Value Columns'),
pivotMode: t('Pivot Mode'),
groups: t('Groups'),
values: t('Values'),
pivots: t('Pivots'),
toolPanelButton: t('Tool Panel'),
// Enterprise menu items
pinColumn: t('Pin Column'),
valueAggregation: t('Value Aggregation'),
autosizeThiscolumn: t('Autosize This Column'),
autosizeAllColumns: t('Autosize All Columns'),
groupBy: t('Group By'),
ungroupBy: t('Ungroup By'),
resetColumns: t('Reset Columns'),
expandAll: t('Expand All'),
collapseAll: t('Collapse All'),
toolPanel: t('Tool Panel'),
export: t('Export'),
csvExport: t('CSV Export'),
excelExport: t('Excel Export'),
excelXmlExport: t('Excel XML Export'),
// Aggregation functions
sum: t('Sum'),
min: t('Min'),
max: t('Max'),
none: t('None'),
count: t('Count'),
average: t('Average'),
// Standard menu items
copy: t('Copy'),
copyWithHeaders: t('Copy with Headers'),
paste: t('Paste'),
// Column menu and sorting
sortAscending: t('Sort Ascending'),
sortDescending: t('Sort Descending'),
sortUnSort: t('Clear Sort'),
}}
context={{
onColumnHeaderClicked: handleColumnHeaderClick,
initialSortState: getInitialSortState(
serverPaginationData?.sortBy || [],
),
lastFilteredColumn: serverPaginationData?.lastFilteredColumn,
lastFilteredInputPosition:
serverPaginationData?.lastFilteredInputPosition,
}}
/>
<div style={gridFlexStyles}>
<ThemedAgGridReact
ref={gridRef}
onGridReady={onGridReady}
className="ag-container"
rowData={rowData}
headerHeight={36}
rowHeight={30}
columnDefs={colDefsFromProps}
defaultColDef={defaultColDef}
onColumnGroupOpened={params => params.api.sizeColumnsToFit()}
rowSelection="multiple"
animateRows
onCellClicked={handleCellClicked}
onCellContextMenu={handleCellContextMenu}
onCellKeyDown={handleCellKeyDown}
onSelectionChanged={handleSelectionChanged}
onFilterChanged={handleFilterChanged}
onModelUpdated={handleModelUpdated}
onStateUpdated={handleGridStateChange}
initialState={gridInitialState}
maintainColumnOrder
suppressAggFuncInHeader
// Clicking a cell should select (focus) the cell rather than select
// its text content (#106389). enableCellTextSelection forces browser
// text selection on click, which suppresses the cell-focus behavior.
// Because the Enterprise clipboard module isn't registered, native
// text selection was the only way to copy a value, so onCellKeyDown
// (above) restores Ctrl/Cmd+C copy for the focused cell. Full
// multi-cell range selection still requires AG Grid Enterprise, which
// is not available in the Community build used here.
enableCellTextSelection={false}
quickFilterText={serverPagination ? '' : quickFilterText}
suppressMovableColumns={!allowRearrangeColumns}
pagination={pagination}
paginationPageSize={pageSize}
paginationPageSizeSelector={PAGE_SIZE_OPTIONS}
suppressDragLeaveHidesColumns
pinnedBottomRowData={showTotals ? [cleanedTotals] : undefined}
tooltipShowDelay={500}
localeText={{
// Pagination controls
next: t('Next'),
previous: t('Previous'),
page: t('Page'),
more: t('More'),
to: t('to'),
of: t('of'),
first: t('First'),
last: t('Last'),
loadingOoo: t('Loading...'),
// Set Filter
selectAll: t('Select All'),
searchOoo: t('Search...'),
blanks: t('Blanks'),
// Filter operations
filterOoo: t('Filter'),
applyFilter: t('Apply Filter'),
equals: t('Equals'),
notEqual: t('Not Equal'),
lessThan: t('Less Than'),
greaterThan: t('Greater Than'),
lessThanOrEqual: t('Less Than or Equal'),
greaterThanOrEqual: t('Greater Than or Equal'),
inRange: t('In Range'),
contains: t('Contains'),
notContains: t('Not Contains'),
startsWith: t('Starts With'),
endsWith: t('Ends With'),
// Logical conditions
andCondition: t('AND'),
orCondition: t('OR'),
// Panel and group labels
group: t('Group'),
columns: t('Columns'),
filters: t('Filters'),
valueColumns: t('Value Columns'),
pivotMode: t('Pivot Mode'),
groups: t('Groups'),
values: t('Values'),
pivots: t('Pivots'),
toolPanelButton: t('Tool Panel'),
// Enterprise menu items
pinColumn: t('Pin Column'),
valueAggregation: t('Value Aggregation'),
autosizeThiscolumn: t('Autosize This Column'),
autosizeAllColumns: t('Autosize All Columns'),
groupBy: t('Group By'),
ungroupBy: t('Ungroup By'),
resetColumns: t('Reset Columns'),
expandAll: t('Expand All'),
collapseAll: t('Collapse All'),
toolPanel: t('Tool Panel'),
export: t('Export'),
csvExport: t('CSV Export'),
excelExport: t('Excel Export'),
excelXmlExport: t('Excel XML Export'),
// Aggregation functions
sum: t('Sum'),
min: t('Min'),
max: t('Max'),
none: t('None'),
count: t('Count'),
average: t('Average'),
// Standard menu items
copy: t('Copy'),
copyWithHeaders: t('Copy with Headers'),
paste: t('Paste'),
// Column menu and sorting
sortAscending: t('Sort Ascending'),
sortDescending: t('Sort Descending'),
sortUnSort: t('Clear Sort'),
}}
context={{
onColumnHeaderClicked: handleColumnHeaderClick,
initialSortState: getInitialSortState(
serverPaginationData?.sortBy || [],
),
lastFilteredColumn: serverPaginationData?.lastFilteredColumn,
lastFilteredInputPosition:
serverPaginationData?.lastFilteredInputPosition,
}}
/>
</div>
{serverPagination && (
<Pagination
currentPage={serverPaginationData?.currentPage || 0}
@@ -18,16 +18,28 @@
*/
import { t } from '@apache-superset/core/translation';
import {
BinaryQueryObjectFilterClause,
DataRecord,
DataRecordValue,
DateWithFormatter,
extractTextFromHTML,
getTimeFormatterForGranularity,
isEmptyDateInput,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { isEqual } from 'lodash-es';
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
useMemo,
} from 'react';
import { debounce, isEqual } from 'lodash-es';
import {
CellClickedEvent,
CellContextMenuEvent,
SelectionChangedEvent,
} from '@superset-ui/core/components/ThemedAgGridReact';
import {
@@ -37,20 +49,18 @@ import {
SortByItem,
} from './types';
import AgGridDataTable from './AgGridTable';
import { updateTableOwnState } from './utils/externalAPIs';
import { updateTableOwnState, ClientViewSnapshot } from './utils/externalAPIs';
import TimeComparisonVisibility from './AgGridTable/components/TimeComparisonVisibility';
import { useColDefs } from './utils/useColDefs';
import { buildSelectionCrossFilterDataMask } from './utils/getCrossFilterDataMask';
import {
buildSelectionCrossFilterDataMask,
getCrossFilterDataMask,
} from './utils/getCrossFilterDataMask';
import { StyledChartContainer } from './styles';
import type { FilterState } from './utils/filterStateManager';
const getGridHeight = (height: number, includeSearch: boolean | undefined) => {
let calculatedGridHeight = height;
if (includeSearch) {
calculatedGridHeight -= 16;
}
return calculatedGridHeight - 80;
};
import { formatColumnValue } from './utils/formatValue';
import getTimeRangeFromGranularity from './utils/getTimeRangeFromGranularity';
import getScrollBarSize from './utils/getScrollBarSize';
export default function TableChart<D extends DataRecord = DataRecord>(
props: AgGridTableChartTransformedProps<D> & {},
@@ -61,6 +71,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
data,
includeSearch,
allowRearrangeColumns,
allowRenderHtml,
pageSize,
serverPagination,
rowCount,
@@ -88,8 +99,60 @@ export default function TableChart<D extends DataRecord = DataRecord>(
metricSqlExpressions,
rawSummaryColumns,
showNumberedColumn,
onContextMenu,
formData,
} = props;
// The dashboard's layout engine reports a burst of close-but-not-identical
// width/height values while it settles on initial load. Committing each
// intermediate value resizes the chart container and re-fits AG Grid's
// columns once per value; for any column with wrapText/autoHeight (the
// default - see useColDefs), each re-fit can flip a borderline cell across
// its wrap boundary and change that row's height, which is what actually
// reads as "flicker" rather than the container resize itself.
//
// A scrollbar-sized threshold (matching plugin-chart-table/v1's guard)
// filters out sub-pixel noise, but genuine multi-step settling still gets
// through as several real width values in quick succession. Debouncing
// every commit after the first collapses that burst into the single final
// value once it stops changing, while still painting the first available
// size immediately so the chart isn't blank while it waits.
const [tableSize, setTableSize] = useState({ width: 0, height: 0 });
const hasCommittedInitialSize = useRef(false);
const debouncedSetTableSize = useMemo(
() =>
debounce((size: { width: number; height: number }) => {
setTableSize(size);
}, 250),
[],
);
useEffect(
() =>
// Cleanup debounced size commit
() => {
debouncedSetTableSize.cancel();
},
[debouncedSetTableSize],
);
useLayoutEffect(() => {
const scrollBarSize = getScrollBarSize();
const sizeChanged =
Math.abs(width - tableSize.width) > scrollBarSize ||
Math.abs(height - tableSize.height) > scrollBarSize;
if (!sizeChanged) {
return;
}
if (!hasCommittedInitialSize.current) {
hasCommittedInitialSize.current = true;
setTableSize({ width, height });
} else {
debouncedSetTableSize({ width, height });
}
}, [width, height, tableSize, debouncedSetTableSize]);
const [searchOptions, setSearchOptions] = useState<SearchOption[]>([]);
// Extract metric column names for SQL conversion
@@ -114,6 +177,27 @@ export default function TableChart<D extends DataRecord = DataRecord>(
}
}, [columns]);
// Tracks the most recently written ownState so that writes triggered
// asynchronously (e.g. clientView from AG Grid's onModelUpdated, which can
// fire with a stale closure) merge onto the latest known state instead of
// a stale render-time serverPaginationData snapshot. updateTableOwnState
// replaces ownState wholesale, so merging at write time - rather than at
// render time - is what keeps concurrent writers from clobbering one
// another's keys.
const ownStateRef = useRef(serverPaginationData);
useEffect(() => {
ownStateRef.current = serverPaginationData;
}, [serverPaginationData]);
const writeOwnState = useCallback(
(patch: Record<string, unknown>) => {
const nextOwnState = { ...ownStateRef.current, ...patch };
ownStateRef.current = nextOwnState;
updateTableOwnState(setDataMask, nextOwnState);
},
[setDataMask],
);
// A single effect owns every ownState write derived from render state.
// updateTableOwnState replaces ownState wholesale, so separate effects that
// each spread serverPaginationData in the same render would clobber one
@@ -121,7 +205,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// columns and nudging a re-query for missing totals must be one combined
// delta.
useEffect(() => {
const nextOwnState = { ...serverPaginationData };
const patch: Record<string, unknown> = {};
let changed = false;
if (serverPagination && serverPaginationData && rowCount !== undefined) {
@@ -132,7 +216,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
// last remaining page.
const clampedPage = Math.max(0, Math.min(currentPage, totalPages - 1));
if (clampedPage !== currentPage) {
nextOwnState.currentPage = clampedPage;
patch.currentPage = clampedPage;
changed = true;
}
}
@@ -140,22 +224,22 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const primed = (serverPaginationData?.rawSummaryColumns ?? []) as string[];
const requested = Boolean(serverPaginationData?.totalsRequested);
if (isRawRecords && showTotals && !isEqual(primed, rawSummaryColumns)) {
nextOwnState.rawSummaryColumns = rawSummaryColumns;
patch.rawSummaryColumns = rawSummaryColumns;
changed = true;
}
// A renderTrigger toggle re-renders without re-querying; requesting totals
// through ownState dispatches the standard re-query whose buildQuery
// carries the totals query for the active mode.
if (showTotals && totals === undefined && !requested) {
nextOwnState.totalsRequested = true;
patch.totalsRequested = true;
changed = true;
} else if (!showTotals && requested) {
nextOwnState.totalsRequested = false;
patch.totalsRequested = false;
changed = true;
}
if (changed) {
updateTableOwnState(setDataMask, nextOwnState);
writeOwnState(patch);
}
}, [
serverPagination,
@@ -166,7 +250,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
totals,
rawSummaryColumns,
serverPaginationData,
setDataMask,
writeOwnState,
]);
const comparisonColumns = [
@@ -209,8 +293,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
}
// Prepare modified own state for server pagination
const modifiedOwnState = {
...serverPaginationData,
writeOwnState({
agGridFilterModel:
completeFilterState.originalFilterModel &&
Object.keys(completeFilterState.originalFilterModel).length > 0
@@ -223,14 +306,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
lastFilteredInputPosition: completeFilterState.inputPosition,
currentPage: 0, // Reset to first page when filtering
metricSqlExpressions,
};
updateTableOwnState(setDataMask, modifiedOwnState);
});
},
[
setDataMask,
writeOwnState,
serverPagination,
serverPaginationData,
onChartStateChange,
chartState,
metricSqlExpressions,
@@ -273,15 +353,17 @@ export default function TableChart<D extends DataRecord = DataRecord>(
colorPositiveNegative,
columnColorFormatters,
allowRearrangeColumns,
allowRenderHtml,
basicColorFormatters,
isUsingTimeComparison,
emitCrossFilters,
alignPositiveNegative,
slice_id,
conditionalFormatting: formData?.conditional_formatting,
comparisonColorEnabled: formData?.comparison_color_enabled,
comparisonColorScheme: formData?.comparison_color_scheme,
});
const gridHeight = getGridHeight(height, includeSearch);
const isActiveFilterValue = useCallback(
function isActiveFilterValue(key: string, val: DataRecordValue) {
if (!filters || !filters[key]) return false;
@@ -348,7 +430,17 @@ export default function TableChart<D extends DataRecord = DataRecord>(
const handleSelectionChanged = useCallback(
(event: SelectionChangedEvent) => {
if (!emitCrossFilters || !activeColumnRef.current) return;
// Selection changes triggered by the highlight-sync effect (source
// 'api') reflect a filter that was already applied elsewhere (context
// menu, dashboard filter, etc.), so re-deriving and re-dispatching a
// mask from them here would use a stale activeColumnRef and could
// clobber that filter with the wrong column.
if (
!emitCrossFilters ||
!activeColumnRef.current ||
event.source === 'api'
)
return;
const key = activeColumnRef.current;
const selectedRows = event.api.getSelectedRows();
@@ -368,75 +460,204 @@ export default function TableChart<D extends DataRecord = DataRecord>(
[emitCrossFilters, setDataMask, timeGrain, timestampFormatter],
);
const drillColumns = isUsingTimeComparison
? (filteredColumns as InputColumn[])
: (columns as InputColumn[]);
const handleContextMenu = useCallback(
(event: CellContextMenuEvent) => {
if (!onContextMenu || isRawRecords || !event.column || !event.data) {
return;
}
const nativeEvent = event.event as MouseEvent | null | undefined;
if (!nativeEvent) return;
nativeEvent.preventDefault();
nativeEvent.stopPropagation();
const rowData = event.data as Record<string, DataRecordValue>;
const key = event.column.getColId();
const cellValue = event.value as DataRecordValue;
const colDef = event.column.getColDef();
const isMetric = Boolean(
colDef.context?.isMetric || colDef.context?.isPercentMetric,
);
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
drillColumns.forEach(col => {
if (col.isMetric || col.isPercentMetric) return;
const dataRecordValue = rowData[col.key];
if (
dataRecordValue == null ||
(dataRecordValue instanceof DateWithFormatter &&
isEmptyDateInput(dataRecordValue.input))
) {
drillToDetailFilters.push({
col: col.key,
op: 'IS NULL' as any,
val: null,
});
} else if (col.dataType === GenericDataType.Temporal && timeGrain) {
const startTime =
dataRecordValue instanceof Date
? dataRecordValue
: new Date(dataRecordValue as string | number);
if (Number.isNaN(startTime.getTime())) {
// Malformed temporal value: fall back to an equality filter
// instead of building a TEMPORAL_RANGE, since toISOString()
// throws on an Invalid Date and would crash the context menu.
const sanitizedValue = extractTextFromHTML(dataRecordValue);
drillToDetailFilters.push({
col: col.key,
op: '==',
val: sanitizedValue as string | number | boolean,
formattedVal: formatColumnValue(col, sanitizedValue)[1],
});
} else {
const [rangeStartTime, rangeEndTime] = getTimeRangeFromGranularity(
startTime,
timeGrain,
);
const timeRangeValue = `${rangeStartTime.toISOString()} : ${rangeEndTime.toISOString()}`;
drillToDetailFilters.push({
col: col.key,
op: 'TEMPORAL_RANGE',
val: timeRangeValue,
grain: timeGrain,
formattedVal: formatColumnValue(col, dataRecordValue)[1],
});
}
} else {
const sanitizedValue = extractTextFromHTML(dataRecordValue);
drillToDetailFilters.push({
col: col.key,
op: '==',
val: sanitizedValue as string | number | boolean,
formattedVal: formatColumnValue(col, sanitizedValue)[1],
});
}
});
const isCellValueNull =
cellValue == null ||
(cellValue instanceof DateWithFormatter &&
isEmptyDateInput(cellValue.input));
onContextMenu(nativeEvent.clientX, nativeEvent.clientY, {
drillToDetail: drillToDetailFilters,
crossFilter: isMetric
? undefined
: getCrossFilterDataMask({
key,
value: cellValue,
filters,
timeGrain,
isActiveFilterValue,
timestampFormatter,
}),
drillBy: isMetric
? undefined
: {
filters: [
isCellValueNull
? { col: key, op: 'IS NULL' as any, val: null }
: {
col: key,
op: '==' as any,
val: extractTextFromHTML(cellValue),
},
],
groupbyFieldName: 'groupby',
},
});
},
[
onContextMenu,
isRawRecords,
drillColumns,
timeGrain,
filters,
isActiveFilterValue,
timestampFormatter,
],
);
const handleServerPaginationChange = useCallback(
(pageNumber: number, pageSize: number) => {
const modifiedOwnState = {
...serverPaginationData,
writeOwnState({
currentPage: pageNumber,
pageSize,
lastFilteredColumn: undefined,
lastFilteredInputPosition: undefined,
};
updateTableOwnState(setDataMask, modifiedOwnState);
});
},
[setDataMask],
[writeOwnState],
);
const handlePageSizeChange = useCallback(
(pageSize: number) => {
const modifiedOwnState = {
...serverPaginationData,
writeOwnState({
currentPage: 0,
pageSize,
lastFilteredColumn: undefined,
lastFilteredInputPosition: undefined,
};
updateTableOwnState(setDataMask, modifiedOwnState);
});
},
[setDataMask],
[writeOwnState],
);
const handleChangeSearchCol = (searchCol: string) => {
if (!isEqual(searchCol, serverPaginationData?.searchColumn)) {
const modifiedOwnState = {
...serverPaginationData,
if (!isEqual(searchCol, ownStateRef.current?.searchColumn)) {
writeOwnState({
searchColumn: searchCol,
searchText: '',
currentPage: 0, // Reset to first page when the search column changes
lastFilteredColumn: undefined,
lastFilteredInputPosition: undefined,
};
updateTableOwnState(setDataMask, modifiedOwnState);
});
}
};
const handleSearch = useCallback(
(searchText: string) => {
const modifiedOwnState = {
...serverPaginationData,
writeOwnState({
searchColumn:
serverPaginationData?.searchColumn || searchOptions[0]?.value,
(ownStateRef.current?.searchColumn as string | undefined) ||
searchOptions[0]?.value,
searchText,
currentPage: 0, // Reset to first page when searching
lastFilteredColumn: undefined,
lastFilteredInputPosition: undefined,
};
updateTableOwnState(setDataMask, modifiedOwnState);
});
},
[setDataMask, searchOptions],
[writeOwnState, searchOptions],
);
const handleSortByChange = useCallback(
(sortBy: SortByItem[]) => {
if (!serverPagination) return;
const modifiedOwnState = {
...serverPaginationData,
writeOwnState({
sortBy,
lastFilteredColumn: undefined,
lastFilteredInputPosition: undefined,
};
updateTableOwnState(setDataMask, modifiedOwnState);
});
},
[setDataMask, serverPagination],
[writeOwnState, serverPagination],
);
// Feeds the "Export Current View" menu item (EXPORT_CURRENT_VIEW behavior),
// mirroring Table V1's clientView snapshot on ownState. Written through
// writeOwnState (rather than spreading serverPaginationData directly)
// because onModelUpdated can fire with a stale closure relative to other
// ownState writers (e.g. a just-applied filter), and updateTableOwnState
// replaces ownState wholesale.
const handleClientViewChange = useCallback(
(clientView: ClientViewSnapshot) => {
writeOwnState({ clientView });
},
[writeOwnState],
);
const renderTimeComparisonVisibility = (): JSX.Element => (
@@ -455,9 +676,22 @@ export default function TableChart<D extends DataRecord = DataRecord>(
.join('|');
return (
<StyledChartContainer height={height}>
<StyledChartContainer
height={tableSize.height}
onContextMenu={event => {
// Safety net: AG Grid only calls handleContextMenu (which calls
// preventDefault) when it resolves the native contextmenu event to
// a cell. If that per-cell resolution ever misses - e.g. a second,
// near-duplicate contextmenu event dispatched in quick succession by
// some mice's right-button switches - the event still bubbles
// through this container, so the browser's native menu is
// suppressed here regardless of whether AG Grid's own handler ran.
if (!isRawRecords) {
event.preventDefault();
}
}}
>
<AgGridDataTable
gridHeight={gridHeight}
key={descriptionsKey}
data={data || []}
colDefsFromProps={colDefs}
@@ -478,8 +712,10 @@ export default function TableChart<D extends DataRecord = DataRecord>(
metricColumns={metricColumns}
id={slice_id}
handleCellClicked={handleCellClicked}
handleCellContextMenu={handleContextMenu}
handleSelectionChanged={handleSelectionChanged}
filters={filters}
isActiveFilterValue={isActiveFilterValue}
percentMetrics={percentMetrics}
serverPageLength={serverPageLength}
hasServerPageLengthChanged={hasServerPageLengthChanged}
@@ -490,9 +726,10 @@ export default function TableChart<D extends DataRecord = DataRecord>(
showTotals={
showTotals && totals !== undefined && Object.keys(totals).length > 0
}
width={width}
width={tableSize.width}
onColumnStateChange={handleColumnStateChange}
chartState={chartState}
onClientViewChange={handleClientViewChange}
/>
</StyledChartContainer>
);
@@ -674,6 +674,29 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
}
}
// Build the "all records" percent-metric denominator query AFTER all
// filter mutations (interactive group-by, search, AG Grid WHERE/HAVING)
// above, so its denominator reflects the same filtered result set as the
// main query instead of a stale pre-filter snapshot.
const calculationMode = formData.percent_metric_calculation || 'row_limit';
if (
calculationMode === 'all_records' &&
percentMetrics &&
percentMetrics.length > 0
) {
extraQueries.push({
...queryObject,
columns: [],
metrics: percentMetrics,
post_processing: [],
row_limit: 0,
row_offset: 0,
orderby: [],
is_timeseries: false,
});
}
// Create totals query AFTER all filters (including AG Grid filters) are applied
// This ensures we can properly exclude AG Grid WHERE filters from the totals
// In raw records mode the summary is a SUM over the numeric columns primed
@@ -714,33 +737,21 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
: undefined;
if (showAggregateTotals || rawSummaryColumns.length > 0) {
// Create a copy of extras without the AG Grid WHERE clause
// AG Grid filters in extras.where can reference calculated columns
// which aren't available in the totals subquery
const totalsExtras = { ...queryObject.extras };
if (ownState.agGridComplexWhere) {
// Remove AG Grid WHERE clause from totals query
const whereClause = totalsExtras.where;
if (whereClause) {
// Remove the AG Grid filter part from the WHERE clause using string methods
const agGridWhere = ownState.agGridComplexWhere;
let newWhereClause = whereClause;
// Try to remove with " AND " before
newWhereClause = newWhereClause.replace(` AND ${agGridWhere}`, '');
// Try to remove with " AND " after
newWhereClause = newWhereClause.replace(`${agGridWhere} AND `, '');
// If it's the only clause, remove it entirely
if (newWhereClause === agGridWhere) {
newWhereClause = '';
}
if (newWhereClause.trim()) {
totalsExtras.where = newWhereClause;
} else {
delete totalsExtras.where;
}
}
// Start from the original, pre-filter extras (captured before any
// AG Grid WHERE/HAVING or download sqlClauses were merged in above)
// rather than trying to subtract those fragments back out of the
// now-combined `queryObject.extras` string. AG Grid filters can
// reference calculated columns that aren't available once the
// totals subquery drops all grouping columns (columns: []), and that
// applies to HAVING just as much as WHERE, and to the download
// sqlClauses path just as much as the live agGridComplexWhere path —
// starting clean avoids having to special-case each source.
const totalsExtras = { ...extras };
if (!totalsExtras.where) {
delete totalsExtras.where;
}
if (!totalsExtras.having) {
delete totalsExtras.having;
}
extraQueries.push({
@@ -39,6 +39,8 @@ import {
shouldSkipMetricColumn,
isRegularMetric,
isPercentMetric,
ConditionalFormattingConfig,
ObjectFormattingEnum,
ColorSchemeEnum,
} from '@superset-ui/chart-controls';
import { t } from '@apache-superset/core/translation';
@@ -194,6 +196,23 @@ const percentMetricsControl: typeof sharedControls.metrics = {
validators: [],
};
const percentMetricCalculationControl: ControlConfig<'SelectControl'> = {
type: 'SelectControl',
label: t('Percentage metric calculation'),
description: t(
'Row Limit: percentages are calculated based on the subset of data retrieved, respecting the row limit. ' +
'All Records: Percentages are calculated based on the total dataset, ignoring the row limit.',
),
default: 'row_limit',
clearable: false,
choices: [
['row_limit', t('Row limit')],
['all_records', t('All records')],
],
visibility: isAggMode,
renderTrigger: false,
};
/*
Options for row limit control
*/
@@ -431,6 +450,12 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'percent_metric_calculation',
config: percentMetricCalculationControl,
},
],
],
},
{
@@ -481,6 +506,36 @@ const config: ControlPanelConfig = {
},
},
],
[
{
name: 'allow_rearrange_columns',
config: {
type: 'CheckboxControl',
label: t('Allow columns to be rearranged'),
renderTrigger: true,
default: false,
description: t(
"Allow end user to drag-and-drop column headers to rearrange them. Note their changes won't persist for the next time they open the chart.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
isEmpty(controls?.time_compare?.value),
},
},
],
[
{
name: 'allow_render_html',
config: {
type: 'CheckboxControl',
label: t('Render columns in HTML format'),
renderTrigger: true,
default: true,
description: t(
'Renders table cells as HTML when applicable. For example, HTML <a> tags will be rendered as hyperlinks.',
),
},
},
],
],
},
{
@@ -573,11 +628,14 @@ const config: ControlPanelConfig = {
const updatedColtypes: GenericDataType[] = [];
colnames
.map(
(colname, index) => [colname, index] as [string, number],
)
.filter(
colname =>
([colname]) =>
last(colname.split('__')) !== timeComparisonValue,
)
.forEach((colname, index) => {
.forEach(([colname, originalIndex]) => {
if (
shouldSkipMetricColumn({
colname,
@@ -614,7 +672,12 @@ const config: ControlPanelConfig = {
});
} else {
updatedColnames.push(colname);
updatedColtypes.push(coltypes[index]);
// Look up by the column's original position in
// colnames/coltypes, not its position after the
// filter above — those diverge whenever any
// earlier column is a comparison-suffixed one that
// got filtered out.
updatedColtypes.push(coltypes[originalIndex]);
childColumnMap[colname] = false;
timeComparisonColumnMap[colname] = false;
}
@@ -749,24 +812,71 @@ const config: ControlPanelConfig = {
: [];
const chartStatus = chart?.chartStatus;
// Normalize legacy `toAllRow`/`toTextColor` flags saved before
// `columnFormatting`/`objectFormatting` existed, so "entire row"
// formatters set under the old schema keep working.
const value = _?.value ?? [];
if (value && Array.isArray(value)) {
value.forEach(
(item: ConditionalFormattingConfig, index, array) => {
if (
item.colorScheme &&
(typeof item.colorScheme !== 'string' ||
!['Green', 'Red'].includes(item.colorScheme))
) {
if (item.columnFormatting === undefined) {
// eslint-disable-next-line no-param-reassign
array[index] = {
...item,
...(item.toTextColor === true && {
objectFormatting: ObjectFormattingEnum.TEXT_COLOR,
}),
...(item.toAllRow === true && {
columnFormatting: ObjectFormattingEnum.ENTIRE_ROW,
}),
};
}
}
},
);
}
const { colnames, coltypes } =
chart?.queriesResponse?.[0] ?? {};
const numericColumns =
Array.isArray(colnames) && Array.isArray(coltypes)
? colnames
.filter(
(colname: string, index: number) =>
coltypes[index] === GenericDataType.Numeric,
)
.map((colname: string) => ({
value: colname,
label: Array.isArray(verboseMap)
? colname
: (verboseMap[colname] ?? colname),
dataType:
colnames && coltypes[colnames?.indexOf(colname)],
}))
: [];
const hasColumns =
Array.isArray(colnames) && Array.isArray(coltypes);
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
.filter(
(colname: string, index: number) =>
coltypes[index] === GenericDataType.Numeric,
)
.map((colname: string) => ({
value: colname,
label: Array.isArray(verboseMap)
? colname
: (verboseMap?.[colname] ?? colname),
// Every entry here already passed the Numeric filter
// above, so the type is always Numeric — no need to
// re-look it up (which breaks on duplicate colnames).
dataType: GenericDataType.Numeric,
}))
: [];
const columnOptions = hasTimeComparison
? processComparisonColumns(
numericColumns || [],
@@ -778,6 +888,7 @@ const config: ControlPanelConfig = {
removeIrrelevantConditions: chartStatus === 'success',
columnOptions,
verboseMap,
allColumns,
extraColorChoices,
serverPagination: Boolean(
explore?.controls?.server_pagination?.value,
@@ -31,8 +31,6 @@ import controlPanel from './controlPanel';
import buildQuery from './buildQuery';
import { TableChartFormData, TableChartProps } from './types';
// must export something for the module to be exist in dev mode
export { default as __hack__ } from './types';
export * from './types';
export {
convertAgGridStateToOwnState,
@@ -46,6 +44,7 @@ const metadata = new ChartMetadata({
Behavior.InteractiveChart,
Behavior.DrillToDetail,
Behavior.DrillBy,
'EXPORT_CURRENT_VIEW' as Behavior,
],
category: t('Table'),
canBeAnnotationTypes: ['EVENT', 'INTERVAL'],
@@ -19,6 +19,7 @@
import {
BackendOwnState,
ChartStateConverterOptions,
QuerySortBy,
type AgGridChartState,
type AgGridSortModel,
@@ -353,7 +354,24 @@ export function convertFilterModel(
*/
export function convertAgGridStateToOwnState(
agGridState: AgGridChartState,
options: ChartStateConverterOptions = {},
): Partial<BackendOwnState> {
// In client mode, AG Grid handles sort/filter/pagination locally, so for
// the *live* query none of it needs to reach the backend -- folding it
// into ownState there would only trigger an unnecessary requery/remount.
// A *download* query has no client-side pass to apply that state though:
// dashboard doesn't consume the Explore-only clientView snapshot, so
// exports still need it converted to reproduce the displayed
// sort/filter/columns (options.forExport).
//
// Only an explicit `false` is treated as "definitely client mode":
// legacy persisted table_state/permalinks predate serverPagination and
// have it `undefined`, and treating that the same as `false` would
// silently drop their persisted server-side sort/filter on restore.
if (agGridState.serverPagination === false && !options.forExport) {
return {};
}
const ownState: Partial<BackendOwnState> = {};
const sortBy = convertSortModel(agGridState.sortModel);
@@ -184,6 +184,7 @@ export const PaginationContainer = styled.div`
color: ${theme.colorTextBase};
transform: translateY(-${theme.sizeUnit}px);
background: ${theme.colorBgBase};
flex-shrink: 0;
`}
`;
@@ -353,6 +354,7 @@ export const StyledChartContainer = styled.div<{
.dropdown-controls-container {
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
.time-comparison-dropdown {
@@ -29,6 +29,7 @@ import {
getNumberFormatter,
getTimeFormatter,
getTimeFormatterForGranularity,
normalizeCurrency,
NumberFormats,
QueryMode,
SMART_DATE_ID,
@@ -36,6 +37,7 @@ import {
TimeFormatter,
AgGridChartState,
AgGridFilterModel,
DateWithFormatter,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { isEmpty, isEqual, merge } from 'lodash-es';
@@ -45,7 +47,6 @@ import {
ColorSchemeEnum,
} from '@superset-ui/chart-controls';
import isEqualColumns from './utils/isEqualColumns';
import DateWithFormatter from './utils/DateWithFormatter';
import { BASIC_COLOR_FORMATTERS_ROW_KEY } from './consts';
import {
DataColumnMeta,
@@ -60,7 +61,11 @@ const { DATABASE_DATETIME } = TimeFormats;
function isNumeric(key: string, data: DataRecord[] = []) {
return data.every(
x => x[key] === null || x[key] === undefined || typeof x[key] === 'number',
x =>
x[key] === null ||
x[key] === undefined ||
x[key] === '' ||
typeof x[key] === 'number',
);
}
@@ -168,7 +173,33 @@ const getComparisonColFormatter = (
return formatter;
};
const processComparisonDataRecords = memoizeOne(
// transformProps is a single module-level function shared by every mounted
// instance of this chart plugin on a dashboard (one plugin registration,
// not one per chart). memoizeOne only remembers the single most-recent
// call, so wrapping a function in it directly here means unrelated chart
// instances evict each other's cached result whenever they render in the
// same tick, forcing a full rebuild - with brand-new array/object
// references - even when a given chart's own inputs are unchanged. AG
// Grid treats a new colDefs identity as "columns changed" and re-measures
// autoHeight/wrapText rows, which is what actually reads as a layout
// flicker on a chart that never changed. Keying a separate memoized
// function per chart id isolates each chart's cache from its siblings.
function memoizePerChart<Args extends unknown[], R>(
fn: (...args: Args) => R,
isEqual?: (newArgs: Args, lastArgs: Args) => boolean,
) {
const memoizedByChart = new Map<number, (...args: Args) => R>();
return (sliceId: number, ...args: Args): R => {
let fnForChart = memoizedByChart.get(sliceId);
if (!fnForChart) {
fnForChart = isEqual ? memoizeOne(fn, isEqual) : memoizeOne(fn);
memoizedByChart.set(sliceId, fnForChart);
}
return fnForChart(...args);
};
}
const processComparisonDataRecords = memoizePerChart(
function processComparisonDataRecords(
originalData: DataRecord[] | undefined,
originalColumns: DataColumnMeta[],
@@ -309,7 +340,7 @@ const processComparisonColumns = (
const serverPageLengthMap = new Map();
const processDataRecords = memoizeOne(function processDataRecords(
const processDataRecords = memoizePerChart(function processDataRecords(
data: DataRecord[] | undefined,
columns: DataColumnMeta[],
) {
@@ -336,11 +367,16 @@ const processDataRecords = memoizeOne(function processDataRecords(
return data;
});
const processColumns = memoizeOne(function processColumns(
const processColumns = memoizePerChart(function processColumns(
props: TableChartProps,
) {
const {
datasource: { columnFormats, currencyFormats, verboseMap },
datasource: {
columnFormats,
currencyFormats,
verboseMap,
currencyCodeColumn,
},
rawFormData: {
table_timestamp_format: tableTimestampFormat,
metrics: metrics_,
@@ -352,7 +388,12 @@ const processColumns = memoizeOne(function processColumns(
queriesData,
} = props;
const granularity = extractTimegrain(props.rawFormData);
const { data: records, colnames, coltypes } = queriesData[0] || {};
const {
data: records,
colnames,
coltypes,
detected_currency: detectedCurrency,
} = queriesData[0] || {};
// convert `metrics` and `percentMetrics` to the key names in `data.records`
const metrics = (metrics_ ?? []).map(getMetricLabel);
const rawPercentMetrics = (percentMetrics_ ?? []).map(getMetricLabel);
@@ -363,13 +404,18 @@ const processColumns = memoizeOne(function processColumns(
const rawPercentMetricsSet = new Set(rawPercentMetrics);
const columns: DataColumnMeta[] = (colnames || [])
.map((key: string, originalIndex: number) => ({ key, originalIndex }))
.filter(
key =>
({ key }) =>
// if a metric was only added to percent_metrics, they should not show up in the table.
!(rawPercentMetricsSet.has(key) && !metricsSet.has(key)),
)
.map((key: string, i) => {
const dataType = coltypes[i];
.map(({ key, originalIndex }) => {
// Look up by the column's original position in colnames/coltypes,
// not its position after the filter above — those diverge whenever
// an earlier column (e.g. a percent-metric-only one) got filtered
// out, which would otherwise shift every later column's dataType.
const dataType = coltypes[originalIndex];
const config = columnConfig[key] || {};
// for the purpose of presentation, only numeric values are treated as metrics
// because users can also add things like `MAX(str_col)` as a metric.
@@ -431,10 +477,25 @@ const processColumns = memoizeOne(function processColumns(
// percent metrics have a default format
formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT);
} else if (isMetric || (isNumber && (numberFormat || currency))) {
formatter = currency?.symbol
// Resolve AUTO currency when currency column isn't in query results
let resolvedCurrency = currency;
if (
currency?.symbol === 'AUTO' &&
detectedCurrency &&
(!currencyCodeColumn || !colnames?.includes(currencyCodeColumn))
) {
const normalizedCurrency = normalizeCurrency(detectedCurrency);
if (normalizedCurrency) {
resolvedCurrency = {
...currency,
symbol: normalizedCurrency,
};
}
}
formatter = resolvedCurrency?.symbol
? new CurrencyFormatter({
d3Format: numberFormat,
currency,
currency: resolvedCurrency,
})
: getNumberFormatter(numberFormat);
}
@@ -448,6 +509,7 @@ const processColumns = memoizeOne(function processColumns(
formatter,
config,
description,
currencyCodeColumn,
};
})
.sort((a, b) => {
@@ -494,7 +556,7 @@ const transformProps = (
queriesData = [],
ownState: serverPaginationData,
filterState,
hooks: { setDataMask = () => {}, onChartStateChange },
hooks: { setDataMask = () => {}, onChartStateChange, onContextMenu },
emitCrossFilters,
theme,
} = chartProps;
@@ -526,10 +588,10 @@ const transformProps = (
comparison_color_enabled: comparisonColorEnabled = false,
comparison_color_scheme: comparisonColorScheme = ColorSchemeEnum.Green,
show_numbered_column: showNumberedColumn = false,
allow_rearrange_columns: allowRearrangeColumns = true,
allow_render_html: allowRenderHtml = true,
} = formData;
const allowRearrangeColumns = true;
// Calculate time comparison settings early since they're used in multiple places
const isUsingTimeComparison =
!isEmpty(time_compare) &&
@@ -682,7 +744,7 @@ const transformProps = (
hasServerPageLengthChanged = true;
}
const [, percentMetrics, columns] = processColumns(chartProps);
const [, percentMetrics, columns] = processColumns(slice_id, chartProps);
const timeGrain = extractTimegrain(formData);
@@ -700,20 +762,34 @@ const transformProps = (
);
}
// buildQuery.ts can append an "all records" percent-metric denominator
// query *and* a totals query, independently of each other, both landing
// in extraQueries before the totals one. A fixed totalQuery index would
// silently bind to the wrong query's data (or drop the totals query
// entirely) whenever both are present, so replicate buildQuery.ts's own
// gating condition here to know whether to skip that extra slot.
const hasAllRecordsExtraQuery = Boolean(
formData.percent_metrics?.length &&
(formData.percent_metric_calculation || 'row_limit') === 'all_records',
);
let baseQuery;
let countQuery;
let rowCount;
let totalQuery;
if (serverPagination) {
[baseQuery, countQuery, totalQuery] = queriesData;
[baseQuery, countQuery] = queriesData;
totalQuery = hasAllRecordsExtraQuery ? queriesData[3] : queriesData[2];
rowCount = (countQuery?.data?.[0]?.rowcount as number) ?? 0;
} else {
[baseQuery, totalQuery] = queriesData;
[baseQuery] = queriesData;
totalQuery = hasAllRecordsExtraQuery ? queriesData[2] : queriesData[1];
rowCount = baseQuery?.rowcount ?? 0;
}
const data = processDataRecords(baseQuery?.data, columns);
const data = processDataRecords(slice_id, baseQuery?.data, columns);
const comparisonData = processComparisonDataRecords(
slice_id,
baseQuery?.data,
columns,
comparisonSuffix,
@@ -793,12 +869,12 @@ const transformProps = (
// Map saved metric/calculated column labels to their SQL expressions for filter resolution
const metricSqlExpressions: Record<string, string> = {};
chartProps.datasource.metrics.forEach(metric => {
(chartProps.datasource?.metrics ?? []).forEach(metric => {
if (metric.metric_name && metric.expression) {
metricSqlExpressions[metric.metric_name] = metric.expression;
}
});
chartProps.datasource.columns.forEach(col => {
(chartProps.datasource?.columns ?? []).forEach(col => {
if (col.column_name && col.expression) {
metricSqlExpressions[col.column_name] = col.expression;
if (col.verbose_name && col.verbose_name !== col.column_name) {
@@ -811,7 +887,7 @@ const transformProps = (
// backed by a dataset (physical or calculated) column can be summed
// server-side; free-form SQL expression columns are excluded.
const datasetColumnNames = new Set(
chartProps.datasource.columns
(chartProps.datasource?.columns ?? [])
.map(col => col.column_name)
.filter((name): name is string => Boolean(name)),
);
@@ -849,6 +925,7 @@ const transformProps = (
filters: filterState.filters,
emitCrossFilters,
allowRearrangeColumns,
allowRenderHtml,
slice_id,
serverPagination,
rowCount,
@@ -873,6 +950,7 @@ const transformProps = (
chartState,
onChartStateChange,
showNumberedColumn,
onContextMenu,
};
};
@@ -39,6 +39,7 @@ import {
JsonObject,
Metric,
AgGridChartState,
ContextMenuFilters,
} from '@superset-ui/core';
import {
ColDef,
@@ -81,6 +82,7 @@ export type TableChartFormData = QueryFormData & {
time_grain_sqla?: TimeGranularity;
column_config?: Record<string, TableColumnConfig>;
allow_rearrange_columns?: boolean;
allow_render_html?: boolean;
show_numbered_column?: boolean;
};
@@ -134,6 +136,11 @@ export interface AgGridTableChartTransformedProps<
onChartStateChange?: (chartState: JsonObject) => void;
chartState?: AgGridChartState;
showNumberedColumn: boolean;
onContextMenu?: (
clientX: number,
clientY: number,
filters?: ContextMenuFilters,
) => void;
}
export interface SortState {
@@ -196,6 +203,7 @@ export interface InputColumn {
originalLabel?: string;
metricName?: string;
description?: string;
currencyCodeColumn?: string;
}
export type ValueRange = [number, number] | null;
@@ -234,5 +242,3 @@ export type Dataset = {
metrics?: Metric[];
verbose_map?: Record<string, string>;
};
export default {};
@@ -20,6 +20,17 @@
import { SetDataMaskHook } from '@superset-ui/core';
import { SortByItem } from '../types';
export interface ClientViewColumn {
key: string;
label: string;
}
export interface ClientViewSnapshot {
rows: Record<string, unknown>[];
columns: ClientViewColumn[];
count: number;
}
interface TableOwnState {
currentPage?: number;
pageSize?: number;
@@ -29,6 +40,7 @@ interface TableOwnState {
sortBy?: SortByItem[];
rawSummaryColumns?: string[];
totalsRequested?: boolean;
clientView?: ClientViewSnapshot;
}
export const updateTableOwnState = (

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