@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,1213 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
|
2
|
+
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
3
|
+
import { useTrafficSources, useTrafficFlows, useTrafficConnect, useSetTrafficSource } from '../../api/traffic'
|
|
4
|
+
import { useClusterInfo } from '../../api/client'
|
|
5
|
+
import type { TrafficWizardState, AggregatedFlow } from '../../types'
|
|
6
|
+
import { TrafficWizard } from './TrafficWizard'
|
|
7
|
+
import { TrafficGraph, type TrafficGraphSelection } from './TrafficGraph'
|
|
8
|
+
import { TrafficFilterSidebar } from './TrafficFilterSidebar'
|
|
9
|
+
import { TrafficFlowListProvider } from './TrafficFlowListContext'
|
|
10
|
+
import { Loader2, RefreshCw, Filter, Plug, ChevronDown, List } from 'lucide-react'
|
|
11
|
+
import { clsx } from 'clsx'
|
|
12
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
13
|
+
import { useDock } from '../dock'
|
|
14
|
+
|
|
15
|
+
// Addon types for filtering
|
|
16
|
+
export type AddonMode = 'show' | 'group' | 'hide'
|
|
17
|
+
|
|
18
|
+
// Cluster addons that can be grouped/hidden (infrastructure, not traffic-flow)
|
|
19
|
+
const CLUSTER_ADDON_NAMESPACES = new Set([
|
|
20
|
+
// Certificate management
|
|
21
|
+
'cert-manager',
|
|
22
|
+
// Secrets management
|
|
23
|
+
'external-secrets',
|
|
24
|
+
'sealed-secrets',
|
|
25
|
+
'vault',
|
|
26
|
+
// Backup
|
|
27
|
+
'velero',
|
|
28
|
+
// Monitoring & metrics
|
|
29
|
+
'gmp-system',
|
|
30
|
+
'gmp-public',
|
|
31
|
+
'datadog',
|
|
32
|
+
'monitoring',
|
|
33
|
+
'observability',
|
|
34
|
+
'opencost',
|
|
35
|
+
'prometheus',
|
|
36
|
+
'grafana',
|
|
37
|
+
'kube-state-metrics',
|
|
38
|
+
// Logging
|
|
39
|
+
'loki',
|
|
40
|
+
'logging',
|
|
41
|
+
'fluentd',
|
|
42
|
+
'fluentbit',
|
|
43
|
+
// DNS
|
|
44
|
+
'external-dns',
|
|
45
|
+
// Autoscaling
|
|
46
|
+
'cluster-autoscaler',
|
|
47
|
+
'karpenter',
|
|
48
|
+
'keda',
|
|
49
|
+
// GitOps & CI/CD
|
|
50
|
+
'argocd',
|
|
51
|
+
'argo-rollouts',
|
|
52
|
+
'argo-workflows',
|
|
53
|
+
'flux-system',
|
|
54
|
+
// Policy
|
|
55
|
+
'gatekeeper-system',
|
|
56
|
+
// Config management
|
|
57
|
+
'reloader',
|
|
58
|
+
// Database operators
|
|
59
|
+
'cloud-native-pg',
|
|
60
|
+
'cnpg-system',
|
|
61
|
+
'postgres-operator',
|
|
62
|
+
'mysql-operator',
|
|
63
|
+
'redis-operator',
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
// Addon workload names (for detection when namespace isn't enough)
|
|
67
|
+
const CLUSTER_ADDON_NAMES = new Set([
|
|
68
|
+
'coredns',
|
|
69
|
+
'metrics-server',
|
|
70
|
+
'cluster-autoscaler',
|
|
71
|
+
'kube-dns',
|
|
72
|
+
'kube-state-metrics',
|
|
73
|
+
'reloader',
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
// Traffic-flow related addons that should NEVER be grouped/hidden
|
|
77
|
+
// These are essential for understanding traffic patterns
|
|
78
|
+
const TRAFFIC_FLOW_NAMESPACES = new Set([
|
|
79
|
+
'ingress-nginx',
|
|
80
|
+
'nginx-ingress',
|
|
81
|
+
'traefik',
|
|
82
|
+
'contour',
|
|
83
|
+
'kong',
|
|
84
|
+
'ambassador',
|
|
85
|
+
'emissary',
|
|
86
|
+
'haproxy-ingress',
|
|
87
|
+
'istio-system',
|
|
88
|
+
'istio-ingress',
|
|
89
|
+
'linkerd',
|
|
90
|
+
'consul',
|
|
91
|
+
'envoy-gateway-system',
|
|
92
|
+
'gateway-system',
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
const TRAFFIC_FLOW_NAMES = new Set([
|
|
96
|
+
'ingress-nginx-controller',
|
|
97
|
+
'nginx-ingress-controller',
|
|
98
|
+
'traefik',
|
|
99
|
+
'contour',
|
|
100
|
+
'envoy',
|
|
101
|
+
'kong',
|
|
102
|
+
'ambassador',
|
|
103
|
+
'istio-ingressgateway',
|
|
104
|
+
'istio-proxy',
|
|
105
|
+
'linkerd-proxy',
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
// Check if an endpoint is a cluster addon (can be grouped/hidden)
|
|
109
|
+
// Exported for use in TrafficGraph
|
|
110
|
+
export function isClusterAddon(name: string, namespace: string | undefined): boolean {
|
|
111
|
+
// Never treat traffic-flow addons as regular addons
|
|
112
|
+
if (namespace && TRAFFIC_FLOW_NAMESPACES.has(namespace)) return false
|
|
113
|
+
if (TRAFFIC_FLOW_NAMES.has(name)) return false
|
|
114
|
+
|
|
115
|
+
// Check namespace-based addons
|
|
116
|
+
if (namespace && CLUSTER_ADDON_NAMESPACES.has(namespace)) return true
|
|
117
|
+
|
|
118
|
+
// Check name-based addons
|
|
119
|
+
if (CLUSTER_ADDON_NAMES.has(name)) return true
|
|
120
|
+
|
|
121
|
+
// Check for common addon naming patterns
|
|
122
|
+
if (name.includes('prometheus') || name.includes('grafana') ||
|
|
123
|
+
name.includes('datadog') || name.includes('fluentd') ||
|
|
124
|
+
name.includes('metrics-server') || name.includes('coredns')) {
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// System namespaces to hide by default
|
|
132
|
+
const SYSTEM_NAMESPACES = new Set([
|
|
133
|
+
'kube-system',
|
|
134
|
+
'kube-public',
|
|
135
|
+
'kube-node-lease',
|
|
136
|
+
'cert-manager',
|
|
137
|
+
'caretta',
|
|
138
|
+
'cilium',
|
|
139
|
+
'calico-system',
|
|
140
|
+
'tigera-operator',
|
|
141
|
+
'gatekeeper-system',
|
|
142
|
+
'argo-rollouts',
|
|
143
|
+
'argocd',
|
|
144
|
+
'flux-system',
|
|
145
|
+
'monitoring',
|
|
146
|
+
'observability',
|
|
147
|
+
'istio-system',
|
|
148
|
+
'linkerd',
|
|
149
|
+
// Phase 1.1: Additional infrastructure namespaces
|
|
150
|
+
'node', // Node-level traffic (often 35%+ of flows)
|
|
151
|
+
'gmp-system', // GKE Managed Prometheus
|
|
152
|
+
'gmp-public', // GKE Managed Prometheus public
|
|
153
|
+
'datadog', // Datadog monitoring
|
|
154
|
+
'opencost', // OpenCost
|
|
155
|
+
'external-dns', // External DNS controller
|
|
156
|
+
'ingress-nginx', // NGINX Ingress Controller
|
|
157
|
+
'traefik', // Traefik
|
|
158
|
+
'velero', // Velero backup
|
|
159
|
+
'vault', // HashiCorp Vault
|
|
160
|
+
'external-secrets', // External Secrets Operator
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
// Detect internal load balancer IPs (appear as "external" but are internal)
|
|
164
|
+
function isInternalLoadBalancer(name: string): boolean {
|
|
165
|
+
// GKE internal LB IPs (10.x.x.x range)
|
|
166
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true
|
|
167
|
+
// AWS internal LB pattern (172.16-31.x.x)
|
|
168
|
+
if (/^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(name)) return true
|
|
169
|
+
// Azure internal LB pattern
|
|
170
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(name)) return true
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Patterns for external service aggregation (Phase 4.2)
|
|
175
|
+
const EXTERNAL_SERVICE_PATTERNS: { pattern: RegExp; display: string; category: string }[] = [
|
|
176
|
+
{ pattern: /.*\.mongodb\.net\.?$/, display: 'MongoDB Atlas', category: 'database' },
|
|
177
|
+
{ pattern: /.*\.mongodb\.com\.?$/, display: 'MongoDB Atlas', category: 'database' },
|
|
178
|
+
{ pattern: /.*\.redis\.cloud\.?$/, display: 'Redis Cloud', category: 'database' },
|
|
179
|
+
{ pattern: /.*\.rds\.amazonaws\.com\.?$/, display: 'AWS RDS', category: 'database' },
|
|
180
|
+
{ pattern: /.*\.amazonaws\.com\.?$/, display: 'AWS Services', category: 'cloud' },
|
|
181
|
+
{ pattern: /.*\.googleapis\.com\.?$/, display: 'Google APIs', category: 'cloud' },
|
|
182
|
+
// GCE VM patterns - various formats (IP.bc.googleusercontent.com, with/without trailing dot)
|
|
183
|
+
{ pattern: /[\d.-]+\.bc\.googleusercontent\.com\.?$/i, display: 'GCE VMs', category: 'cloud' },
|
|
184
|
+
{ pattern: /.*\.googleusercontent\.com\.?$/i, display: 'Google Cloud', category: 'cloud' },
|
|
185
|
+
{ pattern: /.*\.azure\.com\.?$/, display: 'Azure Services', category: 'cloud' },
|
|
186
|
+
{ pattern: /.*\.blob\.core\.windows\.net\.?$/, display: 'Azure Blob', category: 'cloud' },
|
|
187
|
+
{ pattern: /.*\.sentry\.io\.?$/, display: 'Sentry', category: 'monitoring' },
|
|
188
|
+
{ pattern: /.*\.datadoghq\.com\.?$/, display: 'Datadog', category: 'monitoring' },
|
|
189
|
+
{ pattern: /.*\.stripe\.com\.?$/, display: 'Stripe', category: 'payment' },
|
|
190
|
+
{ pattern: /.*\.auth0\.com\.?$/, display: 'Auth0', category: 'auth' },
|
|
191
|
+
{ pattern: /.*\.okta\.com\.?$/, display: 'Okta', category: 'auth' },
|
|
192
|
+
{ pattern: /.*\.sendgrid\.net\.?$/, display: 'SendGrid', category: 'email' },
|
|
193
|
+
{ pattern: /.*\.mailgun\.org\.?$/, display: 'Mailgun', category: 'email' },
|
|
194
|
+
{ pattern: /.*\.slack\.com\.?$/, display: 'Slack', category: 'messaging' },
|
|
195
|
+
{ pattern: /.*\.twilio\.com\.?$/, display: 'Twilio', category: 'messaging' },
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
// Port-based service detection (when hostname doesn't give enough info)
|
|
199
|
+
const PORT_SERVICE_MAP: Record<number, { name: string; category: string }> = {
|
|
200
|
+
27017: { name: 'MongoDB', category: 'database' },
|
|
201
|
+
27018: { name: 'MongoDB', category: 'database' },
|
|
202
|
+
5432: { name: 'PostgreSQL', category: 'database' },
|
|
203
|
+
3306: { name: 'MySQL', category: 'database' },
|
|
204
|
+
6379: { name: 'Redis', category: 'database' },
|
|
205
|
+
9042: { name: 'Cassandra', category: 'database' },
|
|
206
|
+
9200: { name: 'Elasticsearch', category: 'database' },
|
|
207
|
+
9300: { name: 'Elasticsearch', category: 'database' },
|
|
208
|
+
443: { name: 'HTTPS', category: 'web' },
|
|
209
|
+
80: { name: 'HTTP', category: 'web' },
|
|
210
|
+
8080: { name: 'HTTP', category: 'web' },
|
|
211
|
+
8443: { name: 'HTTPS', category: 'web' },
|
|
212
|
+
5672: { name: 'RabbitMQ', category: 'messaging' },
|
|
213
|
+
9092: { name: 'Kafka', category: 'messaging' },
|
|
214
|
+
4222: { name: 'NATS', category: 'messaging' },
|
|
215
|
+
11211: { name: 'Memcached', category: 'cache' },
|
|
216
|
+
25: { name: 'SMTP', category: 'email' },
|
|
217
|
+
587: { name: 'SMTP', category: 'email' },
|
|
218
|
+
53: { name: 'DNS', category: 'infra' },
|
|
219
|
+
22: { name: 'SSH', category: 'infra' },
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Get aggregated display name for external services (considers both hostname and port)
|
|
223
|
+
function getExternalServiceName(name: string, port?: number): { name: string; aggregated: boolean; category?: string } {
|
|
224
|
+
// Check for port-based service first (more reliable than hostname guessing)
|
|
225
|
+
const portService = port ? PORT_SERVICE_MAP[port] : undefined
|
|
226
|
+
|
|
227
|
+
// Try hostname patterns
|
|
228
|
+
for (const { pattern, display, category } of EXTERNAL_SERVICE_PATTERNS) {
|
|
229
|
+
if (pattern.test(name)) {
|
|
230
|
+
// If we also have port info, combine them for clarity (e.g., "MongoDB (GCE VMs)")
|
|
231
|
+
if (portService && display !== portService.name) {
|
|
232
|
+
return { name: `${portService.name} (${display})`, aggregated: true, category: portService.category }
|
|
233
|
+
}
|
|
234
|
+
return { name: display, aggregated: true, category }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// If hostname doesn't match but we have a known port, aggregate by service type
|
|
239
|
+
if (portService) {
|
|
240
|
+
return { name: portService.name, aggregated: true, category: portService.category }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { name, aggregated: false }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
// Cilium reserved identities (internal infrastructure traffic)
|
|
248
|
+
const CILIUM_RESERVED_IDENTITIES = new Set([
|
|
249
|
+
'host', // Node-level traffic
|
|
250
|
+
'health', // Cilium health probes
|
|
251
|
+
'init', // Initialization identity
|
|
252
|
+
'unmanaged', // Unmanaged endpoints
|
|
253
|
+
])
|
|
254
|
+
|
|
255
|
+
// Check if an address is IPv6 link-local or multicast (infrastructure noise)
|
|
256
|
+
function isIPv6Infrastructure(name: string): boolean {
|
|
257
|
+
// Link-local (fe80::/10)
|
|
258
|
+
if (name.toLowerCase().startsWith('fe80:')) return true
|
|
259
|
+
// Multicast (ff00::/8) - includes ff02::2 (all routers), ff02::1 (all nodes), etc.
|
|
260
|
+
if (name.toLowerCase().startsWith('ff0')) return true
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check if an endpoint is a system/infrastructure component
|
|
265
|
+
function isSystemEndpoint(name: string, namespace: string | undefined, kind: string): boolean {
|
|
266
|
+
// System namespaces
|
|
267
|
+
if (namespace && SYSTEM_NAMESPACES.has(namespace)) {
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Cilium reserved identities (show up as External kind with reserved names)
|
|
272
|
+
if (kind === 'External' && CILIUM_RESERVED_IDENTITIES.has(name)) {
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// IPv6 link-local and multicast addresses (infrastructure noise)
|
|
277
|
+
if (isIPv6Infrastructure(name)) {
|
|
278
|
+
return true
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Node-level traffic
|
|
282
|
+
if (kind === 'node' || kind === 'Node') {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Cloud metadata services (AWS, GCE, Azure)
|
|
287
|
+
if (name.startsWith('169.254.') || name === 'instance-data.ec2.internal') {
|
|
288
|
+
return true
|
|
289
|
+
}
|
|
290
|
+
if (name === 'metadata.google.internal' || name === 'metadata.google.internal.') {
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
if (name === 'metadata.azure.com' || name.endsWith('.metadata.azure.com')) {
|
|
294
|
+
return true
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Localhost / loopback traffic (within-pod communication, health checks)
|
|
298
|
+
if (name === '127.0.0.1' || name === 'localhost' || name.startsWith('127.')) {
|
|
299
|
+
return true
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 0.0.0.0 - binding address, not a real destination
|
|
303
|
+
if (name === '0.0.0.0') {
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Kubernetes API server in default namespace
|
|
308
|
+
if (namespace === 'default' && name === 'kubernetes') {
|
|
309
|
+
return true
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// IP-based names (internal cluster IPs)
|
|
313
|
+
if (/^\d{1,3}-\d{1,3}-\d{1,3}-\d{1,3}\./.test(name)) {
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// EC2 instance hostnames
|
|
318
|
+
if (/^ec2-\d+-\d+-\d+-\d+\./.test(name) || /^ip-\d+-\d+-\d+-\d+\./.test(name)) {
|
|
319
|
+
return true
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Internal load balancer IPs that appear as "external"
|
|
323
|
+
if (kind === 'External' && isInternalLoadBalancer(name)) {
|
|
324
|
+
return true
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return false
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Helper to check if endpoint is external (case-insensitive)
|
|
331
|
+
function isExternal(kind: string): boolean {
|
|
332
|
+
return kind.toLowerCase() === 'external'
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface TrafficViewProps {
|
|
336
|
+
namespaces: string[]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function TrafficView({ namespaces }: TrafficViewProps) {
|
|
340
|
+
const [wizardState, setWizardState] = useState<TrafficWizardState>('detecting')
|
|
341
|
+
const [timeRange, setTimeRange] = useState<string>('5m')
|
|
342
|
+
const [hideSystem, setHideSystem] = useState(true)
|
|
343
|
+
const [hideExternal, setHideExternal] = useState(false)
|
|
344
|
+
const [minConnections, setMinConnections] = useState(0)
|
|
345
|
+
const [showNamespaceGroups, setShowNamespaceGroups] = useState(true)
|
|
346
|
+
const [aggregateExternal, setAggregateExternal] = useState(true)
|
|
347
|
+
const [detectServices, setDetectServices] = useState(true)
|
|
348
|
+
const [collapseInternet, setCollapseInternet] = useState(true)
|
|
349
|
+
const [addonMode, setAddonMode] = useState<AddonMode>('show')
|
|
350
|
+
const [graphSelection, setGraphSelection] = useState<TrafficGraphSelection | null>(null)
|
|
351
|
+
const dock = useDock()
|
|
352
|
+
|
|
353
|
+
// Dock: offset past sidebar, close flows tab on unmount
|
|
354
|
+
const flowsTabIdRef = useRef<string | null>(null)
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
dock.setLeftOffset(288)
|
|
357
|
+
return () => {
|
|
358
|
+
dock.setLeftOffset(0)
|
|
359
|
+
// Close the flows tab when leaving traffic view
|
|
360
|
+
if (flowsTabIdRef.current) {
|
|
361
|
+
dock.removeTab(flowsTabIdRef.current)
|
|
362
|
+
flowsTabIdRef.current = null
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
366
|
+
|
|
367
|
+
const [hiddenNamespaces, setHiddenNamespaces] = useState<Set<string>>(new Set())
|
|
368
|
+
// L7 filters (Hubble-only)
|
|
369
|
+
const [l7Protocol, setL7Protocol] = useState<string>('all')
|
|
370
|
+
const [l7Methods, setL7Methods] = useState<Set<string>>(new Set())
|
|
371
|
+
const [l7StatusRanges, setL7StatusRanges] = useState<Set<string>>(new Set())
|
|
372
|
+
const [l7Verdicts, setL7Verdicts] = useState<Set<string>>(new Set())
|
|
373
|
+
const [dnsPattern, setDnsPattern] = useState('')
|
|
374
|
+
const [isConnecting, setIsConnecting] = useState(false)
|
|
375
|
+
const [connectionError, setConnectionError] = useState<string | null>(null)
|
|
376
|
+
const queryClient = useQueryClient()
|
|
377
|
+
const connectMutation = useTrafficConnect()
|
|
378
|
+
const setSourceMutation = useSetTrafficSource()
|
|
379
|
+
const hasAutoConnectedRef = useRef(false)
|
|
380
|
+
const [sourcePickerOpen, setSourcePickerOpen] = useState(false)
|
|
381
|
+
const sourcePickerRef = useRef<HTMLDivElement>(null)
|
|
382
|
+
|
|
383
|
+
// Track cluster context to reset state on cluster change
|
|
384
|
+
const { data: clusterInfo } = useClusterInfo()
|
|
385
|
+
const lastClusterRef = useRef<string | null>(null)
|
|
386
|
+
|
|
387
|
+
// Reset state when cluster context changes
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
const currentCluster = clusterInfo?.context || null
|
|
390
|
+
if (lastClusterRef.current !== null && lastClusterRef.current !== currentCluster) {
|
|
391
|
+
// Cluster changed - reset wizard state and invalidate traffic queries
|
|
392
|
+
setWizardState('detecting')
|
|
393
|
+
setConnectionError(null)
|
|
394
|
+
hasAutoConnectedRef.current = false
|
|
395
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-sources'] })
|
|
396
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-flows'] })
|
|
397
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-connection'] })
|
|
398
|
+
}
|
|
399
|
+
lastClusterRef.current = currentCluster
|
|
400
|
+
}, [clusterInfo?.context, queryClient])
|
|
401
|
+
|
|
402
|
+
// Close source picker on outside click (capture phase to beat ReactFlow)
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
if (!sourcePickerOpen) return
|
|
405
|
+
const handler = (e: MouseEvent) => {
|
|
406
|
+
if (sourcePickerRef.current && !sourcePickerRef.current.contains(e.target as Node)) {
|
|
407
|
+
setSourcePickerOpen(false)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
document.addEventListener('mousedown', handler, true)
|
|
411
|
+
return () => document.removeEventListener('mousedown', handler, true)
|
|
412
|
+
}, [sourcePickerOpen])
|
|
413
|
+
|
|
414
|
+
const {
|
|
415
|
+
data: sourcesData,
|
|
416
|
+
isLoading: sourcesLoading,
|
|
417
|
+
refetch: refetchSources,
|
|
418
|
+
} = useTrafficSources()
|
|
419
|
+
|
|
420
|
+
const {
|
|
421
|
+
data: flowsData,
|
|
422
|
+
isLoading: flowsLoading,
|
|
423
|
+
isFetching: flowsFetching,
|
|
424
|
+
refetch: refetchFlowsRaw,
|
|
425
|
+
} = useTrafficFlows({
|
|
426
|
+
namespaces,
|
|
427
|
+
since: timeRange,
|
|
428
|
+
// Only fetch flows when connected (not connecting and no connection error)
|
|
429
|
+
enabled: wizardState === 'ready' && !isConnecting && !connectionError,
|
|
430
|
+
})
|
|
431
|
+
const [refetchFlows, isRefreshAnimating] = useRefreshAnimation(refetchFlowsRaw)
|
|
432
|
+
|
|
433
|
+
// Auto-retry when flows return with warning but no data (e.g., port-forward not ready yet)
|
|
434
|
+
useEffect(() => {
|
|
435
|
+
if (flowsData?.warning && (!flowsData.aggregated || flowsData.aggregated.length === 0) && !flowsFetching) {
|
|
436
|
+
const timer = setTimeout(() => refetchFlowsRaw(), 2000)
|
|
437
|
+
return () => clearTimeout(timer)
|
|
438
|
+
}
|
|
439
|
+
}, [flowsData, flowsFetching, refetchFlowsRaw])
|
|
440
|
+
|
|
441
|
+
// Filter flows based on user preferences
|
|
442
|
+
// Note: namespace filtering is done server-side via the global namespace selector
|
|
443
|
+
const filteredFlows = useMemo<AggregatedFlow[]>(() => {
|
|
444
|
+
if (!flowsData?.aggregated) return []
|
|
445
|
+
|
|
446
|
+
return flowsData.aggregated.filter(flow => {
|
|
447
|
+
const sourceIsSystem = isSystemEndpoint(flow.source.name, flow.source.namespace, flow.source.kind)
|
|
448
|
+
const destIsSystem = isSystemEndpoint(flow.destination.name, flow.destination.namespace, flow.destination.kind)
|
|
449
|
+
|
|
450
|
+
// If hiding system, skip flows where EITHER endpoint is a system component
|
|
451
|
+
if (hideSystem && (sourceIsSystem || destIsSystem)) {
|
|
452
|
+
return false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Always filter out non-useful traffic (regardless of hideSystem setting)
|
|
456
|
+
const isAlwaysFiltered = (name: string) =>
|
|
457
|
+
// Cloud metadata services
|
|
458
|
+
name === 'metadata.google.internal' ||
|
|
459
|
+
name === 'metadata.google.internal.' ||
|
|
460
|
+
name.startsWith('169.254.') ||
|
|
461
|
+
name === 'instance-data.ec2.internal' ||
|
|
462
|
+
// Loopback / bind addresses - not real traffic
|
|
463
|
+
name === 'localhost' ||
|
|
464
|
+
name === '127.0.0.1' ||
|
|
465
|
+
name.startsWith('127.') ||
|
|
466
|
+
name === '0.0.0.0'
|
|
467
|
+
|
|
468
|
+
if (isAlwaysFiltered(flow.source.name) || isAlwaysFiltered(flow.destination.name)) {
|
|
469
|
+
return false
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// If hiding external, skip flows with external endpoints
|
|
473
|
+
if (hideExternal) {
|
|
474
|
+
if (isExternal(flow.source.kind) || isExternal(flow.destination.kind)) {
|
|
475
|
+
return false
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Addon mode: hide
|
|
480
|
+
if (addonMode === 'hide') {
|
|
481
|
+
const sourceIsAddon = isClusterAddon(flow.source.name, flow.source.namespace)
|
|
482
|
+
const destIsAddon = isClusterAddon(flow.destination.name, flow.destination.namespace)
|
|
483
|
+
if (sourceIsAddon || destIsAddon) {
|
|
484
|
+
return false
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Connection threshold filter
|
|
489
|
+
if (flow.connections < minConnections) {
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Filter by hidden namespaces - hide flow if EITHER endpoint is in a hidden namespace
|
|
494
|
+
if (hiddenNamespaces.size > 0) {
|
|
495
|
+
const sourceNs = flow.source.namespace
|
|
496
|
+
const destNs = flow.destination.namespace
|
|
497
|
+
if (sourceNs && hiddenNamespaces.has(sourceNs)) return false
|
|
498
|
+
if (destNs && hiddenNamespaces.has(destNs)) return false
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Protocol filter
|
|
502
|
+
if (l7Protocol === 'HTTP' && flow.l7Protocol !== 'HTTP') return false
|
|
503
|
+
if (l7Protocol === 'DNS' && flow.l7Protocol !== 'DNS') return false
|
|
504
|
+
if (l7Protocol === 'TCP' && flow.l7Protocol) return false // TCP = no L7
|
|
505
|
+
|
|
506
|
+
// L7 sub-filters (only apply when active)
|
|
507
|
+
if (l7Methods.size > 0) {
|
|
508
|
+
if (!flow.topHTTPPaths?.some(p => l7Methods.has(p.method))) return false
|
|
509
|
+
}
|
|
510
|
+
if (l7StatusRanges.size > 0) {
|
|
511
|
+
if (!flow.httpStatusCounts || !Array.from(l7StatusRanges).some(r => (flow.httpStatusCounts?.[r] ?? 0) > 0)) return false
|
|
512
|
+
}
|
|
513
|
+
if (l7Verdicts.size > 0) {
|
|
514
|
+
if (!flow.verdictCounts || !Array.from(l7Verdicts).some(v => (flow.verdictCounts?.[v] ?? 0) > 0)) return false
|
|
515
|
+
}
|
|
516
|
+
if (dnsPattern) {
|
|
517
|
+
const pattern = dnsPattern.toLowerCase()
|
|
518
|
+
if (!flow.topDNSQueries?.some(q => q.query.toLowerCase().includes(pattern))) return false
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return true
|
|
522
|
+
})
|
|
523
|
+
}, [flowsData?.aggregated, hideSystem, hideExternal, minConnections, hiddenNamespaces, addonMode, l7Protocol, l7Methods, l7StatusRanges, l7Verdicts, dnsPattern])
|
|
524
|
+
|
|
525
|
+
// Filter raw flows with the same base filters (for list view)
|
|
526
|
+
const filteredRawFlows = useMemo(() => {
|
|
527
|
+
if (!flowsData?.flows) return []
|
|
528
|
+
return flowsData.flows.filter(flow => {
|
|
529
|
+
const sourceIsSystem = isSystemEndpoint(flow.source.name, flow.source.namespace, flow.source.kind)
|
|
530
|
+
const destIsSystem = isSystemEndpoint(flow.destination.name, flow.destination.namespace, flow.destination.kind)
|
|
531
|
+
if (hideSystem && (sourceIsSystem || destIsSystem)) return false
|
|
532
|
+
|
|
533
|
+
const isAlwaysFiltered = (name: string) =>
|
|
534
|
+
name === 'metadata.google.internal' || name === 'metadata.google.internal.' ||
|
|
535
|
+
name.startsWith('169.254.') || name === 'instance-data.ec2.internal' ||
|
|
536
|
+
name === 'localhost' || name === '127.0.0.1' || name.startsWith('127.') || name === '0.0.0.0'
|
|
537
|
+
if (isAlwaysFiltered(flow.source.name) || isAlwaysFiltered(flow.destination.name)) return false
|
|
538
|
+
|
|
539
|
+
if (hideExternal && (isExternal(flow.source.kind) || isExternal(flow.destination.kind))) return false
|
|
540
|
+
|
|
541
|
+
if (addonMode === 'hide') {
|
|
542
|
+
if (isClusterAddon(flow.source.name, flow.source.namespace) || isClusterAddon(flow.destination.name, flow.destination.namespace)) return false
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (hiddenNamespaces.size > 0) {
|
|
546
|
+
if (flow.source.namespace && hiddenNamespaces.has(flow.source.namespace)) return false
|
|
547
|
+
if (flow.destination.namespace && hiddenNamespaces.has(flow.destination.namespace)) return false
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Protocol filter
|
|
551
|
+
if (l7Protocol === 'HTTP' && flow.l7Protocol !== 'HTTP') return false
|
|
552
|
+
if (l7Protocol === 'DNS' && flow.l7Protocol !== 'DNS') return false
|
|
553
|
+
if (l7Protocol === 'TCP' && flow.l7Protocol) return false
|
|
554
|
+
|
|
555
|
+
// L7 sub-filters on individual flow fields
|
|
556
|
+
if (l7Methods.size > 0) {
|
|
557
|
+
if (!flow.httpMethod || !l7Methods.has(flow.httpMethod)) return false
|
|
558
|
+
}
|
|
559
|
+
if (l7StatusRanges.size > 0) {
|
|
560
|
+
if (!flow.httpStatus) return false
|
|
561
|
+
const bucket = `${Math.floor(flow.httpStatus / 100)}xx`
|
|
562
|
+
if (!l7StatusRanges.has(bucket)) return false
|
|
563
|
+
}
|
|
564
|
+
if (l7Verdicts.size > 0) {
|
|
565
|
+
if (!flow.verdict || !l7Verdicts.has(flow.verdict)) return false
|
|
566
|
+
}
|
|
567
|
+
if (dnsPattern) {
|
|
568
|
+
if (!flow.dnsQuery || !flow.dnsQuery.toLowerCase().includes(dnsPattern.toLowerCase())) return false
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return true
|
|
572
|
+
})
|
|
573
|
+
}, [flowsData?.flows, hideSystem, hideExternal, hiddenNamespaces, addonMode, l7Protocol, l7Methods, l7StatusRanges, l7Verdicts, dnsPattern])
|
|
574
|
+
|
|
575
|
+
// Apply graph selection to filter raw flows for the list panel
|
|
576
|
+
const listFlows = useMemo(() => {
|
|
577
|
+
if (!graphSelection) return filteredRawFlows
|
|
578
|
+
if (graphSelection.type === 'node' && graphSelection.nodeId) {
|
|
579
|
+
const id = graphSelection.nodeId
|
|
580
|
+
return filteredRawFlows.filter(f => {
|
|
581
|
+
const srcId = f.source.namespace ? `${f.source.namespace}/${f.source.name}` : f.source.name
|
|
582
|
+
const dstId = f.destination.namespace ? `${f.destination.namespace}/${f.destination.name}` : f.destination.name
|
|
583
|
+
return srcId === id || dstId === id
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
if (graphSelection.type === 'edge' && graphSelection.sourceId && graphSelection.destId) {
|
|
587
|
+
return filteredRawFlows.filter(f => {
|
|
588
|
+
const srcId = f.source.namespace ? `${f.source.namespace}/${f.source.name}` : f.source.name
|
|
589
|
+
const dstId = f.destination.namespace ? `${f.destination.namespace}/${f.destination.name}` : f.destination.name
|
|
590
|
+
// Match either direction (request goes A→B, response goes B→A)
|
|
591
|
+
return (srcId === graphSelection.sourceId && dstId === graphSelection.destId) ||
|
|
592
|
+
(srcId === graphSelection.destId && dstId === graphSelection.sourceId)
|
|
593
|
+
})
|
|
594
|
+
}
|
|
595
|
+
return filteredRawFlows
|
|
596
|
+
}, [filteredRawFlows, graphSelection])
|
|
597
|
+
|
|
598
|
+
// Open flow list in the bottom dock
|
|
599
|
+
const openFlowListDock = useCallback(() => {
|
|
600
|
+
const id = dock.addTab({ type: 'traffic-flows', title: 'Traffic Flows' })
|
|
601
|
+
flowsTabIdRef.current = id
|
|
602
|
+
}, [dock])
|
|
603
|
+
|
|
604
|
+
// Auto-open flows dock when Hubble raw flows are available
|
|
605
|
+
const hasAutoOpenedFlowsRef = useRef(false)
|
|
606
|
+
useEffect(() => {
|
|
607
|
+
if (flowsData?.flows && flowsData.flows.length > 0 && !hasAutoOpenedFlowsRef.current) {
|
|
608
|
+
hasAutoOpenedFlowsRef.current = true
|
|
609
|
+
openFlowListDock()
|
|
610
|
+
}
|
|
611
|
+
}, [flowsData?.flows, openFlowListDock])
|
|
612
|
+
|
|
613
|
+
// Show L7 filters only when flows actually contain L7 data
|
|
614
|
+
const hasL7Data = useMemo(() => {
|
|
615
|
+
if (!flowsData?.aggregated) return false
|
|
616
|
+
return flowsData.aggregated.some(f => f.l7Protocol || f.topHTTPPaths || f.topDNSQueries)
|
|
617
|
+
}, [flowsData?.aggregated])
|
|
618
|
+
|
|
619
|
+
// Toggle L7 filter helpers
|
|
620
|
+
const toggleL7Method = useCallback((method: string) => {
|
|
621
|
+
setL7Methods(prev => { const next = new Set(prev); next.has(method) ? next.delete(method) : next.add(method); return next })
|
|
622
|
+
}, [])
|
|
623
|
+
const toggleL7StatusRange = useCallback((range: string) => {
|
|
624
|
+
setL7StatusRanges(prev => { const next = new Set(prev); next.has(range) ? next.delete(range) : next.add(range); return next })
|
|
625
|
+
}, [])
|
|
626
|
+
const toggleL7Verdict = useCallback((verdict: string) => {
|
|
627
|
+
setL7Verdicts(prev => { const next = new Set(prev); next.has(verdict) ? next.delete(verdict) : next.add(verdict); return next })
|
|
628
|
+
}, [])
|
|
629
|
+
|
|
630
|
+
// Toggle namespace visibility
|
|
631
|
+
const toggleNamespace = useCallback((ns: string) => {
|
|
632
|
+
setHiddenNamespaces(prev => {
|
|
633
|
+
const next = new Set(prev)
|
|
634
|
+
if (next.has(ns)) {
|
|
635
|
+
next.delete(ns)
|
|
636
|
+
} else {
|
|
637
|
+
next.add(ns)
|
|
638
|
+
}
|
|
639
|
+
return next
|
|
640
|
+
})
|
|
641
|
+
}, [])
|
|
642
|
+
|
|
643
|
+
// Process flows for external service aggregation (Phase 4.2)
|
|
644
|
+
// Also tracks service categories for coloring external nodes
|
|
645
|
+
const { processedFlows, serviceCategories } = useMemo<{
|
|
646
|
+
processedFlows: AggregatedFlow[]
|
|
647
|
+
serviceCategories: Map<string, string>
|
|
648
|
+
}>(() => {
|
|
649
|
+
const categories = new Map<string, string>()
|
|
650
|
+
|
|
651
|
+
// Helper to get service info (optionally using port-based detection)
|
|
652
|
+
const getServiceInfo = (name: string, port: number) => {
|
|
653
|
+
return getExternalServiceName(name, detectServices ? port : undefined)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (!aggregateExternal) {
|
|
657
|
+
// Even without aggregation, detect service categories for coloring (destinations only)
|
|
658
|
+
filteredFlows.forEach(flow => {
|
|
659
|
+
if (isExternal(flow.destination.kind)) {
|
|
660
|
+
const info = getServiceInfo(flow.destination.name, flow.port)
|
|
661
|
+
if (info.category) {
|
|
662
|
+
categories.set(flow.destination.name, info.category)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// Don't apply port-based detection to sources - port tells us the destination service
|
|
666
|
+
})
|
|
667
|
+
return { processedFlows: filteredFlows, serviceCategories: categories }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Aggregate flows to the same external service
|
|
671
|
+
const aggregatedMap = new Map<string, AggregatedFlow>()
|
|
672
|
+
|
|
673
|
+
filteredFlows.forEach(flow => {
|
|
674
|
+
// Only aggregate destinations based on port/hostname - sources keep their original name
|
|
675
|
+
// Port-based detection (MongoDB:27017) only makes sense for destinations
|
|
676
|
+
const sourceAgg = isExternal(flow.source.kind)
|
|
677
|
+
? getExternalServiceName(flow.source.name) // No port - hostname patterns only
|
|
678
|
+
: { name: flow.source.name, aggregated: false }
|
|
679
|
+
const destAgg = isExternal(flow.destination.kind)
|
|
680
|
+
? getServiceInfo(flow.destination.name, flow.port) // Full detection with port
|
|
681
|
+
: { name: flow.destination.name, aggregated: false }
|
|
682
|
+
|
|
683
|
+
// Track categories for coloring (destinations only - sources don't get port-based categories)
|
|
684
|
+
if (destAgg.category) categories.set(destAgg.name, destAgg.category)
|
|
685
|
+
|
|
686
|
+
// Create a unique key for the aggregated flow (without port since we aggregate by service)
|
|
687
|
+
const sourceKey = flow.source.namespace
|
|
688
|
+
? `${flow.source.namespace}/${sourceAgg.name}`
|
|
689
|
+
: sourceAgg.name
|
|
690
|
+
const destKey = flow.destination.namespace
|
|
691
|
+
? `${flow.destination.namespace}/${destAgg.name}`
|
|
692
|
+
: destAgg.name
|
|
693
|
+
// Group by service name, not by port (all MongoDB connections become one edge)
|
|
694
|
+
const key = `${sourceKey}->${destKey}`
|
|
695
|
+
|
|
696
|
+
const existing = aggregatedMap.get(key)
|
|
697
|
+
if (existing) {
|
|
698
|
+
// Merge connections and bytes
|
|
699
|
+
existing.connections += flow.connections
|
|
700
|
+
existing.bytesSent += flow.bytesSent
|
|
701
|
+
existing.bytesRecv += flow.bytesRecv
|
|
702
|
+
existing.flowCount += flow.flowCount
|
|
703
|
+
if (flow.requestCount) {
|
|
704
|
+
existing.requestCount = (existing.requestCount || 0) + flow.requestCount
|
|
705
|
+
}
|
|
706
|
+
if (flow.errorCount) {
|
|
707
|
+
existing.errorCount = (existing.errorCount || 0) + flow.errorCount
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
// Create new aggregated flow with modified names
|
|
711
|
+
aggregatedMap.set(key, {
|
|
712
|
+
...flow,
|
|
713
|
+
source: sourceAgg.aggregated
|
|
714
|
+
? { ...flow.source, name: sourceAgg.name }
|
|
715
|
+
: flow.source,
|
|
716
|
+
destination: destAgg.aggregated
|
|
717
|
+
? { ...flow.destination, name: destAgg.name }
|
|
718
|
+
: flow.destination,
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
return { processedFlows: Array.from(aggregatedMap.values()), serviceCategories: categories }
|
|
724
|
+
}, [filteredFlows, aggregateExternal, detectServices])
|
|
725
|
+
|
|
726
|
+
// Collapse inbound internet traffic (external sources → internal destinations)
|
|
727
|
+
const internetCollapsedFlows = useMemo<AggregatedFlow[]>(() => {
|
|
728
|
+
if (!collapseInternet) return processedFlows
|
|
729
|
+
|
|
730
|
+
// Group flows where external sources connect to internal destinations
|
|
731
|
+
const internetFlowsMap = new Map<string, AggregatedFlow>() // destKey -> aggregated flow
|
|
732
|
+
const nonInternetFlows: AggregatedFlow[] = []
|
|
733
|
+
|
|
734
|
+
processedFlows.forEach(flow => {
|
|
735
|
+
const sourceIsExternal = isExternal(flow.source.kind)
|
|
736
|
+
const destIsInternal = !isExternal(flow.destination.kind)
|
|
737
|
+
|
|
738
|
+
// Only collapse external → internal flows (inbound internet traffic)
|
|
739
|
+
if (sourceIsExternal && destIsInternal) {
|
|
740
|
+
// Create a key based on destination + port
|
|
741
|
+
const destKey = flow.destination.namespace
|
|
742
|
+
? `${flow.destination.namespace}/${flow.destination.name}:${flow.port}`
|
|
743
|
+
: `${flow.destination.name}:${flow.port}`
|
|
744
|
+
|
|
745
|
+
const existing = internetFlowsMap.get(destKey)
|
|
746
|
+
if (existing) {
|
|
747
|
+
// Merge into existing "Internet" flow
|
|
748
|
+
existing.connections += flow.connections
|
|
749
|
+
existing.bytesSent += flow.bytesSent
|
|
750
|
+
existing.bytesRecv += flow.bytesRecv
|
|
751
|
+
existing.flowCount += flow.flowCount
|
|
752
|
+
} else {
|
|
753
|
+
// Create new "Internet" → destination flow
|
|
754
|
+
internetFlowsMap.set(destKey, {
|
|
755
|
+
...flow,
|
|
756
|
+
source: {
|
|
757
|
+
name: 'Internet',
|
|
758
|
+
namespace: '',
|
|
759
|
+
kind: 'Internet',
|
|
760
|
+
},
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
nonInternetFlows.push(flow)
|
|
765
|
+
}
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
return [...nonInternetFlows, ...Array.from(internetFlowsMap.values())]
|
|
769
|
+
}, [processedFlows, collapseInternet])
|
|
770
|
+
|
|
771
|
+
// When grouping addons:
|
|
772
|
+
// 1. Aggregate internet → addon into single edge to group
|
|
773
|
+
// 2. Aggregate addon → kubernetes into single edge from group
|
|
774
|
+
const finalFlows = useMemo<AggregatedFlow[]>(() => {
|
|
775
|
+
if (addonMode !== 'group') return internetCollapsedFlows
|
|
776
|
+
|
|
777
|
+
// Track totals for aggregated edges
|
|
778
|
+
let addonInternetTotal = 0
|
|
779
|
+
let addonToK8sTotal = 0
|
|
780
|
+
const processedFlows: AggregatedFlow[] = []
|
|
781
|
+
|
|
782
|
+
// Check if destination is the kubernetes API server
|
|
783
|
+
const isKubernetesAPI = (name: string, namespace: string | undefined) => {
|
|
784
|
+
return name === 'kubernetes' && (!namespace || namespace === 'default')
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
internetCollapsedFlows.forEach(flow => {
|
|
788
|
+
const sourceIsAddon = isClusterAddon(flow.source.name, flow.source.namespace)
|
|
789
|
+
const destIsAddon = isClusterAddon(flow.destination.name, flow.destination.namespace)
|
|
790
|
+
const sourceIsInternet = flow.source.kind === 'Internet'
|
|
791
|
+
const destIsK8sAPI = isKubernetesAPI(flow.destination.name, flow.destination.namespace)
|
|
792
|
+
|
|
793
|
+
// Internet → Addon: aggregate into single edge to group
|
|
794
|
+
if (sourceIsInternet && destIsAddon) {
|
|
795
|
+
addonInternetTotal += flow.connections
|
|
796
|
+
processedFlows.push({
|
|
797
|
+
...flow,
|
|
798
|
+
source: {
|
|
799
|
+
name: 'addon-internet',
|
|
800
|
+
namespace: '',
|
|
801
|
+
kind: 'SkipEdge', // Create addon node but skip individual edge
|
|
802
|
+
},
|
|
803
|
+
})
|
|
804
|
+
}
|
|
805
|
+
// Addon → Kubernetes API: aggregate into single edge from group
|
|
806
|
+
else if (sourceIsAddon && destIsK8sAPI) {
|
|
807
|
+
addonToK8sTotal += flow.connections
|
|
808
|
+
processedFlows.push({
|
|
809
|
+
...flow,
|
|
810
|
+
destination: {
|
|
811
|
+
...flow.destination,
|
|
812
|
+
kind: 'SkipEdge', // Create kubernetes node but skip individual edge
|
|
813
|
+
},
|
|
814
|
+
})
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
processedFlows.push(flow)
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
// Add virtual flow for Internet → Addon Group edge
|
|
822
|
+
if (addonInternetTotal > 0) {
|
|
823
|
+
processedFlows.push({
|
|
824
|
+
source: {
|
|
825
|
+
name: 'addon-internet',
|
|
826
|
+
namespace: '',
|
|
827
|
+
kind: 'AddonInternet',
|
|
828
|
+
},
|
|
829
|
+
destination: {
|
|
830
|
+
name: 'addon-group-target',
|
|
831
|
+
namespace: '',
|
|
832
|
+
kind: 'AddonGroupTarget',
|
|
833
|
+
},
|
|
834
|
+
protocol: 'tcp',
|
|
835
|
+
port: 0,
|
|
836
|
+
connections: addonInternetTotal,
|
|
837
|
+
bytesSent: 0,
|
|
838
|
+
bytesRecv: 0,
|
|
839
|
+
flowCount: 1,
|
|
840
|
+
lastSeen: new Date().toISOString(),
|
|
841
|
+
})
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Add virtual flow for Addon Group → Kubernetes edge
|
|
845
|
+
if (addonToK8sTotal > 0) {
|
|
846
|
+
processedFlows.push({
|
|
847
|
+
source: {
|
|
848
|
+
name: 'addon-group-source',
|
|
849
|
+
namespace: '',
|
|
850
|
+
kind: 'AddonGroupSource',
|
|
851
|
+
},
|
|
852
|
+
destination: {
|
|
853
|
+
name: 'kubernetes',
|
|
854
|
+
namespace: 'default',
|
|
855
|
+
kind: 'Service',
|
|
856
|
+
},
|
|
857
|
+
protocol: 'tcp',
|
|
858
|
+
port: 443,
|
|
859
|
+
connections: addonToK8sTotal,
|
|
860
|
+
bytesSent: 0,
|
|
861
|
+
bytesRecv: 0,
|
|
862
|
+
flowCount: 1,
|
|
863
|
+
lastSeen: new Date().toISOString(),
|
|
864
|
+
})
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return processedFlows
|
|
868
|
+
}, [internetCollapsedFlows, addonMode])
|
|
869
|
+
|
|
870
|
+
// Stats for display
|
|
871
|
+
const flowStats = useMemo(() => {
|
|
872
|
+
const total = flowsData?.aggregated?.length || 0
|
|
873
|
+
const filtered = filteredFlows.length
|
|
874
|
+
const shown = finalFlows.length
|
|
875
|
+
const hidden = total - filtered
|
|
876
|
+
const aggregated = filtered - shown
|
|
877
|
+
return { total, filtered, shown, hidden, aggregated }
|
|
878
|
+
}, [flowsData?.aggregated?.length, filteredFlows.length, finalFlows.length])
|
|
879
|
+
|
|
880
|
+
// Compute hot path threshold (top 10% of connections)
|
|
881
|
+
const hotPathThreshold = useMemo(() => {
|
|
882
|
+
if (finalFlows.length === 0) return 0
|
|
883
|
+
const connectionCounts = finalFlows.map(f => f.connections).sort((a, b) => b - a)
|
|
884
|
+
const topTenPercentIndex = Math.max(0, Math.floor(connectionCounts.length * 0.1) - 1)
|
|
885
|
+
return connectionCounts[topTenPercentIndex] || connectionCounts[0] || 0
|
|
886
|
+
}, [finalFlows])
|
|
887
|
+
|
|
888
|
+
// Extract unique namespaces with node counts (from filtered flows, excluding namespace filter itself)
|
|
889
|
+
// This shows only namespaces that pass other filters (hideSystem, hideExternal, minConnections)
|
|
890
|
+
const namespacesWithCounts = useMemo(() => {
|
|
891
|
+
const nsCounts = new Map<string, Set<string>>() // namespace -> set of node names
|
|
892
|
+
|
|
893
|
+
// Use flows filtered by everything EXCEPT namespace filter
|
|
894
|
+
const flows = (flowsData?.aggregated || []).filter(flow => {
|
|
895
|
+
const sourceIsSystem = isSystemEndpoint(flow.source.name, flow.source.namespace, flow.source.kind)
|
|
896
|
+
const destIsSystem = isSystemEndpoint(flow.destination.name, flow.destination.namespace, flow.destination.kind)
|
|
897
|
+
|
|
898
|
+
if (hideSystem && (sourceIsSystem || destIsSystem)) {
|
|
899
|
+
return false
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (hideExternal) {
|
|
903
|
+
if (isExternal(flow.source.kind) || isExternal(flow.destination.kind)) {
|
|
904
|
+
return false
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (flow.connections < minConnections) {
|
|
909
|
+
return false
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return true
|
|
913
|
+
})
|
|
914
|
+
|
|
915
|
+
flows.forEach(flow => {
|
|
916
|
+
// Count source nodes
|
|
917
|
+
if (flow.source.namespace && flow.source.kind.toLowerCase() !== 'external') {
|
|
918
|
+
if (!nsCounts.has(flow.source.namespace)) {
|
|
919
|
+
nsCounts.set(flow.source.namespace, new Set())
|
|
920
|
+
}
|
|
921
|
+
nsCounts.get(flow.source.namespace)!.add(flow.source.name)
|
|
922
|
+
}
|
|
923
|
+
// Count destination nodes
|
|
924
|
+
if (flow.destination.namespace && flow.destination.kind.toLowerCase() !== 'external') {
|
|
925
|
+
if (!nsCounts.has(flow.destination.namespace)) {
|
|
926
|
+
nsCounts.set(flow.destination.namespace, new Set())
|
|
927
|
+
}
|
|
928
|
+
nsCounts.get(flow.destination.namespace)!.add(flow.destination.name)
|
|
929
|
+
}
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
return Array.from(nsCounts.entries()).map(([name, nodes]) => ({
|
|
933
|
+
name,
|
|
934
|
+
nodeCount: nodes.size,
|
|
935
|
+
}))
|
|
936
|
+
}, [flowsData?.aggregated, hideSystem, hideExternal, minConnections])
|
|
937
|
+
|
|
938
|
+
// Determine wizard state based on sources detection
|
|
939
|
+
useEffect(() => {
|
|
940
|
+
if (sourcesLoading) {
|
|
941
|
+
setWizardState('detecting')
|
|
942
|
+
return
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (!sourcesData) {
|
|
946
|
+
setWizardState('not_found')
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Only consider sources with status 'available' as ready
|
|
951
|
+
const availableSources = sourcesData.detected.filter(s => s.status === 'available')
|
|
952
|
+
if (availableSources.length > 0) {
|
|
953
|
+
setWizardState('ready')
|
|
954
|
+
} else {
|
|
955
|
+
setWizardState('not_found')
|
|
956
|
+
}
|
|
957
|
+
}, [sourcesData, sourcesLoading])
|
|
958
|
+
|
|
959
|
+
// Shared connection handler — used by auto-connect and retry buttons
|
|
960
|
+
const handleConnect = useCallback(() => {
|
|
961
|
+
setIsConnecting(true)
|
|
962
|
+
setConnectionError(null)
|
|
963
|
+
queryClient.removeQueries({ queryKey: ['traffic-flows'] })
|
|
964
|
+
|
|
965
|
+
connectMutation.mutate(undefined, {
|
|
966
|
+
onSuccess: (data) => {
|
|
967
|
+
setIsConnecting(false)
|
|
968
|
+
if (!data.connected && data.error) {
|
|
969
|
+
setConnectionError(data.error)
|
|
970
|
+
hasAutoConnectedRef.current = false // allow retry
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
onError: (error) => {
|
|
974
|
+
setIsConnecting(false)
|
|
975
|
+
setConnectionError(error.message)
|
|
976
|
+
hasAutoConnectedRef.current = false // allow retry
|
|
977
|
+
},
|
|
978
|
+
})
|
|
979
|
+
}, [connectMutation, queryClient])
|
|
980
|
+
|
|
981
|
+
// Auto-connect when source is detected
|
|
982
|
+
useEffect(() => {
|
|
983
|
+
if (wizardState === 'ready' && !hasAutoConnectedRef.current && !isConnecting) {
|
|
984
|
+
hasAutoConnectedRef.current = true
|
|
985
|
+
handleConnect()
|
|
986
|
+
}
|
|
987
|
+
}, [wizardState, isConnecting, handleConnect])
|
|
988
|
+
|
|
989
|
+
// Show wizard if no traffic source detected
|
|
990
|
+
if (wizardState !== 'ready') {
|
|
991
|
+
return (
|
|
992
|
+
<TrafficWizard
|
|
993
|
+
state={wizardState}
|
|
994
|
+
setState={setWizardState}
|
|
995
|
+
sourcesData={sourcesData}
|
|
996
|
+
sourcesLoading={sourcesLoading}
|
|
997
|
+
onRefetch={refetchSources}
|
|
998
|
+
/>
|
|
999
|
+
)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<TrafficFlowListProvider flows={listFlows} graphSelection={graphSelection} clearSelection={() => setGraphSelection(null)}>
|
|
1004
|
+
<div className="flex h-full w-full">
|
|
1005
|
+
{/* Sidebar */}
|
|
1006
|
+
<TrafficFilterSidebar
|
|
1007
|
+
hideSystem={hideSystem}
|
|
1008
|
+
setHideSystem={setHideSystem}
|
|
1009
|
+
hideExternal={hideExternal}
|
|
1010
|
+
setHideExternal={setHideExternal}
|
|
1011
|
+
minConnections={minConnections}
|
|
1012
|
+
setMinConnections={setMinConnections}
|
|
1013
|
+
showNamespaceGroups={showNamespaceGroups}
|
|
1014
|
+
setShowNamespaceGroups={setShowNamespaceGroups}
|
|
1015
|
+
collapseInternet={collapseInternet}
|
|
1016
|
+
setCollapseInternet={setCollapseInternet}
|
|
1017
|
+
addonMode={addonMode}
|
|
1018
|
+
setAddonMode={setAddonMode}
|
|
1019
|
+
aggregateExternal={aggregateExternal}
|
|
1020
|
+
setAggregateExternal={setAggregateExternal}
|
|
1021
|
+
detectServices={detectServices}
|
|
1022
|
+
setDetectServices={setDetectServices}
|
|
1023
|
+
timeRange={timeRange}
|
|
1024
|
+
setTimeRange={setTimeRange}
|
|
1025
|
+
isHubble={sourcesData?.active === 'hubble' && hasL7Data}
|
|
1026
|
+
l7Protocol={l7Protocol}
|
|
1027
|
+
setL7Protocol={setL7Protocol}
|
|
1028
|
+
l7Methods={l7Methods}
|
|
1029
|
+
onToggleL7Method={toggleL7Method}
|
|
1030
|
+
l7StatusRanges={l7StatusRanges}
|
|
1031
|
+
onToggleL7StatusRange={toggleL7StatusRange}
|
|
1032
|
+
l7Verdicts={l7Verdicts}
|
|
1033
|
+
onToggleL7Verdict={toggleL7Verdict}
|
|
1034
|
+
dnsPattern={dnsPattern}
|
|
1035
|
+
setDnsPattern={setDnsPattern}
|
|
1036
|
+
namespaces={namespacesWithCounts}
|
|
1037
|
+
hiddenNamespaces={hiddenNamespaces}
|
|
1038
|
+
onToggleNamespace={toggleNamespace}
|
|
1039
|
+
/>
|
|
1040
|
+
|
|
1041
|
+
{/* Main content area */}
|
|
1042
|
+
<div className="flex-1 relative min-w-0">
|
|
1043
|
+
{/* Floating controls — overlaid on graph like topology view */}
|
|
1044
|
+
{(() => {
|
|
1045
|
+
const availableSources = sourcesData?.detected.filter(s => s.status === 'available') || []
|
|
1046
|
+
const activeName = sourcesData?.active
|
|
1047
|
+
const activeSource = availableSources.find(s => s.name === activeName) || availableSources[0]
|
|
1048
|
+
|
|
1049
|
+
const handleSwitchSource = (name: string) => {
|
|
1050
|
+
if (name === activeSource?.name) { setSourcePickerOpen(false); return }
|
|
1051
|
+
setSourcePickerOpen(false)
|
|
1052
|
+
setIsConnecting(true)
|
|
1053
|
+
setConnectionError(null)
|
|
1054
|
+
hasAutoConnectedRef.current = true
|
|
1055
|
+
setSourceMutation.mutate(name, {
|
|
1056
|
+
onSuccess: () => {
|
|
1057
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-sources'] })
|
|
1058
|
+
connectMutation.mutate(undefined, {
|
|
1059
|
+
onSuccess: (data) => {
|
|
1060
|
+
setIsConnecting(false)
|
|
1061
|
+
if (!data.connected && data.error) setConnectionError(data.error)
|
|
1062
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-flows'] })
|
|
1063
|
+
},
|
|
1064
|
+
onError: (error) => { setIsConnecting(false); setConnectionError(error.message) },
|
|
1065
|
+
})
|
|
1066
|
+
},
|
|
1067
|
+
onError: (error) => { setIsConnecting(false); setConnectionError(error.message) },
|
|
1068
|
+
})
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return (
|
|
1072
|
+
<>
|
|
1073
|
+
{/* Top-left: source status pill */}
|
|
1074
|
+
<div className="absolute top-3 left-3 z-10 flex items-center gap-2">
|
|
1075
|
+
{activeSource && (
|
|
1076
|
+
<div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-[11px]">
|
|
1077
|
+
{isConnecting ? (
|
|
1078
|
+
<>
|
|
1079
|
+
<Loader2 className="h-3 w-3 animate-spin text-blue-400" />
|
|
1080
|
+
<span className="text-blue-400">Connecting...</span>
|
|
1081
|
+
</>
|
|
1082
|
+
) : connectionError ? (
|
|
1083
|
+
<>
|
|
1084
|
+
<span className="w-2 h-2 rounded-full bg-yellow-500" />
|
|
1085
|
+
<span className="text-theme-text-secondary">{activeSource.name}</span>
|
|
1086
|
+
<button onClick={handleConnect} className="text-yellow-500 hover:text-yellow-400 font-medium">retry</button>
|
|
1087
|
+
</>
|
|
1088
|
+
) : (
|
|
1089
|
+
<>
|
|
1090
|
+
<span className="w-2 h-2 rounded-full bg-green-500" />
|
|
1091
|
+
{availableSources.length > 1 ? (
|
|
1092
|
+
<div className="relative" ref={sourcePickerRef}>
|
|
1093
|
+
<button onClick={() => setSourcePickerOpen(!sourcePickerOpen)} className="flex items-center gap-1 text-theme-text-secondary hover:text-theme-text-primary">
|
|
1094
|
+
{activeSource.name} <ChevronDown className="h-3 w-3" />
|
|
1095
|
+
</button>
|
|
1096
|
+
{sourcePickerOpen && (
|
|
1097
|
+
<div className="absolute top-full left-0 mt-1 z-50 bg-theme-surface border border-theme-border rounded-md shadow-lg py-1 min-w-[120px]">
|
|
1098
|
+
{availableSources.map(source => (
|
|
1099
|
+
<button key={source.name} onClick={() => handleSwitchSource(source.name)}
|
|
1100
|
+
className={clsx('w-full text-left px-3 py-1 text-xs hover:bg-theme-hover capitalize', source.name === activeSource.name && 'text-blue-400')}>
|
|
1101
|
+
{source.name}
|
|
1102
|
+
</button>
|
|
1103
|
+
))}
|
|
1104
|
+
</div>
|
|
1105
|
+
)}
|
|
1106
|
+
</div>
|
|
1107
|
+
) : (
|
|
1108
|
+
<span className="text-theme-text-secondary">{activeSource.name}</span>
|
|
1109
|
+
)}
|
|
1110
|
+
</>
|
|
1111
|
+
)}
|
|
1112
|
+
</div>
|
|
1113
|
+
)}
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
1116
|
+
{/* Top-right: stats + actions */}
|
|
1117
|
+
<div className="absolute top-3 right-3 z-10 flex items-center gap-2">
|
|
1118
|
+
{flowsData?.flows && flowsData.flows.length > 0 && (
|
|
1119
|
+
<button onClick={openFlowListDock}
|
|
1120
|
+
className="flex items-center gap-1 px-2 py-1 text-[10px] rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1121
|
+
title="Open flow list in dock">
|
|
1122
|
+
<List className="w-3 h-3" /> Flows
|
|
1123
|
+
</button>
|
|
1124
|
+
)}
|
|
1125
|
+
<div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-[10px] text-theme-text-tertiary">
|
|
1126
|
+
{flowStats.shown}/{flowStats.total}
|
|
1127
|
+
<button onClick={refetchFlows} disabled={flowsLoading || isRefreshAnimating}
|
|
1128
|
+
className={clsx('p-0.5 rounded hover:text-theme-text-primary transition-colors', (flowsLoading || isRefreshAnimating) && 'opacity-50')}>
|
|
1129
|
+
{flowsLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className={clsx('h-3 w-3', isRefreshAnimating && 'animate-spin')} />}
|
|
1130
|
+
</button>
|
|
1131
|
+
</div>
|
|
1132
|
+
</div>
|
|
1133
|
+
</>
|
|
1134
|
+
)
|
|
1135
|
+
})()}
|
|
1136
|
+
|
|
1137
|
+
{isConnecting || (flowsFetching && finalFlows.length === 0) ? (
|
|
1138
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
1139
|
+
<div className="flex items-center gap-2 text-theme-text-secondary">
|
|
1140
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
1141
|
+
<span>{isConnecting ? 'Connecting to traffic source...' : 'Loading traffic data...'}</span>
|
|
1142
|
+
</div>
|
|
1143
|
+
</div>
|
|
1144
|
+
) : finalFlows.length > 0 ? (
|
|
1145
|
+
<TrafficGraph
|
|
1146
|
+
flows={finalFlows}
|
|
1147
|
+
hotPathThreshold={hotPathThreshold}
|
|
1148
|
+
showNamespaceGroups={showNamespaceGroups}
|
|
1149
|
+
serviceCategories={serviceCategories}
|
|
1150
|
+
addonMode={addonMode}
|
|
1151
|
+
trafficSource={sourcesData?.active || ''}
|
|
1152
|
+
onSelectionChange={setGraphSelection}
|
|
1153
|
+
/>
|
|
1154
|
+
) : connectionError ? (
|
|
1155
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
1156
|
+
<div className="text-center space-y-3">
|
|
1157
|
+
<Plug className="h-12 w-12 text-yellow-500 mx-auto" />
|
|
1158
|
+
<p className="text-theme-text-secondary">Connection failed</p>
|
|
1159
|
+
<p className="text-xs text-theme-text-tertiary max-w-md">
|
|
1160
|
+
{connectionError}
|
|
1161
|
+
</p>
|
|
1162
|
+
<button
|
|
1163
|
+
onClick={handleConnect}
|
|
1164
|
+
className="px-3 py-1.5 text-sm btn-brand rounded"
|
|
1165
|
+
>
|
|
1166
|
+
Retry Connection
|
|
1167
|
+
</button>
|
|
1168
|
+
</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
) : (
|
|
1171
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
1172
|
+
<div className="text-center space-y-2">
|
|
1173
|
+
<Filter className="h-12 w-12 text-theme-text-tertiary mx-auto" />
|
|
1174
|
+
{flowStats.total > 0 && flowStats.shown === 0 ? (
|
|
1175
|
+
<>
|
|
1176
|
+
<p className="text-theme-text-secondary">All traffic is filtered out</p>
|
|
1177
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
1178
|
+
{flowStats.total} flows hidden by current filters.
|
|
1179
|
+
<button
|
|
1180
|
+
onClick={() => {
|
|
1181
|
+
setHideSystem(false)
|
|
1182
|
+
setHideExternal(false)
|
|
1183
|
+
setMinConnections(0)
|
|
1184
|
+
}}
|
|
1185
|
+
className="ml-1 text-blue-400 hover:underline"
|
|
1186
|
+
>
|
|
1187
|
+
Show all
|
|
1188
|
+
</button>
|
|
1189
|
+
</p>
|
|
1190
|
+
</>
|
|
1191
|
+
) : flowsData?.warning ? (
|
|
1192
|
+
<>
|
|
1193
|
+
<p className="text-theme-text-secondary">Unable to fetch traffic data</p>
|
|
1194
|
+
<p className="text-xs text-yellow-500 max-w-md">
|
|
1195
|
+
{flowsData.warning}
|
|
1196
|
+
</p>
|
|
1197
|
+
</>
|
|
1198
|
+
) : (
|
|
1199
|
+
<>
|
|
1200
|
+
<p className="text-theme-text-secondary">No traffic observed</p>
|
|
1201
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
1202
|
+
Traffic will appear here once connections are made between services
|
|
1203
|
+
</p>
|
|
1204
|
+
</>
|
|
1205
|
+
)}
|
|
1206
|
+
</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
)}
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
</TrafficFlowListProvider>
|
|
1212
|
+
)
|
|
1213
|
+
}
|