@skyhook-io/radar-app 1.10.0 → 1.11.0
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/package.json +6 -6
- package/src/App.tsx +2 -6
- package/src/api/client.ts +138 -6
- package/src/api/policy.test.ts +38 -0
- package/src/api/policy.ts +166 -2
- package/src/components/home/HomeView.tsx +15 -3
- package/src/components/home/NetworkPolicyCoverageCard.test.tsx +81 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +50 -7
- package/src/components/home/TopologyPreview.tsx +57 -11
- package/src/components/home/mcpToolCatalog.ts +15 -5
- package/src/components/resources/ResourcesView.tsx +15 -3
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +116 -1
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +227 -0
- package/src/components/resources/renderers/CNPGImageCatalogRenderer.tsx +123 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +152 -0
- package/src/components/resources/renderers/KyvernoPolicyCoverage.tsx +65 -0
- package/src/components/resources/renderers/KyvernoPolicyQueued.render.test.tsx +59 -0
- package/src/components/resources/renderers/KyvernoPolicyQueued.test.ts +99 -0
- package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +184 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +24 -1
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +44 -1
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +75 -1
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +35 -1
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +37 -20
- package/src/components/traffic/TrafficFlowList.tsx +16 -2
- package/src/components/traffic/TrafficGraph.tsx +150 -58
- package/src/components/traffic/TrafficView.tsx +168 -52
- package/src/components/traffic/TrafficWizard.tsx +13 -1
- package/src/components/traffic/trafficFilters.test.ts +103 -0
- package/src/components/traffic/trafficFilters.ts +117 -0
- package/src/components/ui/DiagnosticsOverlay.test.ts +75 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +47 -2
- package/src/components/workload/WorkloadView.tsx +55 -7
- package/src/utils/navigation.ts +44 -2
- package/src/utils/network-policy-navigation.test.ts +68 -0
- package/src/utils/topology-selection.test.ts +40 -0
- package/src/utils/topology-selection.ts +39 -0
|
@@ -23,6 +23,7 @@ import { isClusterAddon, type AddonMode } from './TrafficView'
|
|
|
23
23
|
import { SEVERITY_BADGE, SEVERITY_DOT, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
24
24
|
import { getNamespaceColor } from '../../utils/traffic-colors'
|
|
25
25
|
import { Tooltip } from '../ui/Tooltip'
|
|
26
|
+
import { isRateBasedSource } from './trafficFilters'
|
|
26
27
|
|
|
27
28
|
const elk = new ELK()
|
|
28
29
|
|
|
@@ -280,16 +281,21 @@ function TrafficNode({ data }: { data: TrafficNodeData }) {
|
|
|
280
281
|
)}>
|
|
281
282
|
:{portInfo.port}
|
|
282
283
|
</span>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
284
|
+
{/* A port serving traffic with no HTTP has no request rate to show, and
|
|
285
|
+
"0" beside the port reads as an idle port rather than an unmeasured
|
|
286
|
+
one. The port itself is still worth listing. */}
|
|
287
|
+
{portInfo.connections > 0 && (
|
|
288
|
+
<span className={clsx(
|
|
289
|
+
'truncate',
|
|
290
|
+
data.isHotPath
|
|
291
|
+
? 'text-orange-400'
|
|
292
|
+
: (hasNamespaceColor || isAddonNode)
|
|
293
|
+
? 'text-white/60'
|
|
294
|
+
: 'text-theme-text-tertiary'
|
|
295
|
+
)}>
|
|
296
|
+
{formatConnections(portInfo.connections)}{data.connLabel === 'req/s' ? '/s' : ''}
|
|
297
|
+
</span>
|
|
298
|
+
)}
|
|
293
299
|
</div>
|
|
294
300
|
))}
|
|
295
301
|
{data.ports.filter(p => p.port !== 0).length > MAX_VISIBLE_PORTS && (
|
|
@@ -303,7 +309,7 @@ function TrafficNode({ data }: { data: TrafficNodeData }) {
|
|
|
303
309
|
</div>
|
|
304
310
|
)}
|
|
305
311
|
{/* Total connections (only if no ports shown, or all ports are 0) */}
|
|
306
|
-
{(!data.ports || data.ports.filter(p => p.port !== 0).length === 0) && data.totalConnections && data.totalConnections > 0 && (
|
|
312
|
+
{(!data.ports || data.ports.filter(p => p.port !== 0).length === 0) && data.totalConnections != null && data.totalConnections > 0 && (
|
|
307
313
|
<div className="mt-1">
|
|
308
314
|
<span className={clsx(
|
|
309
315
|
'text-xs truncate',
|
|
@@ -339,6 +345,19 @@ function TrafficLegend() {
|
|
|
339
345
|
</svg>
|
|
340
346
|
<span className="text-theme-text-secondary">HTTP / gRPC</span>
|
|
341
347
|
</div>
|
|
348
|
+
<div className="flex items-center gap-2">
|
|
349
|
+
<svg width="24" height="8" className="shrink-0" aria-hidden="true">
|
|
350
|
+
<line x1="0" y1="4" x2="20" y2="4" stroke="currentColor" strokeWidth="2" className="text-theme-text-tertiary" />
|
|
351
|
+
<polygon points="20,1 24,4 20,7" className="fill-theme-text-tertiary" />
|
|
352
|
+
</svg>
|
|
353
|
+
<span className="text-theme-text-secondary">Arrow = who opened it</span>
|
|
354
|
+
</div>
|
|
355
|
+
<div className="flex items-center gap-2">
|
|
356
|
+
<svg width="24" height="8" className="shrink-0" aria-hidden="true">
|
|
357
|
+
<line x1="0" y1="4" x2="24" y2="4" stroke="currentColor" strokeWidth="2" className="text-theme-text-tertiary" />
|
|
358
|
+
</svg>
|
|
359
|
+
<span className="text-theme-text-secondary">No arrow = direction unknown</span>
|
|
360
|
+
</div>
|
|
342
361
|
<div className="flex items-center gap-2">
|
|
343
362
|
<svg width="24" height="8" className="shrink-0">
|
|
344
363
|
<line x1="0" y1="4" x2="24" y2="4" stroke="#06b6d4" strokeWidth="2" />
|
|
@@ -402,12 +421,14 @@ function DetailsPanel({
|
|
|
402
421
|
selection,
|
|
403
422
|
onClose,
|
|
404
423
|
flows,
|
|
405
|
-
|
|
424
|
+
isRateBased,
|
|
406
425
|
}: {
|
|
407
426
|
selection: Selection
|
|
408
427
|
onClose: () => void
|
|
409
428
|
flows: AggregatedFlow[]
|
|
410
|
-
|
|
429
|
+
// True for sources that measure a rate rather than counting connections —
|
|
430
|
+
// Istio and Beyla both do. Changes the wording and the /s suffix.
|
|
431
|
+
isRateBased: boolean
|
|
411
432
|
}) {
|
|
412
433
|
if (!selection) return null
|
|
413
434
|
|
|
@@ -548,10 +569,10 @@ function DetailsPanel({
|
|
|
548
569
|
Service: <span className="text-theme-text-primary capitalize">{nodeData.serviceCategory}</span>
|
|
549
570
|
</div>
|
|
550
571
|
)}
|
|
551
|
-
{nodeData.totalConnections && (
|
|
572
|
+
{nodeData.totalConnections != null && nodeData.totalConnections > 0 && (
|
|
552
573
|
<div className="text-xs text-theme-text-secondary">
|
|
553
|
-
{
|
|
554
|
-
{formatConnections(nodeData.totalConnections)}{
|
|
574
|
+
{isRateBased ? 'Total request rate' : 'Total connections'}: <span className="text-theme-text-primary font-medium">
|
|
575
|
+
{formatConnections(nodeData.totalConnections)}{isRateBased ? '/s' : ''}
|
|
555
576
|
</span>
|
|
556
577
|
</div>
|
|
557
578
|
)}
|
|
@@ -605,7 +626,7 @@ function DetailsPanel({
|
|
|
605
626
|
.map(([proto, count]) => (
|
|
606
627
|
<span key={proto} className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-theme-bg text-theme-text-secondary">
|
|
607
628
|
<span className="font-medium">{proto}</span>
|
|
608
|
-
<span className="text-theme-text-tertiary">{formatConnections(count)}</span>
|
|
629
|
+
<span className="text-theme-text-tertiary">{formatConnections(count)}{isRateBased ? '/s' : ''}</span>
|
|
609
630
|
</span>
|
|
610
631
|
))}
|
|
611
632
|
</div>
|
|
@@ -672,7 +693,15 @@ function DetailsPanel({
|
|
|
672
693
|
<span className="text-theme-text-primary truncate flex-1">
|
|
673
694
|
{flow.source.name}
|
|
674
695
|
</span>
|
|
675
|
-
|
|
696
|
+
{/* Same rule as the canvas and the edge panel: an edge nobody
|
|
697
|
+
could orient is not drawn with an arrow. */}
|
|
698
|
+
{flow.directionUnknown ? (
|
|
699
|
+
<Tooltip content="Neither end was reported as the initiator">
|
|
700
|
+
<span className="text-theme-text-tertiary shrink-0">—</span>
|
|
701
|
+
</Tooltip>
|
|
702
|
+
) : (
|
|
703
|
+
<ArrowRight className="h-3 w-3 text-theme-text-tertiary shrink-0" />
|
|
704
|
+
)}
|
|
676
705
|
{flow.port !== 0 && (
|
|
677
706
|
<span className="text-blue-400 font-mono">:{flow.port}</span>
|
|
678
707
|
)}
|
|
@@ -681,17 +710,19 @@ function DetailsPanel({
|
|
|
681
710
|
<span className="px-1 py-0.5 rounded bg-theme-bg text-theme-text-tertiary uppercase">
|
|
682
711
|
{flow.protocol || 'tcp'}
|
|
683
712
|
</span>
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
713
|
+
{flow.connections > 0 && (
|
|
714
|
+
<span className="text-theme-text-secondary">
|
|
715
|
+
{formatConnections(flow.connections)} {isRateBased ? 'req/s' : 'conn'}
|
|
716
|
+
</span>
|
|
717
|
+
)}
|
|
687
718
|
{(flow.bytesSent > 0 || flow.bytesRecv > 0) && (
|
|
688
719
|
<span className="text-theme-text-tertiary">
|
|
689
720
|
{formatBytes(flow.bytesSent + flow.bytesRecv)}
|
|
690
721
|
</span>
|
|
691
722
|
)}
|
|
692
|
-
{flow.errorCount && flow.errorCount > 0 && (
|
|
723
|
+
{flow.errorCount != null && flow.errorCount > 0 && (
|
|
693
724
|
<span className="text-red-400">
|
|
694
|
-
{formatConnections(flow.errorCount)} err
|
|
725
|
+
{formatConnections(flow.errorCount)} err{isRateBased ? '/s' : ''}
|
|
695
726
|
</span>
|
|
696
727
|
)}
|
|
697
728
|
</div>
|
|
@@ -713,7 +744,13 @@ function DetailsPanel({
|
|
|
713
744
|
.map((flow, i) => (
|
|
714
745
|
<div key={i} className="text-xs p-2 rounded bg-theme-elevated space-y-1">
|
|
715
746
|
<div className="flex items-center gap-1.5">
|
|
716
|
-
|
|
747
|
+
{flow.directionUnknown ? (
|
|
748
|
+
<Tooltip content="Neither end was reported as the initiator">
|
|
749
|
+
<span className="text-theme-text-tertiary shrink-0">—</span>
|
|
750
|
+
</Tooltip>
|
|
751
|
+
) : (
|
|
752
|
+
<ArrowRight className="h-3 w-3 text-theme-text-tertiary shrink-0" />
|
|
753
|
+
)}
|
|
717
754
|
<span className="text-theme-text-primary truncate flex-1">
|
|
718
755
|
{flow.destination.name}
|
|
719
756
|
</span>
|
|
@@ -725,17 +762,19 @@ function DetailsPanel({
|
|
|
725
762
|
<span className="px-1 py-0.5 rounded bg-theme-bg text-theme-text-tertiary uppercase">
|
|
726
763
|
{flow.protocol || 'tcp'}
|
|
727
764
|
</span>
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
765
|
+
{flow.connections > 0 && (
|
|
766
|
+
<span className="text-theme-text-secondary">
|
|
767
|
+
{formatConnections(flow.connections)} {isRateBased ? 'req/s' : 'conn'}
|
|
768
|
+
</span>
|
|
769
|
+
)}
|
|
731
770
|
{(flow.bytesSent > 0 || flow.bytesRecv > 0) && (
|
|
732
771
|
<span className="text-theme-text-tertiary">
|
|
733
772
|
{formatBytes(flow.bytesSent + flow.bytesRecv)}
|
|
734
773
|
</span>
|
|
735
774
|
)}
|
|
736
|
-
{flow.errorCount && flow.errorCount > 0 && (
|
|
775
|
+
{flow.errorCount != null && flow.errorCount > 0 && (
|
|
737
776
|
<span className="text-red-400">
|
|
738
|
-
{formatConnections(flow.errorCount)} err
|
|
777
|
+
{formatConnections(flow.errorCount)} err{isRateBased ? '/s' : ''}
|
|
739
778
|
</span>
|
|
740
779
|
)}
|
|
741
780
|
</div>
|
|
@@ -753,7 +792,16 @@ function DetailsPanel({
|
|
|
753
792
|
<div className="space-y-2">
|
|
754
793
|
<div className="flex items-center gap-2 text-sm">
|
|
755
794
|
<span className="text-theme-text-primary truncate">{edgeData.source.split('/').pop()}</span>
|
|
756
|
-
|
|
795
|
+
{edgeData.flow?.directionUnknown ? (
|
|
796
|
+
// The graph omits the arrowhead on this edge because nothing
|
|
797
|
+
// established which end opened the conversation. An arrow here
|
|
798
|
+
// would assert exactly what the graph is declining to claim.
|
|
799
|
+
<Tooltip content="Neither end was reported as the initiator">
|
|
800
|
+
<span className="text-theme-text-tertiary shrink-0">—</span>
|
|
801
|
+
</Tooltip>
|
|
802
|
+
) : (
|
|
803
|
+
<ArrowRight className="h-4 w-4 text-theme-text-tertiary shrink-0" />
|
|
804
|
+
)}
|
|
757
805
|
<span className="text-theme-text-primary truncate">{edgeData.target.split('/').pop()}</span>
|
|
758
806
|
</div>
|
|
759
807
|
|
|
@@ -772,12 +820,17 @@ function DetailsPanel({
|
|
|
772
820
|
: edgeData.protocol}
|
|
773
821
|
</div>
|
|
774
822
|
</div>
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
823
|
+
{(!isRateBased || edgeData.connections > 0) && (
|
|
824
|
+
// A rate-based source reports no request rate for traffic with no
|
|
825
|
+
// HTTP, and "0/s" beside a data figure in megabytes reads as "no
|
|
826
|
+
// traffic" rather than "not measured".
|
|
827
|
+
<div className="p-2 rounded bg-theme-elevated">
|
|
828
|
+
<div className="text-theme-text-tertiary">{isRateBased ? 'Request Rate' : 'Connections'}</div>
|
|
829
|
+
<div className="text-theme-text-primary font-medium">
|
|
830
|
+
{formatConnections(edgeData.connections)}{isRateBased ? '/s' : ''}
|
|
831
|
+
</div>
|
|
779
832
|
</div>
|
|
780
|
-
|
|
833
|
+
)}
|
|
781
834
|
{edgeData.flow && (edgeData.flow.bytesSent > 0 || edgeData.flow.bytesRecv > 0) && (
|
|
782
835
|
<div className="p-2 rounded bg-theme-elevated">
|
|
783
836
|
<div className="text-theme-text-tertiary">Data</div>
|
|
@@ -786,7 +839,7 @@ function DetailsPanel({
|
|
|
786
839
|
</div>
|
|
787
840
|
</div>
|
|
788
841
|
)}
|
|
789
|
-
{edgeData.flow?.requestCount && edgeData.flow.requestCount > 0 && (
|
|
842
|
+
{edgeData.flow?.requestCount != null && edgeData.flow.requestCount > 0 && (
|
|
790
843
|
<div className="p-2 rounded bg-theme-elevated">
|
|
791
844
|
<div className="text-theme-text-tertiary">Requests</div>
|
|
792
845
|
<div className="text-theme-text-primary font-medium">
|
|
@@ -794,12 +847,12 @@ function DetailsPanel({
|
|
|
794
847
|
</div>
|
|
795
848
|
</div>
|
|
796
849
|
)}
|
|
797
|
-
{edgeData.flow?.errorCount && edgeData.flow.errorCount > 0 && (
|
|
850
|
+
{edgeData.flow?.errorCount != null && edgeData.flow.errorCount > 0 && (
|
|
798
851
|
<div className="p-2 rounded bg-red-500/10 border border-red-500/30">
|
|
799
852
|
<div className="text-red-400">Errors (5xx)</div>
|
|
800
853
|
<div className="text-red-400 font-medium">
|
|
801
854
|
{formatConnections(edgeData.flow.errorCount)}/s
|
|
802
|
-
{edgeData.flow.requestCount && edgeData.flow.requestCount > 0 && (
|
|
855
|
+
{edgeData.flow.requestCount != null && edgeData.flow.requestCount > 0 && (
|
|
803
856
|
<span className="text-red-300 ml-1">
|
|
804
857
|
({((edgeData.flow.errorCount / edgeData.flow.requestCount) * 100).toFixed(1)}%)
|
|
805
858
|
</span>
|
|
@@ -910,17 +963,23 @@ function DetailsPanel({
|
|
|
910
963
|
{edgeData.flow && (
|
|
911
964
|
<div className="space-y-1 pt-2 border-t border-theme-border">
|
|
912
965
|
<div className="text-xs text-theme-text-secondary">
|
|
913
|
-
Source: <span className="text-theme-text-primary">{edgeData.flow.source.name}</span>
|
|
966
|
+
{edgeData.flow.directionUnknown ? 'Endpoint' : 'Source'}: <span className="text-theme-text-primary">{edgeData.flow.source.name}</span>
|
|
914
967
|
{edgeData.flow.source.namespace && (
|
|
915
968
|
<span className="text-theme-text-tertiary"> ({edgeData.flow.source.namespace})</span>
|
|
916
969
|
)}
|
|
917
970
|
</div>
|
|
918
971
|
<div className="text-xs text-theme-text-secondary">
|
|
919
|
-
Destination: <span className="text-theme-text-primary">{edgeData.flow.destination.name}</span>
|
|
972
|
+
{edgeData.flow.directionUnknown ? 'Endpoint' : 'Destination'}: <span className="text-theme-text-primary">{edgeData.flow.destination.name}</span>
|
|
920
973
|
{edgeData.flow.destination.namespace && (
|
|
921
974
|
<span className="text-theme-text-tertiary"> ({edgeData.flow.destination.namespace})</span>
|
|
922
975
|
)}
|
|
923
976
|
</div>
|
|
977
|
+
{edgeData.flow.directionUnknown && (
|
|
978
|
+
<div className="text-xs text-theme-text-tertiary mt-1">
|
|
979
|
+
Which endpoint opened this conversation was not reported, so the
|
|
980
|
+
two are listed in a fixed order rather than as caller and callee.
|
|
981
|
+
</div>
|
|
982
|
+
)}
|
|
924
983
|
{edgeData.flow.destination.kind.toLowerCase() === 'external' && (
|
|
925
984
|
<div className="text-xs text-yellow-400 mt-1">
|
|
926
985
|
External service
|
|
@@ -998,9 +1057,20 @@ function FitViewOnChange({
|
|
|
998
1057
|
return null
|
|
999
1058
|
}
|
|
1000
1059
|
|
|
1060
|
+
// A pair can carry both oriented and unoriented traffic at the same port, and the
|
|
1061
|
+
// backend keeps those as separate aggregates because no single answer about the
|
|
1062
|
+
// direction is right for both. Without the suffix they would share an edge id and
|
|
1063
|
+
// one would silently replace the other. Only unoriented edges are suffixed, so
|
|
1064
|
+
// every id a source that always orients its flows produces is unchanged.
|
|
1065
|
+
function trafficEdgeId(sourceId: string, destId: string, flow: AggregatedFlow): string {
|
|
1066
|
+
return `${sourceId}->${destId}:${flow.port}${flow.directionUnknown ? ':unoriented' : ''}`
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1001
1069
|
export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups = false, serviceCategories, addonMode = 'show', trafficSource = '', onSelectionChange }: TrafficGraphProps) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1070
|
+
// Beyla is rate-based too: its Connections field carries requests per second,
|
|
1071
|
+
// not a connection count, so it needs the same label Istio gets.
|
|
1072
|
+
const isRateBased = isRateBasedSource(trafficSource)
|
|
1073
|
+
const connLabel = isRateBased ? 'req/s' : 'conn'
|
|
1004
1074
|
const [layoutedNodes, setLayoutedNodes] = useState<Node<TrafficNodeData>[]>([])
|
|
1005
1075
|
const [layoutedEdges, setLayoutedEdges] = useState<Edge[]>([])
|
|
1006
1076
|
const [selection, setSelection] = useState<Selection | null>(null)
|
|
@@ -1015,7 +1085,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1015
1085
|
const destId = flow.destination.namespace
|
|
1016
1086
|
? `${flow.destination.namespace}/${flow.destination.name}`
|
|
1017
1087
|
: flow.destination.name
|
|
1018
|
-
const edgeId =
|
|
1088
|
+
const edgeId = trafficEdgeId(sourceId, destId, flow)
|
|
1019
1089
|
map.set(edgeId, flow)
|
|
1020
1090
|
})
|
|
1021
1091
|
return map
|
|
@@ -1198,7 +1268,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1198
1268
|
}
|
|
1199
1269
|
|
|
1200
1270
|
// Create edge with visual encoding (Phase 2.1, 2.2, 2.3)
|
|
1201
|
-
const edgeId =
|
|
1271
|
+
const edgeId = trafficEdgeId(sourceId, destId, flow)
|
|
1202
1272
|
const isHotEdge = flow.connections >= hotPathThreshold && hotPathThreshold > 0
|
|
1203
1273
|
const hasErrors = (flow.errorCount ?? 0) > 0
|
|
1204
1274
|
|
|
@@ -1218,20 +1288,34 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1218
1288
|
const strokeWidth = getEdgeWidth(flow.connections)
|
|
1219
1289
|
|
|
1220
1290
|
// Phase 2.2: Edge label - connection count with unit suffix + L7 details
|
|
1221
|
-
const connStr =
|
|
1291
|
+
const connStr = isRateBased
|
|
1222
1292
|
? `${formatConnections(flow.connections)}/s`
|
|
1223
1293
|
: formatConnections(flow.connections)
|
|
1224
1294
|
const l7Label = flow.l7Protocol ? `${flow.l7Protocol} · ` : ''
|
|
1225
1295
|
const latencyLabel = flow.latencyP50Ms ? ` · ${formatLatency(flow.latencyP50Ms)}` : ''
|
|
1296
|
+
// errorCount is a rate for a rate-based source, exactly like connections above.
|
|
1297
|
+
// Without the suffix a destination failing once a second reads as one error.
|
|
1226
1298
|
const errorLabel = hasErrors
|
|
1227
|
-
? ` · ${formatConnections(flow.errorCount ?? 0)} err`
|
|
1299
|
+
? ` · ${formatConnections(flow.errorCount ?? 0)} err${isRateBased ? '/s' : ''}`
|
|
1228
1300
|
: ''
|
|
1229
|
-
|
|
1301
|
+
// A source that measures rates has no count for an edge with no requests on
|
|
1302
|
+
// it — a plain TCP conversation, or one whose direction is unknown. Printing
|
|
1303
|
+
// "0/s" there reads as no traffic, on an edge that may be carrying megabytes.
|
|
1304
|
+
const countLabel = isRateBased && !flow.connections ? '' : connStr
|
|
1305
|
+
const edgeLabel = [l7Label + countLabel, latencyLabel, errorLabel]
|
|
1306
|
+
.join('')
|
|
1307
|
+
.replace(/^ · /, '')
|
|
1230
1308
|
|
|
1231
1309
|
edgeList.push({
|
|
1232
1310
|
id: edgeId,
|
|
1233
1311
|
source: sourceId,
|
|
1234
1312
|
target: destId,
|
|
1313
|
+
// React Flow's default is "Edge from A to B", which states a direction this
|
|
1314
|
+
// edge is deliberately drawn without. Assistive technology should hear the
|
|
1315
|
+
// same uncertainty the missing arrowhead conveys.
|
|
1316
|
+
ariaLabel: flow.directionUnknown
|
|
1317
|
+
? `Connection between ${sourceId} and ${destId}, direction unknown`
|
|
1318
|
+
: `Edge from ${sourceId} to ${destId}`,
|
|
1235
1319
|
type: 'smoothstep',
|
|
1236
1320
|
animated: isHotEdge, // Animate hot paths
|
|
1237
1321
|
label: edgeLabel,
|
|
@@ -1249,12 +1333,20 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1249
1333
|
stroke: strokeColor,
|
|
1250
1334
|
...(hasDrops && { strokeDasharray: '6 3' }),
|
|
1251
1335
|
},
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1336
|
+
// No arrowhead when the initiator is unknown. Beyla labels both halves of a
|
|
1337
|
+
// UDP conversation the same way, so an arrow would be asserting something
|
|
1338
|
+
// the data does not say; the edge still carries the traffic in both
|
|
1339
|
+
// directions.
|
|
1340
|
+
...(flow.directionUnknown
|
|
1341
|
+
? {}
|
|
1342
|
+
: {
|
|
1343
|
+
markerEnd: {
|
|
1344
|
+
type: MarkerType.ArrowClosed,
|
|
1345
|
+
width: 16,
|
|
1346
|
+
height: 16,
|
|
1347
|
+
color: strokeColor,
|
|
1348
|
+
},
|
|
1349
|
+
}),
|
|
1258
1350
|
})
|
|
1259
1351
|
})
|
|
1260
1352
|
|
|
@@ -1264,7 +1356,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1264
1356
|
addonGroupEdge, // Pass this for adding after group is created
|
|
1265
1357
|
addonGroupOutEdge, // Pass this for adding group → kubernetes edge
|
|
1266
1358
|
}
|
|
1267
|
-
}, [flows, hotPathThreshold, showNamespaceGroups, serviceCategories, addonMode,
|
|
1359
|
+
}, [flows, hotPathThreshold, showNamespaceGroups, serviceCategories, addonMode, isRateBased, connLabel])
|
|
1268
1360
|
|
|
1269
1361
|
// Apply ELK layout
|
|
1270
1362
|
const applyLayout = useCallback(async () => {
|
|
@@ -1389,7 +1481,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1389
1481
|
target: 'addon-group',
|
|
1390
1482
|
type: 'smoothstep',
|
|
1391
1483
|
animated: isHotEdge,
|
|
1392
|
-
label: formatConnections(connections),
|
|
1484
|
+
label: isRateBased ? `${formatConnections(connections)}/s` : formatConnections(connections),
|
|
1393
1485
|
labelBgStyle: {
|
|
1394
1486
|
fill: '#581c87', // purple-900
|
|
1395
1487
|
fillOpacity: 0.9,
|
|
@@ -1423,7 +1515,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1423
1515
|
target: targetId,
|
|
1424
1516
|
type: 'smoothstep',
|
|
1425
1517
|
animated: isHotEdge,
|
|
1426
|
-
label: formatConnections(connections),
|
|
1518
|
+
label: isRateBased ? `${formatConnections(connections)}/s` : formatConnections(connections),
|
|
1427
1519
|
labelBgStyle: {
|
|
1428
1520
|
fill: '#581c87', // purple-900
|
|
1429
1521
|
fillOpacity: 0.9,
|
|
@@ -1462,7 +1554,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1462
1554
|
setLayoutedNodes(positionedNodes)
|
|
1463
1555
|
setLayoutedEdges(rawEdges)
|
|
1464
1556
|
}
|
|
1465
|
-
}, [rawNodes, rawEdges, addonMode, addonGroupEdge, addonGroupOutEdge, hotPathThreshold])
|
|
1557
|
+
}, [rawNodes, rawEdges, addonMode, addonGroupEdge, addonGroupOutEdge, hotPathThreshold, isRateBased])
|
|
1466
1558
|
|
|
1467
1559
|
// Run layout when flows change
|
|
1468
1560
|
useEffect(() => {
|
|
@@ -1556,7 +1648,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1556
1648
|
selection={selection}
|
|
1557
1649
|
onClose={() => setSelection(null)}
|
|
1558
1650
|
flows={flows}
|
|
1559
|
-
|
|
1651
|
+
isRateBased={isRateBased}
|
|
1560
1652
|
/>
|
|
1561
1653
|
)}
|
|
1562
1654
|
</div>
|