Two CI follow-ups:
1. \`TabsRenderer.test.tsx\` rendered the component without a Redux
Provider, which broke after Phase 4b added \`<ComponentThemeProvider>\`
wrapping the body — the provider's \`useEffectiveThemeId\` calls
\`useSelector\`. Pass \`useRedux: true\` on every \`render(...)\` call
in the test (rerender shares the same wrapper, so it doesn't need
the option).
2. After fixing #1, the test then failed on \`useThemeContext\` throwing
because there's no \`<ThemeProvider>\` in the test tree either. Make
\`ComponentThemeProvider\` resilient: read the underlying
\`ThemeContext\` directly via \`useContext\` (instead of going through
\`useThemeContext\`'s mandatory throw) and pass through children
when no theme context is mounted. Required exporting \`ThemeContext\`
from \`ThemeProvider\`.
Same defensive posture also helps storybook stories and embedded
contexts that don't always set up a dashboard ThemeProvider.
11/11 TabsRenderer tests passing locally; the 14 ComponentThemeProvider
tests + 4 ComponentHeaderControls tests + 3 setComponentThemeId tests
all still pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure formatting / oxlint auto-fixes from running pre-commit:
- Inline a single-element array literal in
`ComponentHeaderControls.test.tsx` (prettier).
- Drop a redundant `?? {}` on a spread in
`useEffectiveThemeId.test.ts` (oxlint).
- Spread `?? {}` cleanup in
`setComponentThemeId.test.ts` (prettier).
No behavior change. Was caught by CI's pre-commit job because the
local pre-commit hook hadn't run on these files at commit time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
License Check (Apache RAT) flagged `SIP.md` at the repo root for
missing the license header. Add it as an HTML comment so the
rendered Markdown is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CI lint-frontend caught two type errors:
1. \`setComponentThemeId.ts\` imported \`AppDispatch\` from
\`src/dashboard/types\` — that name isn't exported there. The
project's pattern (used by \`dashboardLayout.ts\`,
\`hydrate.ts\`, \`sqlLab.ts\`) is to declare a local
\`type AppDispatch = ThunkDispatch<RootState, undefined, AnyAction>\`.
Match that.
2. \`useEffectiveThemeId.ts\` had a \`const node = layout[cursorId]\`
inside a loop where TS couldn't infer \`node\`'s type cleanly
(TS7022 — implicit any in self-referencing initializer, likely from
the inline \`as { themeId?: number | null } | undefined\` cast on
\`node.meta\`). Drop the cast (LayoutItemMeta already has
\`themeId?: number | null\` from Phase 1) and add an explicit
\`LayoutItem | undefined\` annotation on \`node\`.
All 17 unit tests still pass. Project \`tsc --noEmit\` is clean for
the touched files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the modal is open, the targeted component re-renders with the
candidate theme as soon as the user picks an option — no dashboard-
dirty round-trip required. Cancel reverts; Apply commits to Redux.
Implementation:
- New \`previewThemeStore\` (module-level subscribable map keyed by
layoutId, distinguishing "no preview" / "preview value=null" /
"preview value=number"). Tiny surface: \`set\`/\`clear\`/\`get\`/
\`subscribe\`. No-op \`set\` / \`clear\` calls don't fire listeners.
- \`useEffectiveThemeId\` now subscribes via \`useSyncExternalStore\`
and prefers the preview value over the Redux-resolved id when
present.
- \`ThemeSelectorModal\` writes the in-flight selection through the
store as the user picks options; cleanup on close (Cancel, X
button, escape) clears it. Apply dispatches the Redux action
*before* hiding, so the post-cleanup re-resolution lands on the
saved value (no flicker).
Snapshot of the resolved id at open-time goes through a \`useRef\`
because \`currentThemeId\` itself becomes reactive (it would already
reflect the in-flight preview), so we can't read it for "what should
we revert to?".
6 new tests for the preview store: get-undefined-for-unknown,
set-stores-numeric, set-stores-explicit-null, clear-removes,
subscriber-fires-on-real-change-and-not-on-no-op, multi-layoutId-
independence. Total dashboard ComponentThemeProvider suite is now
14 passing tests.
Drops "live preview" from the deferred-items list in SIP.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same three-step recipe applied to each grid-component type:
(a) wrap body in <ComponentThemeProvider layoutId={id}>
(b) add "Apply theme" item to the component's menu via
ComponentHeaderControls
(c) mount <ThemeSelectorModal> gated on editMode
- TabsRenderer (4b): wraps StyledTabsContainer; dots menu lands in the
existing left HoverMenu next to drag/delete.
- Row (4c): wraps WithPopoverMenu body; dots menu in the left HoverMenu
next to drag/delete/setting-icon. The existing gear icon (opens the
BackgroundStyleDropdown focus popover) is preserved as-is.
- Column (4d): same recipe as Row, top-positioned HoverMenu.
- Markdown (4e): class component, so themeModalOpen lives on
this.state. Dots menu lands inside the existing WithPopoverMenu
menuItems array next to MarkdownModeDropdown; the Edit/Preview
toggle is intentionally preserved unchanged.
Note on scope: the SIP originally imagined Phase 4 would also converge
MarkdownModeDropdown and the Row/Column gear icon onto the shared dots
menu. Those user-visible UX displacements are intentionally deferred
so this phase adds the theming affordance *additively* — every existing
menu control is untouched. The menu-pattern unification can be picked
up later without coupling it to theming.
Functional outcome: every grid-component type (Chart, Markdown, Row,
Column, Tabs) now supports the full inheritance chain end-to-end:
Instance -> Dashboard -> Tab -> Row/Col -> Chart/Markdown. Setting a
themeId at any level applies to that subtree; clearing it falls
through to the parent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the loop on per-component theming for Chart: adds an "Apply
theme" item to \`SliceHeaderControls\` (gated on dashboard edit mode)
that opens the Phase-3 \`ThemeSelectorModal\`. On save, the Phase-3
\`setComponentThemeId\` action updates \`meta.themeId\`; the Phase-1
\`ComponentThemeProvider\` (already wrapping ChartHolder since Phase 1)
re-resolves and re-renders the chart with the new theme tokens.
The full inheritance chain (Instance → Dashboard → Tab → Row/Col →
Chart) is functionally complete for Chart with this commit. Subsequent
per-component PRs (Markdown, Row, Column, Tabs) will repeat the same
three-step recipe — menu item, modal mount, body wrapper — in
isolation so each user-visible menu/UX change can be reviewed without
dragging in the theming framework changes (already merged via Phases
1-3).
- Adds \`MenuKeys.ApplyTheme\` to the dashboard menu-key enum.
- \`SliceHeaderControls\` gets local \`themeModalOpen\` state, a
Redux selector for \`dashboardState.editMode\`, a handler that opens
the modal on menu click, the menu item itself (push gated on
editMode), and a \`<ThemeSelectorModal>\` mounted with the
component's \`layoutId\`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
End-to-end mechanism for applying a CRUD theme to a single dashboard
grid component. Two pieces:
1. `setComponentThemeId(componentId, themeId | null)` — thin Redux
action that merges \`themeId\` into the target component's \`meta\`
via the existing \`updateComponents\` thunk, preserving every other
meta field. Explicit \`null\` clears the override and falls back to
the inherited theme; the resolver in Phase 1 treats null and
undefined identically. No-ops when the component id isn't in the
layout.
2. \`ThemeSelectorModal\` — parent-owned modal that fetches non-system
themes (same query as the dashboard Properties modal:
\`is_system:false\` filter on \`/api/v1/theme/\`), preselects the
currently-resolved override via the Phase-1 \`useEffectiveThemeId\`
hook, and exposes Apply / Cancel / Clear-override-(inherit) actions.
Each call site provides \`layoutId\` + the \`show\`/\`onHide\` toggle.
No call site for the modal yet — Phase 4 wires the "Apply theme" menu
item into each component's \`ComponentHeaderControls\` to open it.
3 passing tests on the action: merge preserves other meta keys, clear
stores explicit null (not undefined), no-op for missing component.
SIP.md updated with the Phase 3 implementation notes and the deferred-
to-Phase-4 wiring detail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shared vertical-dots menu component for dashboard grid components.
Generic `items: ComponentMenuItem[]` API — each component (Chart,
Markdown, Row, Column, Tabs) plugs in its own list; the visual chrome
(dots icon trigger, dropdown surface, accessible label, divider
handling, danger/disabled styling) lives in this one component.
Built on `MenuDotsDropdown` from `@superset-ui/core/components` so the
trigger styling matches Chart's existing `SliceHeaderControls` — Phase
4's per-component PRs will converge `SliceHeaderControls` and the
other menu patterns (Markdown's `MarkdownModeDropdown`, Row/Col's
gear-icon + `WithPopoverMenu`) onto this same component.
Phase 2 lands the component + tests only. The actual per-component
menu conversions are user-visible UX changes (e.g. Markdown loses its
toggle-style Edit/Preview switcher and gains a dots menu) and ship in
Phase 4 alongside theme wiring per component, so each can be reviewed
in isolation rather than as a sweeping refactor.
4 passing tests: empty items renders nothing, trigger renders, onClick
fires from menu selection, disabled items don't fire onClick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the skeleton for granular (per-component) theming on dashboard grid
components, with the inheritance chain:
Instance theme -> Dashboard theme -> Tab theme
-> Row/Col theme -> Chart/Markdown theme
This commit lands Phase 1 from the SIP (`SIP.md` at repo root): the
storage shape and the resolver, wired into `ChartHolder` as the
proof-of-concept call site. No UI yet — `themeId` must be set via Redux
devtools / position_json hand-edit to verify visually. Phase 2 will
introduce the `ComponentHeaderControls` menu and Phase 3 the
`ThemeSelectorModal` that drives this from a real UI.
Surface:
- `LayoutItemMeta.themeId?: number | null` — optional CRUD theme id
stored per-component in `position_json` meta (no schema migration; the
meta map is already open-ended). `null` and `undefined` both mean
"inherit from parent".
- `pickEffectiveThemeId(layoutId, layout)` — pure resolver. Walks
`parents` up the layout map from the given node until it finds a
numeric `themeId` or hits `DASHBOARD_ROOT_ID`. Hop-capped at 32 to
defend against malformed parents chains.
- `useEffectiveThemeId(layoutId)` — Redux hook variant.
- `<ComponentThemeProvider layoutId={...}>` — wraps children in the
resolved theme's `SupersetThemeProvider`. Lazy-fetches via the
existing `ThemeController.createDashboardThemeProvider`, which already
caches by id so N components sharing one theme = 1 fetch. Pass-through
when no ancestor sets a `themeId`.
- `ChartHolder.tsx` — wraps the existing `<AntdThemeProvider>` (which is
a popup-container shim for fullscreen mode, not a token provider) so
per-component tokens are set before antd's ConfigProvider for popup
targeting.
Tests: 8 unit cases for `pickEffectiveThemeId` covering own / inherited /
null-skip / no-ancestor / root-stop / malformed-parents / other-meta /
missing-id.
Closes the spirit of the closed PR #36749 (which became unrebasable
after .jsx -> .tsx + React 18 + theme controller churn).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Draft Superset Improvement Proposal for component-level theming on
dashboard grid components (Charts, Markdown, Row, Column, Tabs) with
inheritance Instance -> Dashboard -> Tab -> Row/Col -> Chart.
Supersedes the closed PR #36749 (became unrebasable after .jsx -> .tsx
conversion, React 18 upgrade, and theme-controller churn since 2025-12).
This is a living document — kept in lockstep with the work on
`feat/granular-theming-v2`. Each phase updates the status / shortcomings
/ test-plan sections.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>