@skyhook-io/k8s-ui 1.5.1 → 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 +451 -100
- package/src/components/logs/LogToolbarSelects.tsx +17 -6
- package/src/components/logs/LogsViewer.tsx +8 -7
- package/src/components/logs/StructuredLogLine.tsx +125 -43
- 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/src/components/logs/useLogSearch.ts +5 -0
- package/src/components/resources/resource-utils-argo.ts +9 -2
- package/src/utils/log-format.ts +77 -12
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ChevronDown } from 'lucide-react'
|
|
2
2
|
import { Tooltip } from '../ui/Tooltip'
|
|
3
|
+
import { getLogPalette, type LogPalette } from './log-palette'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
// See log-palette.ts for why the viewer uses explicit palette classes
|
|
6
|
+
// instead of theme-* tokens.
|
|
7
|
+
function selectClass(palette: LogPalette): string {
|
|
8
|
+
return `appearance-none ${palette.elevatedBg} ${palette.textPrimary} text-xs rounded px-2 py-1.5 border ${palette.borderLight} focus:outline-none focus:ring-1 focus:ring-blue-500`
|
|
9
|
+
}
|
|
6
10
|
|
|
7
11
|
// ── ContainerSelect ───────────────────────────────────────────────────────────
|
|
8
12
|
|
|
@@ -12,21 +16,24 @@ interface ContainerSelectProps {
|
|
|
12
16
|
onChange: (value: string) => void
|
|
13
17
|
/** If true, prepend an "All containers" option with value="" */
|
|
14
18
|
includeAll?: boolean
|
|
19
|
+
/** Dark/light palette selector. Defaults to `true` (dark viewer). */
|
|
20
|
+
isDark?: boolean
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
export function ContainerSelect({ containers, value, onChange, includeAll = false }: ContainerSelectProps) {
|
|
23
|
+
export function ContainerSelect({ containers, value, onChange, includeAll = false, isDark = true }: ContainerSelectProps) {
|
|
18
24
|
if (containers.length <= 1 && !includeAll) return null
|
|
25
|
+
const palette = getLogPalette(isDark)
|
|
19
26
|
return (
|
|
20
27
|
<div className="relative">
|
|
21
28
|
<select
|
|
22
29
|
value={value}
|
|
23
30
|
onChange={(e) => onChange(e.target.value)}
|
|
24
|
-
className={`${
|
|
31
|
+
className={`${selectClass(palette)} pr-6`}
|
|
25
32
|
>
|
|
26
33
|
{includeAll && <option value="">All containers</option>}
|
|
27
34
|
{containers.map(c => <option key={c} value={c}>{c}</option>)}
|
|
28
35
|
</select>
|
|
29
|
-
<ChevronDown className=
|
|
36
|
+
<ChevronDown className={`absolute right-1.5 top-1/2 -translate-y-1/2 w-3 h-3 ${palette.textSecondary} pointer-events-none`} />
|
|
30
37
|
</div>
|
|
31
38
|
)
|
|
32
39
|
}
|
|
@@ -39,6 +46,8 @@ interface LogRangeSelectProps {
|
|
|
39
46
|
/** Line count options to show. Defaults to [100, 500, 1000, 5000]. */
|
|
40
47
|
lineOptions?: number[]
|
|
41
48
|
tooltip?: string
|
|
49
|
+
/** Dark/light palette selector. Defaults to `true` (dark viewer). */
|
|
50
|
+
isDark?: boolean
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
export function LogRangeSelect({
|
|
@@ -46,13 +55,15 @@ export function LogRangeSelect({
|
|
|
46
55
|
onChange,
|
|
47
56
|
lineOptions = [100, 500, 1000, 5000],
|
|
48
57
|
tooltip = 'How many logs to load — by line count or time range',
|
|
58
|
+
isDark = true,
|
|
49
59
|
}: LogRangeSelectProps) {
|
|
60
|
+
const palette = getLogPalette(isDark)
|
|
50
61
|
return (
|
|
51
62
|
<Tooltip content={tooltip} position="bottom">
|
|
52
63
|
<select
|
|
53
64
|
value={value}
|
|
54
65
|
onChange={(e) => onChange(e.target.value)}
|
|
55
|
-
className={`${
|
|
66
|
+
className={`${selectClass(palette)} pr-5`}
|
|
56
67
|
>
|
|
57
68
|
<optgroup label="Lines">
|
|
58
69
|
{lineOptions.map(n => (
|
|
@@ -6,6 +6,7 @@ import { useLogStream } from './useLogStream'
|
|
|
6
6
|
import { ContainerSelect, LogRangeSelect } from './LogToolbarSelects'
|
|
7
7
|
import { LogCore } from './LogCore'
|
|
8
8
|
import type { DownloadFormat } from './LogCore'
|
|
9
|
+
import type { LogPalette } from './log-palette'
|
|
9
10
|
import { Tooltip } from '../ui/Tooltip'
|
|
10
11
|
import { useToast } from '../ui/Toast'
|
|
11
12
|
|
|
@@ -117,23 +118,23 @@ export function LogsViewer({
|
|
|
117
118
|
}
|
|
118
119
|
}, [entries, podName, selectedContainer, overrideDownload, showError, showSuccess])
|
|
119
120
|
|
|
120
|
-
const
|
|
121
|
+
const renderToolbarExtra = ({ isDark, palette }: { isDark: boolean; palette: LogPalette }) => (
|
|
121
122
|
<>
|
|
122
|
-
<ContainerSelect containers={containers} value={selectedContainer} onChange={setSelectedContainer} />
|
|
123
|
+
<ContainerSelect containers={containers} value={selectedContainer} onChange={setSelectedContainer} isDark={isDark} />
|
|
123
124
|
|
|
124
125
|
<Tooltip content="Show logs from the pod's previous instance (if it was restarted). Useful for troubleshooting crashed containers." position="bottom">
|
|
125
|
-
<label className=
|
|
126
|
+
<label className={`flex items-center gap-1.5 text-xs ${palette.textSecondary}`}>
|
|
126
127
|
<input
|
|
127
128
|
type="checkbox"
|
|
128
129
|
checked={showPrevious}
|
|
129
130
|
onChange={(e) => setShowPrevious(e.target.checked)}
|
|
130
|
-
className=
|
|
131
|
+
className={`w-3 h-3 rounded ${palette.borderLight} ${palette.elevatedBg} text-blue-500 focus:ring-blue-500 focus:ring-offset-0`}
|
|
131
132
|
/>
|
|
132
|
-
<span className=
|
|
133
|
+
<span className={`border-b border-dotted ${isDark ? 'border-slate-500' : 'border-slate-400'}`}>Previous</span>
|
|
133
134
|
</label>
|
|
134
135
|
</Tooltip>
|
|
135
136
|
|
|
136
|
-
<LogRangeSelect value={logRange} onChange={setLogRange} />
|
|
137
|
+
<LogRangeSelect value={logRange} onChange={setLogRange} isDark={isDark} />
|
|
137
138
|
</>
|
|
138
139
|
)
|
|
139
140
|
|
|
@@ -148,7 +149,7 @@ export function LogsViewer({
|
|
|
148
149
|
onRefresh={loadLogs}
|
|
149
150
|
onDownload={downloadLogs}
|
|
150
151
|
onClear={clear}
|
|
151
|
-
toolbarExtra={
|
|
152
|
+
toolbarExtra={renderToolbarExtra}
|
|
152
153
|
forceDark={forceDark}
|
|
153
154
|
/>
|
|
154
155
|
)
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
|
-
import { ChevronRight, ChevronDown } from 'lucide-react'
|
|
2
|
+
import { ChevronRight, ChevronDown, Filter } from 'lucide-react'
|
|
3
3
|
import type { LogLevel } from './useLogBuffer'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
highlightJson,
|
|
7
|
-
unescapeJsonStrings,
|
|
8
|
-
parseLogfmt,
|
|
9
|
-
SYNTAX_COLOR_KEY,
|
|
10
|
-
SYNTAX_COLOR_STRING,
|
|
11
|
-
} from '../../utils/log-format'
|
|
12
|
-
import { SEVERITY_BADGE_BORDERED } from '../../utils/badge-colors'
|
|
4
|
+
import { unescapeJsonStrings, parseLogfmt } from '../../utils/log-format'
|
|
5
|
+
import { getLogPalette, getLogLevelColor, type LogPalette } from './log-palette'
|
|
13
6
|
|
|
14
7
|
interface StructuredLogLineProps {
|
|
15
8
|
content: string
|
|
@@ -17,9 +10,22 @@ interface StructuredLogLineProps {
|
|
|
17
10
|
wordWrap: boolean
|
|
18
11
|
isLogfmt?: boolean
|
|
19
12
|
defaultExpanded?: boolean
|
|
13
|
+
/**
|
|
14
|
+
* When provided, field values in the expanded view become filterable — hovering
|
|
15
|
+
* shows a chip that, on click, calls onFilterValue(value) so the parent can
|
|
16
|
+
* add the value to the log search/filter.
|
|
17
|
+
*/
|
|
18
|
+
onFilterValue?: (value: string) => void
|
|
19
|
+
/**
|
|
20
|
+
* Whether the log viewer is in dark mode. Determines the color palette used
|
|
21
|
+
* for text, hover states, and level badges. Defaults to `true` since the
|
|
22
|
+
* viewer defaults to dark mode.
|
|
23
|
+
*/
|
|
24
|
+
isDark?: boolean
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultExpanded }: StructuredLogLineProps) {
|
|
27
|
+
export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultExpanded, onFilterValue, isDark = true }: StructuredLogLineProps) {
|
|
28
|
+
const palette = useMemo(() => getLogPalette(isDark), [isDark])
|
|
23
29
|
// null = user hasn't toggled this line; defers to defaultExpanded (global toggle)
|
|
24
30
|
const [localExpanded, setLocalExpanded] = useState<boolean | null>(null)
|
|
25
31
|
const expanded = localExpanded ?? defaultExpanded ?? false
|
|
@@ -37,7 +43,7 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
37
43
|
|
|
38
44
|
if (!parsed) {
|
|
39
45
|
return (
|
|
40
|
-
<span className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${
|
|
46
|
+
<span className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${getLogLevelColor(level, isDark)}`}>
|
|
41
47
|
{content}
|
|
42
48
|
</span>
|
|
43
49
|
)
|
|
@@ -47,8 +53,8 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
47
53
|
|
|
48
54
|
const toggle = () => setLocalExpanded(!expanded)
|
|
49
55
|
const chevron = expanded
|
|
50
|
-
? <ChevronDown className=
|
|
51
|
-
: <ChevronRight className=
|
|
56
|
+
? <ChevronDown className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
57
|
+
: <ChevronRight className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
52
58
|
|
|
53
59
|
return (
|
|
54
60
|
<span>
|
|
@@ -56,30 +62,32 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
56
62
|
// Collapsed: entire summary line is clickable
|
|
57
63
|
<span
|
|
58
64
|
onClick={toggle}
|
|
59
|
-
className={`cursor-pointer
|
|
65
|
+
className={`cursor-pointer ${palette.hoverSurface} rounded px-0.5 -ml-0.5 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}
|
|
60
66
|
>
|
|
61
67
|
<span className="inline-flex items-center align-middle mr-0.5">{chevron}</span>
|
|
62
|
-
<SummaryLine obj={parsed} />
|
|
63
|
-
<span className=
|
|
68
|
+
<SummaryLine obj={parsed} palette={palette} />
|
|
69
|
+
<span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
|
|
64
70
|
</span>
|
|
65
71
|
) : (
|
|
66
72
|
// Expanded: summary header is clickable to collapse, JSON content is selectable
|
|
67
73
|
<>
|
|
68
74
|
<span
|
|
69
75
|
onClick={toggle}
|
|
70
|
-
className=
|
|
76
|
+
className={`cursor-pointer ${palette.hoverSurface} rounded px-0.5 -ml-0.5`}
|
|
71
77
|
>
|
|
72
78
|
<span className="inline-flex items-center align-middle mr-0.5">{chevron}</span>
|
|
73
|
-
<SummaryLine obj={parsed} />
|
|
74
|
-
<span className=
|
|
79
|
+
<SummaryLine obj={parsed} palette={palette} />
|
|
80
|
+
<span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
|
|
75
81
|
</span>
|
|
76
82
|
<span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
|
|
77
83
|
{isLogfmt ? (
|
|
78
|
-
<ExpandedLogfmt obj={parsed} />
|
|
84
|
+
<ExpandedLogfmt obj={parsed} onFilterValue={onFilterValue} palette={palette} />
|
|
79
85
|
) : (
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
<JsonExpanded
|
|
87
|
+
text={unescapeJsonStrings(JSON.stringify(parsed, null, 2))}
|
|
88
|
+
onFilterValue={onFilterValue}
|
|
89
|
+
palette={palette}
|
|
90
|
+
/>
|
|
83
91
|
)}
|
|
84
92
|
</span>
|
|
85
93
|
</>
|
|
@@ -88,7 +96,78 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
88
96
|
)
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Render a primitive log-field value with an optional filter chip that appears on hover.
|
|
101
|
+
* Clicking the chip calls onFilter(value) so the caller can push it into log search.
|
|
102
|
+
*/
|
|
103
|
+
function FilterableValue({
|
|
104
|
+
value, onFilter, color, palette,
|
|
105
|
+
}: { value: string; onFilter?: (v: string) => void; color?: string; palette: LogPalette }) {
|
|
106
|
+
if (!onFilter) {
|
|
107
|
+
return <span style={color ? { color } : undefined}>{value}</span>
|
|
108
|
+
}
|
|
109
|
+
return (
|
|
110
|
+
<span className={`group/flt inline-flex items-baseline align-baseline gap-0.5 rounded ${palette.hoverSurface}`}>
|
|
111
|
+
<span style={color ? { color } : undefined}>{value}</span>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={(e) => { e.stopPropagation(); onFilter(value) }}
|
|
115
|
+
className={`opacity-0 group-hover/flt:opacity-100 transition-opacity ${palette.textTertiary} ${palette.hoverText} px-0.5`}
|
|
116
|
+
title={`Filter to lines containing "${value}"`}
|
|
117
|
+
aria-label={`Filter to lines containing ${value}`}
|
|
118
|
+
>
|
|
119
|
+
<Filter className="w-3 h-3 inline" />
|
|
120
|
+
</button>
|
|
121
|
+
</span>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render pretty-printed JSON with filterable primitive values while preserving
|
|
127
|
+
* the layout that JSON.stringify(..., null, 2) produces. Tokenizes the string
|
|
128
|
+
* and emits React nodes so the hover chip can be wired per value.
|
|
129
|
+
*/
|
|
130
|
+
function JsonExpanded({ text, onFilterValue, palette }: { text: string; onFilterValue?: (v: string) => void; palette: LogPalette }) {
|
|
131
|
+
const tokenRe = /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|\b(true|false)\b|\b(null)\b/g
|
|
132
|
+
const nodes: React.ReactNode[] = []
|
|
133
|
+
let lastIndex = 0
|
|
134
|
+
let match: RegExpExecArray | null
|
|
135
|
+
let idx = 0
|
|
136
|
+
while ((match = tokenRe.exec(text)) !== null) {
|
|
137
|
+
if (match.index > lastIndex) {
|
|
138
|
+
nodes.push(<span key={`t${idx++}`}>{text.slice(lastIndex, match.index)}</span>)
|
|
139
|
+
}
|
|
140
|
+
const [, key, str, num, bool, nil] = match
|
|
141
|
+
if (key !== undefined) {
|
|
142
|
+
nodes.push(<span key={`k${idx++}`} style={{ color: palette.syntaxKey }}>{key}</span>)
|
|
143
|
+
nodes.push(<span key={`c${idx++}`}>:</span>)
|
|
144
|
+
} else if (str !== undefined) {
|
|
145
|
+
// Unescape the quoted string for the filter value (users expect to filter on
|
|
146
|
+
// the displayed string, not JSON-escaped bytes).
|
|
147
|
+
const inner = str.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\')
|
|
148
|
+
nodes.push(
|
|
149
|
+
<FilterableValue key={`s${idx++}`} value={inner} onFilter={onFilterValue} color={palette.syntaxString} palette={palette} />
|
|
150
|
+
)
|
|
151
|
+
} else if (num !== undefined) {
|
|
152
|
+
nodes.push(
|
|
153
|
+
<FilterableValue key={`n${idx++}`} value={num} onFilter={onFilterValue} color={palette.syntaxNumber} palette={palette} />
|
|
154
|
+
)
|
|
155
|
+
} else if (bool !== undefined) {
|
|
156
|
+
nodes.push(
|
|
157
|
+
<FilterableValue key={`b${idx++}`} value={bool} onFilter={onFilterValue} color={palette.syntaxBoolean} palette={palette} />
|
|
158
|
+
)
|
|
159
|
+
} else if (nil !== undefined) {
|
|
160
|
+
nodes.push(<span key={`z${idx++}`} style={{ color: palette.syntaxNull }}>{nil}</span>)
|
|
161
|
+
}
|
|
162
|
+
lastIndex = tokenRe.lastIndex
|
|
163
|
+
}
|
|
164
|
+
if (lastIndex < text.length) {
|
|
165
|
+
nodes.push(<span key={`t${idx++}`}>{text.slice(lastIndex)}</span>)
|
|
166
|
+
}
|
|
167
|
+
return <>{nodes}</>
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function SummaryLine({ obj, palette }: { obj: Record<string, unknown>; palette: LogPalette }) {
|
|
92
171
|
const lvl = obj.level ?? obj.severity ?? obj.lvl ?? nestedField(obj, 'log', 'level')
|
|
93
172
|
const msg = obj.msg ?? obj.message
|
|
94
173
|
const rawErr = obj.error ?? obj.err
|
|
@@ -100,33 +179,36 @@ function SummaryLine({ obj }: { obj: Record<string, unknown> }) {
|
|
|
100
179
|
return (
|
|
101
180
|
<>
|
|
102
181
|
{lvl != null && (
|
|
103
|
-
<span className={`${getLevelBadgeColor(lvl)} text-[10px] font-semibold px-1 py-px rounded mr-1.5 inline-block`}>
|
|
182
|
+
<span className={`${getLevelBadgeColor(lvl, palette)} text-[10px] font-semibold px-1 py-px rounded mr-1.5 inline-block`}>
|
|
104
183
|
{formatLevel(lvl)}
|
|
105
184
|
</span>
|
|
106
185
|
)}
|
|
107
186
|
{typeof msg === 'string' && (
|
|
108
|
-
<span className=
|
|
187
|
+
<span className={palette.textPrimary}>{msg}</span>
|
|
109
188
|
)}
|
|
110
189
|
{typeof err === 'string' && (
|
|
111
|
-
<span className=
|
|
190
|
+
<span className={`${palette.textError} ml-2`}>error={err}</span>
|
|
112
191
|
)}
|
|
113
192
|
{typeof caller === 'string' && (
|
|
114
|
-
<span className=
|
|
193
|
+
<span className={`${palette.textDisabled} ml-2`}>{caller}</span>
|
|
115
194
|
)}
|
|
116
195
|
</>
|
|
117
196
|
)
|
|
118
197
|
}
|
|
119
198
|
|
|
120
|
-
function ExpandedLogfmt({ obj }: { obj: Record<string, unknown
|
|
199
|
+
function ExpandedLogfmt({ obj, onFilterValue, palette }: { obj: Record<string, unknown>; onFilterValue?: (v: string) => void; palette: LogPalette }) {
|
|
121
200
|
return (
|
|
122
201
|
<>
|
|
123
|
-
{Object.entries(obj).map(([key, val]) =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
{Object.entries(obj).map(([key, val]) => {
|
|
203
|
+
const str = String(val)
|
|
204
|
+
return (
|
|
205
|
+
<div key={key}>
|
|
206
|
+
<span style={{ color: palette.syntaxKey }}>{key}</span>
|
|
207
|
+
<span className={palette.textTertiary}>=</span>
|
|
208
|
+
<FilterableValue value={str} onFilter={onFilterValue} color={palette.syntaxString} palette={palette} />
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
})}
|
|
130
212
|
</>
|
|
131
213
|
)
|
|
132
214
|
}
|
|
@@ -149,7 +231,7 @@ function formatLevel(lvl: unknown): string {
|
|
|
149
231
|
return String(lvl).toUpperCase()
|
|
150
232
|
}
|
|
151
233
|
|
|
152
|
-
function getLevelBadgeColor(lvl: unknown): string {
|
|
234
|
+
function getLevelBadgeColor(lvl: unknown, palette: LogPalette): string {
|
|
153
235
|
let normalized: string
|
|
154
236
|
if (typeof lvl === 'number') {
|
|
155
237
|
// Pino/bunyan numeric levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
|
|
@@ -160,9 +242,9 @@ function getLevelBadgeColor(lvl: unknown): string {
|
|
|
160
242
|
} else {
|
|
161
243
|
normalized = String(lvl).toLowerCase()
|
|
162
244
|
}
|
|
163
|
-
if (/^(error|err|fatal|panic|critical|crit)$/.test(normalized)) return
|
|
164
|
-
if (/^(warn|warning)$/.test(normalized)) return
|
|
165
|
-
if (/^(info|information|notice)$/.test(normalized)) return
|
|
166
|
-
if (/^(debug|dbg|trace|verbose)$/.test(normalized)) return
|
|
167
|
-
return
|
|
245
|
+
if (/^(error|err|fatal|panic|critical|crit)$/.test(normalized)) return palette.levelBadgeError
|
|
246
|
+
if (/^(warn|warning)$/.test(normalized)) return palette.levelBadgeWarn
|
|
247
|
+
if (/^(info|information|notice)$/.test(normalized)) return palette.levelBadgeInfo
|
|
248
|
+
if (/^(debug|dbg|trace|verbose)$/.test(normalized)) return palette.levelBadgeDebug
|
|
249
|
+
return palette.levelBadgeNeutral
|
|
168
250
|
}
|
|
@@ -7,6 +7,7 @@ import { useLogStream } from './useLogStream'
|
|
|
7
7
|
import { ContainerSelect, LogRangeSelect } from './LogToolbarSelects'
|
|
8
8
|
import { LogCore } from './LogCore'
|
|
9
9
|
import type { DownloadFormat } from './LogCore'
|
|
10
|
+
import type { LogPalette } from './log-palette'
|
|
10
11
|
import type { WorkloadPodInfo } from '../../types'
|
|
11
12
|
import { useToast } from '../ui/Toast'
|
|
12
13
|
|
|
@@ -48,11 +49,6 @@ export interface WorkloadLogsViewerProps {
|
|
|
48
49
|
forceDark?: boolean
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const POD_COLORS = [
|
|
52
|
-
'text-blue-400', 'text-green-400', 'text-yellow-400', 'text-purple-400',
|
|
53
|
-
'text-pink-400', 'text-cyan-400', 'text-orange-400', 'text-lime-400',
|
|
54
|
-
]
|
|
55
|
-
|
|
56
52
|
export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark }: WorkloadLogsViewerProps) {
|
|
57
53
|
const [selectedContainer, setSelectedContainer] = useState<string>('')
|
|
58
54
|
const [pods, setPods] = useState<WorkloadPodInfo[]>([])
|
|
@@ -67,9 +63,12 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
67
63
|
const { entries, append, set, clear } = useLogBuffer()
|
|
68
64
|
const { isStreaming, startStreaming, stopStreaming } = useLogStream()
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
// Map pod.name → index. Color classes are resolved at render time from the
|
|
67
|
+
// current palette (see LogCore / pod-filter dropdown below) so toggling
|
|
68
|
+
// isDark re-themes pod labels without re-fetching.
|
|
69
|
+
const podColorIndex = useMemo(() => {
|
|
70
|
+
const m = new Map<string, number>()
|
|
71
|
+
pods.forEach((pod, i) => m.set(pod.name, i))
|
|
73
72
|
return m
|
|
74
73
|
}, [pods])
|
|
75
74
|
|
|
@@ -88,15 +87,15 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
88
87
|
setSelectedPods(new Set(result.pods.map(p => p.name)))
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
const
|
|
92
|
-
result.pods.forEach((pod, i) =>
|
|
90
|
+
const indexByPod = new Map<string, number>()
|
|
91
|
+
result.pods.forEach((pod, i) => indexByPod.set(pod.name, i))
|
|
93
92
|
|
|
94
93
|
set(result.logs.map(log => ({
|
|
95
94
|
timestamp: log.timestamp,
|
|
96
95
|
content: log.content,
|
|
97
96
|
container: log.container,
|
|
98
97
|
pod: log.pod,
|
|
99
|
-
|
|
98
|
+
podColorIndex: indexByPod.get(log.pod),
|
|
100
99
|
})))
|
|
101
100
|
} catch (err) {
|
|
102
101
|
console.error('Failed to fetch workload logs:', err)
|
|
@@ -129,7 +128,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
129
128
|
content: data.content || '',
|
|
130
129
|
container: data.container || '',
|
|
131
130
|
pod: data.pod || '',
|
|
132
|
-
|
|
131
|
+
podColorIndex: podColorIndex.get(data.pod || ''),
|
|
133
132
|
})
|
|
134
133
|
}
|
|
135
134
|
},
|
|
@@ -159,7 +158,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
159
158
|
},
|
|
160
159
|
'Workload log stream error',
|
|
161
160
|
)
|
|
162
|
-
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append,
|
|
161
|
+
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append, podColorIndex, selectedPods.size])
|
|
163
162
|
|
|
164
163
|
const allContainers = useMemo(() => {
|
|
165
164
|
const s = new Set<string>()
|
|
@@ -217,14 +216,14 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
217
216
|
}
|
|
218
217
|
}, [filteredEntries, name, overrideDownload, showError, showSuccess])
|
|
219
218
|
|
|
220
|
-
const
|
|
219
|
+
const renderToolbarExtra = ({ isDark, palette }: { isDark: boolean; palette: LogPalette }) => (
|
|
221
220
|
<>
|
|
222
221
|
{/* Pod filter */}
|
|
223
222
|
<div className="relative">
|
|
224
223
|
<button
|
|
225
224
|
onClick={() => setShowPodFilter(v => !v)}
|
|
226
225
|
className={`flex items-center gap-1.5 px-2 py-1.5 text-xs rounded transition-colors ${
|
|
227
|
-
showPodFilter ?
|
|
226
|
+
showPodFilter ? palette.toolbarActive : `${palette.elevatedBg} ${palette.textSecondary} ${palette.hoverBg}`
|
|
228
227
|
}`}
|
|
229
228
|
>
|
|
230
229
|
<Filter className="w-3 h-3" />
|
|
@@ -233,27 +232,36 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
233
232
|
</button>
|
|
234
233
|
|
|
235
234
|
{showPodFilter && (
|
|
236
|
-
<div className=
|
|
237
|
-
<div className=
|
|
238
|
-
<button onClick={toggleAllPods} className=
|
|
235
|
+
<div className={`absolute top-full left-0 mt-1 w-64 ${palette.menuBg} border ${palette.border} rounded-lg shadow-lg z-50 max-h-64 overflow-y-auto`}>
|
|
236
|
+
<div className={`p-2 border-b ${palette.border}`}>
|
|
237
|
+
<button onClick={toggleAllPods} className={`text-xs ${palette.textAccent} hover:underline`}>
|
|
239
238
|
{pods.every(p => selectedPods.has(p.name)) ? 'Deselect all' : 'Select all'}
|
|
240
239
|
</button>
|
|
241
240
|
</div>
|
|
242
|
-
{pods.map(pod =>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
241
|
+
{pods.map(pod => {
|
|
242
|
+
const dotBg = palette.podColors[(podColorIndex.get(pod.name) ?? 0) % palette.podColors.length].bg
|
|
243
|
+
let readyColor: string
|
|
244
|
+
if (pod.ready) {
|
|
245
|
+
readyColor = isDark ? 'text-emerald-400' : 'text-emerald-700'
|
|
246
|
+
} else {
|
|
247
|
+
readyColor = isDark ? 'text-amber-400' : 'text-amber-700'
|
|
248
|
+
}
|
|
249
|
+
return (
|
|
250
|
+
<label key={pod.name} className={`flex items-center gap-2 px-3 py-2 ${palette.hoverBg}`}>
|
|
251
|
+
<input
|
|
252
|
+
type="checkbox"
|
|
253
|
+
checked={selectedPods.has(pod.name)}
|
|
254
|
+
onChange={() => togglePod(pod.name)}
|
|
255
|
+
className={`w-3 h-3 rounded ${palette.borderLight} ${palette.elevatedBg} text-blue-500 focus:ring-blue-500 focus:ring-offset-0`}
|
|
256
|
+
/>
|
|
257
|
+
<span className={`w-2 h-2 rounded-full ${dotBg}`} />
|
|
258
|
+
<span className={`text-xs ${palette.textPrimary} truncate flex-1`}>{pod.name}</span>
|
|
259
|
+
<span className={`text-xs ${readyColor}`}>
|
|
260
|
+
{pod.ready ? 'Ready' : 'Not Ready'}
|
|
261
|
+
</span>
|
|
262
|
+
</label>
|
|
263
|
+
)
|
|
264
|
+
})}
|
|
257
265
|
</div>
|
|
258
266
|
)}
|
|
259
267
|
</div>
|
|
@@ -263,6 +271,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
263
271
|
value={selectedContainer}
|
|
264
272
|
onChange={setSelectedContainer}
|
|
265
273
|
includeAll
|
|
274
|
+
isDark={isDark}
|
|
266
275
|
/>
|
|
267
276
|
|
|
268
277
|
<LogRangeSelect
|
|
@@ -270,6 +279,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
270
279
|
onChange={setLogRange}
|
|
271
280
|
lineOptions={[50, 100, 500, 1000]}
|
|
272
281
|
tooltip="How many logs to load per pod — by line count or time range"
|
|
282
|
+
isDark={isDark}
|
|
273
283
|
/>
|
|
274
284
|
</>
|
|
275
285
|
)
|
|
@@ -284,7 +294,7 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
284
294
|
onRefresh={loadLogs}
|
|
285
295
|
onDownload={downloadLogs}
|
|
286
296
|
onClear={clear}
|
|
287
|
-
toolbarExtra={
|
|
297
|
+
toolbarExtra={renderToolbarExtra}
|
|
288
298
|
showPodName
|
|
289
299
|
emptyMessage={pods.length === 0 ? 'No pods found' : 'No logs available'}
|
|
290
300
|
errorMessage={fetchError}
|