@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,301 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { X } from 'lucide-react'
3
+ import { clsx } from 'clsx'
4
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
5
+ import { useActiveShortcuts, type ShortcutCategory } from '../../hooks/useKeyboardShortcuts'
6
+
7
+ interface ShortcutHelpOverlayProps {
8
+ onClose: () => void
9
+ currentView?: string
10
+ /** Controls fade-in/out animation (driven by useAnimatedUnmount) */
11
+ isOpen?: boolean
12
+ }
13
+
14
+ // Categories that always appear at the top
15
+ const GLOBAL_CATEGORIES: ShortcutCategory[] = ['Navigation', 'General']
16
+
17
+ // Categories tied to contextual UI (not a specific view)
18
+ const CONTEXT_CATEGORIES: ShortcutCategory[] = ['Drawer', 'Dock']
19
+
20
+ // Preferred ordering within the view section
21
+ const VIEW_CATEGORY_ORDER: ShortcutCategory[] = [
22
+ 'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm',
23
+ ]
24
+
25
+ const VIEW_LABELS: Record<string, string> = {
26
+ home: 'Home',
27
+ topology: 'Topology',
28
+ resources: 'Resources',
29
+ timeline: 'Timeline',
30
+ helm: 'Helm',
31
+ traffic: 'Traffic',
32
+ }
33
+
34
+ type ShortcutEntry = { description: string; keys: string[] }
35
+
36
+ function KbdKey({ children }: { children: string }) {
37
+ return (
38
+ <kbd className="inline-flex items-center justify-center min-w-[1.5rem] h-6 px-1.5 text-xs font-mono font-medium bg-theme-elevated border border-theme-border-light rounded text-theme-text-primary shadow-sm">
39
+ {children}
40
+ </kbd>
41
+ )
42
+ }
43
+
44
+ function ShortcutKeys({ keys }: { keys: string }) {
45
+ // Handle multi-key sequences like "g g"
46
+ if (keys.includes(' ') && !keys.includes('+')) {
47
+ const parts = keys.split(' ')
48
+ return (
49
+ <span className="flex items-center gap-1">
50
+ {parts.map((part, i) => (
51
+ <span key={i} className="flex items-center gap-0.5">
52
+ {i > 0 && <span className="text-theme-text-tertiary text-xs mx-0.5"></span>}
53
+ <KbdKey>{part}</KbdKey>
54
+ </span>
55
+ ))}
56
+ </span>
57
+ )
58
+ }
59
+
60
+ // Handle modifier combos like "Cmd+K", "Ctrl+D", "Shift+N"
61
+ // But not the literal "+" key itself
62
+ if (keys.includes('+') && keys !== '+') {
63
+ const parts = keys.split('+')
64
+ return (
65
+ <span className="flex items-center gap-0.5">
66
+ {parts.map((part, i) => (
67
+ <span key={i} className="flex items-center gap-0.5">
68
+ {i > 0 && <span className="text-theme-text-tertiary text-[10px]">+</span>}
69
+ <KbdKey>{formatKeyLabel(part)}</KbdKey>
70
+ </span>
71
+ ))}
72
+ </span>
73
+ )
74
+ }
75
+
76
+ // Single key
77
+ return <KbdKey>{formatKeyLabel(keys)}</KbdKey>
78
+ }
79
+
80
+ function formatKeyLabel(key: string): string {
81
+ const isMac = typeof navigator !== 'undefined' && navigator.platform.includes('Mac')
82
+ switch (key.toLowerCase()) {
83
+ case 'cmd':
84
+ case 'meta': return isMac ? '⌘' : 'Ctrl'
85
+ case 'ctrl': return 'Ctrl'
86
+ case 'shift': return isMac ? '⇧' : 'Shift'
87
+ case 'alt': return isMac ? '⌥' : 'Alt'
88
+ case 'escape': return 'Esc'
89
+ case 'enter': return '↵'
90
+ case 'arrowup': return '↑'
91
+ case 'arrowdown': return '↓'
92
+ case 'arrowleft': return '←'
93
+ case 'arrowright': return '→'
94
+ case '`': return '`'
95
+ default: return key
96
+ }
97
+ }
98
+
99
+ function CategoryBlock({ category, entries }: { category: string; entries: ShortcutEntry[] }) {
100
+ return (
101
+ <div>
102
+ <h3 className="text-xs font-semibold text-theme-text-tertiary uppercase tracking-wider mb-2.5">
103
+ {category}
104
+ </h3>
105
+ <div className="space-y-1.5">
106
+ {entries.map(entry => (
107
+ <div key={entry.description} className="flex items-center justify-between py-1">
108
+ <span className="text-sm text-theme-text-secondary">{entry.description}</span>
109
+ <span className="flex items-center gap-1.5 ml-4 shrink-0">
110
+ {entry.keys.map((k, i) => (
111
+ <span key={k} className="flex items-center gap-1.5">
112
+ {i > 0 && <span className="text-theme-text-tertiary text-[10px]">/</span>}
113
+ <ShortcutKeys keys={k} />
114
+ </span>
115
+ ))}
116
+ </span>
117
+ </div>
118
+ ))}
119
+ </div>
120
+ </div>
121
+ )
122
+ }
123
+
124
+ // Balance categories into two columns by item count (descending-greedy for best packing)
125
+ function balanceColumns(categories: ShortcutCategory[], grouped: Map<ShortcutCategory, ShortcutEntry[]>): [ShortcutCategory[], ShortcutCategory[]] {
126
+ const sorted = [...categories].sort((a, b) =>
127
+ (grouped.get(b)?.length || 0) - (grouped.get(a)?.length || 0)
128
+ )
129
+ const leftSet = new Set<ShortcutCategory>()
130
+ const rightSet = new Set<ShortcutCategory>()
131
+ let leftCount = 0, rightCount = 0
132
+
133
+ for (const cat of sorted) {
134
+ const count = grouped.get(cat)!.length
135
+ if (leftCount <= rightCount) {
136
+ leftSet.add(cat)
137
+ leftCount += count
138
+ } else {
139
+ rightSet.add(cat)
140
+ rightCount += count
141
+ }
142
+ }
143
+
144
+ // Preserve original ordering within each column
145
+ const left = categories.filter(c => leftSet.has(c))
146
+ const right = categories.filter(c => rightSet.has(c))
147
+ return [left, right]
148
+ }
149
+
150
+ function TwoColumnSection({ categories, grouped }: { categories: ShortcutCategory[]; grouped: Map<ShortcutCategory, ShortcutEntry[]> }) {
151
+ if (categories.length === 0) return null
152
+
153
+ if (categories.length <= 2) {
154
+ // Sort larger category to the left for visual balance
155
+ const sorted = [...categories].sort((a, b) =>
156
+ (grouped.get(b)?.length || 0) - (grouped.get(a)?.length || 0)
157
+ )
158
+ return (
159
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-4">
160
+ {sorted.map(cat => (
161
+ <CategoryBlock key={cat} category={cat} entries={grouped.get(cat)!} />
162
+ ))}
163
+ </div>
164
+ )
165
+ }
166
+
167
+ const [left, right] = balanceColumns(categories, grouped)
168
+ return (
169
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8">
170
+ <div className="space-y-4">
171
+ {left.map(cat => <CategoryBlock key={cat} category={cat} entries={grouped.get(cat)!} />)}
172
+ </div>
173
+ <div className="space-y-4">
174
+ {right.map(cat => <CategoryBlock key={cat} category={cat} entries={grouped.get(cat)!} />)}
175
+ </div>
176
+ </div>
177
+ )
178
+ }
179
+
180
+ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: ShortcutHelpOverlayProps) {
181
+ const shortcuts = useActiveShortcuts()
182
+ const overlayRef = useRef<HTMLDivElement>(null)
183
+
184
+ // Close on Escape
185
+ useEffect(() => {
186
+ const handler = (e: KeyboardEvent) => {
187
+ if (e.key === 'Escape') {
188
+ e.preventDefault()
189
+ e.stopPropagation()
190
+ onClose()
191
+ }
192
+ }
193
+ // Use capture to intercept before the shortcut system
194
+ document.addEventListener('keydown', handler, true)
195
+ return () => document.removeEventListener('keydown', handler, true)
196
+ }, [onClose])
197
+
198
+ // Group shortcuts by category, merging duplicates with same description
199
+ // (e.g., "Zoom in" registered for both + and = shows as one row)
200
+ const grouped = new Map<ShortcutCategory, ShortcutEntry[]>()
201
+ for (const s of shortcuts) {
202
+ if (s.id === 'help-toggle') continue
203
+ const list = grouped.get(s.category) || []
204
+ const existing = list.find(item => item.description === s.description)
205
+ if (existing) {
206
+ existing.keys.push(s.keys)
207
+ } else {
208
+ list.push({ description: s.description, keys: [s.keys] })
209
+ }
210
+ grouped.set(s.category, list)
211
+ }
212
+
213
+ // Split categories into sections
214
+ const globalCategories = GLOBAL_CATEGORIES.filter(c => grouped.has(c))
215
+ const viewCategories = VIEW_CATEGORY_ORDER.filter(
216
+ c => grouped.has(c) && !GLOBAL_CATEGORIES.includes(c) && !CONTEXT_CATEGORIES.includes(c)
217
+ )
218
+ const contextCategories = CONTEXT_CATEGORIES.filter(c => grouped.has(c))
219
+
220
+ const viewLabel = currentView ? VIEW_LABELS[currentView] : null
221
+ const hasViewSection = viewCategories.length > 0
222
+ const hasContextSection = contextCategories.length > 0
223
+ const isEmpty = globalCategories.length === 0 && !hasViewSection && !hasContextSection
224
+
225
+ return (
226
+ <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
227
+ {/* Backdrop */}
228
+ <div
229
+ className={clsx(
230
+ 'absolute inset-0 bg-theme-base/70 backdrop-blur-sm',
231
+ TRANSITION_BACKDROP,
232
+ isOpen ? 'opacity-100' : 'opacity-0'
233
+ )}
234
+ onClick={onClose}
235
+ />
236
+
237
+ {/* Panel */}
238
+ <div
239
+ ref={overlayRef}
240
+ className={clsx(
241
+ 'relative w-full max-w-2xl max-h-[80vh] dialog overflow-hidden flex flex-col',
242
+ TRANSITION_PANEL,
243
+ isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-[0.97]'
244
+ )}
245
+ >
246
+ {/* Header */}
247
+ <div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border">
248
+ <h2 className="text-base font-semibold text-theme-text-primary">Keyboard Shortcuts</h2>
249
+ <button
250
+ onClick={onClose}
251
+ className="p-1.5 rounded-md text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover transition-colors"
252
+ >
253
+ <X className="w-4 h-4" />
254
+ </button>
255
+ </div>
256
+
257
+ {/* Content */}
258
+ <div className="overflow-y-auto px-5 py-4 space-y-5">
259
+ {/* Global section — Navigation + General */}
260
+ <TwoColumnSection categories={globalCategories} grouped={grouped} />
261
+
262
+ {/* View-specific section */}
263
+ {hasViewSection && (
264
+ <>
265
+ {viewLabel && (
266
+ <div className="flex items-center gap-3">
267
+ <div className="h-px flex-1 bg-theme-border-light" />
268
+ <span className="text-[10px] font-semibold text-theme-text-tertiary uppercase tracking-wider">
269
+ {viewLabel}
270
+ </span>
271
+ <div className="h-px flex-1 bg-theme-border-light" />
272
+ </div>
273
+ )}
274
+ <TwoColumnSection categories={viewCategories} grouped={grouped} />
275
+ </>
276
+ )}
277
+
278
+ {/* Contextual section (Drawer, Dock) */}
279
+ {hasContextSection && (
280
+ <TwoColumnSection categories={contextCategories} grouped={grouped} />
281
+ )}
282
+
283
+ {/* Empty state */}
284
+ {isEmpty && (
285
+ <p className="text-sm text-theme-text-tertiary text-center py-8">
286
+ No keyboard shortcuts registered.
287
+ </p>
288
+ )}
289
+ </div>
290
+
291
+ {/* Footer */}
292
+ <div className="px-5 py-2.5 border-t border-theme-border bg-theme-surface/50">
293
+ <div className="flex items-center justify-between text-xs text-theme-text-tertiary">
294
+ <span>Press <KbdKey>?</KbdKey> to toggle this overlay</span>
295
+ <span>Press <KbdKey>Esc</KbdKey> to close</span>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </div>
300
+ )
301
+ }
@@ -0,0 +1 @@
1
+ export { ToastProvider, useToast, showApiError, showApiSuccess } from '@skyhook-io/k8s-ui'
@@ -0,0 +1 @@
1
+ export { Tooltip, WithTooltip } from '@skyhook-io/k8s-ui/components/ui/Tooltip'
@@ -0,0 +1,299 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { Download, X, Copy, Check, RotateCw, ArrowDownToLine, Loader2 } from 'lucide-react'
3
+ import { useQueryClient } from '@tanstack/react-query'
4
+ import {
5
+ useVersionCheck,
6
+ useStartDesktopUpdate,
7
+ useDesktopUpdateStatus,
8
+ useApplyDesktopUpdate,
9
+ } from '../../api/client'
10
+ import type { DesktopUpdateState } from '../../api/client'
11
+
12
+ const DISMISSED_KEY = 'radar-update-dismissed'
13
+
14
+ export function UpdateNotification() {
15
+ const queryClient = useQueryClient()
16
+ const { data: versionInfo } = useVersionCheck()
17
+ const [dismissed, setDismissed] = useState(false)
18
+ const [copied, setCopied] = useState(false)
19
+ const [copyFailed, setCopyFailed] = useState(false)
20
+
21
+ // Desktop update state
22
+ const [desktopUpdating, setDesktopUpdating] = useState(false)
23
+ const startUpdate = useStartDesktopUpdate()
24
+ const applyUpdate = useApplyDesktopUpdate()
25
+ const { data: updateStatus } = useDesktopUpdateStatus(desktopUpdating)
26
+
27
+ const isDesktop = versionInfo?.installMethod === 'desktop'
28
+
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.
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', () => {
38
+ setDismissed(false)
39
+ try { localStorage.removeItem(DISMISSED_KEY) } catch { /* ignore */ }
40
+ queryClient.invalidateQueries({ queryKey: ['version-check'] })
41
+ })
42
+
43
+ return cleanup
44
+ }, [queryClient])
45
+
46
+ // Log version check errors for debugging
47
+ useEffect(() => {
48
+ if (versionInfo?.error) {
49
+ console.debug('[radar] Version check failed:', versionInfo.error)
50
+ }
51
+ }, [versionInfo?.error])
52
+
53
+ // Check if this version was already dismissed
54
+ useEffect(() => {
55
+ if (versionInfo?.latestVersion) {
56
+ try {
57
+ const dismissedVersion = localStorage.getItem(DISMISSED_KEY)
58
+ if (dismissedVersion === versionInfo.latestVersion) {
59
+ setDismissed(true)
60
+ }
61
+ } catch {
62
+ // localStorage unavailable (e.g. Safari private mode)
63
+ }
64
+ }
65
+ }, [versionInfo?.latestVersion])
66
+
67
+ // Stop polling when update reaches a terminal state
68
+ useEffect(() => {
69
+ if (updateStatus?.state === 'error' || updateStatus?.state === 'idle') {
70
+ setDesktopUpdating(false)
71
+ }
72
+ }, [updateStatus?.state])
73
+
74
+ const handleDismiss = () => {
75
+ try {
76
+ if (versionInfo?.latestVersion) {
77
+ localStorage.setItem(DISMISSED_KEY, versionInfo.latestVersion)
78
+ }
79
+ } catch {
80
+ // localStorage unavailable — dismiss in-memory only
81
+ }
82
+ setDismissed(true)
83
+ }
84
+
85
+ const handleCopyCommand = async () => {
86
+ if (versionInfo?.updateCommand) {
87
+ try {
88
+ await navigator.clipboard.writeText(versionInfo.updateCommand)
89
+ setCopied(true)
90
+ setTimeout(() => setCopied(false), 2000)
91
+ } catch (err) {
92
+ console.debug('[radar] Clipboard write failed:', err)
93
+ setCopyFailed(true)
94
+ setTimeout(() => setCopyFailed(false), 2000)
95
+ }
96
+ }
97
+ }
98
+
99
+ const handleStartDesktopUpdate = () => {
100
+ startUpdate.mutate(undefined, {
101
+ onSuccess: () => setDesktopUpdating(true),
102
+ })
103
+ }
104
+
105
+ // Don't show if no update available, dismissed, or error
106
+ if (!versionInfo?.updateAvailable || dismissed) {
107
+ return null
108
+ }
109
+
110
+ // Determine what the current effective state is
111
+ const effectiveState: DesktopUpdateState = updateStatus?.state ?? 'idle'
112
+
113
+ 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">
115
+ <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">
117
+ <UpdateIcon state={effectiveState} />
118
+ </div>
119
+ <div className="flex-1 min-w-0">
120
+ <h4 className="text-sm font-medium text-theme-text-primary">
121
+ <UpdateTitle state={effectiveState} />
122
+ </h4>
123
+ <p className="text-xs text-theme-text-secondary mt-1">
124
+ Radar {versionInfo.latestVersion} is available.{' '}
125
+ You're on {versionInfo.currentVersion}.
126
+ </p>
127
+
128
+ {/* Desktop: in-app update flow */}
129
+ {isDesktop && (
130
+ <DesktopUpdateControls
131
+ state={effectiveState}
132
+ progress={updateStatus?.progress}
133
+ error={updateStatus?.error}
134
+ starting={startUpdate.isPending}
135
+ onStart={handleStartDesktopUpdate}
136
+ onApply={() => applyUpdate.mutate()}
137
+ onRetry={handleStartDesktopUpdate}
138
+ />
139
+ )}
140
+
141
+ {/* CLI: show update command with copy button for package managers */}
142
+ {!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>
150
+ ) : (
151
+ /* Direct download - show release link */
152
+ !isDesktop && versionInfo.releaseUrl && (
153
+ <a
154
+ href={versionInfo.releaseUrl}
155
+ target="_blank"
156
+ rel="noopener noreferrer"
157
+ className="inline-flex items-center gap-1 mt-2 text-xs font-medium text-blue-400 hover:text-blue-300"
158
+ >
159
+ Download from GitHub →
160
+ </a>
161
+ )
162
+ )}
163
+ </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
+ </div>
175
+ </div>
176
+ )
177
+ }
178
+
179
+ function CopyIcon({ copied, failed }: { copied: boolean; failed: boolean }) {
180
+ if (copied) return <Check className="w-3.5 h-3.5 text-green-400 shrink-0" />
181
+ if (failed) return <X className="w-3.5 h-3.5 text-red-400 shrink-0" />
182
+ return <Copy className="w-3.5 h-3.5 text-theme-text-tertiary shrink-0" />
183
+ }
184
+
185
+ function UpdateIcon({ state }: { state: DesktopUpdateState }) {
186
+ switch (state) {
187
+ case 'downloading':
188
+ case 'applying':
189
+ return <Loader2 className="w-4 h-4 text-blue-400 animate-spin" />
190
+ case 'ready':
191
+ return <ArrowDownToLine className="w-4 h-4 text-green-400" />
192
+ default:
193
+ return <Download className="w-4 h-4 text-blue-400" />
194
+ }
195
+ }
196
+
197
+ function UpdateTitle({ state }: { state: DesktopUpdateState }) {
198
+ switch (state) {
199
+ case 'ready':
200
+ return <>Update Ready</>
201
+ case 'applying':
202
+ return <>Applying Update...</>
203
+ default:
204
+ return <>Update Available</>
205
+ }
206
+ }
207
+
208
+ // DesktopUpdateControls renders the update action area for desktop installs.
209
+ function DesktopUpdateControls({
210
+ state,
211
+ progress,
212
+ error,
213
+ starting,
214
+ onStart,
215
+ onApply,
216
+ onRetry,
217
+ }: {
218
+ state: DesktopUpdateState
219
+ progress?: number
220
+ error?: string
221
+ starting?: boolean
222
+ onStart: () => void
223
+ onApply: () => void
224
+ onRetry: () => void
225
+ }) {
226
+ switch (state) {
227
+ case 'idle':
228
+ return (
229
+ <button
230
+ onClick={onStart}
231
+ disabled={starting}
232
+ className="mt-2 px-3 py-1.5 btn-brand text-xs font-medium rounded"
233
+ >
234
+ {starting ? (
235
+ <span className="inline-flex items-center gap-1.5">
236
+ <Loader2 className="w-3 h-3 animate-spin" />
237
+ Starting...
238
+ </span>
239
+ ) : (
240
+ 'Update Now'
241
+ )}
242
+ </button>
243
+ )
244
+
245
+ case 'downloading':
246
+ return (
247
+ <div className="mt-2 space-y-1">
248
+ <div className="w-full bg-theme-elevated rounded-full h-1.5 overflow-hidden">
249
+ <div
250
+ className="bg-blue-500 h-full rounded-full transition-all duration-300"
251
+ style={{ width: `${Math.round((progress ?? 0) * 100)}%` }}
252
+ />
253
+ </div>
254
+ <p className="text-xs text-theme-text-tertiary">
255
+ Downloading... {Math.round((progress ?? 0) * 100)}%
256
+ </p>
257
+ </div>
258
+ )
259
+
260
+ case 'ready':
261
+ return (
262
+ <div className="mt-2 flex gap-2">
263
+ <button
264
+ onClick={onApply}
265
+ className="px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-medium rounded transition-colors"
266
+ >
267
+ Restart Now
268
+ </button>
269
+ </div>
270
+ )
271
+
272
+ case 'applying':
273
+ return (
274
+ <div className="mt-2 flex items-center gap-2">
275
+ <Loader2 className="w-3.5 h-3.5 text-blue-400 animate-spin" />
276
+ <p className="text-xs text-theme-text-secondary">Applying update...</p>
277
+ </div>
278
+ )
279
+
280
+ case 'error':
281
+ return (
282
+ <div className="mt-2 space-y-1.5">
283
+ {!starting && <p className="text-xs text-red-400">{error || 'Update failed'}</p>}
284
+ <button
285
+ onClick={onRetry}
286
+ disabled={starting}
287
+ className="inline-flex items-center gap-1 px-3 py-1.5 bg-theme-elevated hover:bg-theme-surface-hover text-xs font-medium text-theme-text-primary rounded transition-colors disabled:opacity-50"
288
+ >
289
+ {starting ? (
290
+ <Loader2 className="w-3 h-3 animate-spin" />
291
+ ) : (
292
+ <RotateCw className="w-3 h-3" />
293
+ )}
294
+ {starting ? 'Starting...' : 'Retry'}
295
+ </button>
296
+ </div>
297
+ )
298
+ }
299
+ }
@@ -0,0 +1 @@
1
+ export { YamlEditor, YamlDiffEditor } from '@skyhook-io/k8s-ui'