@skyhook-io/k8s-ui 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -1,6 +1,6 @@
1
1
  import { useRef, useCallback, useState, useMemo, useEffect, type ReactNode } from 'react'
2
2
  import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'
3
- import { Play, Square, Download, Search, X, Terminal, RotateCcw, ChevronUp, ChevronDown, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter, Braces } from 'lucide-react'
3
+ import { Play, Square, Download, Search, X, Terminal, RotateCcw, ChevronUp, ChevronDown, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter, Braces, Sun, Moon } from 'lucide-react'
4
4
  import type { LogEntry, LogLevel } from './useLogBuffer'
5
5
  import { useLogSearch } from './useLogSearch'
6
6
  import { StructuredLogLine } from './StructuredLogLine'
@@ -28,6 +28,8 @@ interface LogCoreProps {
28
28
  showPodName?: boolean
29
29
  emptyMessage?: string
30
30
  errorMessage?: string | null
31
+ /** Force dark mode on the logs container (default: true). Logs are typically dark-themed. */
32
+ forceDark?: boolean
31
33
  }
32
34
 
33
35
  const LEVEL_OPTIONS: { level: LogLevel; label: string; color: string; activeColor: string }[] = [
@@ -52,9 +54,13 @@ export function LogCore({
52
54
  showPodName = false,
53
55
  emptyMessage = 'No logs available',
54
56
  errorMessage,
57
+ forceDark = true,
55
58
  }: LogCoreProps) {
56
59
  const virtuosoRef = useRef<VirtuosoHandle>(null)
57
60
  const [atBottom, setAtBottom] = useState(true)
61
+ const [isDark, setIsDark] = useState(() => {
62
+ try { const v = localStorage.getItem('radar-logs-dark'); return v !== null ? v !== 'false' : forceDark } catch { return forceDark }
63
+ })
58
64
  const [wordWrap, setWordWrap] = useState(() => {
59
65
  try { return localStorage.getItem('radar-logs-wrap') !== 'false' } catch { return true }
60
66
  })
@@ -171,7 +177,7 @@ export function LogCore({
171
177
  : -1
172
178
 
173
179
  return (
174
- <div className="flex flex-col h-full bg-theme-base">
180
+ <div className={`flex flex-col h-full bg-theme-base${isDark ? ' dark' : ''}`} style={{ colorScheme: isDark ? 'dark' : 'light', fontFamily: "'SF Mono', 'Cascadia Code', 'Fira Code', Menlo, Consolas, 'DejaVu Sans Mono', monospace" }}>
175
181
  {/* Toolbar */}
176
182
  <div className="flex items-center gap-2 px-3 py-2 border-b border-theme-border bg-theme-surface">
177
183
  {toolbarExtra}
@@ -303,6 +309,20 @@ export function LogCore({
303
309
  )}
304
310
  </div>
305
311
 
312
+ {/* Dark/Light toggle */}
313
+ <Tooltip content={isDark ? 'Light mode' : 'Dark mode'} delay={TIP_DELAY} position="bottom">
314
+ <button
315
+ onClick={() => {
316
+ const next = !isDark
317
+ setIsDark(next)
318
+ try { localStorage.setItem('radar-logs-dark', String(next)) } catch {}
319
+ }}
320
+ className="p-1.5 rounded text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated"
321
+ >
322
+ {isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
323
+ </button>
324
+ </Tooltip>
325
+
306
326
  {/* Clear */}
307
327
  {onClear && (
308
328
  <Tooltip content="Clear logs" delay={TIP_DELAY} position="bottom">
@@ -25,6 +25,8 @@ export interface LogsViewerProps {
25
25
  fetchLogs: (params: LogsFetchParams) => Promise<{ [container: string]: string }>
26
26
  /** If provided, the stream button is enabled. Called to open an SSE connection. */
27
27
  createStream?: (params: Omit<LogsFetchParams, 'previous'>) => EventSource
28
+ /** Force dark mode on the logs container (default: true) */
29
+ forceDark?: boolean
28
30
  }
29
31
 
30
32
  export function LogsViewer({
@@ -34,6 +36,7 @@ export function LogsViewer({
34
36
  initialContainer,
35
37
  fetchLogs,
36
38
  createStream,
39
+ forceDark,
37
40
  }: LogsViewerProps) {
38
41
  const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
39
42
  const [isLoading, setIsLoading] = useState(false)
@@ -141,6 +144,7 @@ export function LogsViewer({
141
144
  onDownload={downloadLogs}
142
145
  onClear={clear}
143
146
  toolbarExtra={toolbarExtra}
147
+ forceDark={forceDark}
144
148
  />
145
149
  )
146
150
  }
@@ -42,6 +42,8 @@ export interface WorkloadLogsViewerProps {
42
42
  * Called to open an SSE connection for the whole workload.
43
43
  */
44
44
  createStream?: (params: WorkloadLogsFetchParams) => EventSource
45
+ /** Force dark mode on the logs container (default: true) */
46
+ forceDark?: boolean
45
47
  }
46
48
 
47
49
  const POD_COLORS = [
@@ -49,7 +51,7 @@ const POD_COLORS = [
49
51
  'text-pink-400', 'text-cyan-400', 'text-orange-400', 'text-lime-400',
50
52
  ]
51
53
 
52
- export function WorkloadLogsViewer({ name, fetchAll, createStream }: WorkloadLogsViewerProps) {
54
+ export function WorkloadLogsViewer({ name, fetchAll, createStream, forceDark }: WorkloadLogsViewerProps) {
53
55
  const [selectedContainer, setSelectedContainer] = useState<string>('')
54
56
  const [pods, setPods] = useState<WorkloadPodInfo[]>([])
55
57
  const [selectedPods, setSelectedPods] = useState<Set<string>>(new Set())
@@ -258,6 +260,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream }: WorkloadLog
258
260
  showPodName
259
261
  emptyMessage={pods.length === 0 ? 'No pods found' : 'No logs available'}
260
262
  errorMessage={fetchError}
263
+ forceDark={forceDark}
261
264
  />
262
265
  )
263
266
  }
@@ -272,12 +272,20 @@ export function escapeRegExp(text: string): string {
272
272
  * K8s timestamps are in RFC3339Nano format: 2024-01-20T10:30:00.123456789Z content
273
273
  */
274
274
  export function parseLogLine(line: string): { timestamp: string; content: string } {
275
+ // K8s timestamp prefix: "2026-03-19T00:01:19.811Z message..."
275
276
  if (line.length > 30 && line[4] === '-' && line[7] === '-' && line[10] === 'T') {
276
277
  const spaceIdx = line.indexOf(' ')
277
278
  if (spaceIdx > 20 && spaceIdx < 40) {
278
279
  return { timestamp: line.slice(0, spaceIdx), content: line.slice(spaceIdx + 1) }
279
280
  }
280
281
  }
282
+ // Structured JSON with "ts" or "timestamp" field
283
+ if (line[0] === '{') {
284
+ const tsMatch = line.match(/"(?:ts|timestamp|time)"\s*:\s*"(\d{4}-\d{2}-\d{2}T[^"]+)"/)
285
+ if (tsMatch) {
286
+ return { timestamp: tsMatch[1], content: line }
287
+ }
288
+ }
281
289
  return { timestamp: '', content: line }
282
290
  }
283
291