@skyhook-io/radar-app 1.14.4 → 1.14.6

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 (98) hide show
  1. package/README.md +2 -9
  2. package/package.json +6 -6
  3. package/src/App.tsx +91 -328
  4. package/src/RadarApp.tsx +3 -8
  5. package/src/api/client.jobset.test.ts +18 -0
  6. package/src/api/client.resource-identity.test.ts +44 -0
  7. package/src/api/client.rightsizing.test.ts +7 -3
  8. package/src/api/client.ts +233 -78
  9. package/src/api/preferences.test.ts +54 -0
  10. package/src/api/preferences.ts +38 -0
  11. package/src/components/CloudFunnelButton.test.tsx +155 -0
  12. package/src/components/CloudFunnelButton.tsx +14 -8
  13. package/src/components/ContextSwitcher.tsx +13 -132
  14. package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
  15. package/src/components/applications/ApplicationsView.tsx +5 -3
  16. package/src/components/audit/AuditSettingsDialog.tsx +15 -11
  17. package/src/components/diagnose/AIMarkdown.tsx +8 -3
  18. package/src/components/diagnose/AgentControls.tsx +26 -3
  19. package/src/components/diagnose/AssessmentCard.tsx +249 -237
  20. package/src/components/diagnose/DiagnoseContext.tsx +1 -0
  21. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
  22. package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
  23. package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
  24. package/src/components/diagnose/InvestigationView.tsx +4 -1
  25. package/src/components/diagnose/agentCatalog.ts +6 -0
  26. package/src/components/diagnose/investigationCase.test.tsx +56 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
  28. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
  29. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
  30. package/src/components/diagnose/investigationEvidence/observations.ts +8 -5
  31. package/src/components/diagnose/investigationEvidence/types.ts +1 -1
  32. package/src/components/diagnose/launch.test.ts +23 -0
  33. package/src/components/diagnose/launch.ts +4 -0
  34. package/src/components/diagnose/parts.test.tsx +159 -2
  35. package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
  36. package/src/components/dock/NodeTerminalTab.tsx +5 -3
  37. package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
  38. package/src/components/dock/WorkloadLogsTab.tsx +2 -1
  39. package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
  40. package/src/components/execution/BatchExecutionView.test.ts +52 -1
  41. package/src/components/execution/BatchExecutionView.tsx +324 -65
  42. package/src/components/execution/JobSetAdmission.test.tsx +42 -0
  43. package/src/components/execution/JobSetAdmission.tsx +17 -0
  44. package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
  45. package/src/components/execution/JobSetMemberComparison.tsx +88 -0
  46. package/src/components/execution/batch-run-actions.test.ts +1 -0
  47. package/src/components/execution/batch-timeline.test.ts +3 -0
  48. package/src/components/execution/execution-definition.test.ts +15 -1
  49. package/src/components/execution/execution-definition.ts +4 -0
  50. package/src/components/execution/member-collection.render.test.tsx +142 -0
  51. package/src/components/gitops/GitOpsView.tsx +33 -2
  52. package/src/components/gitops/destination-toast.test.ts +33 -0
  53. package/src/components/gitops/destination-toast.ts +46 -0
  54. package/src/components/gitops/useDestinationCluster.ts +30 -0
  55. package/src/components/helm/TrackChartSourceDialog.tsx +20 -11
  56. package/src/components/home/MCPSetupDialog.tsx +10 -0
  57. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
  58. package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
  59. package/src/components/nav/PrimaryNavRail.tsx +2 -5
  60. package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
  61. package/src/components/resource/PVCUsageBar.tsx +62 -16
  62. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
  63. package/src/components/resources/ResourcesView.tsx +3 -3
  64. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
  65. package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
  66. package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
  67. package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
  68. package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
  69. package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
  70. package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
  71. package/src/components/rightsizing/RightsizingScanView.tsx +207 -83
  72. package/src/components/rightsizing/copy.test.ts +7 -0
  73. package/src/components/rightsizing/empty-state.test.tsx +67 -0
  74. package/src/components/rightsizing/model.test.ts +120 -2
  75. package/src/components/rightsizing/model.ts +36 -13
  76. package/src/components/rightsizing/notices.test.tsx +176 -0
  77. package/src/components/settings/OperatorManagedNotice.tsx +17 -0
  78. package/src/components/settings/SettingsDialog.tsx +157 -42
  79. package/src/components/settings/settings-state.test.ts +18 -0
  80. package/src/components/settings/settings-state.ts +18 -0
  81. package/src/components/timeline/TimelineView.tsx +9 -3
  82. package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
  83. package/src/components/ui/Markdown.tsx +33 -6
  84. package/src/components/ui/command-items.ts +0 -3
  85. package/src/components/useContextSwitchFlow.tsx +147 -0
  86. package/src/components/workload/WorkloadView.test.ts +17 -1
  87. package/src/components/workload/WorkloadView.tsx +36 -18
  88. package/src/context/ContextSwitchContext.tsx +18 -2
  89. package/src/context/NavCustomization.tsx +4 -58
  90. package/src/context/ThemeContext.tsx +3 -11
  91. package/src/hooks/useClusterLoadState.ts +2 -2
  92. package/src/hooks/useFavorites.ts +3 -5
  93. package/src/utils/auditBadges.ts +1 -1
  94. package/src/utils/navigation.test.ts +18 -1
  95. package/src/utils/navigation.ts +12 -5
  96. package/src/utils/topology-selection.test.ts +56 -0
  97. package/src/utils/topology-selection.ts +9 -9
  98. package/src/components/ui/CommandPalette.tsx +0 -261
