@skyhook-io/radar-app 0.1.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 (251) hide show
  1. package/README.md +67 -0
  2. package/package.json +80 -0
  3. package/src/App.tsx +1538 -0
  4. package/src/RadarApp.tsx +145 -0
  5. package/src/api/apiResources.ts +28 -0
  6. package/src/api/client.ts +2583 -0
  7. package/src/api/config.ts +116 -0
  8. package/src/api/traffic.ts +139 -0
  9. package/src/components/ConnectionErrorView.tsx +272 -0
  10. package/src/components/ContextSwitcher.tsx +481 -0
  11. package/src/components/DebugOverlay.tsx +94 -0
  12. package/src/components/UserMenu.tsx +87 -0
  13. package/src/components/audit/AuditSettingsDialog.tsx +162 -0
  14. package/src/components/audit/AuditView.tsx +123 -0
  15. package/src/components/cost/CostTrendChart.tsx +388 -0
  16. package/src/components/cost/CostView.tsx +545 -0
  17. package/src/components/dock/BottomDock.tsx +96 -0
  18. package/src/components/dock/DockContext.tsx +11 -0
  19. package/src/components/dock/LocalTerminalTab.tsx +22 -0
  20. package/src/components/dock/LogsTab.tsx +26 -0
  21. package/src/components/dock/NodeTerminalTab.tsx +50 -0
  22. package/src/components/dock/TerminalTab.tsx +42 -0
  23. package/src/components/dock/TrafficFlowListTab.tsx +18 -0
  24. package/src/components/dock/WorkloadLogsTab.tsx +23 -0
  25. package/src/components/dock/index.ts +2 -0
  26. package/src/components/gitops/GitOpsActions.tsx +1 -0
  27. package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
  28. package/src/components/gitops/ManagedResourcesList.tsx +1 -0
  29. package/src/components/gitops/SyncCountdown.tsx +1 -0
  30. package/src/components/gitops/index.ts +4 -0
  31. package/src/components/helm/ChartBrowser.tsx +580 -0
  32. package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
  33. package/src/components/helm/HelmView.tsx +475 -0
  34. package/src/components/helm/InstallWizard.tsx +1060 -0
  35. package/src/components/helm/ManifestDiffViewer.tsx +91 -0
  36. package/src/components/helm/ManifestViewer.tsx +61 -0
  37. package/src/components/helm/OwnedResources.tsx +465 -0
  38. package/src/components/helm/RevisionHistory.tsx +167 -0
  39. package/src/components/helm/ValuesDiffPreview.tsx +190 -0
  40. package/src/components/helm/ValuesViewer.tsx +365 -0
  41. package/src/components/helm/helm-utils.ts +37 -0
  42. package/src/components/home/ActivitySummary.tsx +262 -0
  43. package/src/components/home/CertificateHealthCard.tsx +105 -0
  44. package/src/components/home/ClusterHealthCard.tsx +483 -0
  45. package/src/components/home/CostCard.tsx +112 -0
  46. package/src/components/home/HealthRing.tsx +1 -0
  47. package/src/components/home/HelmSummary.tsx +129 -0
  48. package/src/components/home/HomeView.tsx +224 -0
  49. package/src/components/home/MCPSetupDialog.tsx +417 -0
  50. package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
  51. package/src/components/home/TopologyPreview.tsx +219 -0
  52. package/src/components/home/TrafficSummary.tsx +154 -0
  53. package/src/components/logs/JsonLogLine.tsx +1 -0
  54. package/src/components/logs/LogCore.tsx +2 -0
  55. package/src/components/logs/LogsViewer.tsx +44 -0
  56. package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
  57. package/src/components/logs/useLogBuffer.ts +2 -0
  58. package/src/components/logs/useLogSearch.ts +1 -0
  59. package/src/components/portforward/PortForwardButton.tsx +375 -0
  60. package/src/components/portforward/PortForwardManager.tsx +871 -0
  61. package/src/components/resource/PrometheusCharts.tsx +687 -0
  62. package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
  63. package/src/components/resources/ImageFilesystemModal.tsx +745 -0
  64. package/src/components/resources/PodFilesystemModal.tsx +407 -0
  65. package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
  66. package/src/components/resources/ResourcesView.tsx +190 -0
  67. package/src/components/resources/drawer-components.tsx +1 -0
  68. package/src/components/resources/file-browser-utils.ts +35 -0
  69. package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
  70. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
  71. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
  72. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
  73. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
  74. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
  75. package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
  76. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
  77. package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
  78. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
  79. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
  80. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
  81. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
  82. package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
  83. package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
  84. package/src/components/resources/renderers/EventRenderer.tsx +1 -0
  85. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
  86. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
  87. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
  88. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
  89. package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
  90. package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
  91. package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
  92. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
  93. package/src/components/resources/renderers/HPARenderer.tsx +1 -0
  94. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
  95. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
  96. package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
  97. package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
  98. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
  99. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
  100. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
  101. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
  102. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
  103. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
  104. package/src/components/resources/renderers/JobRenderer.tsx +1 -0
  105. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
  106. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
  107. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
  108. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
  109. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
  110. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
  111. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
  112. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
  113. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
  114. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
  116. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
  117. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
  118. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
  119. package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
  120. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
  121. package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
  122. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
  123. package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
  124. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
  125. package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
  126. package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
  127. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
  128. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
  129. package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
  130. package/src/components/resources/renderers/PodRenderer.tsx +94 -0
  131. package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
  132. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
  133. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
  134. package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
  135. package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
  136. package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
  137. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
  138. package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
  139. package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
  140. package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
  141. package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
  142. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
  143. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
  144. package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
  145. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
  146. package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
  147. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
  148. package/src/components/resources/renderers/VPARenderer.tsx +1 -0
  149. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
  150. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
  151. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
  152. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
  153. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
  154. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
  155. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
  156. package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
  157. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
  158. package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
  159. package/src/components/resources/renderers/argo-cells.tsx +1 -0
  160. package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
  161. package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
  162. package/src/components/resources/renderers/eso-cells.tsx +1 -0
  163. package/src/components/resources/renderers/flux-cells.tsx +1 -0
  164. package/src/components/resources/renderers/index.ts +91 -0
  165. package/src/components/resources/renderers/istio-cells.tsx +1 -0
  166. package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
  167. package/src/components/resources/renderers/keda-cells.tsx +1 -0
  168. package/src/components/resources/renderers/knative-cells.tsx +1 -0
  169. package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
  170. package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
  171. package/src/components/resources/renderers/traefik-cells.tsx +1 -0
  172. package/src/components/resources/renderers/trivy-cells.tsx +1 -0
  173. package/src/components/resources/renderers/trivy-shared.tsx +1 -0
  174. package/src/components/resources/renderers/velero-cells.tsx +1 -0
  175. package/src/components/resources/resource-utils-argo.ts +2 -0
  176. package/src/components/resources/resource-utils-certmanager.ts +2 -0
  177. package/src/components/resources/resource-utils-cnpg.ts +2 -0
  178. package/src/components/resources/resource-utils-eso.ts +2 -0
  179. package/src/components/resources/resource-utils-flux.ts +2 -0
  180. package/src/components/resources/resource-utils-istio.ts +2 -0
  181. package/src/components/resources/resource-utils-karpenter.ts +2 -0
  182. package/src/components/resources/resource-utils-keda.ts +2 -0
  183. package/src/components/resources/resource-utils-knative.ts +2 -0
  184. package/src/components/resources/resource-utils-kyverno.ts +2 -0
  185. package/src/components/resources/resource-utils-prometheus.ts +2 -0
  186. package/src/components/resources/resource-utils-traefik.ts +1 -0
  187. package/src/components/resources/resource-utils-trivy.ts +2 -0
  188. package/src/components/resources/resource-utils-velero.ts +2 -0
  189. package/src/components/resources/resource-utils.ts +5 -0
  190. package/src/components/settings/SettingsDialog.tsx +537 -0
  191. package/src/components/shared/CreateResourceDialog.tsx +17 -0
  192. package/src/components/shared/EditableYamlView.tsx +24 -0
  193. package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
  194. package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
  195. package/src/components/timeline/DiffViewer.tsx +1 -0
  196. package/src/components/timeline/TimelineList.tsx +69 -0
  197. package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
  198. package/src/components/timeline/TimelineView.tsx +157 -0
  199. package/src/components/timeline/shared.tsx +1 -0
  200. package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
  201. package/src/components/traffic/TrafficFlowList.tsx +415 -0
  202. package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
  203. package/src/components/traffic/TrafficGraph.tsx +1546 -0
  204. package/src/components/traffic/TrafficView.tsx +1213 -0
  205. package/src/components/traffic/TrafficWizard.tsx +386 -0
  206. package/src/components/traffic/index.ts +3 -0
  207. package/src/components/ui/CodeViewer.tsx +8 -0
  208. package/src/components/ui/CommandPalette.tsx +460 -0
  209. package/src/components/ui/ConfirmDialog.tsx +1 -0
  210. package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
  211. package/src/components/ui/ErrorBoundary.tsx +46 -0
  212. package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
  213. package/src/components/ui/Markdown.tsx +108 -0
  214. package/src/components/ui/MetricsChart.tsx +1 -0
  215. package/src/components/ui/NamespaceSelector.tsx +436 -0
  216. package/src/components/ui/ResourceBar.tsx +1 -0
  217. package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
  218. package/src/components/ui/Toast.tsx +1 -0
  219. package/src/components/ui/Tooltip.tsx +1 -0
  220. package/src/components/ui/UpdateNotification.tsx +299 -0
  221. package/src/components/ui/YamlEditor.tsx +1 -0
  222. package/src/components/workload/WorkloadView.tsx +532 -0
  223. package/src/context/ConnectionContext.tsx +173 -0
  224. package/src/context/ContextSwitchContext.tsx +56 -0
  225. package/src/context/NavCustomization.tsx +62 -0
  226. package/src/context/ThemeContext.tsx +97 -0
  227. package/src/contexts/CapabilitiesContext.tsx +130 -0
  228. package/src/hooks/useAnimatedUnmount.ts +1 -0
  229. package/src/hooks/useDesktopDownload.ts +41 -0
  230. package/src/hooks/useEventSource.ts +262 -0
  231. package/src/hooks/useFavorites.ts +69 -0
  232. package/src/hooks/useKeyboardShortcuts.tsx +7 -0
  233. package/src/hooks/useRefreshAnimation.ts +1 -0
  234. package/src/index.css +243 -0
  235. package/src/index.ts +17 -0
  236. package/src/main.tsx +158 -0
  237. package/src/types/gitops.ts +2 -0
  238. package/src/types.ts +3 -0
  239. package/src/utils/animation.ts +2 -0
  240. package/src/utils/badge-colors.ts +2 -0
  241. package/src/utils/context-name.ts +2 -0
  242. package/src/utils/desktop-download.ts +66 -0
  243. package/src/utils/desktop-open-folder.ts +21 -0
  244. package/src/utils/format.ts +2 -0
  245. package/src/utils/log-format.ts +12 -0
  246. package/src/utils/navigation.ts +23 -0
  247. package/src/utils/resource-hierarchy.ts +2 -0
  248. package/src/utils/resource-icons.ts +2 -0
  249. package/src/utils/skeleton-yaml.ts +2 -0
  250. package/src/utils/traffic-colors.ts +54 -0
  251. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,460 @@
