@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,162 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { X, Plus, Trash2 } from 'lucide-react'
3
+ import { useAuditSettings, useUpdateAuditSettings, useAudit } from '../../api/client'
4
+ import type { CheckMeta } from '@skyhook-io/k8s-ui'
5
+
6
+ interface AuditSettingsDialogProps {
7
+ namespaces: string[]
8
+ onClose: () => void
9
+ }
10
+
11
+ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialogProps) {
12
+ const { data: settings } = useAuditSettings()
13
+ const { data: auditData } = useAudit(namespaces)
14
+ const updateSettings = useUpdateAuditSettings()
15
+ const [ignoredNs, setIgnoredNs] = useState<string[]>([])
16
+ const [disabledChecks, setDisabledChecks] = useState<string[]>([])
17
+ const [newNs, setNewNs] = useState('')
18
+
19
+ useEffect(() => {
20
+ if (settings) {
21
+ setIgnoredNs(settings.ignoredNamespaces || [])
22
+ setDisabledChecks(settings.disabledChecks || [])
23
+ }
24
+ }, [settings])
25
+
26
+ // Get all available checks from the audit response
27
+ const allChecks: CheckMeta[] = auditData?.checks
28
+ ? Object.values(auditData.checks).sort((a, b) => a.title.localeCompare(b.title))
29
+ : []
30
+
31
+ const addNamespace = () => {
32
+ const ns = newNs.trim()
33
+ if (ns && !ignoredNs.includes(ns)) {
34
+ setIgnoredNs([...ignoredNs, ns])
35
+ setNewNs('')
36
+ }
37
+ }
38
+
39
+ const toggleCheck = (checkID: string) => {
40
+ if (disabledChecks.includes(checkID)) {
41
+ setDisabledChecks(disabledChecks.filter(c => c !== checkID))
42
+ } else {
43
+ setDisabledChecks([...disabledChecks, checkID])
44
+ }
45
+ }
46
+
47
+ const handleSave = () => {
48
+ updateSettings.mutate(
49
+ { ignoredNamespaces: ignoredNs, disabledChecks },
50
+ { onSuccess: () => onClose() },
51
+ )
52
+ }
53
+
54
+ return (
55
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
56
+ <div className="bg-theme-surface rounded-xl shadow-xl w-full max-w-lg mx-4 max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
57
+ <div className="flex items-center justify-between px-5 py-4 border-b border-theme-border shrink-0">
58
+ <h2 className="text-sm font-semibold text-theme-text-primary">Audit Settings</h2>
59
+ <button onClick={onClose} className="p-1 rounded-lg hover:bg-theme-hover transition-colors">
60
+ <X className="w-4 h-4 text-theme-text-tertiary" />
61
+ </button>
62
+ </div>
63
+
64
+ <div className="px-5 py-4 overflow-y-auto flex-1">
65
+ {/* Ignored Namespaces */}
66
+ <div className="mb-6">
67
+ <label className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider">
68
+ Ignored Namespaces
69
+ </label>
70
+ <p className="text-xs text-theme-text-tertiary mt-1 mb-3">
71
+ Findings in these namespaces are hidden from all views.
72
+ </p>
73
+
74
+ <div className="flex flex-col gap-1.5 mb-3">
75
+ {ignoredNs.map(ns => (
76
+ <div key={ns} className="flex items-center justify-between px-3 py-1.5 bg-theme-elevated rounded-lg">
77
+ <span className="text-sm text-theme-text-primary">{ns}</span>
78
+ <button
79
+ onClick={() => setIgnoredNs(ignoredNs.filter(n => n !== ns))}
80
+ className="p-1 rounded hover:bg-theme-hover text-theme-text-tertiary hover:text-red-400 transition-colors"
81
+ >
82
+ <Trash2 className="w-3.5 h-3.5" />
83
+ </button>
84
+ </div>
85
+ ))}
86
+ {ignoredNs.length === 0 && (
87
+ <div className="text-xs text-theme-text-tertiary py-2">No namespaces ignored.</div>
88
+ )}
89
+ </div>
90
+
91
+ <div className="flex gap-2">
92
+ <input
93
+ type="text"
94
+ value={newNs}
95
+ onChange={e => setNewNs(e.target.value)}
96
+ onKeyDown={e => { if (e.key === 'Enter') addNamespace() }}
97
+ placeholder="Add namespace..."
98
+ className="flex-1 px-3 py-1.5 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-skyhook-500"
99
+ />
100
+ <button
101
+ onClick={addNamespace}
102
+ disabled={!newNs.trim()}
103
+ className="px-3 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
104
+ >
105
+ <Plus className="w-4 h-4" />
106
+ </button>
107
+ </div>
108
+ </div>
109
+
110
+ {/* Disabled Checks */}
111
+ <div>
112
+ <label className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider">
113
+ Enabled Checks
114
+ </label>
115
+ <p className="text-xs text-theme-text-tertiary mt-1 mb-3">
116
+ Uncheck to disable specific checks globally across all views.
117
+ </p>
118
+
119
+ <div className="flex flex-col gap-0.5">
120
+ {allChecks.map(check => {
121
+ const disabled = disabledChecks.includes(check.id)
122
+ return (
123
+ <label
124
+ key={check.id}
125
+ className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-theme-hover/50 cursor-pointer transition-colors"
126
+ >
127
+ <input
128
+ type="checkbox"
129
+ checked={!disabled}
130
+ onChange={() => toggleCheck(check.id)}
131
+ className="w-4 h-4 rounded border-theme-border text-skyhook-500 focus:ring-skyhook-500"
132
+ />
133
+ <div className="flex-1 min-w-0">
134
+ <span className="text-sm text-theme-text-primary">{check.title}</span>
135
+ <p className="text-xs text-theme-text-tertiary truncate">{check.description}</p>
136
+ </div>
137
+ </label>
138
+ )
139
+ })}
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <div className="flex justify-end gap-2 px-5 py-3 border-t border-theme-border shrink-0">
145
+ <button
146
+ onClick={onClose}
147
+ className="px-4 py-1.5 text-sm text-theme-text-secondary hover:text-theme-text-primary bg-theme-elevated hover:bg-theme-hover border border-theme-border rounded-lg transition-colors"
148
+ >
149
+ Cancel
150
+ </button>
151
+ <button
152
+ onClick={handleSave}
153
+ disabled={updateSettings.isPending}
154
+ className="px-4 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50"
155
+ >
156
+ {updateSettings.isPending ? 'Saving...' : 'Save'}
157
+ </button>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ )
162
+ }
@@ -0,0 +1,123 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { useAudit, useAuditSettings, useUpdateAuditSettings } from '../../api/client'
3
+ import type { SelectedResource } from '../../types'
4
+ import { AuditFindingsTable } from '@skyhook-io/k8s-ui'
5
+ import { ArrowLeft, ClipboardCheck, Loader2, Settings } from 'lucide-react'
6
+ import { AuditSettingsDialog } from './AuditSettingsDialog'
7
+
8
+ interface AuditViewProps {
9
+ namespaces: string[]
10
+ onBack: () => void
11
+ onNavigateToResource: (resource: SelectedResource) => void
12
+ }
13
+
14
+ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditViewProps) {
15
+ const { data, isLoading, error } = useAudit(namespaces)
16
+ const { data: auditSettings } = useAuditSettings()
17
+ const updateSettings = useUpdateAuditSettings()
18
+ const [showSettings, setShowSettings] = useState(false)
19
+
20
+ const ignoredCount = auditSettings?.ignoredNamespaces?.length ?? 0
21
+
22
+ // Inline hide actions — persist to settings immediately
23
+ const hideCheck = useCallback((checkID: string) => {
24
+ if (!auditSettings) return
25
+ const current = auditSettings.disabledChecks || []
26
+ if (current.includes(checkID)) return
27
+ updateSettings.mutate({ ...auditSettings, disabledChecks: [...current, checkID] })
28
+ }, [auditSettings, updateSettings])
29
+
30
+ const hideCategory = useCallback((category: string) => {
31
+ if (!auditSettings || !data?.checks) return
32
+ const checksInCategory = Object.values(data.checks).filter(c => {
33
+ // Match checks whose findings are in this category
34
+ return data.findings.some(f => f.checkID === c.id && f.category === category)
35
+ }).map(c => c.id)
36
+ const current = auditSettings.disabledChecks || []
37
+ const toAdd = checksInCategory.filter(id => !current.includes(id))
38
+ if (toAdd.length === 0) return
39
+ updateSettings.mutate({ ...auditSettings, disabledChecks: [...current, ...toAdd] })
40
+ }, [auditSettings, data, updateSettings])
41
+
42
+ const hideNamespace = useCallback((ns: string) => {
43
+ if (!auditSettings) return
44
+ const current = auditSettings.ignoredNamespaces || []
45
+ if (current.includes(ns)) return
46
+ updateSettings.mutate({ ...auditSettings, ignoredNamespaces: [...current, ns] })
47
+ }, [auditSettings, updateSettings])
48
+
49
+ if (isLoading) {
50
+ return (
51
+ <div className="flex-1 flex items-center justify-center">
52
+ <div className="flex flex-col items-center gap-3">
53
+ <Loader2 className="w-6 h-6 animate-spin text-theme-text-tertiary" />
54
+ <span className="text-sm text-theme-text-tertiary">Loading audit data...</span>
55
+ </div>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ if (error) {
61
+ return (
62
+ <div className="flex-1 flex items-center justify-center text-theme-text-secondary">
63
+ <p>Failed to load audit data</p>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ if (!data) {
69
+ return (
70
+ <div className="flex-1 flex items-center justify-center text-theme-text-secondary">
71
+ <p>No audit data available</p>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ return (
77
+ <div className="flex-1 flex flex-col min-h-0 p-6 gap-6 overflow-auto">
78
+ {/* Header */}
79
+ <div className="flex items-center gap-4">
80
+ <button
81
+ onClick={onBack}
82
+ className="p-1.5 rounded-lg hover:bg-theme-hover transition-colors"
83
+ >
84
+ <ArrowLeft className="w-5 h-5 text-theme-text-secondary" />
85
+ </button>
86
+ <div className="flex-1">
87
+ <div className="flex items-center gap-2">
88
+ <ClipboardCheck className="w-5 h-5 text-theme-text-secondary" />
89
+ <h1 className="text-lg font-semibold text-theme-text-primary">Cluster Audit</h1>
90
+ </div>
91
+ <p className="text-sm text-theme-text-tertiary mt-1 ml-7">
92
+ Security, reliability, and efficiency checks based on Kubernetes best practices from NSA/CISA guidelines, CIS benchmarks, and industry tools like Polaris and Kubescape.
93
+ </p>
94
+ </div>
95
+ <div className="flex items-center gap-2 shrink-0">
96
+ {ignoredCount > 0 && (
97
+ <button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
98
+ )}
99
+ <button
100
+ onClick={() => setShowSettings(true)}
101
+ className="p-2 rounded-lg hover:bg-theme-hover text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
102
+ title="Audit settings"
103
+ >
104
+ <Settings className="w-4 h-4" />
105
+ </button>
106
+ </div>
107
+ </div>
108
+
109
+ <AuditFindingsTable
110
+ groups={data.groups}
111
+ checks={data.checks}
112
+ onResourceClick={(kind, namespace, name) =>
113
+ onNavigateToResource({ kind, namespace, name })
114
+ }
115
+ onHideCheck={hideCheck}
116
+ onHideCategory={hideCategory}
117
+ onHideNamespace={hideNamespace}
118
+ />
119
+
120
+ {showSettings && <AuditSettingsDialog namespaces={namespaces} onClose={() => setShowSettings(false)} />}
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,388 @@
1
+ import { useState, useMemo, useRef, useCallback } from 'react'
2
+ import { clsx } from 'clsx'
3
+ import { Loader2, TrendingUp } from 'lucide-react'
4
+ import {
5
+ useOpenCostTrend,
6
+ type CostTimeRange,
7
+ type OpenCostTrendSeries,
8
+ } from '../../api/client'
9
+
10
+ const SERIES_COLORS = [
11
+ '#3b82f6', // blue-500
12
+ '#10b981', // emerald-500
13
+ '#f97316', // orange-500
14
+ '#a855f7', // purple-500
15
+ '#ec4899', // pink-500
16
+ '#eab308', // yellow-500
17
+ '#06b6d4', // cyan-500
18
+ '#84cc16', // lime-500
19
+ '#ef4444', // red-500
20
+ ]
21
+
22
+ const TIME_RANGES: { value: CostTimeRange; label: string }[] = [
23
+ { value: '6h', label: '6h' },
24
+ { value: '24h', label: '24h' },
25
+ { value: '7d', label: '7d' },
26
+ ]
27
+
28
+ export function CostTrendChart() {
29
+ const [timeRange, setTimeRange] = useState<CostTimeRange>('24h')
30
+ const { data, isLoading } = useOpenCostTrend(timeRange)
31
+
32
+ if (isLoading) {
33
+ return (
34
+ <div className="rounded-lg border border-theme-border bg-theme-surface/50 p-4">
35
+ <div className="flex items-center justify-center h-[200px] text-theme-text-tertiary">
36
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
37
+ Loading cost trend...
38
+ </div>
39
+ </div>
40
+ )
41
+ }
42
+
43
+ if (!data?.available || !data.series?.length) {
44
+ return null
45
+ }
46
+
47
+ return (
48
+ <div className="rounded-lg border border-theme-border bg-theme-surface/50">
49
+ <div className="flex items-center justify-between px-4 py-2.5 border-b border-theme-border">
50
+ <div className="flex items-center gap-2">
51
+ <TrendingUp className="w-4 h-4 text-theme-text-tertiary" />
52
+ <span className="text-xs font-medium text-theme-text-secondary">Cost Trend</span>
53
+ </div>
54
+ <div className="flex items-center gap-1">
55
+ {TIME_RANGES.map(tr => (
56
+ <button
57
+ key={tr.value}
58
+ onClick={() => setTimeRange(tr.value)}
59
+ className={clsx(
60
+ 'px-2 py-1 text-xs rounded-md transition-colors',
61
+ timeRange === tr.value
62
+ ? 'bg-skyhook-600/20 text-blue-400 font-medium'
63
+ : 'text-theme-text-quaternary hover:text-theme-text-tertiary'
64
+ )}
65
+ >
66
+ {tr.label}
67
+ </button>
68
+ ))}
69
+ </div>
70
+ </div>
71
+ <div className="p-4">
72
+ <StackedAreaChart series={data.series} />
73
+ <ChartLegend series={data.series} />
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
80
+ const svgRef = useRef<SVGSVGElement>(null)
81
+ const [hoverX, setHoverX] = useState<number | null>(null)
82
+
83
+ const width = 1000
84
+ const height = 240
85
+ const marginLeft = 55
86
+ const marginRight = 15
87
+ const marginTop = 8
88
+ const marginBottom = 28
89
+ const plotWidth = width - marginLeft - marginRight
90
+ const plotHeight = height - marginTop - marginBottom
91
+
92
+ // All heavy computation in a single useMemo to avoid hooks-after-early-return
93
+ const chartData = useMemo(() => {
94
+ if (!series.length) return null
95
+
96
+ // Collect all unique timestamps and sort
97
+ const tsSet = new Set<number>()
98
+ for (const s of series) {
99
+ for (const dp of s.dataPoints) {
100
+ tsSet.add(dp.timestamp)
101
+ }
102
+ }
103
+ const timestamps = Array.from(tsSet).sort((a, b) => a - b)
104
+ if (timestamps.length < 2) return null
105
+
106
+ const minTs = timestamps[0]
107
+ const maxTs = timestamps[timestamps.length - 1]
108
+
109
+ const seriesLookups = series.map(s => {
110
+ const map = new Map<number, number>()
111
+ for (const dp of s.dataPoints) {
112
+ map.set(dp.timestamp, dp.value)
113
+ }
114
+ return map
115
+ })
116
+
117
+ // Compute stacked values at each timestamp
118
+ const stacked: number[][] = []
119
+ let maxVal = 0
120
+ for (let si = 0; si < series.length; si++) {
121
+ stacked.push([])
122
+ for (let ti = 0; ti < timestamps.length; ti++) {
123
+ const val = seriesLookups[si].get(timestamps[ti]) ?? 0
124
+ const prev = si > 0 ? stacked[si - 1][ti] : 0
125
+ const cumVal = prev + val
126
+ stacked[si].push(cumVal)
127
+ if (cumVal > maxVal) maxVal = cumVal
128
+ }
129
+ }
130
+
131
+ if (maxVal === 0) maxVal = 1
132
+ const yMax = maxVal + maxVal * 0.1
133
+
134
+ const toX = (ts: number) => marginLeft + ((ts - minTs) / (maxTs - minTs)) * plotWidth
135
+ const toY = (val: number) => marginTop + plotHeight - (val / yMax) * plotHeight
136
+
137
+ // Y axis ticks
138
+ const tickCount = 4
139
+ const yTicks = Array.from({ length: tickCount + 1 }, (_, i) => {
140
+ const val = (yMax / tickCount) * i
141
+ return { val, y: toY(val), label: formatCostAxis(val) }
142
+ })
143
+
144
+ // X axis ticks
145
+ const xTickCount = 6
146
+ const xTicks = Array.from({ length: xTickCount + 1 }, (_, i) => {
147
+ const ts = minTs + ((maxTs - minTs) / xTickCount) * i
148
+ return { ts, x: toX(ts), label: formatTimestamp(ts) }
149
+ })
150
+
151
+ // Build stacked area paths
152
+ const paths = series.map((_, si) => {
153
+ const topPoints = timestamps.map((ts, ti) => ({ x: toX(ts), y: toY(stacked[si][ti]) }))
154
+ const bottomPoints = si > 0
155
+ ? timestamps.map((ts, ti) => ({ x: toX(ts), y: toY(stacked[si - 1][ti]) }))
156
+ : timestamps.map(ts => ({ x: toX(ts), y: toY(0) }))
157
+
158
+ const topPath = topPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
159
+ const bottomPath = [...bottomPoints].reverse().map((p, i) => `${i === 0 ? 'L' : 'L'}${p.x},${p.y}`).join(' ')
160
+ const areaPath = topPath + ' ' + bottomPath + ' Z'
161
+ const linePath = topPoints.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')
162
+
163
+ return { areaPath, linePath, color: SERIES_COLORS[si % SERIES_COLORS.length] }
164
+ })
165
+
166
+ return { timestamps, stacked, minTs, maxTs, yMax, seriesLookups, toX, toY, yTicks, xTicks, paths }
167
+ }, [series])
168
+
169
+ // Hover data — depends on hoverX + chartData, must be a separate hook (called unconditionally)
170
+ const hoverData = useMemo(() => {
171
+ if (hoverX === null || !chartData) return null
172
+ const { timestamps, minTs, maxTs, seriesLookups, toX } = chartData
173
+ const clampedX = Math.max(marginLeft, Math.min(marginLeft + plotWidth, hoverX))
174
+ const frac = (clampedX - marginLeft) / plotWidth
175
+ const ts = minTs + frac * (maxTs - minTs)
176
+
177
+ let closestTi = 0
178
+ let closestDist = Infinity
179
+ for (let ti = 0; ti < timestamps.length; ti++) {
180
+ const dist = Math.abs(timestamps[ti] - ts)
181
+ if (dist < closestDist) {
182
+ closestDist = dist
183
+ closestTi = ti
184
+ }
185
+ }
186
+
187
+ const closestTs = timestamps[closestTi]
188
+ let total = 0
189
+ const points = series.map((s, si) => {
190
+ const val = seriesLookups[si].get(closestTs) ?? 0
191
+ total += val
192
+ return { namespace: s.namespace, value: val, color: SERIES_COLORS[si % SERIES_COLORS.length] }
193
+ })
194
+
195
+ return { ts: closestTs, x: toX(closestTs), total, points }
196
+ }, [hoverX, chartData, series])
197
+
198
+ const handleMouseMove = useCallback((e: React.MouseEvent<SVGRectElement>) => {
199
+ const svg = svgRef.current
200
+ if (!svg) return
201
+ const ctm = svg.getScreenCTM()
202
+ if (!ctm) return
203
+ setHoverX((e.clientX - ctm.e) / ctm.a)
204
+ }, [])
205
+
206
+ // Early return AFTER all hooks
207
+ if (!chartData) return null
208
+
209
+ const { yTicks, xTicks, paths } = chartData
210
+
211
+ return (
212
+ <div className="relative">
213
+ <svg
214
+ ref={svgRef}
215
+ viewBox={`0 0 ${width} ${height}`}
216
+ className="w-full"
217
+ preserveAspectRatio="xMidYMid meet"
218
+ >
219
+ {/* Grid lines */}
220
+ {yTicks.map((tick, i) => (
221
+ <line
222
+ key={`grid-${i}`}
223
+ x1={marginLeft} y1={tick.y}
224
+ x2={width - marginRight} y2={tick.y}
225
+ stroke="currentColor"
226
+ className="text-theme-border/30"
227
+ strokeWidth="1"
228
+ strokeDasharray={i === 0 ? undefined : '4 4'}
229
+ />
230
+ ))}
231
+
232
+ {/* Y axis labels */}
233
+ {yTicks.map((tick, i) => (
234
+ <text
235
+ key={`ylabel-${i}`}
236
+ x={marginLeft - 6}
237
+ y={tick.y + 4}
238
+ textAnchor="end"
239
+ className="fill-theme-text-secondary"
240
+ fontSize="10"
241
+ fontFamily="ui-monospace, monospace"
242
+ >
243
+ {tick.label}
244
+ </text>
245
+ ))}
246
+
247
+ {/* X axis labels */}
248
+ {xTicks.map((tick, i) => (
249
+ <text
250
+ key={`xlabel-${i}`}
251
+ x={tick.x}
252
+ y={height - 4}
253
+ textAnchor="middle"
254
+ className="fill-theme-text-secondary"
255
+ fontSize="10"
256
+ fontFamily="ui-monospace, monospace"
257
+ >
258
+ {tick.label}
259
+ </text>
260
+ ))}
261
+
262
+ {/* Stacked area fills (render bottom to top) */}
263
+ {paths.map((p, i) => (
264
+ <path
265
+ key={`area-${i}`}
266
+ d={p.areaPath}
267
+ fill={p.color + '33'}
268
+ />
269
+ ))}
270
+
271
+ {/* Lines (top edges of each area) */}
272
+ {paths.map((p, i) => (
273
+ <path
274
+ key={`line-${i}`}
275
+ d={p.linePath}
276
+ fill="none"
277
+ stroke={p.color}
278
+ strokeWidth="1.5"
279
+ strokeLinejoin="round"
280
+ />
281
+ ))}
282
+
283
+ {/* Hover crosshair */}
284
+ {hoverData && (
285
+ <line
286
+ x1={hoverData.x} y1={marginTop}
287
+ x2={hoverData.x} y2={marginTop + plotHeight}
288
+ stroke="currentColor"
289
+ className="text-theme-text-tertiary"
290
+ strokeWidth="1"
291
+ strokeDasharray="4 4"
292
+ />
293
+ )}
294
+
295
+ {/* Mouse event overlay */}
296
+ <rect
297
+ x={marginLeft} y={marginTop}
298
+ width={plotWidth} height={plotHeight}
299
+ fill="transparent"
300
+ style={{ cursor: 'crosshair' }}
301
+ onMouseMove={handleMouseMove}
302
+ onMouseLeave={() => setHoverX(null)}
303
+ />
304
+ </svg>
305
+
306
+ {/* Tooltip */}
307
+ {hoverData && (
308
+ <div
309
+ className="absolute top-0 pointer-events-none z-10"
310
+ style={{
311
+ left: `${(hoverData.x / width) * 100}%`,
312
+ transform: hoverData.x > width * 0.65 ? 'translateX(calc(-100% - 12px))' : 'translateX(12px)',
313
+ }}
314
+ >
315
+ <div className="bg-theme-surface border border-theme-border rounded-lg shadow-lg px-3 py-2 text-xs whitespace-nowrap">
316
+ <div className="text-theme-text-tertiary mb-1.5 font-mono">
317
+ {new Date(hoverData.ts * 1000).toLocaleString([], {
318
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
319
+ })}
320
+ </div>
321
+ {hoverData.points
322
+ .filter(p => p.value > 0)
323
+ .sort((a, b) => b.value - a.value)
324
+ .map((p, i) => (
325
+ <div key={i} className="flex items-center gap-2 py-0.5">
326
+ <div
327
+ className="w-2 h-2 rounded-full shrink-0"
328
+ style={{ backgroundColor: p.color }}
329
+ />
330
+ <span className="text-theme-text-secondary">{p.namespace}</span>
331
+ <span className="text-theme-text-primary font-semibold ml-auto pl-3 tabular-nums">
332
+ {formatCostTooltip(p.value)}
333
+ </span>
334
+ </div>
335
+ ))}
336
+ <div className="border-t border-theme-border/50 mt-1 pt-1 flex justify-between text-theme-text-primary font-semibold">
337
+ <span>Total</span>
338
+ <span className="tabular-nums">{formatCostTooltip(hoverData.total)}</span>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ )}
343
+ </div>
344
+ )
345
+ }
346
+
347
+ function ChartLegend({ series }: { series: OpenCostTrendSeries[] }) {
348
+ return (
349
+ <div className="flex flex-wrap gap-x-4 gap-y-1 mt-2">
350
+ {series.map((s, i) => (
351
+ <div key={s.namespace} className="flex items-center gap-1.5 text-xs text-theme-text-tertiary">
352
+ <div
353
+ className="w-2.5 h-2.5 rounded-full shrink-0"
354
+ style={{ backgroundColor: SERIES_COLORS[i % SERIES_COLORS.length] }}
355
+ />
356
+ <span>{s.namespace}</span>
357
+ </div>
358
+ ))}
359
+ </div>
360
+ )
361
+ }
362
+
363
+ function formatCostAxis(value: number): string {
364
+ if (value >= 1000) return `$${(value / 1000).toFixed(0)}k`
365
+ if (value >= 1) return `$${value.toFixed(1)}`
366
+ if (value >= 0.01) return `$${value.toFixed(2)}`
367
+ if (value > 0) return `$${value.toFixed(3)}`
368
+ return '$0'
369
+ }
370
+
371
+ function formatCostTooltip(value: number): string {
372
+ if (value >= 1000) return `$${(value / 1000).toFixed(1)}k/hr`
373
+ if (value >= 1) return `$${value.toFixed(2)}/hr`
374
+ if (value >= 0.01) return `$${value.toFixed(3)}/hr`
375
+ if (value > 0) return `$${value.toFixed(4)}/hr`
376
+ return '$0.00/hr'
377
+ }
378
+
379
+ function formatTimestamp(unix: number): string {
380
+ const d = new Date(unix * 1000)
381
+ const now = new Date()
382
+ const diffHours = (now.getTime() - d.getTime()) / (1000 * 60 * 60)
383
+ // Show date+time for ranges > 24h, just time otherwise
384
+ if (diffHours > 36) {
385
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
386
+ }
387
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
388
+ }