@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
|
@@ -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,17 +1,8 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
2
|
import { ChevronRight, ChevronDown, Filter } from 'lucide-react'
|
|
3
3
|
import type { LogLevel } from './useLogBuffer'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
unescapeJsonStrings,
|
|
7
|
-
parseLogfmt,
|
|
8
|
-
SYNTAX_COLOR_KEY,
|
|
9
|
-
SYNTAX_COLOR_STRING,
|
|
10
|
-
SYNTAX_COLOR_NUMBER,
|
|
11
|
-
SYNTAX_COLOR_BOOLEAN,
|
|
12
|
-
SYNTAX_COLOR_NULL,
|
|
13
|
-
} from '../../utils/log-format'
|
|
14
|
-
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'
|
|
15
6
|
|
|
16
7
|
interface StructuredLogLineProps {
|
|
17
8
|
content: string
|
|
@@ -25,9 +16,16 @@ interface StructuredLogLineProps {
|
|
|
25
16
|
* add the value to the log search/filter.
|
|
26
17
|
*/
|
|
27
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
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultExpanded, onFilterValue }: StructuredLogLineProps) {
|
|
27
|
+
export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultExpanded, onFilterValue, isDark = true }: StructuredLogLineProps) {
|
|
28
|
+
const palette = useMemo(() => getLogPalette(isDark), [isDark])
|
|
31
29
|
// null = user hasn't toggled this line; defers to defaultExpanded (global toggle)
|
|
32
30
|
const [localExpanded, setLocalExpanded] = useState<boolean | null>(null)
|
|
33
31
|
const expanded = localExpanded ?? defaultExpanded ?? false
|
|
@@ -45,7 +43,7 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
45
43
|
|
|
46
44
|
if (!parsed) {
|
|
47
45
|
return (
|
|
48
|
-
<span className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${
|
|
46
|
+
<span className={`${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'} ${getLogLevelColor(level, isDark)}`}>
|
|
49
47
|
{content}
|
|
50
48
|
</span>
|
|
51
49
|
)
|
|
@@ -55,8 +53,8 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
55
53
|
|
|
56
54
|
const toggle = () => setLocalExpanded(!expanded)
|
|
57
55
|
const chevron = expanded
|
|
58
|
-
? <ChevronDown className=
|
|
59
|
-
: <ChevronRight className=
|
|
56
|
+
? <ChevronDown className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
57
|
+
: <ChevronRight className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
60
58
|
|
|
61
59
|
return (
|
|
62
60
|
<span>
|
|
@@ -64,30 +62,31 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
64
62
|
// Collapsed: entire summary line is clickable
|
|
65
63
|
<span
|
|
66
64
|
onClick={toggle}
|
|
67
|
-
className={`cursor-pointer
|
|
65
|
+
className={`cursor-pointer ${palette.hoverSurface} rounded px-0.5 -ml-0.5 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}
|
|
68
66
|
>
|
|
69
67
|
<span className="inline-flex items-center align-middle mr-0.5">{chevron}</span>
|
|
70
|
-
<SummaryLine obj={parsed} />
|
|
71
|
-
<span className=
|
|
68
|
+
<SummaryLine obj={parsed} palette={palette} />
|
|
69
|
+
<span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
|
|
72
70
|
</span>
|
|
73
71
|
) : (
|
|
74
72
|
// Expanded: summary header is clickable to collapse, JSON content is selectable
|
|
75
73
|
<>
|
|
76
74
|
<span
|
|
77
75
|
onClick={toggle}
|
|
78
|
-
className=
|
|
76
|
+
className={`cursor-pointer ${palette.hoverSurface} rounded px-0.5 -ml-0.5`}
|
|
79
77
|
>
|
|
80
78
|
<span className="inline-flex items-center align-middle mr-0.5">{chevron}</span>
|
|
81
|
-
<SummaryLine obj={parsed} />
|
|
82
|
-
<span className=
|
|
79
|
+
<SummaryLine obj={parsed} palette={palette} />
|
|
80
|
+
<span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
|
|
83
81
|
</span>
|
|
84
82
|
<span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
|
|
85
83
|
{isLogfmt ? (
|
|
86
|
-
<ExpandedLogfmt obj={parsed} onFilterValue={onFilterValue} />
|
|
84
|
+
<ExpandedLogfmt obj={parsed} onFilterValue={onFilterValue} palette={palette} />
|
|
87
85
|
) : (
|
|
88
86
|
<JsonExpanded
|
|
89
87
|
text={unescapeJsonStrings(JSON.stringify(parsed, null, 2))}
|
|
90
88
|
onFilterValue={onFilterValue}
|
|
89
|
+
palette={palette}
|
|
91
90
|
/>
|
|
92
91
|
)}
|
|
93
92
|
</span>
|
|
@@ -102,18 +101,18 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
102
101
|
* Clicking the chip calls onFilter(value) so the caller can push it into log search.
|
|
103
102
|
*/
|
|
104
103
|
function FilterableValue({
|
|
105
|
-
value, onFilter, color,
|
|
106
|
-
}: { value: string; onFilter?: (v: string) => void; color?: string }) {
|
|
104
|
+
value, onFilter, color, palette,
|
|
105
|
+
}: { value: string; onFilter?: (v: string) => void; color?: string; palette: LogPalette }) {
|
|
107
106
|
if (!onFilter) {
|
|
108
107
|
return <span style={color ? { color } : undefined}>{value}</span>
|
|
109
108
|
}
|
|
110
109
|
return (
|
|
111
|
-
<span className=
|
|
110
|
+
<span className={`group/flt inline-flex items-baseline align-baseline gap-0.5 rounded ${palette.hoverSurface}`}>
|
|
112
111
|
<span style={color ? { color } : undefined}>{value}</span>
|
|
113
112
|
<button
|
|
114
113
|
type="button"
|
|
115
114
|
onClick={(e) => { e.stopPropagation(); onFilter(value) }}
|
|
116
|
-
className=
|
|
115
|
+
className={`opacity-0 group-hover/flt:opacity-100 transition-opacity ${palette.textTertiary} ${palette.hoverText} px-0.5`}
|
|
117
116
|
title={`Filter to lines containing "${value}"`}
|
|
118
117
|
aria-label={`Filter to lines containing ${value}`}
|
|
119
118
|
>
|
|
@@ -128,7 +127,7 @@ function FilterableValue({
|
|
|
128
127
|
* the layout that JSON.stringify(..., null, 2) produces. Tokenizes the string
|
|
129
128
|
* and emits React nodes so the hover chip can be wired per value.
|
|
130
129
|
*/
|
|
131
|
-
function JsonExpanded({ text, onFilterValue }: { text: string; onFilterValue?: (v: string) => void }) {
|
|
130
|
+
function JsonExpanded({ text, onFilterValue, palette }: { text: string; onFilterValue?: (v: string) => void; palette: LogPalette }) {
|
|
132
131
|
const tokenRe = /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|\b(true|false)\b|\b(null)\b/g
|
|
133
132
|
const nodes: React.ReactNode[] = []
|
|
134
133
|
let lastIndex = 0
|
|
@@ -140,25 +139,25 @@ function JsonExpanded({ text, onFilterValue }: { text: string; onFilterValue?: (
|
|
|
140
139
|
}
|
|
141
140
|
const [, key, str, num, bool, nil] = match
|
|
142
141
|
if (key !== undefined) {
|
|
143
|
-
nodes.push(<span key={`k${idx++}`} style={{ color:
|
|
142
|
+
nodes.push(<span key={`k${idx++}`} style={{ color: palette.syntaxKey }}>{key}</span>)
|
|
144
143
|
nodes.push(<span key={`c${idx++}`}>:</span>)
|
|
145
144
|
} else if (str !== undefined) {
|
|
146
145
|
// Unescape the quoted string for the filter value (users expect to filter on
|
|
147
146
|
// the displayed string, not JSON-escaped bytes).
|
|
148
147
|
const inner = str.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\')
|
|
149
148
|
nodes.push(
|
|
150
|
-
<FilterableValue key={`s${idx++}`} value={inner} onFilter={onFilterValue} color={
|
|
149
|
+
<FilterableValue key={`s${idx++}`} value={inner} onFilter={onFilterValue} color={palette.syntaxString} palette={palette} />
|
|
151
150
|
)
|
|
152
151
|
} else if (num !== undefined) {
|
|
153
152
|
nodes.push(
|
|
154
|
-
<FilterableValue key={`n${idx++}`} value={num} onFilter={onFilterValue} color={
|
|
153
|
+
<FilterableValue key={`n${idx++}`} value={num} onFilter={onFilterValue} color={palette.syntaxNumber} palette={palette} />
|
|
155
154
|
)
|
|
156
155
|
} else if (bool !== undefined) {
|
|
157
156
|
nodes.push(
|
|
158
|
-
<FilterableValue key={`b${idx++}`} value={bool} onFilter={onFilterValue} color={
|
|
157
|
+
<FilterableValue key={`b${idx++}`} value={bool} onFilter={onFilterValue} color={palette.syntaxBoolean} palette={palette} />
|
|
159
158
|
)
|
|
160
159
|
} else if (nil !== undefined) {
|
|
161
|
-
nodes.push(<span key={`z${idx++}`} style={{ color:
|
|
160
|
+
nodes.push(<span key={`z${idx++}`} style={{ color: palette.syntaxNull }}>{nil}</span>)
|
|
162
161
|
}
|
|
163
162
|
lastIndex = tokenRe.lastIndex
|
|
164
163
|
}
|
|
@@ -168,7 +167,7 @@ function JsonExpanded({ text, onFilterValue }: { text: string; onFilterValue?: (
|
|
|
168
167
|
return <>{nodes}</>
|
|
169
168
|
}
|
|
170
169
|
|
|
171
|
-
function SummaryLine({ obj }: { obj: Record<string, unknown
|
|
170
|
+
function SummaryLine({ obj, palette }: { obj: Record<string, unknown>; palette: LogPalette }) {
|
|
172
171
|
const lvl = obj.level ?? obj.severity ?? obj.lvl ?? nestedField(obj, 'log', 'level')
|
|
173
172
|
const msg = obj.msg ?? obj.message
|
|
174
173
|
const rawErr = obj.error ?? obj.err
|
|
@@ -180,33 +179,33 @@ function SummaryLine({ obj }: { obj: Record<string, unknown> }) {
|
|
|
180
179
|
return (
|
|
181
180
|
<>
|
|
182
181
|
{lvl != null && (
|
|
183
|
-
<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`}>
|
|
184
183
|
{formatLevel(lvl)}
|
|
185
184
|
</span>
|
|
186
185
|
)}
|
|
187
186
|
{typeof msg === 'string' && (
|
|
188
|
-
<span className=
|
|
187
|
+
<span className={palette.textPrimary}>{msg}</span>
|
|
189
188
|
)}
|
|
190
189
|
{typeof err === 'string' && (
|
|
191
|
-
<span className=
|
|
190
|
+
<span className={`${palette.textError} ml-2`}>error={err}</span>
|
|
192
191
|
)}
|
|
193
192
|
{typeof caller === 'string' && (
|
|
194
|
-
<span className=
|
|
193
|
+
<span className={`${palette.textDisabled} ml-2`}>{caller}</span>
|
|
195
194
|
)}
|
|
196
195
|
</>
|
|
197
196
|
)
|
|
198
197
|
}
|
|
199
198
|
|
|
200
|
-
function ExpandedLogfmt({ obj, onFilterValue }: { obj: Record<string, unknown>; onFilterValue?: (v: string) => void }) {
|
|
199
|
+
function ExpandedLogfmt({ obj, onFilterValue, palette }: { obj: Record<string, unknown>; onFilterValue?: (v: string) => void; palette: LogPalette }) {
|
|
201
200
|
return (
|
|
202
201
|
<>
|
|
203
202
|
{Object.entries(obj).map(([key, val]) => {
|
|
204
203
|
const str = String(val)
|
|
205
204
|
return (
|
|
206
205
|
<div key={key}>
|
|
207
|
-
<span style={{ color:
|
|
208
|
-
<span className=
|
|
209
|
-
<FilterableValue value={str} onFilter={onFilterValue} color={
|
|
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} />
|
|
210
209
|
</div>
|
|
211
210
|
)
|
|
212
211
|
})}
|
|
@@ -232,7 +231,7 @@ function formatLevel(lvl: unknown): string {
|
|
|
232
231
|
return String(lvl).toUpperCase()
|
|
233
232
|
}
|
|
234
233
|
|
|
235
|
-
function getLevelBadgeColor(lvl: unknown): string {
|
|
234
|
+
function getLevelBadgeColor(lvl: unknown, palette: LogPalette): string {
|
|
236
235
|
let normalized: string
|
|
237
236
|
if (typeof lvl === 'number') {
|
|
238
237
|
// Pino/bunyan numeric levels: 10=trace, 20=debug, 30=info, 40=warn, 50=error, 60=fatal
|
|
@@ -243,9 +242,9 @@ function getLevelBadgeColor(lvl: unknown): string {
|
|
|
243
242
|
} else {
|
|
244
243
|
normalized = String(lvl).toLowerCase()
|
|
245
244
|
}
|
|
246
|
-
if (/^(error|err|fatal|panic|critical|crit)$/.test(normalized)) return
|
|
247
|
-
if (/^(warn|warning)$/.test(normalized)) return
|
|
248
|
-
if (/^(info|information|notice)$/.test(normalized)) return
|
|
249
|
-
if (/^(debug|dbg|trace|verbose)$/.test(normalized)) return
|
|
250
|
-
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
|
|
251
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}
|