@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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/components/dock/TerminalTab.tsx +4 -2
  3. package/src/components/gitops/GitOpsTableView.tsx +22 -2
  4. package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
  5. package/src/components/issues/IssuesView.tsx +169 -17
  6. package/src/components/issues/index.ts +1 -1
  7. package/src/components/issues/issues.test.ts +4 -4
  8. package/src/components/issues/severity.ts +5 -0
  9. package/src/components/issues/types.ts +74 -0
  10. package/src/components/logs/LogCore.tsx +13 -2
  11. package/src/components/logs/LogToolbarSelects.tsx +6 -2
  12. package/src/components/logs/LogsViewer.tsx +66 -10
  13. package/src/components/logs/WorkloadLogsViewer.tsx +60 -8
  14. package/src/components/logs/useLogStream.ts +41 -3
  15. package/src/components/resources/ResourcesView.tsx +550 -52
  16. package/src/components/resources/column-filter-serialization.test.ts +26 -0
  17. package/src/components/resources/get-default-container-name.test.ts +31 -0
  18. package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
  19. package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
  20. package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
  21. package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
  22. package/src/components/resources/renderers/PodRenderer.tsx +2 -2
  23. package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
  24. package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
  25. package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
  26. package/src/components/resources/renderers/dra-cells.tsx +80 -0
  27. package/src/components/resources/renderers/index.ts +8 -0
  28. package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
  29. package/src/components/resources/resource-utils-dra.ts +90 -0
  30. package/src/components/resources/resource-utils-nvidia.ts +63 -0
  31. package/src/components/resources/resource-utils.ts +62 -4
  32. package/src/components/shared/CreateResourceDialog.tsx +32 -3
  33. package/src/components/shared/EditableYamlView.tsx +31 -2
  34. package/src/components/shared/ResourceActionsBar.tsx +5 -4
  35. package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
  36. package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
  37. package/src/components/ui/ConfirmDialog.tsx +1 -1
  38. package/src/components/ui/drawer-components.tsx +23 -1
  39. package/src/types/core.ts +1 -0
  40. package/src/utils/api-resources.ts +21 -0
  41. package/src/utils/custom-columns.test.ts +111 -0
  42. package/src/utils/custom-columns.ts +49 -0
  43. package/src/utils/extended-resources.test.ts +152 -0
  44. package/src/utils/extended-resources.ts +121 -0
  45. 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
- useEffect(() => { loadLogs() }, [loadLogs])
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={stopStreaming}
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
- useEffect(() => { loadLogs() }, [loadLogs])
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 error',
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={stopStreaming}
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', () => setIsStreaming(false))
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
- handleSSEError(event, errorContext, () => { setIsStreaming(false); es.close() })
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
  }