Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions packages/k8s-ui/src/components/logs/LogCore.theme.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// @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(
<LogCore
entries={[]}
isLoading={false}
isStreaming={false}
onStopStream={noop}
onRefresh={noop}
onDownload={noop}
{...props}
/>,
),
)
}
const toggle = () => element.querySelector<HTMLButtonElement>('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('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()
})
})
25 changes: 23 additions & 2 deletions packages/k8s-ui/src/components/logs/LogCore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -152,12 +158,14 @@ export function LogCore({
emptyCommand,
errorMessage,
forceDark,
defaultDark = true,
}: LogCoreProps) {
const virtuosoRef = useRef<VirtuosoHandle>(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<boolean>(() => {
if (typeof forceDark === 'boolean') return forceDark
Expand All @@ -166,13 +174,26 @@ 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])
// 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 {
const v = localStorage.getItem('radar-logs-dark')
if (v === 'true' || v === 'false') {
setIsDark(v === 'true')
return
}
} catch {}
setIsDark(defaultDark)
}, [defaultDark, forceDark])
const palette = useMemo(() => getLogPalette(isDark), [isDark])
const toggleDark = useCallback(() => {
if (themeLocked) return
Expand Down
6 changes: 5 additions & 1 deletion packages/k8s-ui/src/components/logs/LogsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export interface LogsViewerProps {
createStream?: (params: Omit<LogsFetchParams, 'previous'>) => 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
Expand All @@ -47,6 +49,7 @@ export function LogsViewer({
createStream,
overrideDownload,
forceDark,
defaultDark,
autoStream = false,
}: LogsViewerProps) {
const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
Expand Down Expand Up @@ -190,6 +193,7 @@ export function LogsViewer({
onClear={clear}
toolbarExtra={renderToolbarExtra}
forceDark={forceDark}
defaultDark={defaultDark}
/>
)
}
7 changes: 5 additions & 2 deletions packages/k8s-ui/src/components/logs/WorkloadLogsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string>('')
const [pods, setPods] = useState<WorkloadPodInfo[]>([])
const [selectedPods, setSelectedPods] = useState<Set<string>>(new Set())
Expand Down Expand Up @@ -400,6 +402,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
emptyCommand={emptyCommand}
errorMessage={entries.length === 0 ? fetchError || streamError : null}
forceDark={forceDark}
defaultDark={defaultDark}
/></div></div>
)
}
3 changes: 3 additions & 0 deletions web/src/components/logs/LogsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ export function LogsViewer({ namespace, podName, containers, initialContainer, a
fetchLogs={fetchLogs}
createStream={makeStream}
overrideDownload={desktopDownload}
// 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}
/>
)
Expand Down
3 changes: 3 additions & 0 deletions web/src/components/logs/WorkloadLogsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ export function WorkloadLogsViewer({ kind, namespace, name, autoStream = true, s
fetchAll={fetchAll}
createStream={snapshotOnly ? undefined : makeStream}
overrideDownload={desktopDownload}
// 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}
/>
)
Expand Down
Loading