Skip to content

Commit 7c9f788

Browse files
committed
feat(dashboard): adding a dedicated dashboard
Signed-off-by: kairav mittal <kairavmittal@gmail.com>
1 parent 267c708 commit 7c9f788

14 files changed

Lines changed: 1379 additions & 0 deletions

File tree

build/charts/antrea-ui/templates/clusterrole.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ rules:
1818
verbs:
1919
- list
2020
- get
21+
- apiGroups:
22+
- stats.antrea.io
23+
resources:
24+
- nodelatencystats
25+
verbs:
26+
- list
27+
- get
2128
- apiGroups:
2229
- crd.antrea.io
2330
resources:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright 2026 Antrea Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import api from './axios';
18+
import { handleError } from './common';
19+
20+
export interface TargetIPLatencyStats {
21+
targetIP: string
22+
lastSendTime?: string
23+
lastRecvTime?: string
24+
lastMeasuredRTTNanoseconds?: number
25+
}
26+
27+
export interface PeerNodeLatencyStats {
28+
nodeName: string
29+
targetIPLatencyStats?: TargetIPLatencyStats[]
30+
}
31+
32+
export interface NodeLatencyStats {
33+
metadata: {
34+
name: string
35+
}
36+
peerNodeLatencyStats?: PeerNodeLatencyStats[]
37+
}
38+
39+
export const nodeLatencyStatsAPI = {
40+
fetchAll: async (): Promise<NodeLatencyStats[]> => {
41+
return api.get<{ items: NodeLatencyStats[] }>(
42+
`k8s/apis/stats.antrea.io/v1alpha1/nodelatencystats`,
43+
).then((response) => response.data.items).catch((error) => {
44+
console.error("Unable to fetch Node Latency Stats");
45+
handleError(error);
46+
});
47+
},
48+
};

client/web/antrea-ui/src/components/nav.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import {
2424
dashboardIcon, dashboardIconName,
2525
bugIcon, bugIconName,
2626
eyeIcon, eyeIconName,
27+
nodesIcon, nodesIconName,
2728
} from '@cds/core/icon';
2829

2930
ClarityIcons.addIcons(
3031
cogIcon,
3132
dashboardIcon,
3233
bugIcon,
3334
eyeIcon,
35+
nodesIcon,
3436
);
3537