@@ -37,4 +37,60 @@ describe('findSelectedTopologyNode', () => {
37
37
 
38
38
  expect(findSelectedTopologyNode(topology, selected('two.example', 'widgets'))?.id).toBe('second')
39
39
  })
40
+
41
+ it('matches a pseudo topology kind through its real Kubernetes kind', () => {
42
+ const knative = node('knative', 'KnativeService', 'api', 'serving.knative.dev/v1')
43
+ knative.data.resourceKind = 'Service'
44
+
45
+ expect(findSelectedTopologyNode(
46
+ { nodes: [knative], edges: [] },
47
+ { kind: 'services', namespace: 'default', name: 'api', group: 'serving.knative.dev' },
48
+ )?.id).toBe('knative')
49
+ })
50
+
51
+ it('keeps an omitted or explicit core Service selection on the core node', () => {
52
+ const core = node('core', 'Service', 'api')
53
+ const knative = node('knative', 'KnativeService', 'api', 'serving.knative.dev/v1')
54
+ knative.data.resourceKind = 'Service'
55
+ const topology: Topology = { nodes: [knative, core], edges: [] }
56
+
57
+ expect(findSelectedTopologyNode(topology, {
58
+ kind: 'services', namespace: 'default', name: 'api',
59
+ })?.id).toBe('core')
60
+ expect(findSelectedTopologyNode(topology, {
61
+ kind: 'services', namespace: 'default', name: 'api', group: '',
62
+ })?.id).toBe('core')
63
+ })
64
+
65
+ it('infers a typed Job group when its topology node omits apiVersion', () => {
66
+ const core = node('core', 'Job', 'train')
67
+ const volcano = node('volcano', 'Job', 'train', 'batch.volcano.sh/v1alpha1')
68
+ const topology: Topology = { nodes: [volcano, core], edges: [] }
69
+
70
+ expect(findSelectedTopologyNode(topology, {
71
+ kind: 'jobs', namespace: 'default', name: 'train', group: 'batch',
72
+ })?.id).toBe('core')
73
+ expect(findSelectedTopologyNode(topology, {
74
+ kind: 'jobs', namespace: 'default', name: 'train', group: 'batch.volcano.sh',
75
+ })?.id).toBe('volcano')
76
+ })
77
+
78
+ it('infers the policy group for a typed PodDisruptionBudget node', () => {
79
+ const pdb = node('pdb', 'PodDisruptionBudget', 'budget')
80
+
81
+ expect(findSelectedTopologyNode(
82
+ { nodes: [pdb], edges: [] },
83
+ { kind: 'poddisruptionbudgets', namespace: 'default', name: 'budget', group: 'policy' },
84
+ )?.id).toBe('pdb')
85
+ })
86
+
87
+ it('fails closed for an omitted unknown group when custom resources collide', () => {
88
+ const one = node('one', 'Widget', 'same', 'one.example/v1')
89
+ const two = node('two', 'Widget', 'same', 'two.example/v1')
90
+
91
+ expect(findSelectedTopologyNode(
92
+ { nodes: [one, two], edges: [] },
93
+ { kind: 'widgets', namespace: 'default', name: 'same' },
94
+ )).toBeUndefined()
95
+ })
40
96
  })
