@skyhook-io/k8s-ui 1.5.0 → 1.5.2
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 +2 -2
- package/src/components/dock/LocalTerminalTab.tsx +29 -29
- package/src/components/dock/TerminalTab.tsx +29 -29
- package/src/components/logs/LogCore.tsx +310 -56
- package/src/components/logs/StructuredLogLine.tsx +98 -15
- package/src/components/logs/useLogSearch.ts +5 -0
- package/src/components/resources/ResourcesView.tsx +139 -139
- package/src/components/resources/renderers/NodeRenderer.tsx +45 -31
- package/src/components/resources/resource-utils-argo.ts +9 -2
- package/src/components/ui/HealthRing.tsx +2 -2
- package/src/components/ui/drawer-components.tsx +108 -6
- package/src/topology.css +6 -0
- package/src/utils/log-format.ts +77 -12
|
@@ -1,13 +1,15 @@
|
|
|
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
4
|
import {
|
|
5
5
|
getLevelColor,
|
|
6
|
-
highlightJson,
|
|
7
6
|
unescapeJsonStrings,
|
|
8
7
|
parseLogfmt,
|
|
9
8
|
SYNTAX_COLOR_KEY,
|
|
10
9
|
SYNTAX_COLOR_STRING,
|
|
10
|
+
SYNTAX_COLOR_NUMBER,
|
|
11
|
+
SYNTAX_COLOR_BOOLEAN,
|
|
12
|
+
SYNTAX_COLOR_NULL,
|
|
11
13
|
} from '../../utils/log-format'
|
|
12
14
|
import { SEVERITY_BADGE_BORDERED } from '../../utils/badge-colors'
|
|
13
15
|
|
|
@@ -17,9 +19,15 @@ interface StructuredLogLineProps {
|
|
|
17
19
|
wordWrap: boolean
|
|
18
20
|
isLogfmt?: boolean
|
|
19
21
|
defaultExpanded?: boolean
|
|
22
|
+
/**
|
|
23
|
+
* When provided, field values in the expanded view become filterable — hovering
|
|
24
|
+
* shows a chip that, on click, calls onFilterValue(value) so the parent can
|
|
25
|
+
* add the value to the log search/filter.
|
|
26
|
+
*/
|
|
27
|
+
onFilterValue?: (value: string) => void
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultExpanded }: StructuredLogLineProps) {
|
|
30
|
+
export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultExpanded, onFilterValue }: StructuredLogLineProps) {
|
|
23
31
|
// null = user hasn't toggled this line; defers to defaultExpanded (global toggle)
|
|
24
32
|
const [localExpanded, setLocalExpanded] = useState<boolean | null>(null)
|
|
25
33
|
const expanded = localExpanded ?? defaultExpanded ?? false
|
|
@@ -75,11 +83,12 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
75
83
|
</span>
|
|
76
84
|
<span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
|
|
77
85
|
{isLogfmt ? (
|
|
78
|
-
<ExpandedLogfmt obj={parsed} />
|
|
86
|
+
<ExpandedLogfmt obj={parsed} onFilterValue={onFilterValue} />
|
|
79
87
|
) : (
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
<JsonExpanded
|
|
89
|
+
text={unescapeJsonStrings(JSON.stringify(parsed, null, 2))}
|
|
90
|
+
onFilterValue={onFilterValue}
|
|
91
|
+
/>
|
|
83
92
|
)}
|
|
84
93
|
</span>
|
|
85
94
|
</>
|
|
@@ -88,6 +97,77 @@ export function StructuredLogLine({ content, level, wordWrap, isLogfmt, defaultE
|
|
|
88
97
|
)
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Render a primitive log-field value with an optional filter chip that appears on hover.
|
|
102
|
+
* Clicking the chip calls onFilter(value) so the caller can push it into log search.
|
|
103
|
+
*/
|
|
104
|
+
function FilterableValue({
|
|
105
|
+
value, onFilter, color,
|
|
106
|
+
}: { value: string; onFilter?: (v: string) => void; color?: string }) {
|
|
107
|
+
if (!onFilter) {
|
|
108
|
+
return <span style={color ? { color } : undefined}>{value}</span>
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
<span className="group/flt inline-flex items-baseline align-baseline gap-0.5 rounded hover:bg-theme-surface/60">
|
|
112
|
+
<span style={color ? { color } : undefined}>{value}</span>
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={(e) => { e.stopPropagation(); onFilter(value) }}
|
|
116
|
+
className="opacity-0 group-hover/flt:opacity-100 transition-opacity text-theme-text-tertiary hover:text-theme-text-primary px-0.5"
|
|
117
|
+
title={`Filter to lines containing "${value}"`}
|
|
118
|
+
aria-label={`Filter to lines containing ${value}`}
|
|
119
|
+
>
|
|
120
|
+
<Filter className="w-3 h-3 inline" />
|
|
121
|
+
</button>
|
|
122
|
+
</span>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Render pretty-printed JSON with filterable primitive values while preserving
|
|
128
|
+
* the layout that JSON.stringify(..., null, 2) produces. Tokenizes the string
|
|
129
|
+
* and emits React nodes so the hover chip can be wired per value.
|
|
130
|
+
*/
|
|
131
|
+
function JsonExpanded({ text, onFilterValue }: { text: string; onFilterValue?: (v: string) => void }) {
|
|
132
|
+
const tokenRe = /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|\b(true|false)\b|\b(null)\b/g
|
|
133
|
+
const nodes: React.ReactNode[] = []
|
|
134
|
+
let lastIndex = 0
|
|
135
|
+
let match: RegExpExecArray | null
|
|
136
|
+
let idx = 0
|
|
137
|
+
while ((match = tokenRe.exec(text)) !== null) {
|
|
138
|
+
if (match.index > lastIndex) {
|
|
139
|
+
nodes.push(<span key={`t${idx++}`}>{text.slice(lastIndex, match.index)}</span>)
|
|
140
|
+
}
|
|
141
|
+
const [, key, str, num, bool, nil] = match
|
|
142
|
+
if (key !== undefined) {
|
|
143
|
+
nodes.push(<span key={`k${idx++}`} style={{ color: SYNTAX_COLOR_KEY }}>{key}</span>)
|
|
144
|
+
nodes.push(<span key={`c${idx++}`}>:</span>)
|
|
145
|
+
} else if (str !== undefined) {
|
|
146
|
+
// Unescape the quoted string for the filter value (users expect to filter on
|
|
147
|
+
// the displayed string, not JSON-escaped bytes).
|
|
148
|
+
const inner = str.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\')
|
|
149
|
+
nodes.push(
|
|
150
|
+
<FilterableValue key={`s${idx++}`} value={inner} onFilter={onFilterValue} color={SYNTAX_COLOR_STRING} />
|
|
151
|
+
)
|
|
152
|
+
} else if (num !== undefined) {
|
|
153
|
+
nodes.push(
|
|
154
|
+
<FilterableValue key={`n${idx++}`} value={num} onFilter={onFilterValue} color={SYNTAX_COLOR_NUMBER} />
|
|
155
|
+
)
|
|
156
|
+
} else if (bool !== undefined) {
|
|
157
|
+
nodes.push(
|
|
158
|
+
<FilterableValue key={`b${idx++}`} value={bool} onFilter={onFilterValue} color={SYNTAX_COLOR_BOOLEAN} />
|
|
159
|
+
)
|
|
160
|
+
} else if (nil !== undefined) {
|
|
161
|
+
nodes.push(<span key={`z${idx++}`} style={{ color: SYNTAX_COLOR_NULL }}>{nil}</span>)
|
|
162
|
+
}
|
|
163
|
+
lastIndex = tokenRe.lastIndex
|
|
164
|
+
}
|
|
165
|
+
if (lastIndex < text.length) {
|
|
166
|
+
nodes.push(<span key={`t${idx++}`}>{text.slice(lastIndex)}</span>)
|
|
167
|
+
}
|
|
168
|
+
return <>{nodes}</>
|
|
169
|
+
}
|
|
170
|
+
|
|
91
171
|
function SummaryLine({ obj }: { obj: Record<string, unknown> }) {
|
|
92
172
|
const lvl = obj.level ?? obj.severity ?? obj.lvl ?? nestedField(obj, 'log', 'level')
|
|
93
173
|
const msg = obj.msg ?? obj.message
|
|
@@ -117,16 +197,19 @@ function SummaryLine({ obj }: { obj: Record<string, unknown> }) {
|
|
|
117
197
|
)
|
|
118
198
|
}
|
|
119
199
|
|
|
120
|
-
function ExpandedLogfmt({ obj }: { obj: Record<string, unknown
|
|
200
|
+
function ExpandedLogfmt({ obj, onFilterValue }: { obj: Record<string, unknown>; onFilterValue?: (v: string) => void }) {
|
|
121
201
|
return (
|
|
122
202
|
<>
|
|
123
|
-
{Object.entries(obj).map(([key, val]) =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
203
|
+
{Object.entries(obj).map(([key, val]) => {
|
|
204
|
+
const str = String(val)
|
|
205
|
+
return (
|
|
206
|
+
<div key={key}>
|
|
207
|
+
<span style={{ color: SYNTAX_COLOR_KEY }}>{key}</span>
|
|
208
|
+
<span className="text-theme-text-tertiary">=</span>
|
|
209
|
+
<FilterableValue value={str} onFilter={onFilterValue} color={SYNTAX_COLOR_STRING} />
|
|
210
|
+
</div>
|
|
211
|
+
)
|
|
212
|
+
})}
|
|
130
213
|
</>
|
|
131
214
|
)
|
|
132
215
|
}
|
|
@@ -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,
|