@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,56 @@
1
+ import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
2
+
3
+ export interface ParsedContextInfo {
4
+ raw: string
5
+ provider: string | null
6
+ account: string | null
7
+ region: string | null
8
+ clusterName: string
9
+ }
10
+
11
+ interface ContextSwitchState {
12
+ isSwitching: boolean
13
+ targetContext: ParsedContextInfo | null
14
+ progressMessage: string | null
15
+ startSwitch: (context: ParsedContextInfo) => void
16
+ updateProgress: (message: string) => void
17
+ endSwitch: () => void
18
+ }
19
+
20
+ const ContextSwitchContext = createContext<ContextSwitchState | null>(null)
21
+
22
+ export function ContextSwitchProvider({ children }: { children: ReactNode }) {
23
+ const [isSwitching, setIsSwitching] = useState(false)
24
+ const [targetContext, setTargetContext] = useState<ParsedContextInfo | null>(null)
25
+ const [progressMessage, setProgressMessage] = useState<string | null>(null)
26
+
27
+ const startSwitch = useCallback((context: ParsedContextInfo) => {
28
+ setIsSwitching(true)
29
+ setTargetContext(context)
30
+ setProgressMessage(null)
31
+ }, [])
32
+
33
+ const updateProgress = useCallback((message: string) => {
34
+ setProgressMessage(message)
35
+ }, [])
36
+
37
+ const endSwitch = useCallback(() => {
38
+ setIsSwitching(false)
39
+ setTargetContext(null)
40
+ setProgressMessage(null)
41
+ }, [])
42
+
43
+ return (
44
+ <ContextSwitchContext.Provider value={{ isSwitching, targetContext, progressMessage, startSwitch, updateProgress, endSwitch }}>
45
+ {children}
46
+ </ContextSwitchContext.Provider>
47
+ )
48
+ }
49
+
50
+ export function useContextSwitch() {
51
+ const context = useContext(ContextSwitchContext)
52
+ if (!context) {
53
+ throw new Error('useContextSwitch must be used within ContextSwitchProvider')
54
+ }
55
+ return context
56
+ }
@@ -0,0 +1,62 @@
1
+ // Slot-based customization of Radar's top nav.
2
+ //
3
+ // Lets library consumers (e.g. Radar Hub) swap out the brand area, the
4
+ // context picker, and append items on the right of the action bar —
5
+ // without forking App.tsx or building a parallel nav.
6
+ //
7
+ // The `embedded` flag hides chrome that only makes sense for Radar's
8
+ // standalone OSS binary: GitHub star link, update-from-GitHub notifier,
9
+ // Radar's own OIDC/proxy-mode UserMenu. Consumers typically provide
10
+ // their own auth UI via `rightExtras`.
11
+ //
12
+ // Default (no provider): Radar renders its standalone nav unchanged.
13
+ import { createContext, useContext } from 'react';
14
+ import type { ReactNode } from 'react';
15
+
16
+ interface NavCustomizationBase {
17
+ /** Replaces Radar's Skyhook/radar logo + wordmark. */
18
+ brandSlot?: ReactNode;
19
+ /** Replaces the ContextSwitcher (kubeconfig-context picker). */
20
+ contextSlot?: ReactNode;
21
+ }
22
+
23
+ /**
24
+ * Slot-based customization of Radar's top nav.
25
+ *
26
+ * Standalone-mode consumers pass `embedded: false` (or omit it) and may
27
+ * optionally append items via `rightExtras`. Embedded-mode consumers must
28
+ * supply `rightExtras` — Radar's OSS chrome (GitHub star, update notifier,
29
+ * built-in UserMenu) is hidden, so the host app owns the right side of the
30
+ * nav and must render its own user/auth UI there.
31
+ */
32
+ export type NavCustomization =
33
+ | (NavCustomizationBase & {
34
+ embedded?: false;
35
+ /** Appended to the right of the action bar (before the UserMenu). */
36
+ rightExtras?: ReactNode;
37
+ })
38
+ | (NavCustomizationBase & {
39
+ embedded: true;
40
+ /** Required in embedded mode: Radar's own UserMenu is hidden. */
41
+ rightExtras: ReactNode;
42
+ });
43
+
44
+ const NavCustomizationContext = createContext<NavCustomization>({});
45
+
46
+ export function NavCustomizationProvider({
47
+ value,
48
+ children,
49
+ }: {
50
+ value: NavCustomization | undefined;
51
+ children: ReactNode;
52
+ }) {
53
+ return (
54
+ <NavCustomizationContext.Provider value={value ?? {}}>
55
+ {children}
56
+ </NavCustomizationContext.Provider>
57
+ );
58
+ }
59
+
60
+ export function useNavCustomization(): NavCustomization {
61
+ return useContext(NavCustomizationContext);
62
+ }
@@ -0,0 +1,97 @@
1
+ import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
2
+ import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
3
+
4
+ type Theme = 'dark' | 'light'
5
+
6
+ interface ThemeContextType {
7
+ theme: Theme
8
+ setTheme: (theme: Theme) => void
9
+ toggleTheme: () => void
10
+ }
11
+
12
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
13
+
14
+ const THEME_STORAGE_KEY = 'radar-theme'
15
+
16
+ function getInitialTheme(): Theme {
17
+ // Check localStorage first
18
+ if (typeof window !== 'undefined') {
19
+ const stored = localStorage.getItem(THEME_STORAGE_KEY)
20
+ if (stored === 'light' || stored === 'dark') {
21
+ return stored
22
+ }
23
+ // Check system preference
24
+ if (window.matchMedia('(prefers-color-scheme: light)').matches) {
25
+ return 'light'
26
+ }
27
+ }
28
+ return 'dark' // Default to dark
29
+ }
30
+
31
+ export function ThemeProvider({ children }: { children: ReactNode }) {
32
+ const [theme, setThemeState] = useState<Theme>(getInitialTheme)
33
+
34
+ const setTheme = (newTheme: Theme) => {
35
+ setThemeState(newTheme)
36
+ localStorage.setItem(THEME_STORAGE_KEY, newTheme)
37
+ fetch(apiUrl('/settings'), {
38
+ method: 'PUT',
39
+ credentials: getCredentialsMode(),
40
+ headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
41
+ body: JSON.stringify({ theme: newTheme }),
42
+ }).then((res) => {
43
+ if (!res.ok) console.warn('[settings] Failed to persist theme:', res.status)
44
+ }).catch((err) => console.warn('[settings] Failed to persist theme:', err))
45
+ }
46
+
47
+ const toggleTheme = () => {
48
+ setTheme(theme === 'dark' ? 'light' : 'dark')
49
+ }
50
+
51
+ // Apply theme to document
52
+ useEffect(() => {
53
+ document.documentElement.classList.toggle('dark', theme === 'dark')
54
+ document.documentElement.style.colorScheme = theme
55
+ }, [theme])
56
+
57
+ // Sync theme from server (persisted settings survive port changes in desktop app)
58
+ useEffect(() => {
59
+ fetch(apiUrl('/settings'), { credentials: getCredentialsMode(), headers: getAuthHeaders() })
60
+ .then((res) => res.ok ? res.json() : null)
61
+ .then((data) => {
62
+ if (data?.theme && (data.theme === 'dark' || data.theme === 'light') && data.theme !== theme) {
63
+ setThemeState(data.theme)
64
+ localStorage.setItem(THEME_STORAGE_KEY, data.theme)
65
+ }
66
+ })
67
+ .catch((err) => console.warn('[settings] Failed to load theme from server:', err))
68
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
69
+
70
+ // Listen for system theme changes
71
+ useEffect(() => {
72
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: light)')
73
+ const handleChange = (e: MediaQueryListEvent) => {
74
+ // Only auto-switch if user hasn't explicitly set a preference
75
+ const stored = localStorage.getItem(THEME_STORAGE_KEY)
76
+ if (!stored) {
77
+ setThemeState(e.matches ? 'light' : 'dark')
78
+ }
79
+ }
80
+ mediaQuery.addEventListener('change', handleChange)
81
+ return () => mediaQuery.removeEventListener('change', handleChange)
82
+ }, [])
83
+
84
+ return (
85
+ <ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
86
+ {children}
87
+ </ThemeContext.Provider>
88
+ )
89
+ }
90
+
91
+ export function useTheme() {
92
+ const context = useContext(ThemeContext)
93
+ if (context === undefined) {
94
+ throw new Error('useTheme must be used within a ThemeProvider')
95
+ }
96
+ return context
97
+ }
@@ -0,0 +1,130 @@
1
+ import { createContext, useContext, useMemo, ReactNode } from 'react'
2
+ import { useCapabilities, useNamespaceCapabilities } from '../api/client'
3
+ import type { Capabilities, ResourcePermissions } from '../types'
4
+
5
+ // Default capabilities for local development (when running locally, all features work)
6
+ const defaultCapabilities: Capabilities = {
7
+ exec: true,
8
+ localTerminal: true,
9
+ logs: true,
10
+ portForward: true,
11
+ secrets: true,
12
+ secretsUpdate: true,
13
+ helmWrite: true,
14
+ nodeWrite: true,
15
+ mcpEnabled: true,
16
+ }
17
+
18
+ // Restricted capabilities for error/failure cases (fail-closed)
19
+ const restrictedCapabilities: Capabilities = {
20
+ exec: false,
21
+ localTerminal: false,
22
+ logs: false,
23
+ portForward: false,
24
+ secrets: false,
25
+ secretsUpdate: false,
26
+ helmWrite: false,
27
+ nodeWrite: false,
28
+ mcpEnabled: false,
29
+ }
30
+
31
+ const CapabilitiesContext = createContext<Capabilities>(defaultCapabilities)
32
+
33
+ export function CapabilitiesProvider({ children }: { children: ReactNode }) {
34
+ const { data: capabilities, error } = useCapabilities()
35
+
36
+ // Determine which capabilities to use:
37
+ // 1. If we have fetched capabilities, use them
38
+ // 2. If there's an error, use restricted (fail-closed)
39
+ // 3. If still loading, use defaults (assumes local dev where everything works)
40
+ let value: Capabilities
41
+ if (capabilities) {
42
+ value = capabilities
43
+ } else if (error) {
44
+ // Log error for debugging and use restricted capabilities
45
+ console.error('Failed to fetch capabilities, using restricted mode:', error)
46
+ value = restrictedCapabilities
47
+ } else {
48
+ // Still loading - use defaults for smooth UX
49
+ value = defaultCapabilities
50
+ }
51
+
52
+ return (
53
+ <CapabilitiesContext.Provider value={value}>
54
+ {children}
55
+ </CapabilitiesContext.Provider>
56
+ )
57
+ }
58
+
59
+ export function useCapabilitiesContext(): Capabilities {
60
+ return useContext(CapabilitiesContext)
61
+ }
62
+
63
+ // Convenience hooks for specific capabilities
64
+ export function useCanExec(): boolean {
65
+ return useContext(CapabilitiesContext).exec
66
+ }
67
+
68
+ export function useCanViewLogs(): boolean {
69
+ return useContext(CapabilitiesContext).logs
70
+ }
71
+
72
+ export function useCanPortForward(): boolean {
73
+ return useContext(CapabilitiesContext).portForward
74
+ }
75
+
76
+ export function useCanViewSecrets(): boolean {
77
+ return useContext(CapabilitiesContext).secrets
78
+ }
79
+
80
+ export function useCanUpdateSecrets(): boolean {
81
+ return useContext(CapabilitiesContext).secretsUpdate
82
+ }
83
+
84
+ export function useCanHelmWrite(): boolean {
85
+ return useContext(CapabilitiesContext).helmWrite
86
+ }
87
+
88
+ export function useCanNodeWrite(): boolean {
89
+ return useContext(CapabilitiesContext).nodeWrite
90
+ }
91
+
92
+ // RBAC resource permission hooks
93
+ export function useResourcePermissions(): ResourcePermissions | undefined {
94
+ return useContext(CapabilitiesContext).resources
95
+ }
96
+
97
+ export function useRestrictedResources(): string[] {
98
+ const resources = useContext(CapabilitiesContext).resources
99
+ return useMemo(() => {
100
+ if (!resources) return []
101
+ return Object.entries(resources)
102
+ .filter(([, allowed]) => !allowed)
103
+ .map(([kind]) => kind)
104
+ }, [resources])
105
+ }
106
+
107
+ export function useHasLimitedAccess(): boolean {
108
+ const resources = useContext(CapabilitiesContext).resources
109
+ if (!resources) return false
110
+ return Object.values(resources).some(allowed => !allowed)
111
+ }
112
+
113
+ // Namespace-scoped capability hooks: lazily re-check exec/logs/portForward
114
+ // scoped to a specific namespace when global RBAC checks denied them.
115
+ // Falls back to global capability values while the namespace check is loading
116
+ // or when all capabilities are already granted.
117
+ export function useNamespacedCapabilities(namespace: string | undefined) {
118
+ const globalCaps = useContext(CapabilitiesContext)
119
+ const { data: nsCaps, error } = useNamespaceCapabilities(namespace, globalCaps)
120
+
121
+ if (error) {
122
+ console.warn(`Failed to fetch namespace capabilities for ${namespace}, using global:`, error)
123
+ }
124
+
125
+ return useMemo(() => ({
126
+ canExec: nsCaps?.exec ?? globalCaps.exec,
127
+ canViewLogs: nsCaps?.logs ?? globalCaps.logs,
128
+ canPortForward: nsCaps?.portForward ?? globalCaps.portForward,
129
+ }), [globalCaps.exec, globalCaps.logs, globalCaps.portForward, nsCaps])
130
+ }
@@ -0,0 +1 @@
1
+ export { useAnimatedUnmount } from '@skyhook-io/k8s-ui/hooks/useAnimatedUnmount'
@@ -0,0 +1,41 @@
1
+ import { createElement, useState, useEffect, useCallback } from 'react'
2
+ import { FolderOpen } from 'lucide-react'
3
+ import { useToast } from '../components/ui/Toast'
4
+ import { isDesktopApp, desktopSaveFile } from '../utils/desktop-download'
5
+ import { openFile, openFolder } from '../utils/desktop-open-folder'
6
+
7
+ /**
8
+ * Returns a download override function when running in the desktop app,
9
+ * or undefined when running in a browser (so the default blob URL approach is used).
10
+ * The returned function shows toast notifications for success/failure and silently handles user cancellation.
11
+ */
12
+ export function useDesktopDownload(): ((content: string, mime: string, filename: string) => void) | undefined {
13
+ const [isDesktop, setIsDesktop] = useState(false)
14
+ const { showSuccess, showError } = useToast()
15
+
16
+ useEffect(() => {
17
+ isDesktopApp().then(setIsDesktop)
18
+ }, [])
19
+
20
+ const download = useCallback((content: string, _mime: string, filename: string) => {
21
+ desktopSaveFile(content, filename)
22
+ .then((path) => showSuccess(
23
+ 'File saved',
24
+ path,
25
+ {
26
+ label: 'Show in Finder',
27
+ icon: createElement(FolderOpen, { className: 'w-3.5 h-3.5' }),
28
+ onClick: () => openFolder(path),
29
+ },
30
+ () => openFile(path),
31
+ ))
32
+ .catch((err: Error) => {
33
+ if (err.message !== 'cancelled') {
34
+ showError('Save failed', err.message)
35
+ }
36
+ })
37
+ }, [showSuccess, showError])
38
+
39
+ if (!isDesktop) return undefined
40
+ return download
41
+ }
@@ -0,0 +1,262 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react'
2
+ import type { Topology, K8sEvent, ViewMode } from '../types'
3
+ import type { ConnectionState } from '../context/ConnectionContext'
4
+ import { getApiBase, getCredentialsMode } from '../api/config'
5
+
6
+ interface UseEventSourceReturn {
7
+ topology: Topology | null
8
+ events: K8sEvent[]
9
+ connected: boolean
10
+ reconnect: () => void
11
+ }
12
+
13
+ interface UseEventSourceOptions {
14
+ onContextSwitchComplete?: () => void
15
+ onContextSwitchProgress?: (message: string) => void
16
+ onContextChanged?: (context: string) => void
17
+ onConnectionStateChange?: (status: ConnectionState) => void
18
+ onDeferredReady?: () => void
19
+ onK8sEvent?: (event: K8sEvent) => void
20
+ }
21
+
22
+ const MAX_EVENTS = 100 // Keep last 100 events
23
+
24
+ // Dynamic throttle based on cluster size - fast for small, protective for large
25
+ function getTopologyThrottleMs(nodeCount: number): number {
26
+ if (nodeCount < 100) return 500 // Small clusters: 0.5s
27
+ if (nodeCount < 300) return 1000 // Medium clusters: 1s
28
+ if (nodeCount < 500) return 2000 // Large clusters: 2s
29
+ return 3000 // Very large clusters: 3s
30
+ }
31
+ const INITIAL_RECONNECT_DELAY_MS = 3000
32
+ const MAX_RECONNECT_DELAY_MS = 30000 // Cap at 30 seconds
33
+
34
+ export function useEventSource(
35
+ namespaces: string[],
36
+ viewMode: ViewMode = 'resources',
37
+ options?: UseEventSourceOptions,
38
+ /** When set, SSE reconnects with this namespace filter (for large clusters that require server-side filtering) */
39
+ forceNamespaceFilter?: string[],
40
+ /** When true, evaluates NetworkPolicies and annotates edges with allow/block/unprotected */
41
+ showPolicyEffect?: boolean,
42
+ ): UseEventSourceReturn {
43
+ const [topology, setTopology] = useState<Topology | null>(null)
44
+ const [events, setEvents] = useState<K8sEvent[]>([])
45
+ const [connected, setConnected] = useState(false)
46
+ const eventSourceRef = useRef<EventSource | null>(null)
47
+ const reconnectTimeoutRef = useRef<number | null>(null)
48
+ const waitingForTopologyAfterSwitch = useRef(false)
49
+ const reconnectDelayRef = useRef(INITIAL_RECONNECT_DELAY_MS) // Exponential backoff
50
+
51
+ // Throttling state for topology updates
52
+ const lastTopologyUpdateRef = useRef<number>(0)
53
+ const pendingTopologyRef = useRef<Topology | null>(null)
54
+ const throttleTimeoutRef = useRef<number | null>(null)
55
+ const currentNodeCountRef = useRef<number>(0) // Track node count for dynamic throttle
56
+
57
+ // Serialize namespaces for stable dependency (used for events clearing)
58
+ const namespacesKey = namespaces.join(',')
59
+
60
+ // SSE namespace filter: only used for large clusters that require server-side filtering.
61
+ // Small/medium clusters get all-namespace data and filter on the frontend.
62
+ const sseNamespaceFilter = forceNamespaceFilter?.join(',') || ''
63
+
64
+ // Use ref to avoid stale closures while not triggering reconnection on callback changes
65
+ const optionsRef = useRef(options)
66
+ optionsRef.current = options
67
+
68
+ const connect = useCallback(() => {
69
+ // Clean up existing connection
70
+ if (eventSourceRef.current) {
71
+ eventSourceRef.current.close()
72
+ // Clear stale topology so consumers show loading state instead of old data
73
+ setTopology(null)
74
+ }
75
+ if (reconnectTimeoutRef.current) {
76
+ clearTimeout(reconnectTimeoutRef.current)
77
+ }
78
+
79
+ // Build URL — only pass namespace filter for large clusters (forceNamespaceFilter)
80
+ const params = new URLSearchParams()
81
+ if (sseNamespaceFilter) {
82
+ params.set('namespaces', sseNamespaceFilter)
83
+ }
84
+ if (viewMode && viewMode !== 'resources') {
85
+ params.set('view', viewMode)
86
+ }
87
+ if (showPolicyEffect) {
88
+ params.set('policyEffect', 'true')
89
+ }
90
+ const url = `${getApiBase()}/events/stream${params.toString() ? `?${params}` : ''}`
91
+
92
+ // Mirror the fetch credentials mode: 'include' sets withCredentials so
93
+ // cookies flow cross-origin (embedded in Radar Hub); 'omit' turns them
94
+ // off (pure-bearer auth). Default same-origin standalone behaves as
95
+ // before.
96
+ const es = new EventSource(url, { withCredentials: getCredentialsMode() === 'include' })
97
+ eventSourceRef.current = es
98
+
99
+ es.onopen = () => {
100
+ console.log('SSE connected')
101
+ setConnected(true)
102
+ // Reset backoff on successful connection
103
+ reconnectDelayRef.current = INITIAL_RECONNECT_DELAY_MS
104
+ }
105
+
106
+ es.onerror = (error) => {
107
+ console.error('SSE error:', error)
108
+ setConnected(false)
109
+ es.close()
110
+
111
+ // Reconnect with exponential backoff
112
+ const delay = reconnectDelayRef.current
113
+ reconnectTimeoutRef.current = window.setTimeout(() => {
114
+ console.log(`SSE reconnecting after ${delay}ms...`)
115
+ connect()
116
+ }, delay)
117
+ // Increase delay for next attempt (exponential backoff with cap)
118
+ reconnectDelayRef.current = Math.min(delay * 1.5, MAX_RECONNECT_DELAY_MS)
119
+ }
120
+
121
+ // Handle topology updates with dynamic throttling based on cluster size
122
+ es.addEventListener('topology', (event) => {
123
+ try {
124
+ const data = JSON.parse(event.data) as Topology
125
+ const now = Date.now()
126
+ const timeSinceLastUpdate = now - lastTopologyUpdateRef.current
127
+
128
+ // Update node count for dynamic throttle calculation
129
+ currentNodeCountRef.current = data.nodes?.length || 0
130
+ const throttleMs = getTopologyThrottleMs(currentNodeCountRef.current)
131
+
132
+ // If waiting for topology after context switch, update immediately
133
+ if (waitingForTopologyAfterSwitch.current) {
134
+ waitingForTopologyAfterSwitch.current = false
135
+ lastTopologyUpdateRef.current = now
136
+ setTopology(data)
137
+ optionsRef.current?.onContextSwitchComplete?.()
138
+ return
139
+ }
140
+
141
+ // Throttle updates: if we updated recently, queue this update
142
+ if (timeSinceLastUpdate < throttleMs) {
143
+ pendingTopologyRef.current = data
144
+
145
+ // Schedule update for when throttle period ends (if not already scheduled)
146
+ if (!throttleTimeoutRef.current) {
147
+ const delay = throttleMs - timeSinceLastUpdate
148
+ throttleTimeoutRef.current = window.setTimeout(() => {
149
+ throttleTimeoutRef.current = null
150
+ if (pendingTopologyRef.current) {
151
+ lastTopologyUpdateRef.current = Date.now()
152
+ currentNodeCountRef.current = pendingTopologyRef.current.nodes?.length || 0
153
+ setTopology(pendingTopologyRef.current)
154
+ pendingTopologyRef.current = null
155
+ }
156
+ }, delay)
157
+ }
158
+ } else {
159
+ // Enough time has passed, update immediately
160
+ lastTopologyUpdateRef.current = now
161
+ pendingTopologyRef.current = null
162
+ setTopology(data)
163
+ }
164
+ } catch (e) {
165
+ console.error('Failed to parse topology:', e)
166
+ }
167
+ })
168
+
169
+ // Handle K8s events
170
+ es.addEventListener('k8s_event', (event) => {
171
+ try {
172
+ const data = JSON.parse(event.data) as K8sEvent
173
+ data.timestamp = Date.now()
174
+ setEvents((prev) => [data, ...prev].slice(0, MAX_EVENTS))
175
+ optionsRef.current?.onK8sEvent?.(data)
176
+ } catch (e) {
177
+ console.error('Failed to parse event:', e)
178
+ }
179
+ })
180
+
181
+ // Handle heartbeat (just log, keeps connection alive)
182
+ es.addEventListener('heartbeat', () => {
183
+ // Connection is alive
184
+ })
185
+
186
+ // Handle context switch progress events
187
+ es.addEventListener('context_switch_progress', (event) => {
188
+ try {
189
+ const data = JSON.parse(event.data) as { message: string }
190
+ optionsRef.current?.onContextSwitchProgress?.(data.message)
191
+ } catch (e) {
192
+ console.error('Failed to parse context_switch_progress event:', e)
193
+ }
194
+ })
195
+
196
+ // Handle context changed event - clear state while new data loads
197
+ es.addEventListener('context_changed', (event) => {
198
+ try {
199
+ const data = JSON.parse(event.data) as { context: string }
200
+ console.log('Context changed to:', data.context)
201
+ // Clear topology and events - new data will come via topology event
202
+ setTopology(null)
203
+ setEvents([])
204
+ // Mark that we're waiting for new topology data
205
+ waitingForTopologyAfterSwitch.current = true
206
+ // Notify caller to invalidate caches (e.g., helm releases, resources)
207
+ optionsRef.current?.onContextChanged?.(data.context)
208
+ } catch (e) {
209
+ console.error('Failed to parse context_changed event:', e)
210
+ }
211
+ })
212
+
213
+ // Handle deferred informer sync completion — refetch dashboard data
214
+ es.addEventListener('deferred_ready', () => {
215
+ optionsRef.current?.onDeferredReady?.()
216
+ })
217
+
218
+ // Handle connection state events (for graceful startup)
219
+ es.addEventListener('connection_state', (event) => {
220
+ try {
221
+ const data = JSON.parse(event.data) as ConnectionState
222
+ optionsRef.current?.onConnectionStateChange?.(data)
223
+ } catch (e) {
224
+ console.error('Failed to parse connection_state event:', e)
225
+ }
226
+ })
227
+ }, [sseNamespaceFilter, viewMode, showPolicyEffect])
228
+
229
+ // Reconnect function for manual reconnection
230
+ const reconnect = useCallback(() => {
231
+ connect()
232
+ }, [connect])
233
+
234
+ // Connect on mount and when namespaces/viewMode changes
235
+ useEffect(() => {
236
+ connect()
237
+
238
+ return () => {
239
+ if (eventSourceRef.current) {
240
+ eventSourceRef.current.close()
241
+ }
242
+ if (reconnectTimeoutRef.current) {
243
+ clearTimeout(reconnectTimeoutRef.current)
244
+ }
245
+ if (throttleTimeoutRef.current) {
246
+ clearTimeout(throttleTimeoutRef.current)
247
+ }
248
+ }
249
+ }, [connect])
250
+
251
+ // Clear events when namespaces change
252
+ useEffect(() => {
253
+ setEvents([])
254
+ }, [namespacesKey])
255
+
256
+ return {
257
+ topology,
258
+ events,
259
+ connected,
260
+ reconnect,
261
+ }
262
+ }