@@ -1,16 +1,16 @@
1
1
  import type { SelectedResource, Topology, TopologyNode } from '@skyhook-io/k8s-ui/types/core'
2
+ import { builtinGroupForKind, canonicalResourceGroup } from '@skyhook-io/k8s-ui/utils/api-resources'
2
3
  import { apiVersionToGroup, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
4
+ import { topologyNodeResourceKind } from '@skyhook-io/k8s-ui/utils/topology-neighborhood'
3
5
 
4
6
  function topologyNodeGroup(node: TopologyNode): string | undefined {
5
- const apiVersionGroup = apiVersionToGroup(node.data.apiVersion as string | undefined)
6
- if (apiVersionGroup) return apiVersionGroup
7
+ const apiVersion = node.data.apiVersion
8
+ if (typeof apiVersion === 'string' && apiVersion) return apiVersionToGroup(apiVersion)
7
9
 
8
10
  const sourceGroup = node.data.sourceGroup
9
- if (typeof sourceGroup === 'string' && sourceGroup) return sourceGroup
11
+ if (typeof sourceGroup === 'string') return sourceGroup
10
12
 
11
- // Native NetworkPolicy nodes predate apiVersion in the topology payload.
12
- if (node.kind === 'NetworkPolicy') return 'networking.k8s.io'
13
- return undefined
13
+ return builtinGroupForKind(topologyNodeResourceKind(node))
14
14
  }
15
15
 
16
16
  export function findSelectedTopologyNode(
@@ -22,13 +22,13 @@ export function findSelectedTopologyNode(
22
22
  const candidates = topology?.nodes.filter(node =>
23
23
  ((node.data.namespace as string) || '') === namespace &&
24
24
  node.name === selectedResource.name &&
25
- (kindToPluralWithGroup(node.kind, topologyNodeGroup(node) ?? '') === selectedKind || node.kind === selectedResource.kind),
25
+ (kindToPluralWithGroup(topologyNodeResourceKind(node), topologyNodeGroup(node) ?? '') === selectedKind || topologyNodeResourceKind(node) === selectedResource.kind),
26
26
  ) ?? []
27
27
 
28
28
  if (candidates.length === 0) return undefined
29
29
 
30
- const selectedGroup = selectedResource.group || undefined
31
- if (!selectedGroup) return candidates[0]
30
+ const selectedGroup = canonicalResourceGroup(selectedResource.kind, selectedResource.group)
31
+ if (selectedGroup === undefined) return candidates.length === 1 ? candidates[0] : undefined
32
32
 
33
33
  const groupMatch = candidates.find(node => topologyNodeGroup(node) === selectedGroup)
34
34
  if (groupMatch) return groupMatch
@@ -1,261 +0,0 @@
1
- import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
2
- import { TRANSITION_BACKDROP, TRANSITION_PANEL, overlayTransitionStyle } from '../../utils/animation'
3
- import { Search, X, ChevronRight } from 'lucide-react'
4
- import { clsx } from 'clsx'
5
- import { useCommandItems, bestScore, type CommandItem, type CommandItemCallbacks } from './command-items'
6
- import { Input } from '@skyhook-io/k8s-ui'
7
-
8
- interface CommandPaletteProps extends CommandItemCallbacks {
9
- onClose: () => void
10
- /** Controls fade-in/out animation (driven by useAnimatedUnmount) */
11
- isOpen?: boolean
12
- }
13
-
14
-
15
- export function CommandPalette({ onClose, isOpen = true, ...callbacks }: CommandPaletteProps) {
16
- const [query, setQuery] = useState('')
17
- const [selectedIndex, setSelectedIndex] = useState(0)
18
- const inputRef = useRef<HTMLInputElement>(null)
19
- const resultsRef = useRef<HTMLDivElement>(null)
20
- const isKeyboardNav = useRef(false)
21
-
22
- // The static command items (Views / Kinds / Namespaces / Actions) — shared
23
- // with the standalone omnibar via useCommandItems so the two never drift.
24
- const items = useCommandItems(callbacks)
25
-
26
- // Focus the input once logically open. Not on mount: the presence hook
27
- // mounts the palette two frames before `isOpen` flips, and the root is
28
- // `inert` until then, so a mount-time focus would be refused.
29
- useEffect(() => {
30
- if (isOpen) inputRef.current?.focus()
31
- }, [isOpen])
32
-
33
- // Close on Escape (capture phase to beat the shortcut system) — only while
34
- // logically open: the palette stays mounted through its exit.
35
- useEffect(() => {
36
- if (!isOpen) return
37
- const handler = (e: KeyboardEvent) => {
38
- if (e.key === 'Escape') {
39
- e.preventDefault()
40
- e.stopPropagation()
41
- onClose()
42
- }
43
- }
44
- document.addEventListener('keydown', handler, true)
45
- return () => document.removeEventListener('keydown', handler, true)
46
- }, [isOpen, onClose])
47
-
48
- // Filter and rank results
49
- const filteredItems = useMemo(() => {
50
- if (!query.trim()) {
51
- // Show views and actions when no query
52
- return items.filter(i => i.category === 'Views' || i.category === 'Actions')
53
- }
54
-
55
- return items
56
- .map(item => {
57
- let score = bestScore(item, query)
58
- // Boost core K8s resources over CRDs at the same match quality
59
- if (score > 0 && item.category === 'Resource Kinds' && item.sublabel === 'core') {
60
- score += 10
61
- }
62
- return { item, score }
63
- })
64
- .filter(({ score }) => score > 0)
65
- .sort((a, b) => b.score - a.score)
66
- .slice(0, 20)
67
- .map(({ item }) => item)
68
- }, [items, query])
69
-
70
- // Group filtered items by category
71
- const grouped = useMemo(() => {
72
- const groups = new Map<string, CommandItem[]>()
73
- for (const item of filteredItems) {
74
- const list = groups.get(item.category) || []
75
- list.push(item)
76
- groups.set(item.category, list)
77
- }
78
- return groups
79
- }, [filteredItems])
80
-
81
- // Single source of truth for flat ordering — derived from grouped to match render order
82
- const flatItems = useMemo(() => {
83
- const result: CommandItem[] = []
84
- for (const [, categoryItems] of grouped) {
85
- result.push(...categoryItems)
86
- }
87
- return result
88
- }, [grouped])
89
-
90
- // Reset selection when results change
91
- useEffect(() => {
92
- setSelectedIndex(0)
93
- }, [flatItems.length, query])
94
-
95
- // Scroll selected into view
96
- useEffect(() => {
97
- if (resultsRef.current && selectedIndex >= 0) {
98
- const el = resultsRef.current.querySelector('[data-selected="true"]') as HTMLElement
99
- el?.scrollIntoView({ block: 'nearest' })
100
- }
101
- }, [selectedIndex])
102
-
103
- const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
104
- switch (e.key) {
105
- case 'ArrowDown':
106
- e.preventDefault()
107
- isKeyboardNav.current = true
108
- setSelectedIndex(i => Math.min(i + 1, flatItems.length - 1))
109
- break
110
- case 'ArrowUp':
111
- e.preventDefault()
112
- isKeyboardNav.current = true
113
- setSelectedIndex(i => Math.max(i - 1, 0))
114
- break
115
- case 'Enter':
116
- e.preventDefault()
117
- if (flatItems[selectedIndex]) {
118
- flatItems[selectedIndex].action()
119
- onClose()
120
- }
121
- break
122
- }
123
- }, [flatItems, selectedIndex, onClose])
124
-
125
- // Flatten for index tracking
126
- let flatIndex = 0
127
-
128
- return (
129
- <div className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]" inert={!isOpen || undefined}>
130
- {/* Backdrop */}
131
- <div
132
- className={clsx(
133
- 'absolute inset-0 bg-theme-base/60 backdrop-blur-sm',
134
- TRANSITION_BACKDROP,
135
- isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
136
- )}
137
- style={overlayTransitionStyle(isOpen, 'dialog')}
138
- onClick={isOpen ? onClose : undefined}
139
- />
140
-
141
- {/* Panel */}
142
- <div
143
- className={clsx(
144
- 'relative w-full max-w-lg mx-4 dialog overflow-hidden',
145
- TRANSITION_PANEL,
146
- isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
147
- )}
148
- style={overlayTransitionStyle(isOpen, 'dialog')}
149
- >
150
- {/* Search input */}
151
- <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
152
- <Search className="w-5 h-5 text-theme-text-secondary shrink-0" />
153
- <Input
154
- ref={inputRef}
155
- value={query}
156
- onChange={(e) => setQuery(e.target.value)}
157
- onKeyDown={handleKeyDown}
158
- placeholder="Type a command or search..."
159
- className="flex-1 bg-transparent text-theme-text-primary placeholder-theme-text-disabled outline-none text-sm"
160
- autoFocus
161
- />
162
- {query && (
163
- <button
164
- onClick={() => setQuery('')}
165
- className="p-1 text-theme-text-secondary hover:text-theme-text-primary"
166
- >
167
- <X className="w-4 h-4" />
168
- </button>
169
- )}
170
- <kbd className="px-1.5 py-0.5 text-xs text-theme-text-tertiary bg-theme-elevated rounded border border-theme-border-light">
171
- ESC
172
- </kbd>
173
- </div>
174
-
175
- {/* Results */}
176
- <div ref={resultsRef} className="max-h-[50vh] overflow-y-auto">
177
- {query && filteredItems.length === 0 && (
178
- <div className="px-4 py-8 text-center text-theme-text-tertiary">
179
- <Search className="w-8 h-8 mx-auto mb-2 opacity-50" />
180
- <p>No results for "{query}"</p>
181
- </div>
182
- )}
183
-
184
- {Array.from(grouped.entries()).map(([category, categoryItems]) => (
185
- <div key={category}>
186
- <div className="px-4 py-1.5 text-[10px] font-semibold text-theme-text-tertiary uppercase tracking-wider bg-theme-surface/50 sticky top-0">
187
- {category}
188
- </div>
189
- {categoryItems.map((item) => {
190
- const thisIndex = flatIndex++
191
- const isSelected = thisIndex === selectedIndex
192
- const Icon = item.icon
193
-
194
- return (
195
- <button
196
- key={item.id}
197
- data-selected={isSelected}
198
- onClick={() => { item.action(); onClose() }}
199
- onMouseMove={() => {
200
- if (isKeyboardNav.current) { isKeyboardNav.current = false; return }
201
- setSelectedIndex(thisIndex)
202
- }}
203
- className={clsx(
204
- 'w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors',
205
- isSelected ? 'selection' : 'hover:bg-theme-elevated/30'
206
- )}
207
- >
208
- {Icon && (
209
- <div className={clsx(
210
- 'flex items-center justify-center w-7 h-7 rounded-md shrink-0',
211
- isSelected ? 'selection-strong' : 'bg-theme-elevated/50'
212
- )}>
213
- <Icon className="w-3.5 h-3.5 text-theme-text-secondary" />
214
- </div>
215
- )}
216
- {!Icon && <div className="w-7 h-7 shrink-0" />}
217
-
218
- <div className="flex-1 min-w-0">
219
- <span className="text-sm text-theme-text-primary truncate block">{item.label}</span>
220
- {item.sublabel && (
221
- <span className="text-xs text-theme-text-tertiary truncate block">{item.sublabel}</span>
222
- )}
223
- </div>
224
-
225
- {item.shortcut && (
226
- <kbd className="px-1.5 py-0.5 text-xs text-theme-text-tertiary bg-theme-elevated rounded border border-theme-border-light shrink-0">
227
- {item.shortcut}
228
- </kbd>
229
- )}
230
-
231
- <ChevronRight className={clsx(
232
- 'w-3.5 h-3.5 shrink-0 transition-opacity',
233
- isSelected ? 'text-theme-text-secondary opacity-100' : 'opacity-0'
234
- )} />
235
- </button>
236
- )
237
- })}
238
- </div>
239
- ))}
240
- </div>
241
-
242
- {/* Footer hints */}
243
- {filteredItems.length > 0 && (
244
- <div className="px-4 py-2 border-t border-theme-border bg-theme-surface/50">
245
- <div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
246
- <span className="flex items-center gap-1">
247
- <kbd className="px-1 py-0.5 bg-theme-elevated rounded">↑</kbd>
248
- <kbd className="px-1 py-0.5 bg-theme-elevated rounded">↓</kbd>
249
- Navigate
250
- </span>
251
- <span className="flex items-center gap-1">
252
- <kbd className="px-1.5 py-0.5 bg-theme-elevated rounded">↵</kbd>
253
- Select
254
- </span>
255
- </div>
256
- </div>
257
- )}
258
- </div>
259
- </div>
260
- )
261
- }