@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,415 @@
1
+ import { useState, useMemo } from 'react'
2
+ import type { TrafficFlow } from '../../types'
3
+ import { clsx } from 'clsx'
4
+ import { ChevronDown, ChevronUp, ShieldCheck } from 'lucide-react'
5
+ import { SEVERITY_BADGE, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
6
+ import { useFlowSearch } from './TrafficFlowListContext'
7
+ import { useQuery } from '@tanstack/react-query'
8
+ import { fetchJSON } from '../../api/client'
9
+
10
+ // DNS response code names
11
+ const DNS_RCODES: Record<number, string> = {
12
+ 0: 'NOERROR',
13
+ 1: 'FORMERR',
14
+ 2: 'SERVFAIL',
15
+ 3: 'NXDOMAIN',
16
+ 5: 'REFUSED',
17
+ }
18
+
19
+ function formatLatency(ns: number): string {
20
+ const ms = ns / 1e6
21
+ if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`
22
+ if (ms >= 1) return `${ms.toFixed(1)}ms`
23
+ return `${(ms * 1000).toFixed(0)}µs`
24
+ }
25
+
26
+ function formatBytes(bytes: number): string {
27
+ if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(1)}MB`
28
+ if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(1)}KB`
29
+ return `${bytes}B`
30
+ }
31
+
32
+ function statusColor(status: number): string {
33
+ if (status >= 500) return SEVERITY_TEXT.error
34
+ if (status >= 400) return SEVERITY_TEXT.warning
35
+ if (status >= 300) return SEVERITY_TEXT.neutral
36
+ if (status >= 200) return SEVERITY_TEXT.success
37
+ return 'text-theme-text-secondary'
38
+ }
39
+
40
+ const VERDICT_BADGE: Record<string, string> = {
41
+ forwarded: SEVERITY_BADGE.success,
42
+ dropped: SEVERITY_BADGE.error,
43
+ error: SEVERITY_BADGE.warning,
44
+ }
45
+
46
+ type SortField = 'time' | 'latency' | 'status' | 'method' | 'source' | 'destination'
47
+ type SortDir = 'asc' | 'desc'
48
+
49
+ interface TrafficFlowListProps {
50
+ flows: TrafficFlow[]
51
+ }
52
+
53
+ export function TrafficFlowList({ flows }: TrafficFlowListProps) {
54
+ const [search] = useFlowSearch()
55
+ const [sortField, setSortField] = useState<SortField>('time')
56
+ const [sortDir, setSortDir] = useState<SortDir>('desc')
57
+ const [expandedIdx, setExpandedIdx] = useState<number | null>(null)
58
+
59
+ const toggleSort = (field: SortField) => {
60
+ if (sortField === field) {
61
+ setSortDir(d => d === 'asc' ? 'desc' : 'asc')
62
+ } else {
63
+ setSortField(field)
64
+ setSortDir(field === 'time' ? 'desc' : 'asc')
65
+ }
66
+ }
67
+
68
+ // Deduplicate HTTP REQUEST/RESPONSE pairs: prefer RESPONSE (has status + latency).
69
+ // Keep orphan REQUESTs (no matching RESPONSE) as they indicate missing responses.
70
+ // Deduplicate HTTP REQUEST/RESPONSE pairs: prefer RESPONSE (has status + latency).
71
+ // REQUEST goes client→server, RESPONSE goes server→client (src/dst swapped).
72
+ const deduped = useMemo(() => {
73
+ // RESPONSE goes server→client, REQUEST goes client→server (src/dst swapped).
74
+ // Normalize key: always client|server|method|path
75
+ const responseKeys = new Set<string>()
76
+ for (const f of flows) {
77
+ if (f.l7Protocol === 'HTTP' && f.l7Type === 'RESPONSE') {
78
+ responseKeys.add(`${f.destination.name}|${f.source.name}|${f.httpMethod}|${f.httpPath}`)
79
+ }
80
+ }
81
+ return flows.filter(f => {
82
+ if (f.l7Protocol === 'HTTP' && f.l7Type === 'REQUEST') {
83
+ return !responseKeys.has(`${f.source.name}|${f.destination.name}|${f.httpMethod}|${f.httpPath}`)
84
+ }
85
+ return true
86
+ })
87
+ }, [flows])
88
+
89
+ const filtered = useMemo(() => {
90
+ if (!search) return deduped
91
+ const q = search.toLowerCase()
92
+ return deduped.filter(f =>
93
+ f.source.name.toLowerCase().includes(q) ||
94
+ f.destination.name.toLowerCase().includes(q) ||
95
+ f.httpPath?.toLowerCase().includes(q) ||
96
+ f.httpMethod?.toLowerCase().includes(q) ||
97
+ f.dnsQuery?.toLowerCase().includes(q) ||
98
+ f.l7Protocol?.toLowerCase().includes(q) ||
99
+ f.verdict?.toLowerCase().includes(q)
100
+ )
101
+ }, [deduped, search])
102
+
103
+ const sorted = useMemo(() => {
104
+ const mult = sortDir === 'asc' ? 1 : -1
105
+ return [...filtered].sort((a, b) => {
106
+ switch (sortField) {
107
+ case 'time': return mult * (a.lastSeen.localeCompare(b.lastSeen))
108
+ case 'latency': return mult * ((a.latencyNs ?? 0) - (b.latencyNs ?? 0))
109
+ case 'status': return mult * ((a.httpStatus ?? 0) - (b.httpStatus ?? 0))
110
+ case 'method': return mult * ((a.httpMethod ?? '').localeCompare(b.httpMethod ?? ''))
111
+ case 'source': return mult * (a.source.name.localeCompare(b.source.name))
112
+ case 'destination': return mult * (a.destination.name.localeCompare(b.destination.name))
113
+ default: return 0
114
+ }
115
+ })
116
+ }, [filtered, sortField, sortDir])
117
+
118
+ const SortHeader = ({ field, label, className }: { field: SortField; label: string; className?: string }) => (
119
+ <button
120
+ onClick={() => toggleSort(field)}
121
+ className={clsx('flex items-center gap-0.5 hover:text-theme-text-primary transition-colors', className)}
122
+ >
123
+ {label}
124
+ {sortField === field && (
125
+ sortDir === 'asc' ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />
126
+ )}
127
+ </button>
128
+ )
129
+
130
+ return (
131
+ <div className="absolute inset-0 flex flex-col">
132
+ {/* Table header */}
133
+ <div className="grid grid-cols-[6rem_minmax(0,1fr)_minmax(0,1fr)_minmax(0,2fr)_3.5rem_4rem_5.5rem] items-center gap-x-3 px-3 py-1 border-b border-theme-border text-[10px] text-theme-text-tertiary uppercase tracking-wider font-medium">
134
+ <SortHeader field="time" label="Time" />
135
+ <SortHeader field="source" label="Source" />
136
+ <SortHeader field="destination" label="Destination" />
137
+ <span>Request</span>
138
+ <SortHeader field="status" label="Status" className="justify-end" />
139
+ <SortHeader field="latency" label="Latency" className="justify-end" />
140
+ <span className="text-right">Verdict</span>
141
+ </div>
142
+
143
+ {/* Flow rows */}
144
+ <div className="flex-1 overflow-y-auto">
145
+ {sorted.length === 0 ? (
146
+ <div className="flex items-center justify-center h-32 text-sm text-theme-text-tertiary">
147
+ {search ? 'No flows match the search' : 'No flows to display'}
148
+ </div>
149
+ ) : (
150
+ sorted.map((flow, i) => {
151
+ const isExpanded = expandedIdx === i
152
+ const isHTTP = flow.l7Protocol === 'HTTP'
153
+ const isDNS = flow.l7Protocol === 'DNS'
154
+ const time = flow.lastSeen ? new Date(flow.lastSeen).toLocaleTimeString() : ''
155
+
156
+ return (
157
+ <div key={i}>
158
+ <button
159
+ onClick={() => setExpandedIdx(isExpanded ? null : i)}
160
+ className={clsx(
161
+ 'w-full grid grid-cols-[6rem_minmax(0,1fr)_minmax(0,1fr)_minmax(0,2fr)_3.5rem_4rem_5.5rem] items-center gap-x-3 px-3 py-1.5 text-xs text-left hover:bg-theme-hover transition-colors border-b border-theme-border/50',
162
+ isExpanded && 'bg-theme-elevated',
163
+ flow.verdict === 'dropped' && 'bg-red-500/5',
164
+ flow.httpStatus && flow.httpStatus >= 500 && 'bg-red-500/5',
165
+ )}
166
+ >
167
+ {/* Time */}
168
+ <span className="text-theme-text-tertiary tabular-nums whitespace-nowrap">{time}</span>
169
+
170
+ {/* Source */}
171
+ <span className="truncate text-theme-text-primary" title={flow.source.namespace ? `${flow.source.namespace}/${flow.source.name}` : flow.source.name}>
172
+ {flow.source.name}
173
+ </span>
174
+
175
+ {/* Destination */}
176
+ <span className="truncate text-theme-text-primary" title={flow.destination.namespace ? `${flow.destination.namespace}/${flow.destination.name}` : flow.destination.name}>
177
+ {flow.destination.name}
178
+ </span>
179
+
180
+ {/* Request info */}
181
+ <div className="flex items-center gap-1.5 min-w-0">
182
+ {isHTTP && (
183
+ <>
184
+ <span className={clsx('shrink-0 badge badge-sm text-[10px]', SEVERITY_BADGE.info)}>{flow.httpMethod}</span>
185
+ <span className="truncate text-theme-text-secondary" title={flow.httpPath}>{flow.httpPath}</span>
186
+ {flow.l7Type === 'REQUEST' && <span className={clsx('shrink-0 text-[9px]', SEVERITY_TEXT.warning)}>no response</span>}
187
+ </>
188
+ )}
189
+ {isDNS && (
190
+ <>
191
+ <span className={clsx('shrink-0 badge badge-sm text-[10px]', SEVERITY_BADGE.neutral)}>DNS</span>
192
+ <span className="truncate text-theme-text-secondary" title={flow.dnsQuery}>{flow.dnsQuery}</span>
193
+ </>
194
+ )}
195
+ {!isHTTP && !isDNS && (
196
+ <span className="text-theme-text-tertiary uppercase">{flow.protocol}:{flow.port}</span>
197
+ )}
198
+ </div>
199
+
200
+ {/* Status */}
201
+ <span className={clsx('text-right tabular-nums font-medium whitespace-nowrap',
202
+ isHTTP && flow.httpStatus ? statusColor(flow.httpStatus) : 'text-theme-text-tertiary'
203
+ )}>
204
+ {isHTTP && flow.httpStatus ? flow.httpStatus : isDNS && flow.dnsRCode != null ? (DNS_RCODES[flow.dnsRCode] ?? flow.dnsRCode) : '—'}
205
+ </span>
206
+
207
+ {/* Latency */}
208
+ <span className="text-right tabular-nums text-theme-text-secondary whitespace-nowrap">
209
+ {flow.latencyNs && flow.latencyNs > 0 ? formatLatency(flow.latencyNs) : '—'}
210
+ </span>
211
+
212
+ {/* Verdict */}
213
+ <span className={clsx('text-right badge badge-sm text-[10px] capitalize', VERDICT_BADGE[flow.verdict] ?? SEVERITY_BADGE.neutral)}>
214
+ {flow.verdict}
215
+ </span>
216
+ </button>
217
+
218
+ {/* Expanded detail */}
219
+ {isExpanded && (
220
+ <div className="px-3 py-2 bg-theme-elevated border-b border-theme-border space-y-2 text-xs">
221
+ <div className="grid grid-cols-2 gap-x-6 gap-y-1">
222
+ <div>
223
+ <span className="text-theme-text-tertiary">Source: </span>
224
+ <span className="text-theme-text-primary">{flow.source.name}</span>
225
+ {flow.source.namespace && <span className="text-theme-text-tertiary"> ({flow.source.namespace})</span>}
226
+ {flow.sourceService && <span className="text-theme-text-tertiary"> via {flow.sourceService}</span>}
227
+ </div>
228
+ <div>
229
+ <span className="text-theme-text-tertiary">Destination: </span>
230
+ <span className="text-theme-text-primary">{flow.destination.name}</span>
231
+ {flow.destination.namespace && <span className="text-theme-text-tertiary"> ({flow.destination.namespace})</span>}
232
+ {flow.destService && <span className="text-theme-text-tertiary"> via {flow.destService}</span>}
233
+ </div>
234
+ <div>
235
+ <span className="text-theme-text-tertiary">Protocol: </span>
236
+ <span className="text-theme-text-primary">{flow.l7Protocol ? `${flow.l7Protocol} / ${flow.protocol}` : flow.protocol}</span>
237
+ {flow.httpProtocol && <span className="text-theme-text-tertiary"> ({flow.httpProtocol})</span>}
238
+ </div>
239
+ <div>
240
+ <span className="text-theme-text-tertiary">Port: </span>
241
+ <span className="text-theme-text-primary">{flow.port}</span>
242
+ </div>
243
+ {flow.trafficDirection && (
244
+ <div>
245
+ <span className="text-theme-text-tertiary">Direction: </span>
246
+ <span className="text-theme-text-primary capitalize">{flow.trafficDirection}</span>
247
+ </div>
248
+ )}
249
+ {flow.l7Type && (
250
+ <div>
251
+ <span className="text-theme-text-tertiary">L7 Type: </span>
252
+ <span className="text-theme-text-primary">{flow.l7Type}</span>
253
+ </div>
254
+ )}
255
+ <div>
256
+ <span className="text-theme-text-tertiary">Data: </span>
257
+ <span className="text-theme-text-primary">
258
+ {formatBytes(flow.bytesSent)} sent, {formatBytes(flow.bytesRecv)} recv
259
+ </span>
260
+ </div>
261
+ {flow.latencyNs && flow.latencyNs > 0 && (
262
+ <div>
263
+ <span className="text-theme-text-tertiary">Latency: </span>
264
+ <span className="text-theme-text-primary">{formatLatency(flow.latencyNs)}</span>
265
+ </div>
266
+ )}
267
+ </div>
268
+
269
+ {/* DNS details */}
270
+ {isDNS && (
271
+ <div className="pt-1 border-t border-theme-border/50">
272
+ {flow.dnsIPs && flow.dnsIPs.length > 0 && (
273
+ <div>
274
+ <span className="text-theme-text-tertiary">Resolved IPs: </span>
275
+ <span className="text-theme-text-primary">{flow.dnsIPs.join(', ')}</span>
276
+ </div>
277
+ )}
278
+ {flow.dnsQTypes && flow.dnsQTypes.length > 0 && (
279
+ <div>
280
+ <span className="text-theme-text-tertiary">Query Type: </span>
281
+ <span className="text-theme-text-primary">{flow.dnsQTypes.join(', ')}</span>
282
+ </div>
283
+ )}
284
+ {flow.dnsTTL != null && flow.dnsTTL > 0 && (
285
+ <div>
286
+ <span className="text-theme-text-tertiary">TTL: </span>
287
+ <span className="text-theme-text-primary">{flow.dnsTTL}s</span>
288
+ </div>
289
+ )}
290
+ </div>
291
+ )}
292
+
293
+ {/* HTTP headers */}
294
+ {flow.httpHeaders && flow.httpHeaders.length > 0 && (
295
+ <div className="pt-1 border-t border-theme-border/50">
296
+ <span className="text-theme-text-tertiary">Headers: </span>
297
+ <div className="mt-0.5 space-y-0.5">
298
+ {flow.httpHeaders.map((h, j) => (
299
+ <div key={j} className="text-theme-text-secondary font-mono text-[10px]">{h}</div>
300
+ ))}
301
+ </div>
302
+ </div>
303
+ )}
304
+
305
+ {/* Drop reason + policy correlation */}
306
+ {flow.dropReasonDesc && (
307
+ <div className="pt-1 border-t border-theme-border/50">
308
+ <span className={SEVERITY_TEXT.error}>Drop reason: {flow.dropReasonDesc}</span>
309
+ </div>
310
+ )}
311
+ {flow.verdict === 'dropped' && (
312
+ <PolicyCorrelation flow={flow} />
313
+ )}
314
+ </div>
315
+ )}
316
+ </div>
317
+ )
318
+ })
319
+ )}
320
+ </div>
321
+
322
+ {/* Footer */}
323
+ <div className="px-3 py-1.5 border-t border-theme-border text-[10px] text-theme-text-tertiary">
324
+ {sorted.length} flow{sorted.length !== 1 ? 's' : ''}
325
+ {search && ` (filtered from ${flows.length})`}
326
+ </div>
327
+ </div>
328
+ )
329
+ }
330
+
331
+ interface PolicyEvaluation {
332
+ selectingPolicies: { name: string; namespace?: string; kind: string; effect: string; reason: string }[]
333
+ verdict: string
334
+ }
335
+
336
+ function PolicyCorrelation({ flow }: { flow: TrafficFlow }) {
337
+ const destLabels = flow.destination?.labels
338
+ const srcLabels = flow.source?.labels
339
+ const destNs = flow.destination?.namespace || ''
340
+ const destName = flow.destination?.name || ''
341
+ const srcNs = flow.source?.namespace || ''
342
+ const srcName = flow.source?.name || ''
343
+
344
+ const labelsParam = destLabels ? Object.entries(destLabels).map(([k, v]) => `${k}=${v}`).join(',') : ''
345
+ const srcLabelsParam = srcLabels ? Object.entries(srcLabels).map(([k, v]) => `${k}=${v}`).join(',') : ''
346
+
347
+ const direction = flow.trafficDirection || 'ingress'
348
+ // For egress: the evaluated pod is the source. For ingress: the destination.
349
+ const evalNs = direction === 'egress' ? srcNs : destNs
350
+ const evalName = direction === 'egress' ? srcName : destName
351
+ const evalLabels = direction === 'egress' ? srcLabelsParam : labelsParam
352
+
353
+ // Need either labels or pod name to resolve the evaluated pod
354
+ const canQuery = !!evalNs && (!!evalLabels || !!evalName)
355
+
356
+ const { data, isLoading, isError } = useQuery<PolicyEvaluation>({
357
+ queryKey: ['policy-evaluate', destNs, destName, labelsParam, srcNs, srcName, srcLabelsParam, direction],
358
+ queryFn: () => {
359
+ const params = new URLSearchParams({ namespace: destNs })
360
+ if (labelsParam) params.set('labels', labelsParam)
361
+ if (!labelsParam && destName) params.set('podName', destName)
362
+ if (srcNs) params.set('sourceNamespace', srcNs)
363
+ if (srcLabelsParam) params.set('sourceLabels', srcLabelsParam)
364
+ else if (srcName && srcNs) params.set('sourcePodName', srcName)
365
+ if (direction === 'egress') params.set('direction', 'egress')
366
+ return fetchJSON(`/network-policies/evaluate?${params}`)
367
+ },
368
+ enabled: canQuery,
369
+ staleTime: 30000,
370
+ })
371
+
372
+ if (!canQuery) return null
373
+ if (isLoading) return (
374
+ <div className="pt-1 border-t border-theme-border/50 text-theme-text-tertiary text-[10px]">
375
+ Evaluating policies...
376
+ </div>
377
+ )
378
+ if (isError) return (
379
+ <div className="pt-1 border-t border-theme-border/50 text-theme-text-tertiary text-[10px]">
380
+ Unable to evaluate policies
381
+ </div>
382
+ )
383
+ if (!data || !data.selectingPolicies || data.selectingPolicies.length === 0) return (
384
+ <div className="pt-1 border-t border-theme-border/50">
385
+ <div className="flex items-center gap-1 text-theme-text-tertiary">
386
+ <ShieldCheck className="w-3 h-3" />
387
+ <span className="text-[10px]">No NetworkPolicy selects this destination</span>
388
+ </div>
389
+ </div>
390
+ )
391
+
392
+ return (
393
+ <div className="pt-1 border-t border-theme-border/50 space-y-1">
394
+ <div className="flex items-center gap-1 text-theme-text-secondary">
395
+ <ShieldCheck className="w-3 h-3" />
396
+ <span className="text-[10px] font-medium">
397
+ {data.selectingPolicies.length} selecting {data.selectingPolicies.length === 1 ? 'policy' : 'policies'}
398
+ </span>
399
+ </div>
400
+ {data.selectingPolicies.map((p, i) => (
401
+ <div key={i} className="flex items-start gap-1.5 ml-4">
402
+ <span className={clsx(
403
+ 'shrink-0 mt-0.5 w-1.5 h-1.5 rounded-full',
404
+ p.effect === 'allow' ? 'bg-green-500' : p.effect === 'unknown' ? 'bg-yellow-500' : 'bg-red-500',
405
+ )} />
406
+ <div className="min-w-0">
407
+ <span className="text-[10px] text-theme-text-primary font-medium">{p.name}</span>
408
+ <span className="text-[10px] text-theme-text-tertiary ml-1">({p.kind})</span>
409
+ <div className="text-[10px] text-theme-text-tertiary">{p.reason}</div>
410
+ </div>
411
+ </div>
412
+ ))}
413
+ </div>
414
+ )
415
+ }
@@ -0,0 +1,68 @@
1
+ import { useEffect, useState, type ReactNode } from 'react'
2
+ import type { TrafficFlow } from '../../types'
3
+ import type { TrafficGraphSelection } from './TrafficGraph'
4
+
5
+ interface TrafficFlowListContextValue {
6
+ flows: TrafficFlow[]
7
+ graphSelection: TrafficGraphSelection | null
8
+ clearSelection: () => void
9
+ }
10
+
11
+ // Module-level store — TrafficView writes, dock tab reads.
12
+ let currentValue: TrafficFlowListContextValue = { flows: [], graphSelection: null, clearSelection: () => {} }
13
+ const listeners = new Set<() => void>()
14
+
15
+ function setValue(val: TrafficFlowListContextValue) {
16
+ currentValue = val
17
+ listeners.forEach(fn => fn())
18
+ }
19
+
20
+ export function useTrafficFlowList(): TrafficFlowListContextValue {
21
+ const [, forceUpdate] = useState(0)
22
+ useEffect(() => {
23
+ const listener = () => forceUpdate(n => n + 1)
24
+ listeners.add(listener)
25
+ return () => { listeners.delete(listener) }
26
+ }, [])
27
+ return currentValue
28
+ }
29
+
30
+ // Separate search state shared between dock header and flow list
31
+ let searchValue = ''
32
+ const searchListeners = new Set<() => void>()
33
+
34
+ export function setFlowSearch(val: string) {
35
+ searchValue = val
36
+ searchListeners.forEach(fn => fn())
37
+ }
38
+
39
+ export function useFlowSearch(): [string, (val: string) => void] {
40
+ const [, forceUpdate] = useState(0)
41
+ useEffect(() => {
42
+ const listener = () => forceUpdate(n => n + 1)
43
+ searchListeners.add(listener)
44
+ return () => { searchListeners.delete(listener) }
45
+ }, [])
46
+ return [searchValue, setFlowSearch]
47
+ }
48
+
49
+ // Provider — call this from TrafficView to publish flow data
50
+ export function TrafficFlowListProvider({
51
+ flows,
52
+ graphSelection,
53
+ clearSelection,
54
+ children,
55
+ }: TrafficFlowListContextValue & { children: ReactNode }) {
56
+ useEffect(() => {
57
+ setValue({ flows, graphSelection, clearSelection })
58
+ }, [flows, graphSelection, clearSelection])
59
+
60
+ useEffect(() => {
61
+ return () => {
62
+ setValue({ flows: [], graphSelection: null, clearSelection: () => {} })
63
+ setFlowSearch('')
64
+ }
65
+ }, [])
66
+
67
+ return <>{children}</>
68
+ }