Skip to content

Commit eace020

Browse files
committed
ui: fix network page rendering with decommissioned node
Previously, the network page would fail to render when a node was decommissioned, this was due to a bug where a decommissioned node would show up in the latency data with an undefined latency. This commit fixes that bug, and adds some tests to the Network component. Fixes: CRDB-48165 Release note: None
1 parent e06b49b commit eace020

File tree

2 files changed

+298
-2
lines changed

2 files changed

+298
-2
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
import * as protos from "@cockroachlabs/crdb-protobuf-client";
7+
import { render, screen } from "@testing-library/react";
8+
import { Location } from "history";
9+
import Long from "long";
10+
import React from "react";
11+
import { MemoryRouter } from "react-router-dom";
12+
13+
import { Network } from "./index";
14+
15+
describe("Network", () => {
16+
const defaultProps = {
17+
nodesSummary: {
18+
nodeStatuses: [
19+
{
20+
desc: {
21+
node_id: 1,
22+
address: { address_field: "localhost:26257" },
23+
locality: {
24+
tiers: [
25+
{ key: "region", value: "us-east-1" },
26+
{ key: "zone", value: "a" },
27+
],
28+
},
29+
},
30+
updated_at: Long.fromNumber(1000),
31+
},
32+
{
33+
desc: {
34+
node_id: 2,
35+
address: { address_field: "localhost:26258" },
36+
locality: {
37+
tiers: [
38+
{ key: "region", value: "us-east-1" },
39+
{ key: "zone", value: "b" },
40+
],
41+
},
42+
},
43+
updated_at: Long.fromNumber(1000),
44+
},
45+
] as any[],
46+
nodeStatusByID: {
47+
"1": {
48+
desc: {
49+
node_id: 1,
50+
address: { address_field: "localhost:26257" },
51+
locality: {
52+
tiers: [
53+
{ key: "region", value: "us-east-1" },
54+
{ key: "zone", value: "a" },
55+
],
56+
},
57+
},
58+
updated_at: Long.fromNumber(1000),
59+
},
60+
"2": {
61+
desc: {
62+
node_id: 2,
63+
address: { address_field: "localhost:26258" },
64+
locality: {
65+
tiers: [
66+
{ key: "region", value: "us-east-1" },
67+
{ key: "zone", value: "b" },
68+
],
69+
},
70+
},
71+
updated_at: Long.fromNumber(1000),
72+
},
73+
},
74+
nodeIDs: ["1", "2"],
75+
nodeDisplayNameByID: {
76+
"1": "node-1",
77+
"2": "node-2",
78+
},
79+
storeIDsByNodeID: {
80+
"1": ["1"],
81+
"2": ["2"],
82+
},
83+
nodeLastError: null as Error | null,
84+
livenessByNodeID: {
85+
"1": {
86+
nodeId: 1,
87+
epoch: Long.fromNumber(1),
88+
expiration: Long.fromNumber(1000),
89+
draining: false,
90+
membership:
91+
protos.cockroach.kv.kvserver.liveness.livenesspb.MembershipStatus
92+
.ACTIVE,
93+
} as protos.cockroach.kv.kvserver.liveness.livenesspb.ILiveness,
94+
"2": {
95+
nodeId: 2,
96+
epoch: Long.fromNumber(1),
97+
expiration: Long.fromNumber(1000),
98+
draining: false,
99+
membership:
100+
protos.cockroach.kv.kvserver.liveness.livenesspb.MembershipStatus
101+
.ACTIVE,
102+
} as protos.cockroach.kv.kvserver.liveness.livenesspb.ILiveness,
103+
},
104+
livenessStatusByNodeID: {
105+
"1": protos.cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus
106+
.NODE_STATUS_LIVE,
107+
"2": protos.cockroach.kv.kvserver.liveness.livenesspb.NodeLivenessStatus
108+
.NODE_STATUS_LIVE,
109+
},
110+
},
111+
nodeSummaryErrors: [] as Error[],
112+
connectivity: {
113+
data: {
114+
connections: {
115+
"1": {
116+
peers: {
117+
"2": { latency: { nanos: 1000000 } }, // 1ms
118+
},
119+
},
120+
"2": {
121+
peers: {
122+
"1": { latency: { nanos: 1000000 } }, // 1ms
123+
},
124+
},
125+
},
126+
errors_by_node_id: {},
127+
},
128+
inFlight: false,
129+
valid: true,
130+
unauthorized: false,
131+
},
132+
refreshNodes: jest.fn(),
133+
refreshLiveness: jest.fn(),
134+
refreshConnectivity: jest.fn(),
135+
match: {
136+
params: {},
137+
isExact: true,
138+
path: "/network",
139+
url: "/network",
140+
},
141+
location: {
142+
pathname: "/network",
143+
search: "",
144+
state: null,
145+
hash: "",
146+
} as Location,
147+
history: {} as any,
148+
};
149+
150+
beforeEach(() => {
151+
jest.clearAllMocks();
152+
});
153+
154+
it("renders network page with latency table", () => {
155+
render(
156+
<MemoryRouter>
157+
<Network {...defaultProps} />
158+
</MemoryRouter>,
159+
);
160+
161+
expect(screen.getByText("Network")).toBeTruthy();
162+
expect(screen.getByRole("table")).toBeTruthy();
163+
});
164+
165+
it("calls refresh functions on mount", () => {
166+
render(
167+
<MemoryRouter>
168+
<Network {...defaultProps} />
169+
</MemoryRouter>,
170+
);
171+
172+
expect(defaultProps.refreshNodes).toHaveBeenCalled();
173+
expect(defaultProps.refreshLiveness).toHaveBeenCalled();
174+
expect(defaultProps.refreshConnectivity).toHaveBeenCalled();
175+
});
176+
177+
it("displays message for single node cluster", () => {
178+
const propsWithSingleNode = {
179+
...defaultProps,
180+
nodesSummary: {
181+
nodeStatuses: [defaultProps.nodesSummary.nodeStatuses[0]],
182+
nodeStatusByID: {
183+
"1": defaultProps.nodesSummary.nodeStatusByID["1"],
184+
},
185+
nodeIDs: ["1"],
186+
nodeDisplayNameByID: {
187+
"1": "node-1",
188+
},
189+
storeIDsByNodeID: {
190+
"1": ["1"],
191+
},
192+
nodeLastError: null as Error | null,
193+
livenessByNodeID: {
194+
"1": {
195+
nodeId: 1,
196+
epoch: Long.fromNumber(1),
197+
expiration: Long.fromNumber(1000),
198+
draining: false,
199+
membership:
200+
protos.cockroach.kv.kvserver.liveness.livenesspb.MembershipStatus
201+
.ACTIVE,
202+
} as protos.cockroach.kv.kvserver.liveness.livenesspb.ILiveness,
203+
},
204+
livenessStatusByNodeID: {
205+
"1": protos.cockroach.kv.kvserver.liveness.livenesspb
206+
.NodeLivenessStatus.NODE_STATUS_LIVE,
207+
},
208+
},
209+
connectivity: {
210+
data: {
211+
connections: {},
212+
errors_by_node_id: {},
213+
},
214+
inFlight: false,
215+
valid: true,
216+
unauthorized: false,
217+
},
218+
};
219+
220+
render(
221+
<MemoryRouter>
222+
<Network {...propsWithSingleNode} />
223+
</MemoryRouter>,
224+
);
225+
226+
expect(
227+
screen.getByText(
228+
"Cannot show latency chart for cluster with less than 2 nodes.",
229+
),
230+
).toBeTruthy();
231+
});
232+
233+
it("handles peers with null latency", () => {
234+
const propsWithNullLatency = {
235+
...defaultProps,
236+
connectivity: {
237+
...defaultProps.connectivity,
238+
data: {
239+
connections: {
240+
"1": {
241+
peers: {
242+
"2": {
243+
latency: {
244+
nanos: 1000000,
245+
} as protos.cockroach.server.serverpb.NetworkConnectivityResponse.IPeer["latency"],
246+
}, // 1ms
247+
"3": {
248+
latency:
249+
null as protos.cockroach.server.serverpb.NetworkConnectivityResponse.IPeer["latency"],
250+
}, // null latency
251+
},
252+
},
253+
"2": {
254+
peers: {
255+
"1": {
256+
latency: {
257+
nanos: 1000000,
258+
} as protos.cockroach.server.serverpb.NetworkConnectivityResponse.IPeer["latency"],
259+
}, // 1ms
260+
"3": {
261+
latency:
262+
null as protos.cockroach.server.serverpb.NetworkConnectivityResponse.IPeer["latency"],
263+
}, // null latency
264+
},
265+
},
266+
"3": {
267+
peers: {
268+
"1": {
269+
latency:
270+
null as protos.cockroach.server.serverpb.NetworkConnectivityResponse.IPeer["latency"],
271+
}, // null latency
272+
"2": {
273+
latency:
274+
null as protos.cockroach.server.serverpb.NetworkConnectivityResponse.IPeer["latency"],
275+
}, // null latency
276+
},
277+
},
278+
},
279+
errors_by_node_id: {},
280+
},
281+
inFlight: false,
282+
valid: true,
283+
unauthorized: false,
284+
},
285+
};
286+
287+
render(
288+
<MemoryRouter>
289+
<Network {...propsWithNullLatency} />
290+
</MemoryRouter>,
291+
);
292+
293+
// Verify the component renders without errors
294+
expect(screen.getByText("Network")).toBeTruthy();
295+
expect(screen.getByRole("table")).toBeTruthy();
296+
});
297+
});

pkg/ui/workspaces/db-console/src/views/reports/containers/network/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,8 +444,7 @@ export class Network extends React.Component<NetworkProps, INetworkState> {
444444
values,
445445
(vals: IConnectivity[]) => flatMap(vals, v => Object.values(v.peers)),
446446
(vals: IPeer[]) => flatMap(vals, v => v.latency),
447-
(vals: IDuration[]) =>
448-
filter(vals, v => v !== undefined && v.nanos !== undefined),
447+
(vals: IDuration[]) => filter(vals, v => v && v.nanos),
449448
(vals: IDuration[]) => map(vals, v => util.NanoToMilli(v.nanos)),
450449
])(connections);
451450

0 commit comments

Comments
 (0)