@skyhook-io/k8s-ui 1.7.10 → 1.7.12
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/dock/TerminalTab.tsx +4 -2
- package/src/components/gitops/GitOpsTableView.tsx +22 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
- package/src/components/issues/IssuesView.tsx +169 -17
- package/src/components/issues/index.ts +1 -1
- package/src/components/issues/issues.test.ts +4 -4
- package/src/components/issues/severity.ts +5 -0
- package/src/components/issues/types.ts +74 -0
- package/src/components/logs/LogCore.tsx +13 -2
- package/src/components/logs/LogToolbarSelects.tsx +6 -2
- package/src/components/logs/LogsViewer.tsx +66 -10
- package/src/components/logs/WorkloadLogsViewer.tsx +60 -8
- package/src/components/logs/useLogStream.ts +41 -3
- package/src/components/resources/ResourcesView.tsx +550 -52
- package/src/components/resources/column-filter-serialization.test.ts +26 -0
- package/src/components/resources/get-default-container-name.test.ts +31 -0
- package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
- package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
- package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
- package/src/components/resources/renderers/PodRenderer.tsx +2 -2
- package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
- package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
- package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
- package/src/components/resources/renderers/dra-cells.tsx +80 -0
- package/src/components/resources/renderers/index.ts +8 -0
- package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
- package/src/components/resources/resource-utils-dra.ts +90 -0
- package/src/components/resources/resource-utils-nvidia.ts +63 -0
- package/src/components/resources/resource-utils.ts +62 -4
- package/src/components/shared/CreateResourceDialog.tsx +32 -3
- package/src/components/shared/EditableYamlView.tsx +31 -2
- package/src/components/shared/ResourceActionsBar.tsx +5 -4
- package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
- package/src/components/ui/ConfirmDialog.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +23 -1
- package/src/types/core.ts +1 -0
- package/src/utils/api-resources.ts +21 -0
- package/src/utils/custom-columns.test.ts +111 -0
- package/src/utils/custom-columns.ts +49 -0
- package/src/utils/extended-resources.test.ts +152 -0
- package/src/utils/extended-resources.ts +121 -0
- package/src/utils/index.ts +1 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
2
|
import { parseLogLine, parseLogRange } from '../../utils/log-format'
|
|
3
3
|
import { triggerDownload } from '../../utils/download'
|
|
4
4
|
import { useLogBuffer } from './useLogBuffer'
|
|
@@ -30,6 +30,12 @@ export interface LogsViewerProps {
|
|
|
30
30
|
overrideDownload?: (content: string, mime: string, filename: string) => void
|
|
31
31
|
/** Force dark mode on the logs container (default: true) */
|
|
32
32
|
forceDark?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Open the stream automatically on mount (and on container switch) instead of
|
|
35
|
+
* loading a static snapshot. The user can still Stop, and a manual Stop is not
|
|
36
|
+
* re-armed. Requires `createStream`. Default: false.
|
|
37
|
+
*/
|
|
38
|
+
autoStream?: boolean
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
export function LogsViewer({
|
|
@@ -41,6 +47,7 @@ export function LogsViewer({
|
|
|
41
47
|
createStream,
|
|
42
48
|
overrideDownload,
|
|
43
49
|
forceDark,
|
|
50
|
+
autoStream = false,
|
|
44
51
|
}: LogsViewerProps) {
|
|
45
52
|
const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
|
|
46
53
|
const [isLoading, setIsLoading] = useState(false)
|
|
@@ -51,7 +58,14 @@ export function LogsViewer({
|
|
|
51
58
|
|
|
52
59
|
const { tailLines, sinceSeconds } = parseLogRange(logRange)
|
|
53
60
|
const { entries, append, set, clear } = useLogBuffer()
|
|
54
|
-
const { isStreaming, startStreaming, stopStreaming } = useLogStream()
|
|
61
|
+
const { isStreaming, streamError, connecting, startStreaming, stopStreaming } = useLogStream()
|
|
62
|
+
|
|
63
|
+
const willAutoStream = autoStream && !!createStream
|
|
64
|
+
// Tracks the container we've already auto-started for, so re-renders don't
|
|
65
|
+
// re-open the stream, and a container switch arms a fresh auto-start.
|
|
66
|
+
const autoStartedForRef = useRef<string | null>(null)
|
|
67
|
+
// Once the user explicitly Stops, don't auto-resume for this viewer's lifetime.
|
|
68
|
+
const userStoppedRef = useRef(false)
|
|
55
69
|
|
|
56
70
|
const loadLogs = useCallback(async () => {
|
|
57
71
|
if (!selectedContainer) return
|
|
@@ -72,11 +86,30 @@ export function LogsViewer({
|
|
|
72
86
|
}
|
|
73
87
|
}, [selectedContainer, tailLines, sinceSeconds, showPrevious, fetchLogs, set])
|
|
74
88
|
|
|
75
|
-
|
|
89
|
+
// When auto-streaming the stream supplies the initial tail, so the static
|
|
90
|
+
// snapshot fetch is skipped to avoid a redundant request and a flash of
|
|
91
|
+
// snapshot content before the stream takes over. If the user has Stopped we
|
|
92
|
+
// won't auto-start, so fall back to the snapshot — otherwise a container
|
|
93
|
+
// switch would keep showing the previous container's lines.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!willAutoStream || userStoppedRef.current) loadLogs()
|
|
96
|
+
}, [loadLogs, willAutoStream])
|
|
76
97
|
useEffect(() => { stopStreaming() }, [selectedContainer, stopStreaming])
|
|
77
98
|
|
|
99
|
+
// If auto-stream turns off while a stream is open (e.g. the pod went
|
|
100
|
+
// terminal), stop following so live appends don't race the snapshot.
|
|
101
|
+
const prevWillAutoStreamRef = useRef(willAutoStream)
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (prevWillAutoStreamRef.current && !willAutoStream && isStreaming) stopStreaming()
|
|
104
|
+
prevWillAutoStreamRef.current = willAutoStream
|
|
105
|
+
}, [willAutoStream, isStreaming, stopStreaming])
|
|
106
|
+
|
|
78
107
|
const handleStartStreaming = useCallback(() => {
|
|
79
108
|
if (!createStream) return
|
|
109
|
+
// The stream replays the last N lines (TailLines + Follow); clear first so
|
|
110
|
+
// they don't duplicate lines already in the buffer (the snapshot on the
|
|
111
|
+
// manual path, or an earlier stream on restart).
|
|
112
|
+
clear()
|
|
80
113
|
startStreaming(
|
|
81
114
|
() => createStream({ container: selectedContainer, tailLines: 100, sinceSeconds }),
|
|
82
115
|
{
|
|
@@ -86,8 +119,26 @@ export function LogsViewer({
|
|
|
86
119
|
container: data.container || selectedContainer,
|
|
87
120
|
}),
|
|
88
121
|
},
|
|
122
|
+
'Log stream connection failed',
|
|
89
123
|
)
|
|
90
|
-
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append])
|
|
124
|
+
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append, clear])
|
|
125
|
+
|
|
126
|
+
const handleStopStreaming = useCallback(() => {
|
|
127
|
+
userStoppedRef.current = true
|
|
128
|
+
stopStreaming()
|
|
129
|
+
}, [stopStreaming])
|
|
130
|
+
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (!willAutoStream || !selectedContainer) return
|
|
133
|
+
if (userStoppedRef.current) return
|
|
134
|
+
if (autoStartedForRef.current === selectedContainer) return
|
|
135
|
+
autoStartedForRef.current = selectedContainer
|
|
136
|
+
handleStartStreaming()
|
|
137
|
+
// Reset the arm latch on teardown so a re-run re-streams — without this,
|
|
138
|
+
// React Strict Mode's mount→unmount→mount closes the stream but the latch
|
|
139
|
+
// stays set, leaving the viewer static.
|
|
140
|
+
return () => { autoStartedForRef.current = null }
|
|
141
|
+
}, [willAutoStream, selectedContainer, handleStartStreaming])
|
|
91
142
|
|
|
92
143
|
const downloadLogs = useCallback((format: DownloadFormat) => {
|
|
93
144
|
let content: string
|
|
@@ -122,30 +173,35 @@ export function LogsViewer({
|
|
|
122
173
|
<>
|
|
123
174
|
<ContainerSelect containers={containers} value={selectedContainer} onChange={setSelectedContainer} isDark={isDark} />
|
|
124
175
|
|
|
125
|
-
<Tooltip content="Show logs from the pod's previous instance (if it was restarted). Useful for troubleshooting crashed containers." position="bottom">
|
|
126
|
-
<label className={`flex items-center gap-1.5 text-xs ${palette.textSecondary}`}>
|
|
176
|
+
<Tooltip content={isStreaming ? 'Stop streaming to view the previous instance' : "Show logs from the pod's previous instance (if it was restarted). Useful for troubleshooting crashed containers."} position="bottom">
|
|
177
|
+
<label className={`flex items-center gap-1.5 text-xs ${palette.textSecondary} ${isStreaming ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
|
127
178
|
<input
|
|
128
179
|
type="checkbox"
|
|
129
180
|
checked={showPrevious}
|
|
130
181
|
onChange={(e) => setShowPrevious(e.target.checked)}
|
|
182
|
+
disabled={isStreaming}
|
|
131
183
|
className={`w-3 h-3 rounded ${palette.borderLight} ${palette.elevatedBg} text-blue-500 focus:ring-blue-500 focus:ring-offset-0`}
|
|
132
184
|
/>
|
|
133
185
|
<span className={`border-b border-dotted ${isDark ? 'border-slate-500' : 'border-slate-400'}`}>Previous</span>
|
|
134
186
|
</label>
|
|
135
187
|
</Tooltip>
|
|
136
188
|
|
|
137
|
-
<LogRangeSelect value={logRange} onChange={setLogRange} isDark={isDark} />
|
|
189
|
+
<LogRangeSelect value={logRange} onChange={setLogRange} isDark={isDark} disabled={isStreaming} />
|
|
138
190
|
</>
|
|
139
191
|
)
|
|
140
192
|
|
|
193
|
+
// While the auto-stream is opening (before it first settles), show the
|
|
194
|
+
// loading state rather than the empty-logs placeholder.
|
|
195
|
+
const isConnecting = willAutoStream && connecting && entries.length === 0
|
|
196
|
+
|
|
141
197
|
return (
|
|
142
198
|
<LogCore
|
|
143
199
|
entries={entries}
|
|
144
|
-
isLoading={isLoading}
|
|
145
|
-
errorMessage={fetchError}
|
|
200
|
+
isLoading={isLoading || isConnecting}
|
|
201
|
+
errorMessage={fetchError || (entries.length === 0 ? streamError : null)}
|
|
146
202
|
isStreaming={isStreaming}
|
|
147
203
|
onStartStream={createStream ? handleStartStreaming : undefined}
|
|
148
|
-
onStopStream={
|
|
204
|
+
onStopStream={handleStopStreaming}
|
|
149
205
|
onRefresh={loadLogs}
|
|
150
206
|
onDownload={downloadLogs}
|
|
151
207
|
onClear={clear}
|
|
@@ -47,9 +47,15 @@ export interface WorkloadLogsViewerProps {
|
|
|
47
47
|
overrideDownload?: (content: string, mime: string, filename: string) => void
|
|
48
48
|
/** Force dark mode on the logs container (default: true) */
|
|
49
49
|
forceDark?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Open the stream automatically on mount (and on container switch) instead of
|
|
52
|
+
* loading a static snapshot. The user can still Stop, and a manual Stop is not
|
|
53
|
+
* re-armed. Requires `createStream`. Default: false.
|
|
54
|
+
*/
|
|
55
|
+
autoStream?: boolean
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark }: WorkloadLogsViewerProps) {
|
|
58
|
+
export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownload, forceDark, autoStream = false }: WorkloadLogsViewerProps) {
|
|
53
59
|
const [selectedContainer, setSelectedContainer] = useState<string>('')
|
|
54
60
|
const [pods, setPods] = useState<WorkloadPodInfo[]>([])
|
|
55
61
|
const [selectedPods, setSelectedPods] = useState<Set<string>>(new Set())
|
|
@@ -61,7 +67,12 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
61
67
|
|
|
62
68
|
const { tailLines, sinceSeconds } = parseLogRange(logRange)
|
|
63
69
|
const { entries, append, set, clear } = useLogBuffer()
|
|
64
|
-
const { isStreaming, startStreaming, stopStreaming } = useLogStream()
|
|
70
|
+
const { isStreaming, streamError, connecting, startStreaming, stopStreaming } = useLogStream()
|
|
71
|
+
|
|
72
|
+
const willAutoStream = autoStream && !!createStream
|
|
73
|
+
// null sentinel so the initial selectedContainer ('' = all) still arms once.
|
|
74
|
+
const autoStartedForRef = useRef<string | null>(null)
|
|
75
|
+
const userStoppedRef = useRef(false)
|
|
65
76
|
|
|
66
77
|
// Map pod.name → index. Color classes are resolved at render time from the
|
|
67
78
|
// current palette (see LogCore / pod-filter dropdown below) so toggling
|
|
@@ -105,11 +116,30 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
105
116
|
}
|
|
106
117
|
}, [fetchAll, selectedContainer, tailLines, sinceSeconds, set])
|
|
107
118
|
|
|
108
|
-
|
|
119
|
+
// When auto-streaming the stream supplies the initial tail, so the static
|
|
120
|
+
// snapshot fetch is skipped to avoid a redundant request and a flash of
|
|
121
|
+
// snapshot content before the stream takes over. If the user has Stopped we
|
|
122
|
+
// won't auto-start, so fall back to the snapshot — otherwise a container
|
|
123
|
+
// switch would keep showing the previous selection's lines.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!willAutoStream || userStoppedRef.current) loadLogs()
|
|
126
|
+
}, [loadLogs, willAutoStream])
|
|
109
127
|
useEffect(() => { stopStreaming() }, [selectedContainer, stopStreaming])
|
|
110
128
|
|
|
129
|
+
// If auto-stream turns off while a stream is open, stop following so live
|
|
130
|
+
// appends don't race the snapshot.
|
|
131
|
+
const prevWillAutoStreamRef = useRef(willAutoStream)
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (prevWillAutoStreamRef.current && !willAutoStream && isStreaming) stopStreaming()
|
|
134
|
+
prevWillAutoStreamRef.current = willAutoStream
|
|
135
|
+
}, [willAutoStream, isStreaming, stopStreaming])
|
|
136
|
+
|
|
111
137
|
const handleStartStreaming = useCallback(() => {
|
|
112
138
|
if (!createStream) return
|
|
139
|
+
// The stream replays the last N lines per pod (TailLines + Follow); clear
|
|
140
|
+
// first so they don't duplicate lines already in the buffer (the snapshot on
|
|
141
|
+
// the manual path, or an earlier stream on restart).
|
|
142
|
+
clear()
|
|
113
143
|
startStreaming(
|
|
114
144
|
() => createStream({ container: selectedContainer || undefined, tailLines: 50, sinceSeconds }),
|
|
115
145
|
{
|
|
@@ -156,9 +186,26 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
156
186
|
}
|
|
157
187
|
},
|
|
158
188
|
},
|
|
159
|
-
'Workload log stream
|
|
189
|
+
'Workload log stream connection failed',
|
|
160
190
|
)
|
|
161
|
-
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append, podColorIndex, selectedPods.size])
|
|
191
|
+
}, [createStream, startStreaming, selectedContainer, sinceSeconds, append, podColorIndex, selectedPods.size, clear])
|
|
192
|
+
|
|
193
|
+
const handleStopStreaming = useCallback(() => {
|
|
194
|
+
userStoppedRef.current = true
|
|
195
|
+
stopStreaming()
|
|
196
|
+
}, [stopStreaming])
|
|
197
|
+
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!willAutoStream) return
|
|
200
|
+
if (userStoppedRef.current) return
|
|
201
|
+
if (autoStartedForRef.current === selectedContainer) return
|
|
202
|
+
autoStartedForRef.current = selectedContainer
|
|
203
|
+
handleStartStreaming()
|
|
204
|
+
// Reset the arm latch on teardown so a re-run re-streams — without this,
|
|
205
|
+
// React Strict Mode's mount→unmount→mount closes the stream but the latch
|
|
206
|
+
// stays set, leaving the viewer static.
|
|
207
|
+
return () => { autoStartedForRef.current = null }
|
|
208
|
+
}, [willAutoStream, selectedContainer, handleStartStreaming])
|
|
162
209
|
|
|
163
210
|
const allContainers = useMemo(() => {
|
|
164
211
|
const s = new Set<string>()
|
|
@@ -280,24 +327,29 @@ export function WorkloadLogsViewer({ name, fetchAll, createStream, overrideDownl
|
|
|
280
327
|
lineOptions={[50, 100, 500, 1000]}
|
|
281
328
|
tooltip="How many logs to load per pod — by line count or time range"
|
|
282
329
|
isDark={isDark}
|
|
330
|
+
disabled={isStreaming}
|
|
283
331
|
/>
|
|
284
332
|
</>
|
|
285
333
|
)
|
|
286
334
|
|
|
335
|
+
// While the auto-stream is opening (before it first settles), show the
|
|
336
|
+
// loading state rather than the empty-logs placeholder.
|
|
337
|
+
const isConnecting = willAutoStream && connecting && entries.length === 0
|
|
338
|
+
|
|
287
339
|
return (
|
|
288
340
|
<LogCore
|
|
289
341
|
entries={filteredEntries}
|
|
290
|
-
isLoading={isLoading}
|
|
342
|
+
isLoading={isLoading || isConnecting}
|
|
291
343
|
isStreaming={isStreaming}
|
|
292
344
|
onStartStream={createStream ? handleStartStreaming : undefined}
|
|
293
|
-
onStopStream={
|
|
345
|
+
onStopStream={handleStopStreaming}
|
|
294
346
|
onRefresh={loadLogs}
|
|
295
347
|
onDownload={downloadLogs}
|
|
296
348
|
onClear={clear}
|
|
297
349
|
toolbarExtra={renderToolbarExtra}
|
|
298
350
|
showPodName
|
|
299
351
|
emptyMessage={pods.length === 0 ? 'No pods found' : 'No logs available'}
|
|
300
|
-
errorMessage={fetchError}
|
|
352
|
+
errorMessage={fetchError || (entries.length === 0 ? streamError : null)}
|
|
301
353
|
forceDark={forceDark}
|
|
302
354
|
/>
|
|
303
355
|
)
|
|
@@ -18,12 +18,24 @@ export interface LogStreamHandlers {
|
|
|
18
18
|
*/
|
|
19
19
|
export function useLogStream() {
|
|
20
20
|
const [isStreaming, setIsStreaming] = useState(false)
|
|
21
|
+
// Set when the connection fails (and not on a clean end — see endedRef).
|
|
22
|
+
const [streamError, setStreamError] = useState<string | null>(null)
|
|
23
|
+
// True from a start attempt until the stream first settles (connected / end /
|
|
24
|
+
// error / stop). Lets callers show a connecting spinner that won't reappear
|
|
25
|
+
// after a clean end. Starts true so an auto-stream viewer paints the spinner
|
|
26
|
+
// immediately instead of flashing the empty state.
|
|
27
|
+
const [connecting, setConnecting] = useState(true)
|
|
21
28
|
const eventSourceRef = useRef<EventSource | null>(null)
|
|
29
|
+
// EventSource fires a generic 'error' on the normal close that follows the
|
|
30
|
+
// server's 'end'; this distinguishes a clean end from a real failure.
|
|
31
|
+
const endedRef = useRef(false)
|
|
22
32
|
|
|
23
33
|
const stopStreaming = useCallback(() => {
|
|
24
34
|
eventSourceRef.current?.close()
|
|
25
35
|
eventSourceRef.current = null
|
|
26
36
|
setIsStreaming(false)
|
|
37
|
+
setConnecting(false)
|
|
38
|
+
setStreamError(null)
|
|
27
39
|
}, [])
|
|
28
40
|
|
|
29
41
|
const startStreaming = useCallback((
|
|
@@ -32,10 +44,19 @@ export function useLogStream() {
|
|
|
32
44
|
errorContext = 'Log stream error',
|
|
33
45
|
) => {
|
|
34
46
|
eventSourceRef.current?.close()
|
|
47
|
+
endedRef.current = false
|
|
48
|
+
setStreamError(null)
|
|
49
|
+
setConnecting(true)
|
|
35
50
|
const es = create()
|
|
51
|
+
// Ignore events from a superseded source: closing/replacing an EventSource
|
|
52
|
+
// (Stop, container switch, restart) can fire a late, async 'error' that
|
|
53
|
+
// would otherwise corrupt the new stream's state or show a false failure.
|
|
54
|
+
const isCurrent = () => eventSourceRef.current === es
|
|
36
55
|
|
|
37
56
|
es.addEventListener('connected', (event) => {
|
|
57
|
+
if (!isCurrent()) return
|
|
38
58
|
setIsStreaming(true)
|
|
59
|
+
setConnecting(false)
|
|
39
60
|
if (handlers.onConnected) {
|
|
40
61
|
try { handlers.onConnected(JSON.parse((event as MessageEvent).data)) } catch (e) {
|
|
41
62
|
console.error('Failed to parse connected event:', e)
|
|
@@ -44,12 +65,15 @@ export function useLogStream() {
|
|
|
44
65
|
})
|
|
45
66
|
|
|
46
67
|
es.addEventListener('log', (event) => {
|
|
68
|
+
if (!isCurrent()) return
|
|
69
|
+
setConnecting(false)
|
|
47
70
|
try { handlers.onLog(JSON.parse((event as MessageEvent).data)) } catch (e) {
|
|
48
71
|
console.error('Failed to parse log event:', e)
|
|
49
72
|
}
|
|
50
73
|
})
|
|
51
74
|
|
|
52
75
|
es.addEventListener('pod_added', (event) => {
|
|
76
|
+
if (!isCurrent()) return
|
|
53
77
|
if (handlers.onPodAdded) {
|
|
54
78
|
try { handlers.onPodAdded(JSON.parse((event as MessageEvent).data)) } catch (e) {
|
|
55
79
|
console.error('Failed to parse pod_added event:', e)
|
|
@@ -58,6 +82,7 @@ export function useLogStream() {
|
|
|
58
82
|
})
|
|
59
83
|
|
|
60
84
|
es.addEventListener('pod_removed', (event) => {
|
|
85
|
+
if (!isCurrent()) return
|
|
61
86
|
if (handlers.onPodRemoved) {
|
|
62
87
|
try { handlers.onPodRemoved(JSON.parse((event as MessageEvent).data)) } catch (e) {
|
|
63
88
|
console.error('Failed to parse pod_removed event:', e)
|
|
@@ -65,10 +90,23 @@ export function useLogStream() {
|
|
|
65
90
|
}
|
|
66
91
|
})
|
|
67
92
|
|
|
68
|
-
es.addEventListener('end', () =>
|
|
93
|
+
es.addEventListener('end', () => {
|
|
94
|
+
if (!isCurrent()) return
|
|
95
|
+
endedRef.current = true
|
|
96
|
+
setIsStreaming(false)
|
|
97
|
+
setConnecting(false)
|
|
98
|
+
})
|
|
69
99
|
|
|
70
100
|
es.addEventListener('error', (event) => {
|
|
71
|
-
|
|
101
|
+
if (!isCurrent()) { es.close(); return }
|
|
102
|
+
setIsStreaming(false)
|
|
103
|
+
setConnecting(false)
|
|
104
|
+
es.close()
|
|
105
|
+
// The browser fires 'error' on the normal close that follows a clean
|
|
106
|
+
// 'end'; that's not a failure, so don't log it or surface it.
|
|
107
|
+
if (endedRef.current) return
|
|
108
|
+
handleSSEError(event, errorContext, () => {})
|
|
109
|
+
setStreamError(errorContext)
|
|
72
110
|
})
|
|
73
111
|
|
|
74
112
|
eventSourceRef.current = es
|
|
@@ -77,5 +115,5 @@ export function useLogStream() {
|
|
|
77
115
|
// Cleanup on unmount
|
|
78
116
|
useEffect(() => () => { eventSourceRef.current?.close() }, [])
|
|
79
117
|
|
|
80
|
-
return { isStreaming, startStreaming, stopStreaming }
|
|
118
|
+
return { isStreaming, streamError, connecting, startStreaming, stopStreaming }
|
|
81
119
|
}
|