1
+ import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
2
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
3
+ import { Search, X, ChevronRight } from 'lucide-react'
4
+ import { Home, Network, List, Clock, Package, Activity, Sun, Stethoscope, DollarSign, ShieldCheck } from 'lucide-react'
5
+ import { clsx } from 'clsx'
6
+ import { useNamespaces, useContexts } from '../../api/client'
7
+ import { CORE_RESOURCES, useAPIResources } from '../../api/apiResources'
8
+ import { getResourceIcon } from '../../utils/resource-icons'
9
+ type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'helm' | 'traffic' | 'cost' | 'audit'
10
+
11
+ interface CommandPaletteProps {
12
+ onClose: () => void
13
+ onNavigateView: (view: MainView) => void
14
+ onNavigateKind: (kind: string, group: string) => void
15
+ onSwitchContext: (name: string) => void
16
+ onSetNamespaces: (ns: string[]) => void
17
+ onToggleTheme: () => void
18
+ onShowDiagnostics?: () => void
19
+ /** Controls fade-in/out animation (driven by useAnimatedUnmount) */
20
+ isOpen?: boolean
21
+ }
22
+
23
+ interface CommandItem {
24
+ id: string
25
+ label: string
26
+ sublabel?: string
27
+ category: string
28
+ icon?: React.ComponentType<{ className?: string }>
29
+ shortcut?: string
30
+ action: () => void
31
+ /** Extra terms to match against during search (not displayed) */
32
+ searchTerms?: string[]
33
+ /** Small priority bonus added to the final score (only if the item matched). Used to nudge built-in k8s kinds above CRDs on tied queries like "policy" or "event". */
34
+ priorityBonus?: number
35
+ }
36
+
37
+ // Built-in k8s API groups. Used to nudge these above CRDs on tied matches.
38
+ const CORE_GROUP_BONUS = 10
39
+ const WELL_KNOWN_GROUP_BONUS = 5
40
+ const WELL_KNOWN_GROUPS = new Set([
41
+ 'apps',
42
+ 'batch',
43
+ 'autoscaling',
44
+ 'policy',
45
+ 'networking.k8s.io',
46
+ 'rbac.authorization.k8s.io',
47
+ 'storage.k8s.io',
48
+ 'scheduling.k8s.io',
49
+ 'coordination.k8s.io',
50
+ 'apiextensions.k8s.io',
51
+ 'admissionregistration.k8s.io',
52
+ 'apiregistration.k8s.io',
53
+ 'certificates.k8s.io',
54
+ 'events.k8s.io',
55
+ 'discovery.k8s.io',
56
+ 'flowcontrol.apiserver.k8s.io',
57
+ 'node.k8s.io',
58
+ 'authentication.k8s.io',
59
+ 'authorization.k8s.io',
60
+ ])
61
+
62
+ function groupPriorityBonus(group: string): number {
63
+ if (!group) return CORE_GROUP_BONUS
64
+ if (WELL_KNOWN_GROUPS.has(group)) return WELL_KNOWN_GROUP_BONUS
65
+ return 0
66
+ }
67
+
68
+ // Fuzzy match scoring: exact > prefix > word boundary > substring.
69
+ // Within a tier, a coverage bonus (up to +20) breaks ties in favor of
70
+ // shorter labels — so "serv" picks Service over ServiceAccount, and
71
+ // "service" picks Service (exact) decisively. Bonus is capped below the
72
+ // 25-point tier gap, so tier ordering is preserved.
73
+ function scoreMatch(text: string, query: string): number {
74
+ const lower = text.toLowerCase()
75
+ const q = query.toLowerCase()
76
+ if (!lower.includes(q)) return 0
77
+ let base: number
78
+ if (lower === q) base = 150
79
+ else if (lower.startsWith(q)) base = 100
80
+ else {
81
+ const wordStart = lower.indexOf(q)
82
+ const prev = lower[wordStart - 1]
83
+ base = wordStart > 0 && (prev === ' ' || prev === '/' || prev === '-' || prev === '.') ? 75 : 50
84
+ }
85
+ return base + (q.length / lower.length) * 20
86
+ }
87
+
88
+ function bestScore(item: CommandItem, query: string): number {
89
+ // Primary label gets full score; secondary fields are discounted
90
+ // so that e.g. "node" matching the label "Node" ranks above
91
+ // "UpdateInfo" where "node" only matches the group "nodemanagement.gke.io"
92
+ let best = scoreMatch(item.label, query)
93
+ const secondary = Math.floor(Math.max(
94
+ scoreMatch(item.sublabel || '', query),
95
+ scoreMatch(item.category, query)
96
+ ) * 0.6)
97
+ best = Math.max(best, secondary)
98
+ if (item.searchTerms) {
99
+ for (const term of item.searchTerms) {
100
+ best = Math.max(best, scoreMatch(term, query))
101
+ }
102
+ }
103
+ // Only apply the priority bonus to items that actually matched, so we don't
104
+ // surface unrelated built-ins ahead of a relevant CRD.
105
+ return best > 0 ? best + (item.priorityBonus || 0) : 0
106
+ }
107
+
108
+ export function CommandPalette({
109
+ onClose,
110
+ onNavigateView,
111
+ onNavigateKind,
112
+ onSwitchContext,
113
+ onSetNamespaces,
114
+ onToggleTheme,
115
+ onShowDiagnostics,
116
+ isOpen = true,
117
+ }: CommandPaletteProps) {
118
+ const [query, setQuery] = useState('')
119
+ const [selectedIndex, setSelectedIndex] = useState(0)
120
+ const inputRef = useRef<HTMLInputElement>(null)
121
+ const resultsRef = useRef<HTMLDivElement>(null)
122
+ const isKeyboardNav = useRef(false)
123
+
124
+ const { data: namespacesData } = useNamespaces()
125
+ const { data: contexts } = useContexts()
126
+ const { data: apiResources } = useAPIResources()
127
+
128
+ // Focus input on mount
129
+ useEffect(() => {
130
+ inputRef.current?.focus()
131
+ }, [])
132
+
133
+ // Close on Escape (capture phase to beat the shortcut system)
134
+ useEffect(() => {
135
+ const handler = (e: KeyboardEvent) => {
136
+ if (e.key === 'Escape') {
137
+ e.preventDefault()
138
+ e.stopPropagation()
139
+ onClose()
140
+ }
141
+ }
142
+ document.addEventListener('keydown', handler, true)
143
+ return () => document.removeEventListener('keydown', handler, true)
144
+ }, [onClose])
145
+
146
+ // Build command items
147
+ const items = useMemo<CommandItem[]>(() => {
148
+ const result: CommandItem[] = []
149
+
150
+ // Views
151
+ const viewEntries: { view: MainView; label: string; icon: React.ComponentType<{ className?: string }>; shortcut: string }[] = [
152
+ { view: 'home', label: 'Home', icon: Home, shortcut: '1' },
153
+ { view: 'topology', label: 'Topology', icon: Network, shortcut: '2' },
154
+ { view: 'resources', label: 'Resources', icon: List, shortcut: '3' },
155
+ { view: 'timeline', label: 'Timeline', icon: Clock, shortcut: '4' },
156
+ { view: 'helm', label: 'Helm', icon: Package, shortcut: '5' },
157
+ { view: 'traffic', label: 'Traffic', icon: Activity, shortcut: '6' },
158
+ { view: 'cost', label: 'Cost', icon: DollarSign, shortcut: '7' },
159
+ { view: 'audit', label: 'Audit', icon: ShieldCheck, shortcut: '8' },
160
+ ]
161
+ for (const v of viewEntries) {
162
+ result.push({
163
+ id: `view-${v.view}`,
164
+ label: `Go to ${v.label}`,
165
+ category: 'Views',
166
+ icon: v.icon,
167
+ shortcut: v.shortcut,
168
+ action: () => { onNavigateView(v.view) },
169
+ })
170
+ }
171
+
172
+ // Resource kinds (deduplicate by name+group — backend may return multiple API versions)
173
+ const resources = apiResources || CORE_RESOURCES
174
+ const seenKinds = new Set<string>()
175
+ for (const r of resources) {
176
+ if (!r.verbs?.includes('list')) continue
177
+ const kindKey = `${r.name}/${r.group}`
178
+ if (seenKinds.has(kindKey)) continue
179
+ seenKinds.add(kindKey)
180
+ result.push({
181
+ id: `kind-${r.name}-${r.group}`,
182
+ label: r.kind,
183
+ sublabel: r.group || 'core',
184
+ category: 'Resource Kinds',
185
+ icon: getResourceIcon(r.kind),
186
+ action: () => { onNavigateKind(r.name, r.group) },
187
+ searchTerms: [r.name, r.kind],
188
+ priorityBonus: groupPriorityBonus(r.group),
189
+ })
190
+ }
191
+
192
+ // Contexts
193
+ if (contexts) {
194
+ for (const ctx of contexts) {
195
+ result.push({
196
+ id: `context-${ctx.name}`,
197
+ label: ctx.name,
198
+ sublabel: ctx.isCurrent ? 'current' : ctx.cluster,
199
+ category: 'Contexts',
200
+ action: () => { if (!ctx.isCurrent) onSwitchContext(ctx.name) },
201
+ })
202
+ }
203
+ }
204
+
205
+ // Namespaces
206
+ if (namespacesData) {
207
+ for (const ns of namespacesData) {
208
+ result.push({
209
+ id: `ns-${ns.name}`,
210
+ label: ns.name,
211
+ category: 'Namespaces',
212
+ action: () => { onSetNamespaces([ns.name]) },
213
+ })
214
+ }
215
+ // "All namespaces" option
216
+ result.push({
217
+ id: 'ns-all',
218
+ label: 'All Namespaces',
219
+ category: 'Namespaces',
220
+ action: () => { onSetNamespaces([]) },
221
+ })
222
+ }
223
+
224
+ // Actions
225
+ result.push({
226
+ id: 'action-theme',
227
+ label: 'Toggle Theme',
228
+ category: 'Actions',
229
+ icon: Sun,
230
+ shortcut: 't',
231
+ action: () => { onToggleTheme() },
232
+ })
233
+
234
+ if (onShowDiagnostics) {
235
+ result.push({
236
+ id: 'action-diagnostics',
237
+ label: 'Diagnostics',
238
+ category: 'Actions',
239
+ icon: Stethoscope,
240
+ shortcut: 'Ctrl+Shift+D',
241
+ action: () => { onShowDiagnostics() },
242
+ searchTerms: ['debug', 'health', 'status', 'snapshot'],
243
+ })
244
+ }
245
+
246
+ return result
247
+ // eslint-disable-next-line react-hooks/exhaustive-deps
248
+ }, [apiResources, contexts, namespacesData, onNavigateView, onNavigateKind, onSwitchContext, onSetNamespaces, onToggleTheme, onShowDiagnostics])
249
+
250
+ // Filter and rank results
251
+ const filteredItems = useMemo(() => {
252
+ if (!query.trim()) {
253
+ // Show views and actions when no query
254
+ return items.filter(i => i.category === 'Views' || i.category === 'Actions')
255
+ }
256
+
257
+ return items
258
+ .map(item => {
259
+ let score = bestScore(item, query)
260
+ // Boost core K8s resources over CRDs at the same match quality
261
+ if (score > 0 && item.category === 'Resource Kinds' && item.sublabel === 'core') {
262
+ score += 10
263
+ }
264
+ return { item, score }
265
+ })
266
+ .filter(({ score }) => score > 0)
267
+ .sort((a, b) => b.score - a.score)
268
+ .slice(0, 20)
269
+ .map(({ item }) => item)
270
+ }, [items, query])
271
+
272
+ // Group filtered items by category
273
+ const grouped = useMemo(() => {
274
+ const groups = new Map<string, CommandItem[]>()
275
+ for (const item of filteredItems) {
276
+ const list = groups.get(item.category) || []
277
+ list.push(item)
278
+ groups.set(item.category, list)
279
+ }
280
+ return groups
281
+ }, [filteredItems])
282
+
283
+ // Single source of truth for flat ordering — derived from grouped to match render order
284
+ const flatItems = useMemo(() => {
285
+ const result: CommandItem[] = []
286
+ for (const [, categoryItems] of grouped) {
287
+ result.push(...categoryItems)
288
+ }
289
+ return result
290
+ }, [grouped])
291
+
292
+ // Reset selection when results change
293
+ useEffect(() => {
294
+ setSelectedIndex(0)
295
+ }, [flatItems.length, query])
296
+
297
+ // Scroll selected into view
298
+ useEffect(() => {
299
+ if (resultsRef.current && selectedIndex >= 0) {
300
+ const el = resultsRef.current.querySelector('[data-selected="true"]') as HTMLElement
301
+ el?.scrollIntoView({ block: 'nearest' })
302
+ }
303
+ }, [selectedIndex])
304
+
305
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
306
+ switch (e.key) {
307
+ case 'ArrowDown':
308
+ e.preventDefault()
309
+ isKeyboardNav.current = true
310
+ setSelectedIndex(i => Math.min(i + 1, flatItems.length - 1))
311
+ break
312
+ case 'ArrowUp':
313
+ e.preventDefault()
314
+ isKeyboardNav.current = true
315
+ setSelectedIndex(i => Math.max(i - 1, 0))
316
+ break
317
+ case 'Enter':
318
+ e.preventDefault()
319
+ if (flatItems[selectedIndex]) {
320
+ flatItems[selectedIndex].action()
321
+ onClose()
322
+ }
323
+ break
324
+ }
325
+ }, [flatItems, selectedIndex, onClose])
326
+
327
+ // Flatten for index tracking
328
+ let flatIndex = 0
329
+
330
+ return (
331
+ <div className="fixed inset-0 z-[100] flex items-start justify-center pt-[15vh]">
332
+ {/* Backdrop */}
333
+ <div
334
+ className={clsx(
335
+ 'absolute inset-0 bg-theme-base/60 backdrop-blur-sm',
336
+ TRANSITION_BACKDROP,
337
+ isOpen ? 'opacity-100' : 'opacity-0'
338
+ )}
339
+ onClick={onClose}
340
+ />
341
+
342
+ {/* Panel */}
343
+ <div className={clsx(
344
+ 'relative w-full max-w-lg mx-4 dialog overflow-hidden',
345
+ TRANSITION_PANEL,
346
+ isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
347
+ )}>
348
+ {/* Search input */}
349
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
350
+ <Search className="w-5 h-5 text-theme-text-secondary shrink-0" />
351
+ <input
352
+ ref={inputRef}
353
+ type="text"
354
+ value={query}
355
+ onChange={(e) => setQuery(e.target.value)}
356
+ onKeyDown={handleKeyDown}
357
+ placeholder="Type a command or search..."
358
+ className="flex-1 bg-transparent text-theme-text-primary placeholder-theme-text-disabled outline-none text-sm"
359
+ autoFocus
360
+ />
361
+ {query && (
362
+ <button
363
+ onClick={() => setQuery('')}
364
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary"
365
+ >
366
+ <X className="w-4 h-4" />
367
+ </button>
368
+ )}
369
+ <kbd className="px-1.5 py-0.5 text-xs text-theme-text-tertiary bg-theme-elevated rounded border border-theme-border-light">
370
+ ESC
371
+ </kbd>
372
+ </div>
373
+
374
+ {/* Results */}
375
+ <div ref={resultsRef} className="max-h-[50vh] overflow-y-auto">
376
+ {query && filteredItems.length === 0 && (
377
+ <div className="px-4 py-8 text-center text-theme-text-tertiary">
378
+ <Search className="w-8 h-8 mx-auto mb-2 opacity-50" />
379
+ <p>No results for "{query}"</p>
380
+ </div>
381
+ )}
382
+
383
+ {Array.from(grouped.entries()).map(([category, categoryItems]) => (
384
+ <div key={category}>
385
+ <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">
386
+ {category}
387
+ </div>
388
+ {categoryItems.map((item) => {
389
+ const thisIndex = flatIndex++
390
+ const isSelected = thisIndex === selectedIndex
391
+ const Icon = item.icon
392
+
393
+ return (
394
+ <button
395
+ key={item.id}
396
+ data-selected={isSelected}
397
+ onClick={() => { item.action(); onClose() }}
398
+ onMouseMove={() => {
399
+ if (isKeyboardNav.current) { isKeyboardNav.current = false; return }
400
+ setSelectedIndex(thisIndex)
401
+ }}
402
+ className={clsx(
403
+ 'w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors',
404
+ isSelected ? 'selection' : 'hover:bg-theme-elevated/30'
405
+ )}
406
+ >
407
+ {Icon && (
408
+ <div className={clsx(
409
+ 'flex items-center justify-center w-7 h-7 rounded-md shrink-0',
410
+ isSelected ? 'selection-strong' : 'bg-theme-elevated/50'
411
+ )}>
412
+ <Icon className="w-3.5 h-3.5 text-theme-text-secondary" />
413
+ </div>
414
+ )}
415
+ {!Icon && <div className="w-7 h-7 shrink-0" />}
416
+
417
+ <div className="flex-1 min-w-0">
418
+ <span className="text-sm text-theme-text-primary truncate block">{item.label}</span>
419
+ {item.sublabel && (
420
+ <span className="text-xs text-theme-text-tertiary truncate block">{item.sublabel}</span>
421
+ )}
422
+ </div>
423
+
424
+ {item.shortcut && (
425
+ <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">
426
+ {item.shortcut}
427
+ </kbd>
428
+ )}
429
+
430
+ <ChevronRight className={clsx(
431
+ 'w-3.5 h-3.5 shrink-0 transition-opacity',
432
+ isSelected ? 'text-theme-text-secondary opacity-100' : 'opacity-0'
433
+ )} />
434
+ </button>
435
+ )
436
+ })}
437
+ </div>
438
+ ))}
439
+ </div>
440
+
441
+ {/* Footer hints */}
442
+ {filteredItems.length > 0 && (
443
+ <div className="px-4 py-2 border-t border-theme-border bg-theme-surface/50">
444
+ <div className="flex items-center gap-4 text-xs text-theme-text-tertiary">
445
+ <span className="flex items-center gap-1">
446
+ <kbd className="px-1 py-0.5 bg-theme-elevated rounded">↑</kbd>
447
+ <kbd className="px-1 py-0.5 bg-theme-elevated rounded">↓</kbd>
448
+ Navigate
449
+ </span>
450
+ <span className="flex items-center gap-1">
451
+ <kbd className="px-1.5 py-0.5 bg-theme-elevated rounded">↵</kbd>
452
+ Select
453
+ </span>
454
+ </div>
455
+ </div>
456
+ )}
457
+ </div>
458
+ </div>
459
+ )
460
+ }
@@ -0,0 +1 @@
1
+ export { ConfirmDialog } from '@skyhook-io/k8s-ui/components/ui/ConfirmDialog'