From 7fbd0394a0dfafa30c4e016c1d98c8eea28f9b36 Mon Sep 17 00:00:00 2001 From: Tanbir Date: Wed, 23 Sep 2026 17:09:48 +0200 Subject: [PATCH 1/2] fix(logs): default log viewer palette to the app theme The log viewer always opened dark, and in dark app theme the Sun/Moon toggle was hidden because the theme was passed as forceDark. Add a defaultDark prop that seeds the palette when the user hasn't picked one, follows theme changes until they do, and keeps the toggle visible. Fixes #1790 --- .../components/logs/LogCore.theme.test.tsx | 63 +++++++++++++++++++ .../k8s-ui/src/components/logs/LogCore.tsx | 20 +++++- .../k8s-ui/src/components/logs/LogsViewer.tsx | 6 +- .../components/logs/WorkloadLogsViewer.tsx | 7 ++- web/src/components/logs/LogsViewer.tsx | 2 +- .../components/logs/WorkloadLogsViewer.tsx | 2 +- 6 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx diff --git a/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx b/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx new file mode 100644 index 0000000000..0efd690f3f --- /dev/null +++ b/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx @@ -0,0 +1,63 @@ +// @vitest-environment jsdom +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { LogCore } from './LogCore' + +Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }) + +let root: Root +let element: HTMLDivElement +beforeEach(() => { + localStorage.clear() + element = document.createElement('div') + document.body.appendChild(element) + root = createRoot(element) +}) +afterEach(async () => { + await act(async () => root.unmount()) + element.remove() +}) + +const noop = () => {} +async function render(props: { forceDark?: boolean; defaultDark?: boolean }) { + await act(async () => + root.render( + , + ), + ) +} +const toggle = () => element.querySelector('button[aria-label^="Switch log viewer"]') + +describe('LogCore theme', () => { + it('defaults to dark when no theme hint is given', async () => { + await render({}) + expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to light mode') + }) + + it('follows defaultDark and keeps the toggle available', async () => { + await render({ defaultDark: false }) + expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to dark mode') + await render({ defaultDark: true }) + expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to light mode') + }) + + it('prefers the saved toggle choice over defaultDark', async () => { + localStorage.setItem('radar-logs-dark', 'true') + await render({ defaultDark: false }) + expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to light mode') + }) + + it('hides the toggle when forceDark is set', async () => { + await render({ forceDark: true, defaultDark: false }) + expect(toggle()).toBeNull() + }) +}) diff --git a/packages/k8s-ui/src/components/logs/LogCore.tsx b/packages/k8s-ui/src/components/logs/LogCore.tsx index e1a6d2113f..9907f3c5e9 100644 --- a/packages/k8s-ui/src/components/logs/LogCore.tsx +++ b/packages/k8s-ui/src/components/logs/LogCore.tsx @@ -66,6 +66,12 @@ interface LogCoreProps { * the viewer manages its own palette via localStorage and the Sun/Moon button. */ forceDark?: boolean + /** + * Palette to use when the user hasn't picked one with the Sun/Moon toggle + * (e.g. the host app's theme). Unlike `forceDark`, the toggle stays visible. + * Defaults to dark. + */ + defaultDark?: boolean } interface LevelOption { @@ -152,12 +158,14 @@ export function LogCore({ emptyCommand, errorMessage, forceDark, + defaultDark = true, }: LogCoreProps) { const virtuosoRef = useRef(null) const [atBottom, setAtBottom] = useState(true) const themeLocked = typeof forceDark === 'boolean' // Seed isDark: forceDark prop wins; else localStorage['radar-logs-dark']; - // else default dark. See log-palette.ts for why the viewer is palette-driven + // else defaultDark (the host app's theme, dark if not given). See + // log-palette.ts for why the viewer is palette-driven // instead of theme-token-driven. const [isDark, setIsDark] = useState(() => { if (typeof forceDark === 'boolean') return forceDark @@ -166,13 +174,21 @@ export function LogCore({ if (v === 'false') return false if (v === 'true') return true } catch {} - return true + return defaultDark }) useEffect(() => { if (typeof forceDark === 'boolean') { setIsDark(forceDark) } }, [forceDark]) + // Follow host theme changes until the user picks a palette explicitly. + useEffect(() => { + if (typeof forceDark === 'boolean') return + try { + if (localStorage.getItem('radar-logs-dark') !== null) return + } catch {} + setIsDark(defaultDark) + }, [defaultDark, forceDark]) const palette = useMemo(() => getLogPalette(isDark), [isDark]) const toggleDark = useCallback(() => { if (themeLocked) return diff --git a/packages/k8s-ui/src/components/logs/LogsViewer.tsx b/packages/k8s-ui/src/components/logs/LogsViewer.tsx index 6e0368ac5b..c6ac52fdcb 100644 --- a/packages/k8s-ui/src/components/logs/LogsViewer.tsx +++ b/packages/k8s-ui/src/components/logs/LogsViewer.tsx @@ -28,8 +28,10 @@ export interface LogsViewerProps { createStream?: (params: Omit) => EventSource /** Override the download mechanism (e.g. for desktop apps where blob URLs fail). */ overrideDownload?: (content: string, mime: string, filename: string) => void - /** Force dark mode on the logs container (default: true) */ + /** Pin the logs container to dark (true) or light (false) and hide the toggle */ forceDark?: boolean + /** Palette used until the user toggles it, e.g. the app theme (default: true) */ + defaultDark?: boolean /** * Open the stream automatically on mount (and on container switch) instead of * loading a static snapshot. The user can still Stop, and a manual Stop is not @@ -47,6 +49,7 @@ export function LogsViewer({ createStream, overrideDownload, forceDark, + defaultDark, autoStream = false, }: LogsViewerProps) { const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '') @@ -190,6 +193,7 @@ export function LogsViewer({ onClear={clear} toolbarExtra={renderToolbarExtra} forceDark={forceDark} + defaultDark={defaultDark} /> ) } diff --git a/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx b/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx index 2f8fe643b9..44dce73e23 100644 --- a/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx +++ b/packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx @@ -53,8 +53,10 @@ export interface WorkloadLogsViewerProps { createStream?: (params: WorkloadLogsFetchParams) => EventSource /** Override the download mechanism (e.g. for desktop apps where blob URLs fail). */ overrideDownload?: (content: string, mime: string, filename: string) => void - /** Force dark mode on the logs container (default: true) */ + /** Pin the logs container to dark (true) or light (false) and hide the toggle */ forceDark?: boolean + /** Palette used until the user toggles it, e.g. the app theme (default: true) */ + defaultDark?: boolean /** * Open the stream automatically on mount (and on container switch) instead of * loading a static snapshot. The user can still Stop, and a manual Stop is not @@ -63,7 +65,7 @@ export interface WorkloadLogsViewerProps { autoStream?: boolean } -export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark, autoStream = false }: WorkloadLogsViewerProps) { +export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark, defaultDark, autoStream = false }: WorkloadLogsViewerProps) { const [selectedContainer, setSelectedContainer] = useState('') const [pods, setPods] = useState([]) const [selectedPods, setSelectedPods] = useState>(new Set()) @@ -400,6 +402,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl emptyCommand={emptyCommand} errorMessage={entries.length === 0 ? fetchError || streamError : null} forceDark={forceDark} + defaultDark={defaultDark} /> ) } diff --git a/web/src/components/logs/LogsViewer.tsx b/web/src/components/logs/LogsViewer.tsx index a7ecc290ed..67a42c1aca 100644 --- a/web/src/components/logs/LogsViewer.tsx +++ b/web/src/components/logs/LogsViewer.tsx @@ -43,7 +43,7 @@ export function LogsViewer({ namespace, podName, containers, initialContainer, a fetchLogs={fetchLogs} createStream={makeStream} overrideDownload={desktopDownload} - forceDark={theme === 'dark' ? true : undefined} + defaultDark={theme === 'dark'} autoStream={autoStream} /> ) diff --git a/web/src/components/logs/WorkloadLogsViewer.tsx b/web/src/components/logs/WorkloadLogsViewer.tsx index 72bd2477f2..1d63c7d102 100644 --- a/web/src/components/logs/WorkloadLogsViewer.tsx +++ b/web/src/components/logs/WorkloadLogsViewer.tsx @@ -42,7 +42,7 @@ export function WorkloadLogsViewer({ kind, namespace, name, autoStream = true, s fetchAll={fetchAll} createStream={snapshotOnly ? undefined : makeStream} overrideDownload={desktopDownload} - forceDark={theme === 'dark' ? true : undefined} + defaultDark={theme === 'dark'} autoStream={autoStream} /> ) From fbb6a2c66b00349cf9666d0f19744d191ab4946e Mon Sep 17 00:00:00 2001 From: Nadav Erell Date: Sun, 4 Oct 2026 00:29:24 +0300 Subject: [PATCH 2/2] Keep logs dark in dark theme; start light in light theme Light logs on a dark app are never wanted, so dark theme pins the palette and hides the toggle. Light theme starts light and keeps the toggle, so dark logs on a light app stay one click away. When a pin is lifted the viewer now re-reads the saved choice instead of keeping the pinned palette, and ignores saved values that aren't a palette choice. --- .../src/components/logs/LogCore.theme.test.tsx | 14 ++++++++++++++ packages/k8s-ui/src/components/logs/LogCore.tsx | 9 +++++++-- web/src/components/logs/LogsViewer.tsx | 5 ++++- web/src/components/logs/WorkloadLogsViewer.tsx | 5 ++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx b/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx index 0efd690f3f..2affc30e14 100644 --- a/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx +++ b/packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx @@ -56,6 +56,20 @@ describe('LogCore theme', () => { expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to light mode') }) + it('returns to the saved choice when a forceDark pin is lifted', async () => { + localStorage.setItem('radar-logs-dark', 'false') + await render({ forceDark: true, defaultDark: false }) + expect(toggle()).toBeNull() + await render({ defaultDark: false }) + expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to dark mode') + }) + + it('ignores a saved value that is not a palette choice', async () => { + localStorage.setItem('radar-logs-dark', 'garbage') + await render({ defaultDark: false }) + expect(toggle()?.getAttribute('aria-label')).toBe('Switch log viewer to dark mode') + }) + it('hides the toggle when forceDark is set', async () => { await render({ forceDark: true, defaultDark: false }) expect(toggle()).toBeNull() diff --git a/packages/k8s-ui/src/components/logs/LogCore.tsx b/packages/k8s-ui/src/components/logs/LogCore.tsx index 9907f3c5e9..ad7ce13efd 100644 --- a/packages/k8s-ui/src/components/logs/LogCore.tsx +++ b/packages/k8s-ui/src/components/logs/LogCore.tsx @@ -181,11 +181,16 @@ export function LogCore({ setIsDark(forceDark) } }, [forceDark]) - // Follow host theme changes until the user picks a palette explicitly. + // Re-resolve whenever the host's hint changes, including when a forceDark pin + // is lifted: the user's saved pick wins, otherwise the host default. useEffect(() => { if (typeof forceDark === 'boolean') return try { - if (localStorage.getItem('radar-logs-dark') !== null) return + const v = localStorage.getItem('radar-logs-dark') + if (v === 'true' || v === 'false') { + setIsDark(v === 'true') + return + } } catch {} setIsDark(defaultDark) }, [defaultDark, forceDark]) diff --git a/web/src/components/logs/LogsViewer.tsx b/web/src/components/logs/LogsViewer.tsx index 67a42c1aca..82e2835bfe 100644 --- a/web/src/components/logs/LogsViewer.tsx +++ b/web/src/components/logs/LogsViewer.tsx @@ -43,7 +43,10 @@ export function LogsViewer({ namespace, podName, containers, initialContainer, a fetchLogs={fetchLogs} createStream={makeStream} overrideDownload={desktopDownload} - defaultDark={theme === 'dark'} + // Light logs on a dark app are never wanted: dark theme pins the palette, + // light theme only sets where it starts and leaves the toggle available. + forceDark={theme === 'dark' ? true : undefined} + defaultDark={false} autoStream={autoStream} /> ) diff --git a/web/src/components/logs/WorkloadLogsViewer.tsx b/web/src/components/logs/WorkloadLogsViewer.tsx index 1d63c7d102..9ba745a031 100644 --- a/web/src/components/logs/WorkloadLogsViewer.tsx +++ b/web/src/components/logs/WorkloadLogsViewer.tsx @@ -42,7 +42,10 @@ export function WorkloadLogsViewer({ kind, namespace, name, autoStream = true, s fetchAll={fetchAll} createStream={snapshotOnly ? undefined : makeStream} overrideDownload={desktopDownload} - defaultDark={theme === 'dark'} + // Light logs on a dark app are never wanted: dark theme pins the palette, + // light theme only sets where it starts and leaves the toggle available. + forceDark={theme === 'dark' ? true : undefined} + defaultDark={false} autoStream={autoStream} /> )