@skyhook-io/k8s-ui 1.5.2 → 1.5.3
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 +1 -1
- package/src/components/logs/JsonLogLine.tsx +10 -7
- package/src/components/logs/LogCore.tsx +160 -63
- package/src/components/logs/LogToolbarSelects.tsx +17 -6
- package/src/components/logs/LogsViewer.tsx +8 -7
- package/src/components/logs/StructuredLogLine.tsx +46 -47
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -34
- package/src/components/logs/log-palette.ts +222 -0
- package/src/components/logs/useLogBuffer.ts +6 -1
package/package.json
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
2
|
import { ChevronRight, ChevronDown } from 'lucide-react'
|
|
3
|
-
import { getLevelColor } from '../../utils/log-format'
|
|
4
3
|
import type { LogLevel } from './useLogBuffer'
|
|
4
|
+
import { getLogPalette, getLogLevelColor } from './log-palette'
|
|
5
5
|
|
|
6
6
|
interface JsonLogLineProps {
|
|
7
7
|
content: string
|
|
8
8
|
level: LogLevel
|
|
9
9
|
wordWrap: boolean
|
|
10
|
+
/** Dark/light palette selector. Defaults to `true` for the dark-by-default viewer. */
|
|
11
|
+
isDark?: boolean
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function JsonLogLine({ content, level, wordWrap }: JsonLogLineProps) {
|
|
14
|
+
export function JsonLogLine({ content, level, wordWrap, isDark = true }: JsonLogLineProps) {
|
|
13
15
|
const [expanded, setExpanded] = useState(false)
|
|
14
|
-
const
|
|
16
|
+
const palette = useMemo(() => getLogPalette(isDark), [isDark])
|
|
17
|
+
const levelColor = getLogLevelColor(level, isDark)
|
|
15
18
|
|
|
16
19
|
const parsed = useMemo(() => {
|
|
17
20
|
try {
|
|
@@ -36,17 +39,17 @@ export function JsonLogLine({ content, level, wordWrap }: JsonLogLineProps) {
|
|
|
36
39
|
<span className={levelColor}>
|
|
37
40
|
<button
|
|
38
41
|
onClick={() => setExpanded(!expanded)}
|
|
39
|
-
className=
|
|
42
|
+
className={`inline-flex items-center gap-0.5 ${palette.hoverSurface} rounded px-0.5 -ml-0.5 align-middle`}
|
|
40
43
|
>
|
|
41
44
|
{expanded
|
|
42
|
-
? <ChevronDown className=
|
|
43
|
-
: <ChevronRight className=
|
|
45
|
+
? <ChevronDown className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
46
|
+
: <ChevronRight className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
44
47
|
}
|
|
45
48
|
</button>
|
|
46
49
|
{!expanded ? (
|
|
47
50
|
<span className={wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}>
|
|
48
51
|
{summary}
|
|
49
|
-
<span className=
|
|
52
|
+
<span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
|
|
50
53
|
</span>
|
|
51
54
|
) : (
|
|
52
55
|
<span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
|
|
@@ -1,22 +1,30 @@
|
|
|
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, ChevronRight, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter, Braces, Palette, ListCollapse } from 'lucide-react'
|
|
3
|
+
import { Play, Square, Download, Search, X, Terminal, RotateCcw, ChevronUp, ChevronDown, ChevronRight, CaseSensitive, Regex, WrapText, Clock, Copy, Trash2, Filter, Braces, Palette, ListCollapse, 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'
|
|
7
7
|
import { Tooltip } from '../ui/Tooltip'
|
|
8
8
|
import {
|
|
9
9
|
formatLogTimestamp,
|
|
10
|
-
getLevelColor,
|
|
11
10
|
highlightSearchMatches,
|
|
12
11
|
stripAnsi,
|
|
13
12
|
ansiToHtml,
|
|
14
13
|
type TimestampFormat,
|
|
15
14
|
TIMESTAMP_FORMAT_LABELS,
|
|
16
15
|
} from '../../utils/log-format'
|
|
16
|
+
import { getLogPalette, getLogLevelColor, type LogPalette } from './log-palette'
|
|
17
17
|
|
|
18
18
|
export type DownloadFormat = 'txt' | 'json' | 'csv'
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* `toolbarExtra` may be a plain ReactNode, or a function that receives the
|
|
22
|
+
* current dark/light context so wrappers can produce palette-matched controls.
|
|
23
|
+
*/
|
|
24
|
+
export type ToolbarExtraRenderer =
|
|
25
|
+
| ReactNode
|
|
26
|
+
| ((ctx: { isDark: boolean; palette: LogPalette }) => ReactNode)
|
|
27
|
+
|
|
20
28
|
interface LogCoreProps {
|
|
21
29
|
entries: LogEntry[]
|
|
22
30
|
isLoading: boolean
|
|
@@ -26,21 +34,40 @@ interface LogCoreProps {
|
|
|
26
34
|
onRefresh: () => void
|
|
27
35
|
onDownload: (format: DownloadFormat) => void
|
|
28
36
|
onClear?: () => void
|
|
29
|
-
toolbarExtra?:
|
|
37
|
+
toolbarExtra?: ToolbarExtraRenderer
|
|
30
38
|
showPodName?: boolean
|
|
31
39
|
emptyMessage?: string
|
|
32
40
|
errorMessage?: string | null
|
|
33
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Hard override for the viewer palette. When set, the viewer stays pinned to
|
|
43
|
+
* that mode and hides the in-viewer dark/light toggle. When undefined,
|
|
44
|
+
* the viewer manages its own palette via localStorage and the Sun/Moon button.
|
|
45
|
+
*/
|
|
34
46
|
forceDark?: boolean
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
interface LevelOption {
|
|
50
|
+
level: LogLevel
|
|
51
|
+
label: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const LEVEL_OPTIONS: LevelOption[] = [
|
|
55
|
+
{ level: 'error', label: 'ERR' },
|
|
56
|
+
{ level: 'warn', label: 'WARN' },
|
|
57
|
+
{ level: 'info', label: 'INFO' },
|
|
58
|
+
{ level: 'debug', label: 'DBG' },
|
|
42
59
|
]
|
|
43
60
|
|
|
61
|
+
function getLevelActiveColor(level: LogLevel, palette: LogPalette): string {
|
|
62
|
+
switch (level) {
|
|
63
|
+
case 'error': return palette.levelActiveError
|
|
64
|
+
case 'warn': return palette.levelActiveWarn
|
|
65
|
+
case 'info': return palette.levelActiveInfo
|
|
66
|
+
case 'debug': return palette.levelActiveDebug
|
|
67
|
+
default: return palette.levelActiveDebug
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
44
71
|
const TIMESTAMP_FORMAT_ORDER: TimestampFormat[] = [
|
|
45
72
|
'time-local', 'time-utc', 'iso-local', 'iso-utc', 'relative', 'epoch',
|
|
46
73
|
]
|
|
@@ -82,10 +109,37 @@ export function LogCore({
|
|
|
82
109
|
showPodName = false,
|
|
83
110
|
emptyMessage = 'No logs available',
|
|
84
111
|
errorMessage,
|
|
85
|
-
forceDark
|
|
112
|
+
forceDark,
|
|
86
113
|
}: LogCoreProps) {
|
|
87
114
|
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
|
88
115
|
const [atBottom, setAtBottom] = useState(true)
|
|
116
|
+
const themeLocked = typeof forceDark === 'boolean'
|
|
117
|
+
// Seed isDark: forceDark prop wins; else localStorage['radar-logs-dark'];
|
|
118
|
+
// else default dark. See log-palette.ts for why the viewer is palette-driven
|
|
119
|
+
// instead of theme-token-driven.
|
|
120
|
+
const [isDark, setIsDark] = useState<boolean>(() => {
|
|
121
|
+
if (typeof forceDark === 'boolean') return forceDark
|
|
122
|
+
try {
|
|
123
|
+
const v = localStorage.getItem('radar-logs-dark')
|
|
124
|
+
if (v === 'false') return false
|
|
125
|
+
if (v === 'true') return true
|
|
126
|
+
} catch {}
|
|
127
|
+
return true
|
|
128
|
+
})
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (typeof forceDark === 'boolean') {
|
|
131
|
+
setIsDark(forceDark)
|
|
132
|
+
}
|
|
133
|
+
}, [forceDark])
|
|
134
|
+
const palette = useMemo(() => getLogPalette(isDark), [isDark])
|
|
135
|
+
const toggleDark = useCallback(() => {
|
|
136
|
+
if (themeLocked) return
|
|
137
|
+
setIsDark(prev => {
|
|
138
|
+
const next = !prev
|
|
139
|
+
try { localStorage.setItem('radar-logs-dark', String(next)) } catch {}
|
|
140
|
+
return next
|
|
141
|
+
})
|
|
142
|
+
}, [themeLocked])
|
|
89
143
|
const [wordWrap, setWordWrap] = useState(() => {
|
|
90
144
|
try { return localStorage.getItem('radar-logs-wrap') !== 'false' } catch { return true }
|
|
91
145
|
})
|
|
@@ -296,14 +350,32 @@ export function LogCore({
|
|
|
296
350
|
})
|
|
297
351
|
}, [groupedEntries.length])
|
|
298
352
|
|
|
353
|
+
const toolbarExtraNode = typeof toolbarExtra === 'function'
|
|
354
|
+
? toolbarExtra({ isDark, palette })
|
|
355
|
+
: toolbarExtra
|
|
356
|
+
|
|
357
|
+
// Composite "inactive toolbar button" classes — static literals so
|
|
358
|
+
// Tailwind's class scanner picks them up. Two variants for secondary
|
|
359
|
+
// vs tertiary default text color.
|
|
360
|
+
const iconBtnInactive = isDark
|
|
361
|
+
? 'p-1.5 rounded transition-colors text-slate-400 hover:text-slate-100 hover:bg-slate-800'
|
|
362
|
+
: 'p-1.5 rounded transition-colors text-slate-600 hover:text-slate-900 hover:bg-slate-200'
|
|
363
|
+
const iconBtnInactiveTertiary = isDark
|
|
364
|
+
? 'p-1.5 rounded transition-colors text-slate-500 hover:text-slate-100 hover:bg-slate-800'
|
|
365
|
+
: 'p-1.5 rounded transition-colors text-slate-400 hover:text-slate-900 hover:bg-slate-200'
|
|
366
|
+
// "disabled → tertiary on hover" for inactive level filter chips.
|
|
367
|
+
const levelChipInactive = isDark
|
|
368
|
+
? 'border-transparent text-slate-600 hover:text-slate-500'
|
|
369
|
+
: 'border-transparent text-slate-300 hover:text-slate-400'
|
|
370
|
+
|
|
299
371
|
return (
|
|
300
372
|
<div
|
|
301
|
-
className={`flex flex-col h-full
|
|
302
|
-
style={{ colorScheme:
|
|
373
|
+
className={`flex flex-col h-full ${palette.containerBg}`}
|
|
374
|
+
style={{ colorScheme: isDark ? 'dark' : 'light', fontFamily: "'SF Mono', 'Cascadia Code', 'Fira Code', Menlo, Consolas, 'DejaVu Sans Mono', monospace" }}
|
|
303
375
|
>
|
|
304
376
|
{/* Toolbar */}
|
|
305
|
-
<div className=
|
|
306
|
-
{
|
|
377
|
+
<div className={`flex items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBg}`}>
|
|
378
|
+
{toolbarExtraNode}
|
|
307
379
|
|
|
308
380
|
{/* Stream / Stop toggle — only shown when streaming is supported */}
|
|
309
381
|
{onStartStream && (
|
|
@@ -313,7 +385,7 @@ export function LogCore({
|
|
|
313
385
|
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors ${
|
|
314
386
|
isStreaming
|
|
315
387
|
? 'bg-green-600 text-white hover:bg-green-700'
|
|
316
|
-
:
|
|
388
|
+
: `${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg}`
|
|
317
389
|
}`}
|
|
318
390
|
>
|
|
319
391
|
{isStreaming ? <Square className="w-3 h-3" /> : <Play className="w-3 h-3" />}
|
|
@@ -327,7 +399,7 @@ export function LogCore({
|
|
|
327
399
|
<button
|
|
328
400
|
onClick={onRefresh}
|
|
329
401
|
disabled={isLoading || isStreaming}
|
|
330
|
-
className=
|
|
402
|
+
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded ${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg} disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
331
403
|
>
|
|
332
404
|
<RotateCcw className={`w-3 h-3 ${isLoading ? 'animate-spin' : ''}`} />
|
|
333
405
|
</button>
|
|
@@ -344,8 +416,8 @@ export function LogCore({
|
|
|
344
416
|
onClick={() => toggleLevel(opt.level)}
|
|
345
417
|
className={`px-1.5 py-0.5 text-[10px] font-medium rounded border transition-colors ${
|
|
346
418
|
active
|
|
347
|
-
? opt.
|
|
348
|
-
:
|
|
419
|
+
? getLevelActiveColor(opt.level, palette)
|
|
420
|
+
: levelChipInactive
|
|
349
421
|
}`}
|
|
350
422
|
>
|
|
351
423
|
{opt.label}{count > 0 ? ` ${count}` : ''}
|
|
@@ -363,7 +435,7 @@ export function LogCore({
|
|
|
363
435
|
<button
|
|
364
436
|
onClick={() => setExpandAllStructured(prev => !prev)}
|
|
365
437
|
className={`p-1.5 rounded transition-colors ${
|
|
366
|
-
expandAllStructured ?
|
|
438
|
+
expandAllStructured ? palette.toolbarActive : iconBtnInactive
|
|
367
439
|
}`}
|
|
368
440
|
>
|
|
369
441
|
<Braces className="w-4 h-4" />
|
|
@@ -377,7 +449,7 @@ export function LogCore({
|
|
|
377
449
|
<button
|
|
378
450
|
onClick={toggleTimestamps}
|
|
379
451
|
className={`p-1.5 rounded-l transition-colors ${
|
|
380
|
-
showTimestamps ?
|
|
452
|
+
showTimestamps ? palette.toolbarActive : iconBtnInactive
|
|
381
453
|
}`}
|
|
382
454
|
>
|
|
383
455
|
<Clock className="w-4 h-4" />
|
|
@@ -388,7 +460,7 @@ export function LogCore({
|
|
|
388
460
|
<button
|
|
389
461
|
onClick={() => setShowTsMenu(prev => !prev)}
|
|
390
462
|
className={`px-2 py-1.5 rounded-r text-[10px] font-medium transition-colors whitespace-nowrap ${
|
|
391
|
-
showTimestamps ?
|
|
463
|
+
showTimestamps ? palette.toolbarActive : iconBtnInactiveTertiary
|
|
392
464
|
}`}
|
|
393
465
|
aria-label="Pick timestamp format"
|
|
394
466
|
>
|
|
@@ -399,20 +471,20 @@ export function LogCore({
|
|
|
399
471
|
</button>
|
|
400
472
|
</Tooltip>
|
|
401
473
|
{showTsMenu && (
|
|
402
|
-
<div className=
|
|
403
|
-
<div className=
|
|
474
|
+
<div className={`absolute top-full right-0 mt-1 w-44 ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50`}>
|
|
475
|
+
<div className={`px-3 py-1.5 text-[10px] uppercase tracking-wide ${palette.textTertiary} border-b ${palette.border}`}>
|
|
404
476
|
Timestamp format
|
|
405
477
|
</div>
|
|
406
478
|
{TIMESTAMP_FORMAT_ORDER.map(fmt => (
|
|
407
479
|
<button
|
|
408
480
|
key={fmt}
|
|
409
481
|
onClick={() => pickTsFormat(fmt)}
|
|
410
|
-
className={`w-full text-left px-3 py-1.5 text-xs
|
|
411
|
-
tsFormat === fmt ?
|
|
482
|
+
className={`w-full text-left px-3 py-1.5 text-xs ${palette.hoverBg} flex items-center justify-between ${
|
|
483
|
+
tsFormat === fmt ? palette.textPrimary : palette.textSecondary
|
|
412
484
|
}`}
|
|
413
485
|
>
|
|
414
486
|
<span>{TIMESTAMP_FORMAT_LABELS[fmt]}</span>
|
|
415
|
-
{tsFormat === fmt && <span className=
|
|
487
|
+
{tsFormat === fmt && <span className={`text-[10px] ${palette.textAccent}`}>✓</span>}
|
|
416
488
|
</button>
|
|
417
489
|
))}
|
|
418
490
|
</div>
|
|
@@ -425,7 +497,7 @@ export function LogCore({
|
|
|
425
497
|
<button
|
|
426
498
|
onClick={toggleCollapseStacks}
|
|
427
499
|
className={`p-1.5 rounded transition-colors ${
|
|
428
|
-
collapseStacks ?
|
|
500
|
+
collapseStacks ? palette.toolbarActive : iconBtnInactive
|
|
429
501
|
}`}
|
|
430
502
|
>
|
|
431
503
|
<ListCollapse className="w-4 h-4" />
|
|
@@ -437,7 +509,7 @@ export function LogCore({
|
|
|
437
509
|
<button
|
|
438
510
|
onClick={toggleAnsi}
|
|
439
511
|
className={`p-1.5 rounded transition-colors ${
|
|
440
|
-
ansiEnabled ?
|
|
512
|
+
ansiEnabled ? palette.toolbarActive : iconBtnInactive
|
|
441
513
|
}`}
|
|
442
514
|
>
|
|
443
515
|
<Palette className="w-4 h-4" />
|
|
@@ -449,7 +521,7 @@ export function LogCore({
|
|
|
449
521
|
<button
|
|
450
522
|
onClick={toggleWrap}
|
|
451
523
|
className={`p-1.5 rounded transition-colors ${
|
|
452
|
-
wordWrap ?
|
|
524
|
+
wordWrap ? palette.toolbarActive : iconBtnInactive
|
|
453
525
|
}`}
|
|
454
526
|
>
|
|
455
527
|
<WrapText className="w-4 h-4" />
|
|
@@ -461,30 +533,42 @@ export function LogCore({
|
|
|
461
533
|
<button
|
|
462
534
|
onClick={() => search.isOpen ? search.close() : search.open()}
|
|
463
535
|
className={`p-1.5 rounded transition-colors ${
|
|
464
|
-
search.isOpen ?
|
|
536
|
+
search.isOpen ? palette.toolbarActive : iconBtnInactive
|
|
465
537
|
}`}
|
|
466
538
|
>
|
|
467
539
|
<Search className="w-4 h-4" />
|
|
468
540
|
</button>
|
|
469
541
|
</Tooltip>
|
|
470
542
|
|
|
543
|
+
{!themeLocked && (
|
|
544
|
+
<Tooltip content={isDark ? 'Switch to light mode' : 'Switch to dark mode'} delay={TIP_DELAY} position="bottom">
|
|
545
|
+
<button
|
|
546
|
+
onClick={toggleDark}
|
|
547
|
+
className={iconBtnInactive}
|
|
548
|
+
aria-label={isDark ? 'Switch log viewer to light mode' : 'Switch log viewer to dark mode'}
|
|
549
|
+
>
|
|
550
|
+
{isDark ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
|
551
|
+
</button>
|
|
552
|
+
</Tooltip>
|
|
553
|
+
)}
|
|
554
|
+
|
|
471
555
|
{/* Download */}
|
|
472
556
|
<div className="relative flex items-center" ref={downloadMenuRef}>
|
|
473
557
|
<Tooltip content="Download logs" delay={TIP_DELAY} position="bottom">
|
|
474
558
|
<button
|
|
475
559
|
onClick={() => setShowDownloadMenu(prev => !prev)}
|
|
476
|
-
className=
|
|
560
|
+
className={iconBtnInactive}
|
|
477
561
|
>
|
|
478
562
|
<Download className="w-4 h-4" />
|
|
479
563
|
</button>
|
|
480
564
|
</Tooltip>
|
|
481
565
|
{showDownloadMenu && (
|
|
482
|
-
<div className=
|
|
566
|
+
<div className={`absolute top-full right-0 mt-1 w-32 ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50`}>
|
|
483
567
|
{(['txt', 'json', 'csv'] as DownloadFormat[]).map(fmt => (
|
|
484
568
|
<button
|
|
485
569
|
key={fmt}
|
|
486
570
|
onClick={() => { onDownload(fmt); setShowDownloadMenu(false) }}
|
|
487
|
-
className=
|
|
571
|
+
className={`w-full text-left px-3 py-2 text-xs ${palette.textPrimary} ${palette.hoverBg} first:rounded-t-lg last:rounded-b-lg`}
|
|
488
572
|
>
|
|
489
573
|
{fmt.toUpperCase()}
|
|
490
574
|
</button>
|
|
@@ -498,7 +582,7 @@ export function LogCore({
|
|
|
498
582
|
<Tooltip content="Clear logs" delay={TIP_DELAY} position="bottom">
|
|
499
583
|
<button
|
|
500
584
|
onClick={onClear}
|
|
501
|
-
className=
|
|
585
|
+
className={iconBtnInactive}
|
|
502
586
|
>
|
|
503
587
|
<Trash2 className="w-4 h-4" />
|
|
504
588
|
</button>
|
|
@@ -508,8 +592,8 @@ export function LogCore({
|
|
|
508
592
|
|
|
509
593
|
{/* Search bar */}
|
|
510
594
|
{search.isOpen && (
|
|
511
|
-
<div className=
|
|
512
|
-
<Search className=
|
|
595
|
+
<div className={`flex items-center gap-2 px-3 py-2 border-b ${palette.border} ${palette.toolbarBgMuted}`}>
|
|
596
|
+
<Search className={`w-4 h-4 ${palette.textSecondary} shrink-0`} />
|
|
513
597
|
<input
|
|
514
598
|
type="text"
|
|
515
599
|
value={search.query}
|
|
@@ -526,7 +610,7 @@ export function LogCore({
|
|
|
526
610
|
}
|
|
527
611
|
}}
|
|
528
612
|
placeholder="Search logs..."
|
|
529
|
-
className=
|
|
613
|
+
className={`flex-1 bg-transparent ${palette.textPrimary} text-sm ${palette.placeholder} focus:outline-none min-w-0`}
|
|
530
614
|
autoFocus
|
|
531
615
|
/>
|
|
532
616
|
|
|
@@ -535,7 +619,7 @@ export function LogCore({
|
|
|
535
619
|
<button
|
|
536
620
|
onClick={search.toggleRegex}
|
|
537
621
|
className={`p-1 rounded transition-colors ${
|
|
538
|
-
search.isRegex ?
|
|
622
|
+
search.isRegex ? palette.toolbarActive : `${palette.textTertiary} ${palette.hoverText}`
|
|
539
623
|
}`}
|
|
540
624
|
>
|
|
541
625
|
<Regex className="w-3.5 h-3.5" />
|
|
@@ -547,7 +631,7 @@ export function LogCore({
|
|
|
547
631
|
<button
|
|
548
632
|
onClick={search.toggleCaseSensitive}
|
|
549
633
|
className={`p-1 rounded transition-colors ${
|
|
550
|
-
search.isCaseSensitive ?
|
|
634
|
+
search.isCaseSensitive ? palette.toolbarActive : `${palette.textTertiary} ${palette.hoverText}`
|
|
551
635
|
}`}
|
|
552
636
|
>
|
|
553
637
|
<CaseSensitive className="w-3.5 h-3.5" />
|
|
@@ -559,7 +643,7 @@ export function LogCore({
|
|
|
559
643
|
<button
|
|
560
644
|
onClick={search.toggleFilterMode}
|
|
561
645
|
className={`p-1 rounded transition-colors ${
|
|
562
|
-
search.isFilterMode ?
|
|
646
|
+
search.isFilterMode ? palette.toolbarActive : `${palette.textTertiary} ${palette.hoverText}`
|
|
563
647
|
}`}
|
|
564
648
|
>
|
|
565
649
|
<Filter className="w-3.5 h-3.5" />
|
|
@@ -568,7 +652,7 @@ export function LogCore({
|
|
|
568
652
|
|
|
569
653
|
{search.query && (
|
|
570
654
|
<>
|
|
571
|
-
<span className={`text-xs whitespace-nowrap ${search.regexError ?
|
|
655
|
+
<span className={`text-xs whitespace-nowrap ${search.regexError ? palette.textError : palette.textTertiary}`}>
|
|
572
656
|
{search.regexError
|
|
573
657
|
? 'Invalid regex'
|
|
574
658
|
: search.matchCount > 0
|
|
@@ -581,7 +665,7 @@ export function LogCore({
|
|
|
581
665
|
<button
|
|
582
666
|
onClick={search.goToPrev}
|
|
583
667
|
disabled={search.matchCount === 0}
|
|
584
|
-
className=
|
|
668
|
+
className={`p-1 rounded ${palette.textSecondary} ${palette.hoverText} disabled:opacity-30`}
|
|
585
669
|
>
|
|
586
670
|
<ChevronUp className="w-3.5 h-3.5" />
|
|
587
671
|
</button>
|
|
@@ -590,7 +674,7 @@ export function LogCore({
|
|
|
590
674
|
<button
|
|
591
675
|
onClick={search.goToNext}
|
|
592
676
|
disabled={search.matchCount === 0}
|
|
593
|
-
className=
|
|
677
|
+
className={`p-1 rounded ${palette.textSecondary} ${palette.hoverText} disabled:opacity-30`}
|
|
594
678
|
>
|
|
595
679
|
<ChevronDown className="w-3.5 h-3.5" />
|
|
596
680
|
</button>
|
|
@@ -598,7 +682,7 @@ export function LogCore({
|
|
|
598
682
|
|
|
599
683
|
<button
|
|
600
684
|
onClick={() => search.setQuery('')}
|
|
601
|
-
className=
|
|
685
|
+
className={`p-1 rounded ${palette.textSecondary} ${palette.hoverText}`}
|
|
602
686
|
>
|
|
603
687
|
<X className="w-3 h-3" />
|
|
604
688
|
</button>
|
|
@@ -609,19 +693,19 @@ export function LogCore({
|
|
|
609
693
|
|
|
610
694
|
{/* Log content */}
|
|
611
695
|
{isLoading && entries.length === 0 ? (
|
|
612
|
-
<div className=
|
|
696
|
+
<div className={`flex-1 flex items-center justify-center ${palette.textTertiary}`}>
|
|
613
697
|
<div className="flex items-center gap-2">
|
|
614
698
|
<RotateCcw className="w-4 h-4 animate-spin" />
|
|
615
699
|
<span>Loading logs...</span>
|
|
616
700
|
</div>
|
|
617
701
|
</div>
|
|
618
702
|
) : errorMessage ? (
|
|
619
|
-
<div className=
|
|
703
|
+
<div className={`flex-1 flex flex-col items-center justify-center gap-2 ${palette.textError}`}>
|
|
620
704
|
<Terminal className="w-8 h-8" />
|
|
621
705
|
<span>{errorMessage}</span>
|
|
622
706
|
</div>
|
|
623
707
|
) : groupedEntries.length === 0 ? (
|
|
624
|
-
<div className=
|
|
708
|
+
<div className={`flex-1 flex flex-col items-center justify-center gap-2 ${palette.textTertiary}`}>
|
|
625
709
|
<Terminal className="w-8 h-8" />
|
|
626
710
|
<span>{emptyMessage}</span>
|
|
627
711
|
</div>
|
|
@@ -651,6 +735,8 @@ export function LogCore({
|
|
|
651
735
|
onFilterValue={handleFilterValue}
|
|
652
736
|
isStackExpanded={expandedStacks.has(group.head.id)}
|
|
653
737
|
onToggleStack={toggleStackExpanded}
|
|
738
|
+
isDark={isDark}
|
|
739
|
+
palette={palette}
|
|
654
740
|
/>
|
|
655
741
|
)}
|
|
656
742
|
className="h-full font-mono text-xs"
|
|
@@ -668,20 +754,20 @@ export function LogCore({
|
|
|
668
754
|
)}
|
|
669
755
|
|
|
670
756
|
{/* Keyboard shortcut hints */}
|
|
671
|
-
<div className=
|
|
672
|
-
<Shortcut keys="Ctrl+F" label="Search" />
|
|
673
|
-
<Shortcut keys="Enter" label="Next match" />
|
|
674
|
-
<Shortcut keys="Shift+Enter" label="Prev match" />
|
|
675
|
-
<Shortcut keys="Esc" label="Close search" />
|
|
757
|
+
<div className={`flex items-center gap-4 px-3 py-1 border-t ${palette.border} ${palette.toolbarBg} text-[10px] ${palette.textDisabled}`}>
|
|
758
|
+
<Shortcut keys="Ctrl+F" label="Search" palette={palette} />
|
|
759
|
+
<Shortcut keys="Enter" label="Next match" palette={palette} />
|
|
760
|
+
<Shortcut keys="Shift+Enter" label="Prev match" palette={palette} />
|
|
761
|
+
<Shortcut keys="Esc" label="Close search" palette={palette} />
|
|
676
762
|
</div>
|
|
677
763
|
</div>
|
|
678
764
|
)
|
|
679
765
|
}
|
|
680
766
|
|
|
681
|
-
function Shortcut({ keys, label }: { keys: string; label: string }) {
|
|
767
|
+
function Shortcut({ keys, label, palette }: { keys: string; label: string; palette: LogPalette }) {
|
|
682
768
|
return (
|
|
683
769
|
<span className="flex items-center gap-1">
|
|
684
|
-
<kbd className=
|
|
770
|
+
<kbd className={`px-1 py-px rounded ${palette.elevatedBg} border ${palette.borderLight} font-mono`}>{keys}</kbd>
|
|
685
771
|
<span>{label}</span>
|
|
686
772
|
</span>
|
|
687
773
|
)
|
|
@@ -702,6 +788,8 @@ interface LogLineProps {
|
|
|
702
788
|
onFilterValue?: (value: string) => void
|
|
703
789
|
/** Optional lead element rendered at the start of the row (e.g. stack-trace toggle). */
|
|
704
790
|
leadSlot?: ReactNode
|
|
791
|
+
isDark: boolean
|
|
792
|
+
palette: LogPalette
|
|
705
793
|
}
|
|
706
794
|
|
|
707
795
|
function LogLine({
|
|
@@ -718,8 +806,13 @@ function LogLine({
|
|
|
718
806
|
defaultExpanded,
|
|
719
807
|
onFilterValue,
|
|
720
808
|
leadSlot,
|
|
809
|
+
isDark,
|
|
810
|
+
palette,
|
|
721
811
|
}: LogLineProps) {
|
|
722
|
-
const levelColor =
|
|
812
|
+
const levelColor = getLogLevelColor(entry.level, isDark)
|
|
813
|
+
const podTextColor = entry.podColorIndex !== undefined
|
|
814
|
+
? palette.podColors[entry.podColorIndex % palette.podColors.length].text
|
|
815
|
+
: palette.textPrimary
|
|
723
816
|
|
|
724
817
|
// Determine content rendering. Priority: search highlight > structured > ANSI/plain.
|
|
725
818
|
let contentElement: React.ReactNode
|
|
@@ -741,6 +834,7 @@ function LogLine({
|
|
|
741
834
|
isLogfmt={entry.isLogfmt}
|
|
742
835
|
defaultExpanded={defaultExpanded}
|
|
743
836
|
onFilterValue={onFilterValue}
|
|
837
|
+
isDark={isDark}
|
|
744
838
|
/>
|
|
745
839
|
)
|
|
746
840
|
} else if (ansiEnabled) {
|
|
@@ -765,11 +859,11 @@ function LogLine({
|
|
|
765
859
|
}
|
|
766
860
|
|
|
767
861
|
return (
|
|
768
|
-
<div className={`flex
|
|
862
|
+
<div className={`flex ${palette.hoverSurface} group leading-5 px-2 ${isCurrentMatch ? palette.currentMatchBg : ''}`}>
|
|
769
863
|
{leadSlot}
|
|
770
864
|
{showTimestamp && entry.timestamp && (
|
|
771
865
|
<span
|
|
772
|
-
className=
|
|
866
|
+
className={`${palette.textTertiary} select-none pr-2 whitespace-nowrap`}
|
|
773
867
|
title={entry.timestamp}
|
|
774
868
|
>
|
|
775
869
|
{formatLogTimestamp(entry.timestamp, tsFormat)}
|
|
@@ -777,7 +871,7 @@ function LogLine({
|
|
|
777
871
|
)}
|
|
778
872
|
{showPodName && entry.pod && (
|
|
779
873
|
<span
|
|
780
|
-
className={`${
|
|
874
|
+
className={`${podTextColor} select-none pr-2 whitespace-nowrap min-w-[80px] max-w-[120px] truncate`}
|
|
781
875
|
title={entry.pod}
|
|
782
876
|
>
|
|
783
877
|
[{entry.pod.split('-').slice(-2).join('-')}]
|
|
@@ -786,7 +880,7 @@ function LogLine({
|
|
|
786
880
|
<span className="flex-1 min-w-0">{contentElement}</span>
|
|
787
881
|
<button
|
|
788
882
|
onClick={handleCopy}
|
|
789
|
-
className=
|
|
883
|
+
className={`opacity-0 group-hover:opacity-100 ml-1 p-0.5 rounded ${palette.textTertiary} ${palette.hoverText} shrink-0 transition-opacity`}
|
|
790
884
|
title="Copy line"
|
|
791
885
|
>
|
|
792
886
|
<Copy className="w-3 h-3" />
|
|
@@ -810,16 +904,18 @@ interface LogGroupItemProps {
|
|
|
810
904
|
onFilterValue: (value: string) => void
|
|
811
905
|
isStackExpanded: boolean
|
|
812
906
|
onToggleStack: (id: number) => void
|
|
907
|
+
isDark: boolean
|
|
908
|
+
palette: LogPalette
|
|
813
909
|
}
|
|
814
910
|
|
|
815
911
|
function LogGroupItem(props: LogGroupItemProps) {
|
|
816
|
-
const { group, isStackExpanded, onToggleStack, ...rest } = props
|
|
912
|
+
const { group, isStackExpanded, onToggleStack, palette, ...rest } = props
|
|
817
913
|
const hasStack = group.continuations.length > 0
|
|
818
914
|
|
|
819
915
|
const stackToggle = hasStack ? (
|
|
820
916
|
<button
|
|
821
917
|
onClick={() => onToggleStack(group.head.id)}
|
|
822
|
-
className=
|
|
918
|
+
className={`mr-1 self-start p-0.5 rounded ${palette.textTertiary} ${palette.hoverText} ${palette.hoverSurface} shrink-0`}
|
|
823
919
|
title={isStackExpanded ? 'Collapse stack trace' : `Expand ${group.continuations.length} stack frames`}
|
|
824
920
|
>
|
|
825
921
|
{isStackExpanded
|
|
@@ -833,19 +929,20 @@ function LogGroupItem(props: LogGroupItemProps) {
|
|
|
833
929
|
<LogLine
|
|
834
930
|
entry={group.head}
|
|
835
931
|
{...rest}
|
|
932
|
+
palette={palette}
|
|
836
933
|
leadSlot={stackToggle}
|
|
837
934
|
/>
|
|
838
935
|
{hasStack && !isStackExpanded && (
|
|
839
936
|
<button
|
|
840
937
|
onClick={() => onToggleStack(group.head.id)}
|
|
841
|
-
className=
|
|
938
|
+
className={`block w-full text-left pl-6 pr-2 py-0 text-[10px] ${palette.textTertiary} ${palette.hoverText} ${palette.hoverSurface}`}
|
|
842
939
|
>
|
|
843
940
|
[+{group.continuations.length} stack {group.continuations.length === 1 ? 'line' : 'lines'}]
|
|
844
941
|
</button>
|
|
845
942
|
)}
|
|
846
943
|
{hasStack && isStackExpanded && group.continuations.map(cont => (
|
|
847
944
|
<div key={cont.id} className="pl-4">
|
|
848
|
-
<LogLine entry={cont} {...rest} isCurrentMatch={false} />
|
|
945
|
+
<LogLine entry={cont} {...rest} palette={palette} isCurrentMatch={false} />
|
|
849
946
|
</div>
|
|
850
947
|
))}
|
|
851
948
|
</div>
|