@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,537 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { Settings, X, RotateCcw, Loader2, Copy, Check, Pin } from 'lucide-react'
4
+ import { clsx } from 'clsx'
5
+ import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
6
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
7
+ import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
8
+
9
+ interface Config {
10
+ kubeconfig?: string
11
+ kubeconfigDirs?: string[]
12
+ namespace?: string
13
+ port?: number
14
+ noBrowser?: boolean
15
+ timelineStorage?: 'memory' | 'sqlite'
16
+ timelineDbPath?: string
17
+ historyLimit?: number
18
+ prometheusUrl?: string
19
+ mcp?: boolean | null
20
+ }
21
+
22
+ interface ConfigResponse {
23
+ file: Config
24
+ effective: Config
25
+ isDesktop: boolean
26
+ }
27
+
28
+ interface SettingsDialogProps {
29
+ open: boolean
30
+ onClose: () => void
31
+ }
32
+
33
+ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
34
+ const dialogRef = useRef<HTMLDivElement>(null)
35
+ const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
36
+ const [configData, setConfigData] = useState<ConfigResponse | null>(null)
37
+ const [editedConfig, setEditedConfig] = useState<Config>({})
38
+ const [saving, setSaving] = useState(false)
39
+ const [saveMessage, setSaveMessage] = useState<string | null>(null)
40
+ const [configDirty, setConfigDirty] = useState(false)
41
+ const [loadError, setLoadError] = useState<string | null>(null)
42
+
43
+ // Load config on open
44
+ useEffect(() => {
45
+ if (!open) return
46
+ setSaveMessage(null)
47
+ setConfigDirty(false)
48
+ setLoadError(null)
49
+
50
+ fetch(apiUrl('/config'), { credentials: getCredentialsMode(), headers: getAuthHeaders() })
51
+ .then((res) => {
52
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
53
+ return res.json()
54
+ })
55
+ .then((data: ConfigResponse) => {
56
+ setConfigData(data)
57
+ setEditedConfig(data.file)
58
+ })
59
+ .catch((err) => {
60
+ console.warn('[settings] Failed to load config:', err)
61
+ setLoadError('Failed to load configuration.')
62
+ })
63
+ }, [open])
64
+
65
+ // ESC key
66
+ useEffect(() => {
67
+ if (!open) return
68
+ const handleKeyDown = (e: KeyboardEvent) => {
69
+ if (e.key === 'Escape') {
70
+ e.stopPropagation()
71
+ onClose()
72
+ }
73
+ }
74
+ document.addEventListener('keydown', handleKeyDown, true)
75
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
76
+ }, [open, onClose])
77
+
78
+ // Focus trap
79
+ useEffect(() => {
80
+ if (open && dialogRef.current) {
81
+ dialogRef.current.focus()
82
+ }
83
+ }, [open])
84
+
85
+ const updateConfigField = useCallback(<K extends keyof Config>(field: K, value: Config[K]) => {
86
+ setEditedConfig((prev) => ({ ...prev, [field]: value }))
87
+ setConfigDirty(true)
88
+ setSaveMessage(null)
89
+ }, [])
90
+
91
+ const saveConfig = useCallback(async () => {
92
+ setSaving(true)
93
+ setSaveMessage(null)
94
+ try {
95
+ const res = await fetch(apiUrl('/config'), {
96
+ method: 'PUT',
97
+ credentials: getCredentialsMode(),
98
+ headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
99
+ body: JSON.stringify(editedConfig),
100
+ })
101
+ if (!res.ok) {
102
+ const data = await res.json().catch(() => null)
103
+ setSaveMessage(`Error: ${data?.error || res.statusText}`)
104
+ } else {
105
+ setConfigDirty(false)
106
+ setSaveMessage('Saved. Changes take effect on next launch.')
107
+ }
108
+ } catch (err) {
109
+ setSaveMessage(`Error: ${err}`)
110
+ } finally {
111
+ setSaving(false)
112
+ }
113
+ }, [editedConfig])
114
+
115
+ const resetConfig = useCallback(() => {
116
+ setEditedConfig({})
117
+ setConfigDirty(true)
118
+ setSaveMessage('All fields cleared. Press Save to apply.')
119
+ }, [])
120
+
121
+ if (!shouldRender) return null
122
+
123
+ const isDesktop = configData?.isDesktop ?? false
124
+
125
+ return createPortal(
126
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
127
+ {/* Backdrop */}
128
+ <div
129
+ className={clsx(
130
+ 'absolute inset-0 bg-black/60 backdrop-blur-sm',
131
+ TRANSITION_BACKDROP,
132
+ isOpen ? 'opacity-100' : 'opacity-0'
133
+ )}
134
+ onClick={onClose}
135
+ />
136
+
137
+ {/* Dialog */}
138
+ <div
139
+ ref={dialogRef}
140
+ tabIndex={-1}
141
+ className={clsx(
142
+ 'relative bg-theme-surface border border-theme-border shadow-theme-lg w-full outline-none flex flex-col',
143
+ 'max-sm:inset-0 max-sm:absolute max-sm:rounded-none max-sm:max-h-full max-sm:border-0',
144
+ 'sm:rounded-xl sm:max-w-xl sm:mx-4 sm:max-h-[85vh]',
145
+ TRANSITION_PANEL,
146
+ isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
147
+ )}
148
+ >
149
+ {/* Header */}
150
+ <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
151
+ <div className="flex items-center gap-2">
152
+ <Settings className="w-5 h-5 text-theme-text-secondary" />
153
+ <h2 className="text-lg font-semibold text-theme-text-primary">Settings</h2>
154
+ </div>
155
+ <button
156
+ onClick={onClose}
157
+ className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
158
+ >
159
+ <X className="w-5 h-5" />
160
+ </button>
161
+ </div>
162
+
163
+ {/* Content */}
164
+ <div className="overflow-y-auto p-4 flex-1">
165
+ {loadError && (
166
+ <div className="mb-3 px-3 py-2 text-xs text-amber-700 dark:text-amber-300 bg-amber-500/10 border border-amber-500/20 rounded-md">
167
+ {loadError}
168
+ </div>
169
+ )}
170
+ <StartupConfigTab
171
+ config={editedConfig}
172
+ effectiveConfig={configData?.effective}
173
+ isDesktop={isDesktop}
174
+ onChange={updateConfigField}
175
+ />
176
+ </div>
177
+
178
+ {/* Footer */}
179
+ <div className="flex items-center justify-between gap-3 p-4 border-t border-theme-border shrink-0">
180
+ <div className="flex items-center gap-2">
181
+ <button
182
+ onClick={resetConfig}
183
+ disabled={saving}
184
+ className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-md transition-colors disabled:opacity-50"
185
+ title="Reset all configuration to defaults"
186
+ >
187
+ <RotateCcw className="w-3.5 h-3.5" />
188
+ Reset
189
+ </button>
190
+ {saveMessage && (
191
+ <span className={clsx(
192
+ 'text-xs',
193
+ saveMessage.startsWith('Error') ? 'text-red-400' : 'text-green-400'
194
+ )}>
195
+ {saveMessage}
196
+ </span>
197
+ )}
198
+ </div>
199
+ <button
200
+ onClick={saveConfig}
201
+ disabled={saving || !configDirty}
202
+ className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium btn-brand rounded-md"
203
+ >
204
+ {saving && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
205
+ Save
206
+ </button>
207
+ </div>
208
+ </div>
209
+ </div>,
210
+ document.body
211
+ )
212
+ }
213
+
214
+ // -- Startup Configuration Tab ------------------------------------------------
215
+
216
+ function StartupConfigTab({
217
+ config,
218
+ effectiveConfig,
219
+ isDesktop,
220
+ onChange,
221
+ }: {
222
+ config: Config
223
+ effectiveConfig?: Config
224
+ isDesktop: boolean
225
+ onChange: <K extends keyof Config>(field: K, value: Config[K]) => void
226
+ }) {
227
+ return (
228
+ <div className="space-y-4">
229
+ <p className="text-xs text-theme-text-tertiary">
230
+ Changes require a restart to take effect.
231
+ {isDesktop
232
+ ? ' Quit and relaunch Radar to apply.'
233
+ : ' Stop and restart the radar command to apply.'}
234
+ </p>
235
+
236
+ <ConfigField
237
+ label="Kubeconfig"
238
+ help="Path to kubeconfig file"
239
+ value={config.kubeconfig ?? ''}
240
+ effectiveValue={effectiveConfig?.kubeconfig}
241
+ placeholder="~/.kube/config"
242
+ onChange={(v) => onChange('kubeconfig', v || undefined)}
243
+ />
244
+
245
+ <ConfigField
246
+ label="Kubeconfig Directories"
247
+ help="Comma-separated directories containing kubeconfig files"
248
+ value={config.kubeconfigDirs?.join(', ') ?? ''}
249
+ effectiveValue={effectiveConfig?.kubeconfigDirs?.join(', ')}
250
+ placeholder="/path/to/dir1, /path/to/dir2"
251
+ onChange={(v) => onChange('kubeconfigDirs', v ? v.split(',').map(s => s.trim()).filter(Boolean) : undefined)}
252
+ />
253
+
254
+ <ConfigField
255
+ label="Default Namespace"
256
+ help="Initial namespace filter on startup"
257
+ value={config.namespace ?? ''}
258
+ effectiveValue={effectiveConfig?.namespace}
259
+ placeholder="All namespaces"
260
+ onChange={(v) => onChange('namespace', v || undefined)}
261
+ />
262
+
263
+ <ConfigNumberField
264
+ label="Port"
265
+ help={isDesktop
266
+ ? 'Fixed server port (leave empty for random). Set this to keep a stable MCP endpoint.'
267
+ : 'Server port'}
268
+ value={config.port}
269
+ effectiveValue={effectiveConfig?.port}
270
+ placeholder={isDesktop ? 'Random' : '9280'}
271
+ onChange={(v) => onChange('port', v)}
272
+ />
273
+
274
+ {!isDesktop && (
275
+ <ConfigToggle
276
+ label="Open browser on start"
277
+ value={!(config.noBrowser ?? false)}
278
+ onChange={(v) => onChange('noBrowser', !v ? true : undefined)}
279
+ />
280
+ )}
281
+
282
+ <div className="border-t border-theme-border pt-4 mt-4">
283
+ <h4 className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-3">Timeline</h4>
284
+
285
+ <div className="space-y-4">
286
+ <div>
287
+ <label className="block text-sm font-medium text-theme-text-primary mb-1">
288
+ Storage Backend
289
+ </label>
290
+ <select
291
+ value={config.timelineStorage ?? 'memory'}
292
+ onChange={(e) => onChange('timelineStorage', e.target.value === 'memory' ? undefined : e.target.value as 'sqlite')}
293
+ className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary focus:outline-none focus:border-blue-500"
294
+ >
295
+ <option value="memory">Memory (default)</option>
296
+ <option value="sqlite">SQLite (persistent)</option>
297
+ </select>
298
+ <EffectiveHint current={config.timelineStorage} effective={effectiveConfig?.timelineStorage} />
299
+ </div>
300
+
301
+ <ConfigNumberField
302
+ label="History Limit"
303
+ help="Maximum events to retain"
304
+ value={config.historyLimit}
305
+ effectiveValue={effectiveConfig?.historyLimit}
306
+ placeholder="10000"
307
+ onChange={(v) => onChange('historyLimit', v)}
308
+ />
309
+ </div>
310
+ </div>
311
+
312
+ <div className="border-t border-theme-border pt-4 mt-4">
313
+ <h4 className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-3">Integrations</h4>
314
+
315
+ <div className="space-y-4">
316
+ <ConfigField
317
+ label="Prometheus URL"
318
+ help="Manual Prometheus/VictoriaMetrics URL (skips auto-discovery)"
319
+ value={config.prometheusUrl ?? ''}
320
+ effectiveValue={effectiveConfig?.prometheusUrl}
321
+ placeholder="http://prometheus-server.monitoring:9090"
322
+ onChange={(v) => onChange('prometheusUrl', v || undefined)}
323
+ />
324
+
325
+ <MCPSection
326
+ mcpEnabled={config.mcp ?? true}
327
+ onToggle={(v) => onChange('mcp', v)}
328
+ isDesktop={isDesktop}
329
+ portPinned={config.port != null && config.port > 0}
330
+ onPinPort={(port) => onChange('port', port)}
331
+ />
332
+ </div>
333
+ </div>
334
+ </div>
335
+ )
336
+ }
337
+
338
+ // -- MCP Section --------------------------------------------------------------
339
+
340
+ function MCPSection({
341
+ mcpEnabled,
342
+ onToggle,
343
+ isDesktop,
344
+ portPinned,
345
+ onPinPort,
346
+ }: {
347
+ mcpEnabled: boolean
348
+ onToggle: (value: boolean) => void
349
+ isDesktop: boolean
350
+ portPinned: boolean
351
+ onPinPort: (port: number) => void
352
+ }) {
353
+ const [copied, setCopied] = useState(false)
354
+
355
+ const currentPort = Number(window.location.port) || 80
356
+ const mcpUrl = `http://localhost:${currentPort}/mcp`
357
+
358
+ const handleCopy = () => {
359
+ navigator.clipboard.writeText(mcpUrl)
360
+ setCopied(true)
361
+ setTimeout(() => setCopied(false), 2000)
362
+ }
363
+
364
+ const handlePinPort = () => {
365
+ onPinPort(currentPort)
366
+ }
367
+
368
+ return (
369
+ <div className="space-y-3">
370
+ <ConfigToggle
371
+ label="MCP Server (AI tools)"
372
+ value={mcpEnabled}
373
+ onChange={onToggle}
374
+ />
375
+
376
+ {mcpEnabled && (
377
+ <div className="space-y-2 pl-0.5">
378
+ <div>
379
+ <label className="block text-xs text-theme-text-secondary mb-1">MCP Endpoint</label>
380
+ <div className="flex items-center gap-2">
381
+ <code className="flex-1 px-2.5 py-1.5 text-xs font-mono bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary truncate">
382
+ {mcpUrl}
383
+ </code>
384
+ <button
385
+ onClick={handleCopy}
386
+ className="shrink-0 p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded-md transition-colors"
387
+ title="Copy MCP URL"
388
+ >
389
+ {copied ? <Check className="w-3.5 h-3.5 text-green-500" /> : <Copy className="w-3.5 h-3.5" />}
390
+ </button>
391
+ </div>
392
+ </div>
393
+
394
+ {isDesktop && !portPinned && (
395
+ <div className="flex items-start gap-2 px-2.5 py-2 text-xs bg-amber-500/10 border border-amber-500/20 rounded-md">
396
+ <span className="text-amber-700 dark:text-amber-300 flex-1">
397
+ Port changes on every restart. Pin it to keep a stable MCP endpoint.
398
+ </span>
399
+ <button
400
+ onClick={handlePinPort}
401
+ className="shrink-0 flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-amber-800 dark:text-amber-200 hover:text-amber-900 dark:hover:text-white bg-amber-500/20 hover:bg-amber-500/30 rounded transition-colors"
402
+ >
403
+ <Pin className="w-3 h-3" />
404
+ Pin port {currentPort}
405
+ </button>
406
+ </div>
407
+ )}
408
+
409
+ {isDesktop && portPinned && (
410
+ <p className="text-xs text-green-600 dark:text-green-400/80 px-0.5">
411
+ Port is pinned. MCP endpoint will remain stable across restarts.
412
+ </p>
413
+ )}
414
+ </div>
415
+ )}
416
+ </div>
417
+ )
418
+ }
419
+
420
+ // -- Shared Field Components --------------------------------------------------
421
+
422
+ function ConfigField({
423
+ label,
424
+ help,
425
+ value,
426
+ effectiveValue,
427
+ placeholder,
428
+ onChange,
429
+ }: {
430
+ label: string
431
+ help?: string
432
+ value: string
433
+ effectiveValue?: string
434
+ placeholder?: string
435
+ onChange: (value: string) => void
436
+ }) {
437
+ return (
438
+ <div>
439
+ <label className="block text-sm font-medium text-theme-text-primary mb-1">
440
+ {label}
441
+ </label>
442
+ {help && <p className="text-xs text-theme-text-tertiary mb-1">{help}</p>}
443
+ <input
444
+ type="text"
445
+ value={value}
446
+ onChange={(e) => onChange(e.target.value)}
447
+ placeholder={placeholder}
448
+ className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-blue-500"
449
+ />
450
+ <EffectiveHint current={value || undefined} effective={effectiveValue} />
451
+ </div>
452
+ )
453
+ }
454
+
455
+ function ConfigNumberField({
456
+ label,
457
+ help,
458
+ value,
459
+ effectiveValue,
460
+ placeholder,
461
+ onChange,
462
+ }: {
463
+ label: string
464
+ help?: string
465
+ value?: number
466
+ effectiveValue?: number
467
+ placeholder?: string
468
+ onChange: (value: number | undefined) => void
469
+ }) {
470
+ return (
471
+ <div>
472
+ <label className="block text-sm font-medium text-theme-text-primary mb-1">
473
+ {label}
474
+ </label>
475
+ {help && <p className="text-xs text-theme-text-tertiary mb-1">{help}</p>}
476
+ <input
477
+ type="number"
478
+ value={value ?? ''}
479
+ onChange={(e) => onChange(e.target.value ? parseInt(e.target.value, 10) || undefined : undefined)}
480
+ placeholder={placeholder}
481
+ className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-blue-500"
482
+ />
483
+ <EffectiveHint current={value} effective={effectiveValue} />
484
+ </div>
485
+ )
486
+ }
487
+
488
+ function ConfigToggle({
489
+ label,
490
+ value,
491
+ onChange,
492
+ }: {
493
+ label: string
494
+ value: boolean
495
+ onChange: (value: boolean) => void
496
+ }) {
497
+ return (
498
+ <label className="flex items-center justify-between py-1 cursor-pointer group">
499
+ <span className="text-sm text-theme-text-primary group-hover:text-theme-text-primary">{label}</span>
500
+ <button
501
+ role="switch"
502
+ aria-checked={value}
503
+ onClick={() => onChange(!value)}
504
+ className={clsx(
505
+ 'relative w-9 h-5 rounded-full transition-colors',
506
+ value ? 'bg-skyhook-600' : 'bg-theme-elevated border border-theme-border'
507
+ )}
508
+ >
509
+ <span
510
+ className={clsx(
511
+ 'absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform shadow-sm',
512
+ value && 'translate-x-4'
513
+ )}
514
+ />
515
+ </button>
516
+ </label>
517
+ )
518
+ }
519
+
520
+ function EffectiveHint({
521
+ current,
522
+ effective,
523
+ }: {
524
+ current?: string | number
525
+ effective?: string | number
526
+ }) {
527
+ if (!effective || effective === current) return null
528
+ const currentStr = current != null ? String(current) : ''
529
+ const effectiveStr = String(effective)
530
+ if (currentStr === effectiveStr) return null
531
+
532
+ return (
533
+ <p className="text-xs text-amber-600 dark:text-amber-400/80 mt-0.5">
534
+ Currently running: {effectiveStr} (restart to apply)
535
+ </p>
536
+ )
537
+ }
@@ -0,0 +1,17 @@
1
+ import { type ComponentProps } from 'react'
2
+ import { CreateResourceDialog as BaseCreateResourceDialog } from '@skyhook-io/k8s-ui'
3
+ import { useApplyResource } from '../../api/client'
4
+
5
+ type BaseProps = ComponentProps<typeof BaseCreateResourceDialog>
6
+
7
+ export function CreateResourceDialog(props: Omit<BaseProps, 'onApply' | 'isApplying'>) {
8
+ const applyResource = useApplyResource()
9
+
10
+ return (
11
+ <BaseCreateResourceDialog
12
+ {...props}
13
+ onApply={(params) => applyResource.mutateAsync(params)}
14
+ isApplying={applyResource.isPending}
15
+ />
16
+ )
17
+ }
@@ -0,0 +1,24 @@
1
+ import { type ComponentProps } from 'react'
2
+ import {
3
+ EditableYamlView as BaseEditableYamlView,
4
+ SaveSuccessAnimation,
5
+ } from '@skyhook-io/k8s-ui'
6
+ import { useUpdateResource } from '../../api/client'
7
+
8
+ // Re-export SaveSuccessAnimation as-is (pure component, no wrapper needed)
9
+ export { SaveSuccessAnimation }
10
+
11
+ type BaseProps = ComponentProps<typeof BaseEditableYamlView>
12
+
13
+ export function EditableYamlView(props: Omit<BaseProps, 'onSave' | 'isSaving' | 'saveError'>) {
14
+ const updateResource = useUpdateResource()
15
+
16
+ return (
17
+ <BaseEditableYamlView
18
+ {...props}
19
+ onSave={(params) => updateResource.mutateAsync(params)}
20
+ isSaving={updateResource.isPending}
21
+ saveError={updateResource.error?.message ?? null}
22
+ />
23
+ )
24
+ }
@@ -0,0 +1,70 @@
1
+ import { useState, useRef, useEffect, useMemo } from 'react'
2
+ import { Search } from 'lucide-react'
3
+
4
+ export function LargeClusterNamespacePicker({ namespaces, onSelect }: {
5
+ namespaces: { name: string }[] | undefined
6
+ onSelect: (ns: string) => void
7
+ }) {
8
+ const [search, setSearch] = useState('')
9
+ const inputRef = useRef<HTMLInputElement>(null)
10
+
11
+ useEffect(() => {
12
+ inputRef.current?.focus()
13
+ }, [])
14
+
15
+ const sorted = useMemo(() => {
16
+ if (!namespaces) return []
17
+ return [...namespaces].sort((a, b) => a.name.localeCompare(b.name))
18
+ }, [namespaces])
19
+
20
+ const filtered = useMemo(() => {
21
+ if (!search.trim()) return sorted
22
+ const q = search.toLowerCase()
23
+ return sorted.filter(ns => ns.name.toLowerCase().includes(q))
24
+ }, [sorted, search])
25
+
26
+ return (
27
+ <div className="text-left">
28
+ <div className="relative mb-2">
29
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
30
+ <input
31
+ ref={inputRef}
32
+ type="text"
33
+ value={search}
34
+ onChange={(e) => setSearch(e.target.value)}
35
+ placeholder="Search namespaces..."
36
+ className="w-full bg-theme-base text-theme-text-primary text-sm rounded-lg px-3 py-2 pl-9 border border-theme-border-light focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 placeholder:text-theme-text-tertiary"
37
+ />
38
+ </div>
39
+ <div className="max-h-[240px] overflow-y-auto rounded-lg border border-theme-border bg-theme-base">
40
+ {!namespaces ? (
41
+ <div className="px-3 py-6 text-center text-sm text-theme-text-tertiary">
42
+ Loading namespaces...
43
+ </div>
44
+ ) : filtered.length === 0 ? (
45
+ <div className="px-3 py-6 text-center text-sm text-theme-text-tertiary">
46
+ No namespaces match &ldquo;{search}&rdquo;
47
+ </div>
48
+ ) : (
49
+ filtered.map((ns) => (
50
+ <button
51
+ key={ns.name}
52
+ type="button"
53
+ onClick={() => onSelect(ns.name)}
54
+ className="w-full text-left px-3 py-2 text-sm text-theme-text-primary hover:bg-theme-hover transition-colors border-b border-theme-border last:border-b-0"
55
+ >
56
+ {ns.name}
57
+ </button>
58
+ ))
59
+ )}
60
+ </div>
61
+ {sorted.length > 0 && (
62
+ <p className="mt-2 text-xs text-theme-text-tertiary text-center">
63
+ {filtered.length === sorted.length
64
+ ? `${sorted.length} namespaces`
65
+ : `${filtered.length} of ${sorted.length} namespaces`}
66
+ </p>
67
+ )}
68
+ </div>
69
+ )
70
+ }
@@ -0,0 +1,31 @@
1
+ import { type ComponentProps } from 'react'
2
+ import {
3
+ ResourceRendererDispatch as BaseResourceRendererDispatch,
4
+ getResourceStatus,
5
+ } from '@skyhook-io/k8s-ui'
6
+ import { PrometheusCharts } from '../resource/PrometheusCharts'
7
+ import { useResourceEvents } from '../../api/client'
8
+
9
+ // Re-export getResourceStatus as-is (pure function, no wrapper needed)
10
+ export { getResourceStatus }
11
+
12
+ type BaseProps = ComponentProps<typeof BaseResourceRendererDispatch>
13
+
14
+ export function ResourceRendererDispatch(props: Omit<BaseProps, 'events' | 'eventsLoading' | 'renderMetrics'>) {
15
+ const { data: events, isLoading: eventsLoading } = useResourceEvents(
16
+ props.resource.kind,
17
+ props.resource.namespace,
18
+ props.resource.name
19
+ )
20
+
21
+ return (
22
+ <BaseResourceRendererDispatch
23
+ {...props}
24
+ events={events}
25
+ eventsLoading={eventsLoading}
26
+ renderMetrics={({ kind, namespace, name }) => (
27
+ <PrometheusCharts kind={kind} namespace={namespace} name={name} />
28
+ )}
29
+ />
30
+ )
31
+ }
@@ -0,0 +1 @@
1
+ export { DiffViewer, DiffBadge } from '@skyhook-io/k8s-ui'