@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.
- package/README.md +67 -0
- package/package.json +80 -0
- package/src/App.tsx +1538 -0
- package/src/RadarApp.tsx +145 -0
- package/src/api/apiResources.ts +28 -0
- package/src/api/client.ts +2583 -0
- package/src/api/config.ts +116 -0
- package/src/api/traffic.ts +139 -0
- package/src/components/ConnectionErrorView.tsx +272 -0
- package/src/components/ContextSwitcher.tsx +481 -0
- package/src/components/DebugOverlay.tsx +94 -0
- package/src/components/UserMenu.tsx +87 -0
- package/src/components/audit/AuditSettingsDialog.tsx +162 -0
- package/src/components/audit/AuditView.tsx +123 -0
- package/src/components/cost/CostTrendChart.tsx +388 -0
- package/src/components/cost/CostView.tsx +545 -0
- package/src/components/dock/BottomDock.tsx +96 -0
- package/src/components/dock/DockContext.tsx +11 -0
- package/src/components/dock/LocalTerminalTab.tsx +22 -0
- package/src/components/dock/LogsTab.tsx +26 -0
- package/src/components/dock/NodeTerminalTab.tsx +50 -0
- package/src/components/dock/TerminalTab.tsx +42 -0
- package/src/components/dock/TrafficFlowListTab.tsx +18 -0
- package/src/components/dock/WorkloadLogsTab.tsx +23 -0
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsActions.tsx +1 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
- package/src/components/gitops/ManagedResourcesList.tsx +1 -0
- package/src/components/gitops/SyncCountdown.tsx +1 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/helm/ChartBrowser.tsx +580 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
- package/src/components/helm/HelmView.tsx +475 -0
- package/src/components/helm/InstallWizard.tsx +1060 -0
- package/src/components/helm/ManifestDiffViewer.tsx +91 -0
- package/src/components/helm/ManifestViewer.tsx +61 -0
- package/src/components/helm/OwnedResources.tsx +465 -0
- package/src/components/helm/RevisionHistory.tsx +167 -0
- package/src/components/helm/ValuesDiffPreview.tsx +190 -0
- package/src/components/helm/ValuesViewer.tsx +365 -0
- package/src/components/helm/helm-utils.ts +37 -0
- package/src/components/home/ActivitySummary.tsx +262 -0
- package/src/components/home/CertificateHealthCard.tsx +105 -0
- package/src/components/home/ClusterHealthCard.tsx +483 -0
- package/src/components/home/CostCard.tsx +112 -0
- package/src/components/home/HealthRing.tsx +1 -0
- package/src/components/home/HelmSummary.tsx +129 -0
- package/src/components/home/HomeView.tsx +224 -0
- package/src/components/home/MCPSetupDialog.tsx +417 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
- package/src/components/home/TopologyPreview.tsx +219 -0
- package/src/components/home/TrafficSummary.tsx +154 -0
- package/src/components/logs/JsonLogLine.tsx +1 -0
- package/src/components/logs/LogCore.tsx +2 -0
- package/src/components/logs/LogsViewer.tsx +44 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
- package/src/components/logs/useLogBuffer.ts +2 -0
- package/src/components/logs/useLogSearch.ts +1 -0
- package/src/components/portforward/PortForwardButton.tsx +375 -0
- package/src/components/portforward/PortForwardManager.tsx +871 -0
- package/src/components/resource/PrometheusCharts.tsx +687 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
- package/src/components/resources/ImageFilesystemModal.tsx +745 -0
- package/src/components/resources/PodFilesystemModal.tsx +407 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
- package/src/components/resources/ResourcesView.tsx +190 -0
- package/src/components/resources/drawer-components.tsx +1 -0
- package/src/components/resources/file-browser-utils.ts +35 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/EventRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +1 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/JobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodRenderer.tsx +94 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/VPARenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
- package/src/components/resources/renderers/argo-cells.tsx +1 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
- package/src/components/resources/renderers/eso-cells.tsx +1 -0
- package/src/components/resources/renderers/flux-cells.tsx +1 -0
- package/src/components/resources/renderers/index.ts +91 -0
- package/src/components/resources/renderers/istio-cells.tsx +1 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
- package/src/components/resources/renderers/keda-cells.tsx +1 -0
- package/src/components/resources/renderers/knative-cells.tsx +1 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
- package/src/components/resources/renderers/traefik-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-shared.tsx +1 -0
- package/src/components/resources/renderers/velero-cells.tsx +1 -0
- package/src/components/resources/resource-utils-argo.ts +2 -0
- package/src/components/resources/resource-utils-certmanager.ts +2 -0
- package/src/components/resources/resource-utils-cnpg.ts +2 -0
- package/src/components/resources/resource-utils-eso.ts +2 -0
- package/src/components/resources/resource-utils-flux.ts +2 -0
- package/src/components/resources/resource-utils-istio.ts +2 -0
- package/src/components/resources/resource-utils-karpenter.ts +2 -0
- package/src/components/resources/resource-utils-keda.ts +2 -0
- package/src/components/resources/resource-utils-knative.ts +2 -0
- package/src/components/resources/resource-utils-kyverno.ts +2 -0
- package/src/components/resources/resource-utils-prometheus.ts +2 -0
- package/src/components/resources/resource-utils-traefik.ts +1 -0
- package/src/components/resources/resource-utils-trivy.ts +2 -0
- package/src/components/resources/resource-utils-velero.ts +2 -0
- package/src/components/resources/resource-utils.ts +5 -0
- package/src/components/settings/SettingsDialog.tsx +537 -0
- package/src/components/shared/CreateResourceDialog.tsx +17 -0
- package/src/components/shared/EditableYamlView.tsx +24 -0
- package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
- package/src/components/timeline/DiffViewer.tsx +1 -0
- package/src/components/timeline/TimelineList.tsx +69 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
- package/src/components/timeline/TimelineView.tsx +157 -0
- package/src/components/timeline/shared.tsx +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
- package/src/components/traffic/TrafficFlowList.tsx +415 -0
- package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
- package/src/components/traffic/TrafficGraph.tsx +1546 -0
- package/src/components/traffic/TrafficView.tsx +1213 -0
- package/src/components/traffic/TrafficWizard.tsx +386 -0
- package/src/components/traffic/index.ts +3 -0
- package/src/components/ui/CodeViewer.tsx +8 -0
- package/src/components/ui/CommandPalette.tsx +460 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
- package/src/components/ui/ErrorBoundary.tsx +46 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
- package/src/components/ui/Markdown.tsx +108 -0
- package/src/components/ui/MetricsChart.tsx +1 -0
- package/src/components/ui/NamespaceSelector.tsx +436 -0
- package/src/components/ui/ResourceBar.tsx +1 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
- package/src/components/ui/Toast.tsx +1 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/UpdateNotification.tsx +299 -0
- package/src/components/ui/YamlEditor.tsx +1 -0
- package/src/components/workload/WorkloadView.tsx +532 -0
- package/src/context/ConnectionContext.tsx +173 -0
- package/src/context/ContextSwitchContext.tsx +56 -0
- package/src/context/NavCustomization.tsx +62 -0
- package/src/context/ThemeContext.tsx +97 -0
- package/src/contexts/CapabilitiesContext.tsx +130 -0
- package/src/hooks/useAnimatedUnmount.ts +1 -0
- package/src/hooks/useDesktopDownload.ts +41 -0
- package/src/hooks/useEventSource.ts +262 -0
- package/src/hooks/useFavorites.ts +69 -0
- package/src/hooks/useKeyboardShortcuts.tsx +7 -0
- package/src/hooks/useRefreshAnimation.ts +1 -0
- package/src/index.css +243 -0
- package/src/index.ts +17 -0
- package/src/main.tsx +158 -0
- package/src/types/gitops.ts +2 -0
- package/src/types.ts +3 -0
- package/src/utils/animation.ts +2 -0
- package/src/utils/badge-colors.ts +2 -0
- package/src/utils/context-name.ts +2 -0
- package/src/utils/desktop-download.ts +66 -0
- package/src/utils/desktop-open-folder.ts +21 -0
- package/src/utils/format.ts +2 -0
- package/src/utils/log-format.ts +12 -0
- package/src/utils/navigation.ts +23 -0
- package/src/utils/resource-hierarchy.ts +2 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.ts +2 -0
- package/src/utils/traffic-colors.ts +54 -0
- 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
|
+
}
|