3638
export default function NavTab() {
@@ -57,6 +59,12 @@ export default function NavTab() {
5759
Flow Visibility
5860
</Link>
5961
</CdsNavigationItem>
62+
<CdsNavigationItem>
63+
<Link to="/nodelatency">
64+
<CdsIcon shape={nodesIconName} solid size="sm"></CdsIcon>
65+
Node Latency
66+
</Link>
67+
</CdsNavigationItem>
6068
<CdsNavigationItem>
6169
<Link to="/settings">
6270
<CdsIcon shape={cogIconName} solid size="sm"></CdsIcon>
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Copyright 2026 Antrea Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { CdsCard } from '@cds/react/card';
18+
import { CdsButton } from '@cds/react/button';
19+
import { CdsDivider } from '@cds/react/divider';
20+
import { NodeLink, NodeLatencyModel, nodeLinksFor } from '../routes/nodelatency-util';
21+
22+
function fmtRtt(link: NodeLink): string {
23+
return link.down || link.rttMs === undefined ? 'N/A' : link.rttMs.toFixed(3);
24+
}
25+
26+
function latestRecv(link: NodeLink): string {
27+
const times = link.targets.map(t => t.lastRecvTime).filter((t): t is string => !!t).sort();
28+
const t = times.length ? times[times.length - 1] : undefined;
29+
return t ? new Date(t).toLocaleString() : 'None';
30+
}
31+
32+
function targetIPs(link: NodeLink): string {
33+
return link.targets.map(t => t.targetIP).join(', ') || 'None';
34+
}
35+
36+
function LinkTable(props: { title: string, peerHeader: string, links: NodeLink[], peerOf: (l: NodeLink) => string }) {
37+
return (
38+
<div cds-layout="vertical gap:sm">
39+
<div cds-text="section">{props.title}</div>
40+
{props.links.length === 0 ? (
41+
<p cds-text="secondary">No {props.title.toLowerCase()}.</p>
42+
) : (
43+
<table cds-table="border:all" cds-text="center body">
44+
<thead>
45+
<tr>
46+
<th>{props.peerHeader}</th>
47+
<th>Status</th>
48+
<th>Latency (ms)</th>
49+
<th>Target IP</th>
50+
<th>Last Recv</th>
51+
</tr>
52+
</thead>
53+
<tbody>
54+
{props.links.map((link, idx) => (
55+
<tr key={idx}>
56+
<td>{props.peerOf(link)}</td>
57+
<td style={link.down ? { color: '#e12200' } : undefined}>{link.down ? 'Down' : 'Up'}</td>
58+
<td>{fmtRtt(link)}</td>
59+
<td>{targetIPs(link)}</td>
60+
<td>{latestRecv(link)}</td>
61+
</tr>
62+
))}
63+
</tbody>
64+
</table>
65+
)}
66+
</div>
67+
);
68+
}
69+
70+
export function NodeDetailPanel(props: { model: NodeLatencyModel, node: string, onClose: () => void }) {
71+
const { egress, ingress } = nodeLinksFor(props.model.links, props.node);
72+
const health = props.model.health.get(props.node);
73+
74+
return (
75+
<CdsCard>
76+
<div cds-layout="vertical gap:md">
77+
<div cds-layout="horizontal align:vertical-center gap:md">
78+
<div cds-text="section" cds-layout="align:left">Node: {props.node}</div>
79+
<CdsButton type="button" action="outline" size="sm" onClick={props.onClose}>Clear</CdsButton>
80+
</div>
81+
{health && (
82+
<p cds-text="secondary">
83+
Egress: {health.egressTotal - health.egressDown}/{health.egressTotal} up
84+
{' '}&middot;{' '}
85+
Ingress: {health.ingressTotal - health.ingressDown}/{health.ingressTotal} up
86+
</p>
87+
)}
88+
<CdsDivider cds-card-remove-margin></CdsDivider>
89+
<LinkTable
90+
title="Egress Links"
91+
peerHeader="Target Node"
92+
links={egress}
93+
peerOf={l => l.targetNode}
94+
/>
95+
<LinkTable
96+
title="Ingress Links"
97+
peerHeader="Source Node"
98+
links={ingress}
99+
peerOf={l => l.sourceNode}
100+
/>
101+
</div>
102+
</CdsCard>
103+
);
104+
}
105+
106+
export function ProblemNodesPanel(props: { model: NodeLatencyModel, onSelect: (node: string) => void }) {
107+
const problems = props.model.problemNodes;
108+
if (problems.length === 0) return null;
109+
110+
return (
111+
<CdsCard>
112+
<div cds-layout="vertical gap:md">
113+
<div cds-text="section" style={{ color: '#e12200' }}>
114+
Problem Nodes ({problems.length})
115+
</div>
116+
<CdsDivider cds-card-remove-margin></CdsDivider>
117+
<table cds-table="border:all" cds-text="center body">
118+
<thead>
119+
<tr>
120+
<th>Node</th>
121+
<th>Egress Down</th>
122+
<th>Ingress Down</th>
123+
<th></th>
124+
</tr>
125+
</thead>
126+
<tbody>
127+
{problems.map(h => (
128+
<tr key={h.node}>
129+
<td>{h.node}</td>
130+
<td>{h.egressDown}/{h.egressTotal}</td>
131+
<td>{h.ingressDown}/{h.ingressTotal}</td>
132+
<td>
133+
<CdsButton type="button" action="flat" size="sm" onClick={() => props.onSelect(h.node)}>
134+
Inspect
135+
</CdsButton>
136+
</td>
137+
</tr>
138+
))}
139+
</tbody>
140+
</table>
141+
</div>
142+
</CdsCard>
143+
);
144+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright 2026 Antrea Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { CdsCard } from '@cds/react/card';
18+
import { CdsDivider } from '@cds/react/divider';
19+
import { AggregatedStats } from '../routes/nodelatency-util';
20+
21+
function fmt(v: number): string {
22+
return isNaN(v) ? 'N/A' : v.toFixed(2);
23+
}
24+
25+
export default function NodeLatencyStatsSummary(props: { agg: AggregatedStats }) {
26+
const a = props.agg;
27+
const items: { label: string, value: string, alert?: boolean }[] = [
28+
{ label: 'Nodes', value: a.nodeCount.toString() },
29+
{ label: 'Measured Links', value: a.measuredCount.toString() },
30+
{ label: 'Down Links', value: a.downCount.toString(), alert: a.downCount > 0 },
31+
{ label: 'Mean (ms)', value: fmt(a.meanMs) },
32+
{ label: 'Median (ms)', value: fmt(a.medianMs) },
33+
{ label: 'P90 (ms)', value: fmt(a.p90Ms) },
34+
{ label: 'Max (ms)', value: fmt(a.maxMs) },
35+
];
36+
return (
37+
<CdsCard>
38+
<div cds-layout="vertical gap:md">
39+
<div cds-text="section" cds-layout="p-y:sm">Summary</div>
40+
<CdsDivider cds-card-remove-margin></CdsDivider>
41+
<div cds-layout="horizontal gap:xl wrap:wrap p-y:sm">
42+
{items.map(item => (
43+
<div key={item.label} cds-layout="vertical gap:xs">
44+
<div cds-text="caption" style={{ opacity: 0.7 }}>{item.label}</div>
45+
<div cds-text="heading" style={item.alert ? { color: '#e12200' } : undefined}>
46+
{item.value}
47+
</div>
48+
</div>
49+
))}
50+
</div>
51+
</div>
52+
</CdsCard>
53+
);
54+
}

client/web/antrea-ui/src/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import Summary from './routes/summary';
2424
import Traceflow from './routes/traceflow';
2525
import TraceflowResult from './routes/traceflowresult';
2626
import FlowVisibility from './routes/flowvisibility';
27+
import NodeLatency from './routes/nodelatency';
2728
import Settings from './routes/settings';
2829
import reportWebVitals from './reportWebVitals';
2930

@@ -55,6 +56,10 @@ const router = createBrowserRouter([
5556
path: "flows",
5657
element: <FlowVisibility />,
5758
},
59+
{
60+
path: "nodelatency",
61+
element: <NodeLatency />,
62+
},
5863
{
5964
path: "settings",
6065
element: <Settings />,

0 commit comments

Comments
 (0)