@skyhook-io/radar-app 0.2.2 → 0.3.1

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 (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -0,0 +1,89 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { HelpCircle } from 'lucide-react'
4
+ import { clsx } from 'clsx'
5
+
6
+ // The query operators recognized by the search engine (internal/search/parse.go).
7
+ const OPERATORS: { syntax: string; desc: string }[] = [
8
+ { syntax: 'ns:prod', desc: 'filter by namespace' },
9
+ { syntax: 'kind:Deployment', desc: 'filter by resource kind' },
10
+ { syntax: 'label:app=api', desc: 'filter by label' },
11
+ { syntax: 'image:nginx', desc: 'match container image' },
12
+ { syntax: 'redis cache', desc: 'multiple terms — all must match' },
13
+ ]
14
+
15
+ // A "?" affordance that reveals the search query syntax on HOVER (a transient
16
+ // reference tooltip, not a second click-to-open panel stacked over the results).
17
+ // Self-contained + portaled to <body> so the header's stacking context can't
18
+ // trap it; a short close grace lets the pointer travel onto the card.
19
+ export function SearchSyntaxHelp() {
20
+ const [open, setOpen] = useState(false)
21
+ const btnRef = useRef<HTMLButtonElement>(null)
22
+ const popRef = useRef<HTMLDivElement>(null)
23
+ const closeTimer = useRef<ReturnType<typeof setTimeout>>(undefined)
24
+ const [anchor, setAnchor] = useState<{ right: number; top: number } | null>(null)
25
+
26
+ const show = useCallback(() => { clearTimeout(closeTimer.current); setOpen(true) }, [])
27
+ const scheduleHide = useCallback(() => {
28
+ clearTimeout(closeTimer.current)
29
+ closeTimer.current = setTimeout(() => setOpen(false), 120)
30
+ }, [])
31
+
32
+ useEffect(() => () => clearTimeout(closeTimer.current), [])
33
+
34
+ useEffect(() => {
35
+ if (!open) { setAnchor(null); return }
36
+ const update = () => {
37
+ const el = btnRef.current
38
+ if (el) { const r = el.getBoundingClientRect(); setAnchor({ right: window.innerWidth - r.right, top: r.bottom + 6 }) }
39
+ }
40
+ update()
41
+ window.addEventListener('resize', update)
42
+ window.addEventListener('scroll', update, true)
43
+ return () => {
44
+ window.removeEventListener('resize', update)
45
+ window.removeEventListener('scroll', update, true)
46
+ }
47
+ }, [open])
48
+
49
+ return (
50
+ <>
51
+ <button
52
+ ref={btnRef}
53
+ type="button"
54
+ tabIndex={-1}
55
+ aria-label="Search syntax help"
56
+ onMouseEnter={show}
57
+ onMouseLeave={scheduleHide}
58
+ onMouseDown={(e) => e.preventDefault()}
59
+ className={clsx('shrink-0 rounded p-0.5 cursor-help transition-colors', open ? 'text-theme-text-secondary' : 'text-theme-text-tertiary hover:text-theme-text-secondary')}
60
+ >
61
+ <HelpCircle className="w-3.5 h-3.5" />
62
+ </button>
63
+ {open && anchor && createPortal(
64
+ <div
65
+ ref={popRef}
66
+ onMouseEnter={show}
67
+ onMouseLeave={scheduleHide}
68
+ onMouseDown={(e) => e.stopPropagation()}
69
+ style={{ position: 'fixed', top: anchor.top, right: anchor.right, width: 280 }}
70
+ className="z-[130] dialog shadow-theme-lg p-3"
71
+ >
72
+ <div className="text-[10px] font-semibold uppercase tracking-wider text-theme-text-tertiary mb-2">Search syntax</div>
73
+ <div className="space-y-1.5">
74
+ {OPERATORS.map((op) => (
75
+ <div key={op.syntax} className="flex items-baseline gap-2">
76
+ <code className="shrink-0 rounded bg-theme-elevated border border-theme-border-light px-1.5 py-0.5 text-xs text-theme-text-primary">{op.syntax}</code>
77
+ <span className="text-xs text-theme-text-secondary">{op.desc}</span>
78
+ </div>
79
+ ))}
80
+ </div>
81
+ <div className="mt-2.5 pt-2 border-t border-theme-border text-[11px] text-theme-text-tertiary">
82
+ Type <code className="text-theme-text-secondary">ns:</code> or <code className="text-theme-text-secondary">kind:</code> for value suggestions.
83
+ </div>
84
+ </div>,
85
+ document.body,
86
+ )}
87
+ </>
88
+ )
89
+ }
@@ -19,7 +19,7 @@ const CONTEXT_CATEGORIES: ShortcutCategory[] = ['Drawer', 'Dock']
19
19
 
20
20
  // Preferred ordering within the view section
21
21
  const VIEW_CATEGORY_ORDER: ShortcutCategory[] = [
22
- 'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm',
22
+ 'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm', 'GitOps',
23
23
  ]
24
24
 
25
25
  const VIEW_LABELS: Record<string, string> = {
@@ -28,7 +28,8 @@ const VIEW_LABELS: Record<string, string> = {
28
28
  resources: 'Resources',
29
29
  timeline: 'Timeline',
30
30
  helm: 'Helm',
31
- traffic: 'Traffic',
31
+ gitops: 'GitOps',
32
+ traffic: 'Live Traffic',
32
33
  }
33
34
 
34
35
  type ShortcutEntry = { description: string; keys: string[] }
@@ -8,6 +8,7 @@ import {
8
8
  useApplyDesktopUpdate,
9
9
  } from '../../api/client'
10
10
  import type { DesktopUpdateState } from '../../api/client'
11
+ import { WithTooltip } from './Tooltip'
11
12
 
12
13
  const DISMISSED_KEY = 'radar-update-dismissed'
13
14
 
@@ -26,21 +27,16 @@ export function UpdateNotification() {
26
27
 
27
28
  const isDesktop = versionInfo?.installMethod === 'desktop'
28
29
 
29
- // Listen for "Check for Updates" menu item in desktop app (Wails runtime event).
30
- // Un-dismisses the notification and invalidates the version check cache.
30
+ // Listen for "Check for Updates" menu item in desktop app.
31
31
  useEffect(() => {
32
- const wailsRuntime = (window as unknown as Record<string, unknown>).runtime as
33
- | { EventsOn?: (event: string, callback: () => void) => () => void }
34
- | undefined
35
- if (!wailsRuntime?.EventsOn) return
36
-
37
- const cleanup = wailsRuntime.EventsOn('check-for-updates', () => {
32
+ const handler = () => {
38
33
  setDismissed(false)
39
34
  try { localStorage.removeItem(DISMISSED_KEY) } catch { /* ignore */ }
40
35
  queryClient.invalidateQueries({ queryKey: ['version-check'] })
41
- })
36
+ }
42
37
 
43
- return cleanup
38
+ window.addEventListener('radar:check-for-updates', handler)
39
+ return () => window.removeEventListener('radar:check-for-updates', handler)
44
40
  }, [queryClient])
45
41
 
46
42
  // Log version check errors for debugging
@@ -111,13 +107,13 @@ export function UpdateNotification() {
111
107
  const effectiveState: DesktopUpdateState = updateStatus?.state ?? 'idle'
112
108
 
113
109
  return (
114
- <div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-blue-500/50 rounded-lg shadow-xl p-4 animate-in slide-in-from-right">
110
+ <div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-accent/50 rounded-lg shadow-xl p-4 animate-in slide-in-from-right">
115
111
  <div className="flex items-start gap-3">
116
- <div className="flex items-center justify-center w-8 h-8 bg-blue-500/20 rounded-full shrink-0">
112
+ <div className="flex items-center justify-center w-8 h-8 bg-accent-muted rounded-full shrink-0">
117
113
  <UpdateIcon state={effectiveState} />
118
114
  </div>
119
115
  <div className="flex-1 min-w-0">
120
- <h4 className="text-sm font-medium text-theme-text-primary">
116
+ <h4 className="text-sm font-medium text-theme-text-primary pr-6">
121
117
  <UpdateTitle state={effectiveState} />
122
118
  </h4>
123
119
  <p className="text-xs text-theme-text-secondary mt-1">
@@ -140,38 +136,54 @@ export function UpdateNotification() {
140
136
 
141
137
  {/* CLI: show update command with copy button for package managers */}
142
138
  {!isDesktop && versionInfo.updateCommand ? (
143
- <button
144
- onClick={handleCopyCommand}
145
- className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded text-xs font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
146
- >
147
- <code className="flex-1 text-left truncate">{versionInfo.updateCommand}</code>
148
- <CopyIcon copied={copied} failed={copyFailed} />
149
- </button>
139
+ <>
140
+ <WithTooltip tip={versionInfo.updateCommand} delay={100}>
141
+ <button
142
+ onClick={handleCopyCommand}
143
+ className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
144
+ >
145
+ <code className="inline-code flex-1 truncate text-left text-[11px]">{versionInfo.updateCommand}</code>
146
+ <CopyIcon copied={copied} failed={copyFailed} />
147
+ </button>
148
+ </WithTooltip>
149
+ {/* Direct installs may have placed the binary somewhere the install
150
+ script won't touch — surface a download link as a fallback. */}
151
+ {versionInfo.installMethod === 'direct' && versionInfo.releaseUrl && (
152
+ <a
153
+ href={versionInfo.releaseUrl}
154
+ target="_blank"
155
+ rel="noopener noreferrer"
156
+ className="inline-flex items-center gap-1 mt-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary hover:underline"
157
+ >
158
+ or download from GitHub →
159
+ </a>
160
+ )}
161
+ </>
150
162
  ) : (
151
- /* Direct download - show release link */
152
163
  !isDesktop && versionInfo.releaseUrl && (
153
164
  <a
154
165
  href={versionInfo.releaseUrl}
155
166
  target="_blank"
156
167
  rel="noopener noreferrer"
157
- className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-blue-400 hover:text-blue-300"
168
+ className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-accent-text hover:underline"
158
169
  >
159
170
  Download from GitHub →
160
171
  </a>
161
172
  )
162
173
  )}
163
174
  </div>
164
- {/* Don't show dismiss during active update */}
165
- {effectiveState !== 'downloading' && effectiveState !== 'applying' && (
166
- <button
167
- onClick={handleDismiss}
168
- className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
169
- aria-label="Dismiss"
170
- >
171
- <X className="w-4 h-4" />
172
- </button>
173
- )}
174
175
  </div>
176
+ {/* Dismiss is absolute so it doesn't compress the chip's width.
177
+ fixed on the parent already establishes the positioning context. */}
178
+ {effectiveState !== 'downloading' && effectiveState !== 'applying' && (
179
+ <button
180
+ onClick={handleDismiss}
181
+ className="absolute top-2 right-2 p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
182
+ aria-label="Dismiss"
183
+ >
184
+ <X className="w-4 h-4" />
185
+ </button>
186
+ )}
175
187
  </div>
176
188
  )
177
189
  }
@@ -186,11 +198,11 @@ function UpdateIcon({ state }: { state: DesktopUpdateState }) {
186
198
  switch (state) {
187
199
  case 'downloading':
188
200
  case 'applying':
189
- return <Loader2 className="w-4 h-4 text-blue-400 animate-spin" />
201
+ return <Loader2 className="w-4 h-4 text-accent animate-spin" />
190
202
  case 'ready':
191
203
  return <ArrowDownToLine className="w-4 h-4 text-green-400" />
192
204
  default:
193
- return <Download className="w-4 h-4 text-blue-400" />
205
+ return <Download className="w-4 h-4 text-accent" />
194
206
  }
195
207
  }
196
208
 
@@ -247,7 +259,7 @@ function DesktopUpdateControls({
247
259
  <div className="mt-2 space-y-1">
248
260
  <div className="w-full bg-theme-elevated rounded-full h-1.5 overflow-hidden">
249
261
  <div
250
- className="bg-blue-500 h-full rounded-full transition-all duration-300"
262
+ className="bg-accent h-full rounded-full transition-all duration-300"
251
263
  style={{ width: `${Math.round((progress ?? 0) * 100)}%` }}
252
264
  />
253
265
  </div>
@@ -272,7 +284,7 @@ function DesktopUpdateControls({
272
284
  case 'applying':
273
285
  return (
274
286
  <div className="mt-2 flex items-center gap-2">
275
- <Loader2 className="w-3.5 h-3.5 text-blue-400 animate-spin" />
287
+ <Loader2 className="w-3.5 h-3.5 text-accent animate-spin" />
276
288
  <p className="text-xs text-theme-text-secondary">Applying update...</p>
277
289
  </div>
278
290
  )
@@ -0,0 +1,178 @@
1
+ import { useMemo } from 'react'
2
+ import { Home, Network, List, Clock, Package, Activity, Sun, Stethoscope, DollarSign, ShieldCheck, GitBranch, AlertTriangle, Boxes, Server } from 'lucide-react'
3
+ import { useNamespaces, useContexts } from '../../api/client'
4
+ import { CORE_RESOURCES, useAPIResources } from '../../api/apiResources'
5
+ import { getResourceIcon } from '../../utils/resource-icons'
6
+ import { parseContextName } from '../../utils/context-name'
7
+
8
+ // Drop the disambiguating " (source)" suffix the context list appends, so the
9
+ // GKE/EKS/AKS parser sees the bare context name (mirrors the cluster picker).
10
+ function stripSourceSuffix(name: string, source?: string): string {
11
+ if (!source) return name
12
+ const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
13
+ return name.replace(new RegExp(`\\s+\\(${escaped}(?:\\s+#\\d+)?\\)$`), '')
14
+ }
15
+
16
+ export type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'issues' | 'helm' | 'traffic' | 'cost' | 'checks' | 'gitops' | 'applications'
17
+
18
+ export interface CommandItem {
19
+ id: string
20
+ label: string
21
+ sublabel?: string
22
+ category: string
23
+ icon?: React.ComponentType<{ className?: string }>
24
+ shortcut?: string
25
+ action: () => void
26
+ /** Extra terms to match against during search (not displayed). */
27
+ searchTerms?: string[]
28
+ /** Small priority bonus added to the final score (only if the item matched). */
29
+ priorityBonus?: number
30
+ }
31
+
32
+ // Built-in k8s API groups. Used to nudge these above CRDs on tied matches.
33
+ const CORE_GROUP_BONUS = 10
34
+ const WELL_KNOWN_GROUP_BONUS = 5
35
+ const WELL_KNOWN_GROUPS = new Set([
36
+ 'apps', 'batch', 'autoscaling', 'policy', 'networking.k8s.io', 'rbac.authorization.k8s.io',
37
+ 'storage.k8s.io', 'scheduling.k8s.io', 'coordination.k8s.io', 'apiextensions.k8s.io',
38
+ 'admissionregistration.k8s.io', 'apiregistration.k8s.io', 'certificates.k8s.io',
39
+ 'events.k8s.io', 'discovery.k8s.io', 'flowcontrol.apiserver.k8s.io', 'node.k8s.io',
40
+ 'authentication.k8s.io', 'authorization.k8s.io',
41
+ ])
42
+
43
+ function groupPriorityBonus(group: string): number {
44
+ if (!group) return CORE_GROUP_BONUS
45
+ if (WELL_KNOWN_GROUPS.has(group)) return WELL_KNOWN_GROUP_BONUS
46
+ return 0
47
+ }
48
+
49
+ // Fuzzy match scoring: exact > prefix > word boundary > substring. Within a
50
+ // tier, a coverage bonus (up to +20) breaks ties in favor of shorter labels.
51
+ export function scoreMatch(text: string, query: string): number {
52
+ const lower = text.toLowerCase()
53
+ const q = query.toLowerCase()
54
+ if (!lower.includes(q)) return 0
55
+ let base: number
56
+ if (lower === q) base = 150
57
+ else if (lower.startsWith(q)) base = 100
58
+ else {
59
+ const wordStart = lower.indexOf(q)
60
+ const prev = lower[wordStart - 1]
61
+ base = wordStart > 0 && (prev === ' ' || prev === '/' || prev === '-' || prev === '.') ? 75 : 50
62
+ }
63
+ return base + (q.length / lower.length) * 20
64
+ }
65
+
66
+ export function bestScore(item: CommandItem, query: string): number {
67
+ let best = scoreMatch(item.label, query)
68
+ const secondary = Math.floor(Math.max(scoreMatch(item.sublabel || '', query), scoreMatch(item.category, query)) * 0.6)
69
+ best = Math.max(best, secondary)
70
+ if (item.searchTerms) {
71
+ for (const term of item.searchTerms) best = Math.max(best, scoreMatch(term, query))
72
+ }
73
+ return best > 0 ? best + (item.priorityBonus || 0) : 0
74
+ }
75
+
76
+ export interface CommandItemCallbacks {
77
+ onNavigateView: (view: MainView) => void
78
+ onNavigateKind: (kind: string, group: string) => void
79
+ onSwitchContext: (name: string) => void
80
+ onSetNamespaces: (ns: string[]) => void
81
+ onToggleTheme: () => void
82
+ onShowDiagnostics?: () => void
83
+ }
84
+
85
+ const VIEW_ENTRIES: { view: MainView; label: string; icon: React.ComponentType<{ className?: string }>; shortcut: string }[] = [
86
+ { view: 'home', label: 'Home', icon: Home, shortcut: 'g h' },
87
+ { view: 'resources', label: 'Resources', icon: List, shortcut: 'g r' },
88
+ { view: 'issues', label: 'Issues', icon: AlertTriangle, shortcut: 'g i' },
89
+ { view: 'topology', label: 'Topology', icon: Network, shortcut: 'g t' },
90
+ { view: 'applications', label: 'Applications', icon: Boxes, shortcut: 'g a' },
91
+ { view: 'timeline', label: 'Timeline', icon: Clock, shortcut: 'g l' },
92
+ { view: 'helm', label: 'Helm', icon: Package, shortcut: 'g m' },
93
+ { view: 'gitops', label: 'GitOps', icon: GitBranch, shortcut: 'g o' },
94
+ { view: 'traffic', label: 'Live Traffic', icon: Activity, shortcut: 'g f' },
95
+ { view: 'checks', label: 'Checks', icon: ShieldCheck, shortcut: 'g u' },
96
+ { view: 'cost', label: 'Cost', icon: DollarSign, shortcut: 'g c' },
97
+ ]
98
+
99
+ // The static command-palette items (Views, Resource Kinds, Contexts,
100
+ // Namespaces, Actions) — shared by the centered modal (embedded) and the
101
+ // standalone omnibar so the two never drift.
102
+ export function useCommandItems(cb: CommandItemCallbacks): CommandItem[] {
103
+ const { data: namespacesData } = useNamespaces()
104
+ const { data: contexts } = useContexts()
105
+ const { data: apiResources } = useAPIResources()
106
+
107
+ return useMemo<CommandItem[]>(() => {
108
+ const result: CommandItem[] = []
109
+
110
+ for (const v of VIEW_ENTRIES) {
111
+ result.push({ id: `view-${v.view}`, label: `Go to ${v.label}`, category: 'Views', icon: v.icon, shortcut: v.shortcut, action: () => cb.onNavigateView(v.view) })
112
+ }
113
+
114
+ const resources = apiResources || CORE_RESOURCES
115
+ const seenKinds = new Set<string>()
116
+ for (const r of resources) {
117
+ if (!r.verbs?.includes('list')) continue
118
+ const kindKey = `${r.name}/${r.group}`
119
+ if (seenKinds.has(kindKey)) continue
120
+ seenKinds.add(kindKey)
121
+ result.push({
122
+ // Group shown only when it disambiguates (CRDs) — "core" is noise on
123
+ // built-in kinds. priorityBonus still nudges core/well-known above CRDs.
124
+ id: `kind-${r.name}-${r.group}`, label: r.kind, sublabel: r.group || undefined, category: 'Resource Kinds',
125
+ icon: getResourceIcon(r.kind), action: () => cb.onNavigateKind(r.name, r.group),
126
+ searchTerms: [r.name, r.kind], priorityBonus: groupPriorityBonus(r.group),
127
+ })
128
+ }
129
+
130
+ if (contexts) {
131
+ // Show the friendly parsed cluster name (like the cluster picker), not the
132
+ // raw ARN/gke context string. provider/region may live in the cluster id
133
+ // (e.g. EKS ARN) when the context name is already friendly — fall back to
134
+ // it. Count display names so genuine duplicates (same cluster name from
135
+ // different kubeconfig sources) stay distinguishable; unique ones stay clean.
136
+ const parsedCtx = contexts.map((ctx) => {
137
+ const parsed = parseContextName(stripSourceSuffix(ctx.name, ctx.source))
138
+ const fromCluster = ctx.cluster ? parseContextName(ctx.cluster) : null
139
+ const meta = [parsed.provider ?? fromCluster?.provider, parsed.region ?? fromCluster?.region].filter(Boolean).join(' · ')
140
+ return { ctx, clusterName: parsed.clusterName, account: parsed.account, base: ctx.isCurrent ? 'current' : meta }
141
+ })
142
+ // Disambiguate on the FINAL visible (label, sublabel) pair, not just the
143
+ // cluster name — same name + same provider/region from the same kubeconfig
144
+ // file would otherwise render identically while switching different
145
+ // contexts. Collisions fall back to the raw context name (unique by id).
146
+ const pairCount = new Map<string, number>()
147
+ for (const p of parsedCtx) pairCount.set(`${p.clusterName}\x00${p.base}`, (pairCount.get(`${p.clusterName}\x00${p.base}`) ?? 0) + 1)
148
+ for (const { ctx, clusterName, account, base } of parsedCtx) {
149
+ const collides = (pairCount.get(`${clusterName}\x00${base}`) ?? 0) > 1
150
+ const sub = [base, collides ? ctx.name : ''].filter(Boolean).join(' · ')
151
+ result.push({
152
+ id: `context-${ctx.name}`,
153
+ label: clusterName,
154
+ sublabel: sub || undefined,
155
+ category: 'Clusters',
156
+ icon: Server,
157
+ action: () => { if (!ctx.isCurrent) cb.onSwitchContext(ctx.name) },
158
+ searchTerms: [ctx.name, account || ''].filter(Boolean),
159
+ })
160
+ }
161
+ }
162
+
163
+ if (namespacesData) {
164
+ for (const ns of namespacesData) {
165
+ result.push({ id: `ns-${ns.name}`, label: ns.name, category: 'Namespaces', action: () => cb.onSetNamespaces([ns.name]) })
166
+ }
167
+ result.push({ id: 'ns-all', label: 'All Namespaces', category: 'Namespaces', action: () => cb.onSetNamespaces([]) })
168
+ }
169
+
170
+ result.push({ id: 'action-theme', label: 'Toggle Theme', category: 'Actions', icon: Sun, shortcut: 't', action: () => cb.onToggleTheme() })
171
+ if (cb.onShowDiagnostics) {
172
+ result.push({ id: 'action-diagnostics', label: 'Diagnostics', category: 'Actions', icon: Stethoscope, action: () => cb.onShowDiagnostics?.(), searchTerms: ['debug', 'health', 'status', 'snapshot'] })
173
+ }
174
+
175
+ return result
176
+ // eslint-disable-next-line react-hooks/exhaustive-deps
177
+ }, [apiResources, contexts, namespacesData, cb.onNavigateView, cb.onNavigateKind, cb.onSwitchContext, cb.onSetNamespaces, cb.onToggleTheme, cb.onShowDiagnostics])
178
+ }