@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit color palettes for the log viewer.
|
|
3
|
+
*
|
|
4
|
+
* The log viewer is intentionally self-contained: it does NOT use theme tokens
|
|
5
|
+
* (`text-theme-*`, `bg-theme-*`, etc.) because those are driven by CSS
|
|
6
|
+
* `light-dark()` and don't resolve correctly through the log viewer's forced
|
|
7
|
+
* `color-scheme` container. Instead it flips between two explicit palettes
|
|
8
|
+
* based on its own `isDark` state (toggled by a Sun/Moon button in the
|
|
9
|
+
* toolbar, persisted to `localStorage['radar-logs-dark']`).
|
|
10
|
+
*
|
|
11
|
+
* All class strings are static literals so Tailwind's class scanner picks them
|
|
12
|
+
* up — do not construct them dynamically.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** All color-related class strings used inside the log viewer. */
|
|
16
|
+
export interface LogPalette {
|
|
17
|
+
// Container / surfaces
|
|
18
|
+
containerBg: string
|
|
19
|
+
toolbarBg: string
|
|
20
|
+
toolbarBgMuted: string
|
|
21
|
+
menuBg: string
|
|
22
|
+
elevatedBg: string
|
|
23
|
+
|
|
24
|
+
// Borders
|
|
25
|
+
border: string
|
|
26
|
+
borderLight: string
|
|
27
|
+
|
|
28
|
+
// Text
|
|
29
|
+
textPrimary: string
|
|
30
|
+
textSecondary: string
|
|
31
|
+
textTertiary: string
|
|
32
|
+
textDisabled: string
|
|
33
|
+
/** Standalone error text (inline "error=..." in structured lines, inline validation errors). */
|
|
34
|
+
textError: string
|
|
35
|
+
/** Accent/link text — blue-family. Used for toolbar toggles, "select all" etc. */
|
|
36
|
+
textAccent: string
|
|
37
|
+
|
|
38
|
+
// Placeholder (plain class, applied via `placeholder-*` below)
|
|
39
|
+
placeholder: string
|
|
40
|
+
|
|
41
|
+
// Hover states
|
|
42
|
+
hoverBg: string
|
|
43
|
+
hoverSurface: string
|
|
44
|
+
hoverText: string
|
|
45
|
+
/** Active/selected toolbar controls inside the viewer. */
|
|
46
|
+
toolbarActive: string
|
|
47
|
+
|
|
48
|
+
// Row highlight (current search match)
|
|
49
|
+
currentMatchBg: string
|
|
50
|
+
|
|
51
|
+
// Level-filter button active colors (per-level, full className)
|
|
52
|
+
levelActiveError: string
|
|
53
|
+
levelActiveWarn: string
|
|
54
|
+
levelActiveInfo: string
|
|
55
|
+
levelActiveDebug: string
|
|
56
|
+
|
|
57
|
+
// Level-badge colors used inside StructuredLogLine
|
|
58
|
+
levelBadgeError: string
|
|
59
|
+
levelBadgeWarn: string
|
|
60
|
+
levelBadgeInfo: string
|
|
61
|
+
levelBadgeDebug: string
|
|
62
|
+
levelBadgeNeutral: string
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Pod label colors for WorkloadLogsViewer (aggregated logs across pods).
|
|
66
|
+
* Pairs: one class for the name text, one for the filter-list dot.
|
|
67
|
+
* Pods are round-robined through this array.
|
|
68
|
+
*/
|
|
69
|
+
podColors: Array<{ text: string; bg: string }>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Syntax-highlight colors for structured JSON/logfmt renderings.
|
|
73
|
+
* Applied as inline `style={{ color }}` (not classes) because the values
|
|
74
|
+
* come from log-format.ts and feed React's style prop.
|
|
75
|
+
*/
|
|
76
|
+
syntaxKey: string
|
|
77
|
+
syntaxString: string
|
|
78
|
+
syntaxNumber: string
|
|
79
|
+
syntaxBoolean: string
|
|
80
|
+
syntaxNull: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const DARK_PALETTE: LogPalette = {
|
|
84
|
+
containerBg: 'bg-slate-950',
|
|
85
|
+
toolbarBg: 'bg-slate-900',
|
|
86
|
+
toolbarBgMuted: 'bg-slate-900/60',
|
|
87
|
+
menuBg: 'bg-slate-900',
|
|
88
|
+
elevatedBg: 'bg-slate-800',
|
|
89
|
+
|
|
90
|
+
border: 'border-slate-800',
|
|
91
|
+
borderLight: 'border-slate-700',
|
|
92
|
+
|
|
93
|
+
textPrimary: 'text-slate-100',
|
|
94
|
+
textSecondary: 'text-slate-400',
|
|
95
|
+
textTertiary: 'text-slate-500',
|
|
96
|
+
textDisabled: 'text-slate-600',
|
|
97
|
+
textError: 'text-red-400',
|
|
98
|
+
textAccent: 'text-blue-400',
|
|
99
|
+
|
|
100
|
+
placeholder: 'placeholder-slate-600',
|
|
101
|
+
|
|
102
|
+
hoverBg: 'hover:bg-slate-800',
|
|
103
|
+
hoverSurface: 'hover:bg-slate-800/50',
|
|
104
|
+
hoverText: 'hover:text-slate-100',
|
|
105
|
+
toolbarActive: 'bg-slate-700 text-slate-100 hover:bg-slate-600',
|
|
106
|
+
|
|
107
|
+
currentMatchBg: 'bg-yellow-500/10',
|
|
108
|
+
|
|
109
|
+
levelActiveError: 'bg-red-500/20 text-red-400 border-red-500/40',
|
|
110
|
+
levelActiveWarn: 'bg-amber-500/20 text-amber-400 border-amber-500/40',
|
|
111
|
+
levelActiveInfo: 'bg-blue-500/20 text-blue-400 border-blue-500/40',
|
|
112
|
+
levelActiveDebug: 'bg-slate-700 text-slate-300 border-slate-600',
|
|
113
|
+
|
|
114
|
+
levelBadgeError: 'bg-red-500/20 text-red-400 border border-red-500/40',
|
|
115
|
+
levelBadgeWarn: 'bg-amber-500/20 text-amber-400 border border-amber-500/40',
|
|
116
|
+
levelBadgeInfo: 'bg-blue-500/20 text-blue-400 border border-blue-500/40',
|
|
117
|
+
levelBadgeDebug: 'bg-slate-700 text-slate-300 border border-slate-600',
|
|
118
|
+
levelBadgeNeutral: 'bg-slate-800 text-slate-400 border border-slate-700',
|
|
119
|
+
|
|
120
|
+
podColors: [
|
|
121
|
+
{ text: 'text-blue-400', bg: 'bg-blue-400' },
|
|
122
|
+
{ text: 'text-emerald-400', bg: 'bg-emerald-400' },
|
|
123
|
+
{ text: 'text-amber-400', bg: 'bg-amber-400' },
|
|
124
|
+
{ text: 'text-purple-400', bg: 'bg-purple-400' },
|
|
125
|
+
{ text: 'text-pink-400', bg: 'bg-pink-400' },
|
|
126
|
+
{ text: 'text-cyan-400', bg: 'bg-cyan-400' },
|
|
127
|
+
{ text: 'text-orange-400', bg: 'bg-orange-400' },
|
|
128
|
+
{ text: 'text-lime-400', bg: 'bg-lime-400' },
|
|
129
|
+
],
|
|
130
|
+
|
|
131
|
+
// Hex values tuned for the `bg-slate-950` container.
|
|
132
|
+
syntaxKey: '#7cacf8',
|
|
133
|
+
syntaxString: '#73c991',
|
|
134
|
+
syntaxNumber: '#e5c07b',
|
|
135
|
+
syntaxBoolean: '#c678dd',
|
|
136
|
+
syntaxNull: '#808080',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const LIGHT_PALETTE: LogPalette = {
|
|
140
|
+
containerBg: 'bg-slate-50',
|
|
141
|
+
toolbarBg: 'bg-slate-100',
|
|
142
|
+
toolbarBgMuted: 'bg-slate-100/60',
|
|
143
|
+
menuBg: 'bg-white',
|
|
144
|
+
elevatedBg: 'bg-white',
|
|
145
|
+
|
|
146
|
+
border: 'border-slate-200',
|
|
147
|
+
borderLight: 'border-slate-300',
|
|
148
|
+
|
|
149
|
+
textPrimary: 'text-slate-900',
|
|
150
|
+
textSecondary: 'text-slate-600',
|
|
151
|
+
textTertiary: 'text-slate-400',
|
|
152
|
+
textDisabled: 'text-slate-300',
|
|
153
|
+
textError: 'text-red-700',
|
|
154
|
+
textAccent: 'text-blue-700',
|
|
155
|
+
|
|
156
|
+
placeholder: 'placeholder-slate-400',
|
|
157
|
+
|
|
158
|
+
hoverBg: 'hover:bg-slate-200',
|
|
159
|
+
hoverSurface: 'hover:bg-slate-200/60',
|
|
160
|
+
hoverText: 'hover:text-slate-900',
|
|
161
|
+
toolbarActive: 'bg-slate-200 text-slate-900 hover:bg-slate-300',
|
|
162
|
+
|
|
163
|
+
currentMatchBg: 'bg-yellow-200/60',
|
|
164
|
+
|
|
165
|
+
levelActiveError: 'bg-red-100 text-red-700 border-red-400',
|
|
166
|
+
levelActiveWarn: 'bg-amber-100 text-amber-700 border-amber-400',
|
|
167
|
+
levelActiveInfo: 'bg-blue-100 text-blue-700 border-blue-400',
|
|
168
|
+
levelActiveDebug: 'bg-slate-200 text-slate-700 border-slate-400',
|
|
169
|
+
|
|
170
|
+
levelBadgeError: 'bg-red-100 text-red-700 border border-red-400',
|
|
171
|
+
levelBadgeWarn: 'bg-amber-100 text-amber-700 border border-amber-400',
|
|
172
|
+
levelBadgeInfo: 'bg-blue-100 text-blue-700 border border-blue-400',
|
|
173
|
+
levelBadgeDebug: 'bg-slate-200 text-slate-700 border border-slate-400',
|
|
174
|
+
levelBadgeNeutral: 'bg-slate-100 text-slate-600 border border-slate-300',
|
|
175
|
+
|
|
176
|
+
podColors: [
|
|
177
|
+
{ text: 'text-blue-700', bg: 'bg-blue-700' },
|
|
178
|
+
{ text: 'text-emerald-700', bg: 'bg-emerald-700' },
|
|
179
|
+
{ text: 'text-amber-700', bg: 'bg-amber-700' },
|
|
180
|
+
{ text: 'text-purple-700', bg: 'bg-purple-700' },
|
|
181
|
+
{ text: 'text-pink-700', bg: 'bg-pink-700' },
|
|
182
|
+
{ text: 'text-cyan-700', bg: 'bg-cyan-700' },
|
|
183
|
+
{ text: 'text-orange-700', bg: 'bg-orange-700' },
|
|
184
|
+
{ text: 'text-lime-700', bg: 'bg-lime-700' },
|
|
185
|
+
],
|
|
186
|
+
|
|
187
|
+
// Hex values tuned for the `bg-slate-50` container — darker so they read
|
|
188
|
+
// on a near-white background.
|
|
189
|
+
syntaxKey: '#0b63c0',
|
|
190
|
+
syntaxString: '#2b8a3e',
|
|
191
|
+
syntaxNumber: '#b95f00',
|
|
192
|
+
syntaxBoolean: '#7c3aed',
|
|
193
|
+
syntaxNull: '#6b7280',
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Get the palette for the current dark/light mode. */
|
|
197
|
+
export function getLogPalette(isDark: boolean): LogPalette {
|
|
198
|
+
return isDark ? DARK_PALETTE : LIGHT_PALETTE
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Per-level content color (log text body). */
|
|
202
|
+
export function getLogLevelColor(
|
|
203
|
+
level: 'error' | 'warn' | 'info' | 'debug' | 'unknown',
|
|
204
|
+
isDark: boolean,
|
|
205
|
+
): string {
|
|
206
|
+
if (isDark) {
|
|
207
|
+
switch (level) {
|
|
208
|
+
case 'error': return 'text-red-400'
|
|
209
|
+
case 'warn': return 'text-amber-400'
|
|
210
|
+
case 'info': return 'text-blue-400'
|
|
211
|
+
case 'debug': return 'text-slate-400'
|
|
212
|
+
default: return 'text-slate-100'
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
switch (level) {
|
|
216
|
+
case 'error': return 'text-red-700'
|
|
217
|
+
case 'warn': return 'text-amber-600'
|
|
218
|
+
case 'info': return 'text-blue-700'
|
|
219
|
+
case 'debug': return 'text-slate-500'
|
|
220
|
+
default: return 'text-slate-900'
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -9,7 +9,12 @@ export interface LogEntry {
|
|
|
9
9
|
content: string
|
|
10
10
|
container: string
|
|
11
11
|
pod?: string
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Index into the current palette's `podColors` array. Stored as an index
|
|
14
|
+
* (not a resolved class) so the pod-label color can re-theme when the
|
|
15
|
+
* viewer's isDark state toggles at runtime.
|
|
16
|
+
*/
|
|
17
|
+
podColorIndex?: number
|
|
13
18
|
level: LogLevel
|
|
14
19
|
isJson: boolean
|
|
15
20
|
isLogfmt: boolean
|
|
@@ -8,10 +8,12 @@ interface UseLogSearchReturn {
|
|
|
8
8
|
setQuery: (q: string) => void
|
|
9
9
|
isRegex: boolean
|
|
10
10
|
toggleRegex: () => void
|
|
11
|
+
setIsRegex: (v: boolean) => void
|
|
11
12
|
isCaseSensitive: boolean
|
|
12
13
|
toggleCaseSensitive: () => void
|
|
13
14
|
isFilterMode: boolean
|
|
14
15
|
toggleFilterMode: () => void
|
|
16
|
+
setFilterMode: (v: boolean) => void
|
|
15
17
|
matchCount: number
|
|
16
18
|
currentMatch: number
|
|
17
19
|
/** Indices into the entries array that match */
|
|
@@ -121,6 +123,7 @@ export function useLogSearch(
|
|
|
121
123
|
const toggleRegex = useCallback(() => setIsRegex(p => !p), [])
|
|
122
124
|
const toggleCaseSensitive = useCallback(() => setIsCaseSensitive(p => !p), [])
|
|
123
125
|
const toggleFilterMode = useCallback(() => setIsFilterMode(p => !p), [])
|
|
126
|
+
const setFilterMode = useCallback((v: boolean) => setIsFilterMode(v), [])
|
|
124
127
|
|
|
125
128
|
const open = useCallback(() => setIsOpen(true), [])
|
|
126
129
|
const close = useCallback(() => {
|
|
@@ -133,10 +136,12 @@ export function useLogSearch(
|
|
|
133
136
|
setQuery,
|
|
134
137
|
isRegex,
|
|
135
138
|
toggleRegex,
|
|
139
|
+
setIsRegex,
|
|
136
140
|
isCaseSensitive,
|
|
137
141
|
toggleCaseSensitive,
|
|
138
142
|
isFilterMode,
|
|
139
143
|
toggleFilterMode,
|
|
144
|
+
setFilterMode,
|
|
140
145
|
matchCount: matchIndices.length,
|
|
141
146
|
currentMatch,
|
|
142
147
|
matchIndices,
|
|
@@ -12,9 +12,16 @@ export function getArgoApplicationStatus(app: any): StatusBadge {
|
|
|
12
12
|
const sync = app.status?.sync?.status
|
|
13
13
|
const opPhase = app.status?.operationState?.phase
|
|
14
14
|
|
|
15
|
-
// Check for suspended (no automated sync policy)
|
|
15
|
+
// Check for suspended (no automated sync policy). Honor both the current
|
|
16
|
+
// "radarhq.io/suspended-prune" annotation and the legacy "skyhook.io/..."
|
|
17
|
+
// key still present on Applications suspended by older Radar builds.
|
|
18
|
+
// The annotation value stores the prior prune state as "true"/"false" for
|
|
19
|
+
// restore on resume — both strings are truthy in JS, which is intentional:
|
|
20
|
+
// the *presence* of the annotation is what signals suspended, not its value.
|
|
16
21
|
const hasAutomatedSync = !!app.spec?.syncPolicy?.automated
|
|
17
|
-
|
|
22
|
+
const annotations = app.metadata?.annotations
|
|
23
|
+
const suspendedByRadar = annotations?.['radarhq.io/suspended-prune'] || annotations?.['skyhook.io/suspended-prune']
|
|
24
|
+
if (health === 'Suspended' || (!hasAutomatedSync && suspendedByRadar)) {
|
|
18
25
|
return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
|
|
19
26
|
}
|
|
20
27
|
|
package/src/utils/log-format.ts
CHANGED
|
@@ -5,22 +5,87 @@
|
|
|
5
5
|
|
|
6
6
|
import type { LogLevel } from '../components/logs/useLogBuffer'
|
|
7
7
|
|
|
8
|
+
export type TimestampFormat =
|
|
9
|
+
| 'time-local'
|
|
10
|
+
| 'time-utc'
|
|
11
|
+
| 'iso-local'
|
|
12
|
+
| 'iso-utc'
|
|
13
|
+
| 'relative'
|
|
14
|
+
| 'epoch'
|
|
15
|
+
|
|
16
|
+
export const TIMESTAMP_FORMAT_LABELS: Record<TimestampFormat, string> = {
|
|
17
|
+
'time-local': 'Time (local)',
|
|
18
|
+
'time-utc': 'Time (UTC)',
|
|
19
|
+
'iso-local': 'ISO (local)',
|
|
20
|
+
'iso-utc': 'ISO (UTC)',
|
|
21
|
+
'relative': 'Relative',
|
|
22
|
+
'epoch': 'Epoch',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pad2(n: number): string {
|
|
26
|
+
return n < 10 ? '0' + n : String(n)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatIsoLocal(date: Date): string {
|
|
30
|
+
const offset = -date.getTimezoneOffset()
|
|
31
|
+
const sign = offset >= 0 ? '+' : '-'
|
|
32
|
+
const abs = Math.abs(offset)
|
|
33
|
+
return (
|
|
34
|
+
date.getFullYear() +
|
|
35
|
+
'-' + pad2(date.getMonth() + 1) +
|
|
36
|
+
'-' + pad2(date.getDate()) +
|
|
37
|
+
'T' + pad2(date.getHours()) +
|
|
38
|
+
':' + pad2(date.getMinutes()) +
|
|
39
|
+
':' + pad2(date.getSeconds()) +
|
|
40
|
+
sign + pad2(Math.floor(abs / 60)) + ':' + pad2(abs % 60)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatRelative(date: Date, now: number): string {
|
|
45
|
+
const diffSec = Math.round((now - date.getTime()) / 1000)
|
|
46
|
+
const abs = Math.abs(diffSec)
|
|
47
|
+
const suffix = diffSec >= 0 ? ' ago' : ' from now'
|
|
48
|
+
if (abs < 2) return 'just now'
|
|
49
|
+
if (abs < 60) return `${abs}s${suffix}`
|
|
50
|
+
if (abs < 3600) return `${Math.floor(abs / 60)}m${suffix}`
|
|
51
|
+
if (abs < 86400) return `${Math.floor(abs / 3600)}h${suffix}`
|
|
52
|
+
return `${Math.floor(abs / 86400)}d${suffix}`
|
|
53
|
+
}
|
|
54
|
+
|
|
8
55
|
/**
|
|
9
56
|
* Format a K8s log timestamp for display.
|
|
10
|
-
*
|
|
57
|
+
* Supports multiple display formats and UTC/local time zones.
|
|
58
|
+
* `now` is accepted for testability; defaults to Date.now().
|
|
11
59
|
*/
|
|
12
|
-
export function formatLogTimestamp(
|
|
60
|
+
export function formatLogTimestamp(
|
|
61
|
+
ts: string,
|
|
62
|
+
format: TimestampFormat = 'time-local',
|
|
63
|
+
now?: number,
|
|
64
|
+
): string {
|
|
13
65
|
const date = new Date(ts)
|
|
14
66
|
if (isNaN(date.getTime())) {
|
|
15
|
-
// Fallback: extract HH:MM:SS from ISO timestamp
|
|
16
67
|
return ts.slice(11, 19) || ts
|
|
17
68
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
69
|
+
switch (format) {
|
|
70
|
+
case 'time-utc':
|
|
71
|
+
return date.toISOString().slice(11, 19)
|
|
72
|
+
case 'iso-local':
|
|
73
|
+
return formatIsoLocal(date)
|
|
74
|
+
case 'iso-utc':
|
|
75
|
+
return date.toISOString()
|
|
76
|
+
case 'relative':
|
|
77
|
+
return formatRelative(date, now ?? Date.now())
|
|
78
|
+
case 'epoch':
|
|
79
|
+
return String(Math.floor(date.getTime() / 1000))
|
|
80
|
+
case 'time-local':
|
|
81
|
+
default:
|
|
82
|
+
return date.toLocaleTimeString('en-US', {
|
|
83
|
+
hour12: false,
|
|
84
|
+
hour: '2-digit',
|
|
85
|
+
minute: '2-digit',
|
|
86
|
+
second: '2-digit',
|
|
87
|
+
})
|
|
88
|
+
}
|
|
24
89
|
}
|
|
25
90
|
|
|
26
91
|
/** Map a detected LogLevel to a Tailwind color class. */
|
|
@@ -294,9 +359,9 @@ export function parseLogRange(logRange: string): { tailLines?: number; sinceSeco
|
|
|
294
359
|
// Syntax highlight colors shared between JSON and logfmt rendering
|
|
295
360
|
export const SYNTAX_COLOR_KEY = '#7cacf8'
|
|
296
361
|
export const SYNTAX_COLOR_STRING = '#73c991'
|
|
297
|
-
const SYNTAX_COLOR_NUMBER = '#e5c07b'
|
|
298
|
-
const SYNTAX_COLOR_BOOLEAN = '#c678dd'
|
|
299
|
-
const SYNTAX_COLOR_NULL = '#808080'
|
|
362
|
+
export const SYNTAX_COLOR_NUMBER = '#e5c07b'
|
|
363
|
+
export const SYNTAX_COLOR_BOOLEAN = '#c678dd'
|
|
364
|
+
export const SYNTAX_COLOR_NULL = '#808080'
|
|
300
365
|
|
|
301
366
|
/**
|
|
302
367
|
* Syntax-highlight a pretty-printed JSON string for HTML display.
|