Skip to content

Commit b4bfadd

Browse files
authored
chore: visualize connected edges (#9325)
https://linear.app/unleash/issue/2-3233/visualize-connected-edge-instances Adds a new tab in the Network page to visualize connected Edges. This is behind a `edgeObservability` flag. Also opens up the Network page even if you don't have a Prometheus API configured. When accessing the tabs that require it to set, and it isn't, we show some extra information about this and redirect you to the respective section in our docs. ![image](https://github.com/user-attachments/assets/1689f785-7544-450b-8c33-159609fc0f7d) ![image](https://github.com/user-attachments/assets/a7a14805-0488-41d2-885f-5e11a8495127) ![image](https://github.com/user-attachments/assets/918cba87-5538-4600-a71f-1143b2e33e2a)
1 parent c938b0f commit b4bfadd

File tree

15 files changed

+703
-9
lines changed

15 files changed

+703
-9
lines changed

frontend/src/component/admin/adminRoutes.ts

-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export const adminRoutes: INavigationMenuItem[] = [
6565
path: '/admin/network/*',
6666
title: 'Network',
6767
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
68-
configFlag: 'networkViewEnabled',
6968
group: 'instance',
7069
},
7170
{

frontend/src/component/admin/network/Network.tsx

+21-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import { Tab, Tabs } from '@mui/material';
44
import { Route, Routes, useLocation } from 'react-router-dom';
55
import { TabLink } from 'component/common/TabNav/TabLink';
66
import { PageContent } from 'component/common/PageContent/PageContent';
7+
import { useUiFlag } from 'hooks/useUiFlag';
78

89
const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
10+
const NetworkConnectedEdges = lazy(
11+
() => import('./NetworkConnectedEdges/NetworkConnectedEdges'),
12+
);
913
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
1014
const NetworkTrafficUsage = lazy(
1115
() => import('./NetworkTrafficUsage/NetworkTrafficUsage'),
@@ -20,6 +24,10 @@ const tabs = [
2024
label: 'Traffic',
2125
path: '/admin/network/traffic',
2226
},
27+
{
28+
label: 'Connected Edges',
29+
path: '/admin/network/connected-edges',
30+
},
2331
{
2432
label: 'Data Usage',
2533
path: '/admin/network/data-usage',
@@ -28,6 +36,11 @@ const tabs = [
2836

2937
export const Network = () => {
3038
const { pathname } = useLocation();
39+
const edgeObservabilityEnabled = useUiFlag('edgeObservability');
40+
41+
const filteredTabs = tabs.filter(
42+
({ label }) => label !== 'Connected Edges' || edgeObservabilityEnabled,
43+
);
3144

3245
return (
3346
<div>
@@ -41,7 +54,7 @@ export const Network = () => {
4154
variant='scrollable'
4255
allowScrollButtonsMobile
4356
>
44-
{tabs.map(({ label, path }) => (
57+
{filteredTabs.map(({ label, path }) => (
4558
<Tab
4659
key={label}
4760
value={path}
@@ -57,8 +70,14 @@ export const Network = () => {
5770
}
5871
>
5972
<Routes>
60-
<Route path='traffic' element={<NetworkTraffic />} />
6173
<Route path='*' element={<NetworkOverview />} />
74+
<Route path='traffic' element={<NetworkTraffic />} />
75+
{edgeObservabilityEnabled && (
76+
<Route
77+
path='connected-edges'
78+
element={<NetworkConnectedEdges />}
79+
/>
80+
)}
6281
<Route
6382
path='data-usage'
6483
element={<NetworkTrafficUsage />}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { useLocationSettings } from 'hooks/useLocationSettings';
2+
import type { ConnectedEdge } from 'interfaces/connectedEdge';
3+
import CircleIcon from '@mui/icons-material/Circle';
4+
import ExpandMore from '@mui/icons-material/ExpandMore';
5+
import { formatDateYMDHMS } from 'utils/formatDate';
6+
import {
7+
Accordion,
8+
AccordionDetails,
9+
AccordionSummary,
10+
styled,
11+
Tooltip,
12+
} from '@mui/material';
13+
import { Badge } from 'component/common/Badge/Badge';
14+
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
15+
import { NetworkConnectedEdgeInstanceLatency } from './NetworkConnectedEdgeInstanceLatency';
16+
17+
const StyledInstance = styled('div')(({ theme }) => ({
18+
borderRadius: theme.shape.borderRadiusMedium,
19+
border: '1px solid',
20+
borderColor: theme.palette.secondary.border,
21+
backgroundColor: theme.palette.secondary.light,
22+
display: 'flex',
23+
flexDirection: 'column',
24+
alignItems: 'center',
25+
padding: 0,
26+
zIndex: 1,
27+
marginTop: theme.spacing(1),
28+
}));
29+
30+
const StyledAccordion = styled(Accordion)({
31+
background: 'transparent',
32+
boxShadow: 'none',
33+
});
34+
35+
const StyledAccordionSummary = styled(AccordionSummary, {
36+
shouldForwardProp: (prop) => prop !== 'connectionStatus',
37+
})<{ connectionStatus: InstanceConnectionStatus }>(
38+
({ theme, connectionStatus }) => ({
39+
fontSize: theme.fontSizes.smallBody,
40+
padding: theme.spacing(1),
41+
minHeight: theme.spacing(3),
42+
'& .MuiAccordionSummary-content': {
43+
alignItems: 'center',
44+
gap: theme.spacing(1),
45+
margin: 0,
46+
'&.Mui-expanded': {
47+
margin: 0,
48+
},
49+
'& svg': {
50+
fontSize: theme.fontSizes.mainHeader,
51+
color:
52+
connectionStatus === 'Stale'
53+
? theme.palette.warning.main
54+
: connectionStatus === 'Disconnected'
55+
? theme.palette.error.main
56+
: theme.palette.success.main,
57+
},
58+
},
59+
}),
60+
);
61+
62+
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
63+
display: 'flex',
64+
flexDirection: 'column',
65+
fontSize: theme.fontSizes.smallerBody,
66+
gap: theme.spacing(2),
67+
}));
68+
69+
const StyledDetailRow = styled('div')(({ theme }) => ({
70+
display: 'flex',
71+
justifyContent: 'space-between',
72+
gap: theme.spacing(2),
73+
'& > span': {
74+
display: 'flex',
75+
alignItems: 'center',
76+
},
77+
}));
78+
79+
const StyledBadge = styled(Badge)(({ theme }) => ({
80+
padding: theme.spacing(0, 1),
81+
}));
82+
83+
const getConnectionStatus = ({
84+
reportedAt,
85+
}: ConnectedEdge): InstanceConnectionStatus => {
86+
const reportedTime = new Date(reportedAt).getTime();
87+
const reportedSecondsAgo = (Date.now() - reportedTime) / 1000;
88+
89+
if (reportedSecondsAgo > 360) return 'Disconnected';
90+
if (reportedSecondsAgo > 180) return 'Stale';
91+
92+
return 'Connected';
93+
};
94+
95+
const getCPUPercentage = ({
96+
started,
97+
reportedAt,
98+
cpuUsage,
99+
}: ConnectedEdge): string => {
100+
const cpuUsageSeconds = Number(cpuUsage);
101+
if (!cpuUsageSeconds) return 'No usage';
102+
103+
const startedTimestamp = new Date(started).getTime();
104+
const reportedTimestamp = new Date(reportedAt).getTime();
105+
106+
const totalRuntimeSeconds = (reportedTimestamp - startedTimestamp) / 1000;
107+
if (totalRuntimeSeconds === 0) return 'No usage';
108+
109+
return `${((cpuUsageSeconds / totalRuntimeSeconds) * 100).toFixed(2)} %`;
110+
};
111+
112+
const getMemory = ({ memoryUsage }: ConnectedEdge): string => {
113+
if (!memoryUsage) return 'No usage';
114+
115+
const units = ['B', 'KB', 'MB', 'GB'];
116+
let size = memoryUsage;
117+
let unitIndex = 0;
118+
119+
while (size >= 1024 && unitIndex < units.length - 1) {
120+
size /= 1024;
121+
unitIndex++;
122+
}
123+
124+
return `${size.toFixed(2)} ${units[unitIndex]}`;
125+
};
126+
127+
type InstanceConnectionStatus = 'Connected' | 'Stale' | 'Disconnected';
128+
129+
interface INetworkConnectedEdgeInstanceProps {
130+
instance: ConnectedEdge;
131+
}
132+
133+
export const NetworkConnectedEdgeInstance = ({
134+
instance,
135+
}: INetworkConnectedEdgeInstanceProps) => {
136+
const { locationSettings } = useLocationSettings();
137+
138+
const connectionStatus = getConnectionStatus(instance);
139+
const start = formatDateYMDHMS(instance.started, locationSettings?.locale);
140+
const lastReport = formatDateYMDHMS(
141+
instance.reportedAt,
142+
locationSettings?.locale,
143+
);
144+
const cpuPercentage = getCPUPercentage(instance);
145+
const memory = getMemory(instance);
146+
const archWarning = cpuPercentage === 'No usage' &&
147+
memory === 'No usage' && (
148+
<p>Resource metrics are only available when running on Linux</p>
149+
);
150+
151+
return (
152+
<StyledInstance>
153+
<StyledAccordion>
154+
<StyledAccordionSummary
155+
expandIcon={<ExpandMore />}
156+
connectionStatus={connectionStatus}
157+
>
158+
<Tooltip
159+
arrow
160+
title={`${connectionStatus}. Last reported: ${lastReport}`}
161+
>
162+
<CircleIcon />
163+
</Tooltip>
164+
{instance.id || instance.instanceId}
165+
</StyledAccordionSummary>
166+
<StyledAccordionDetails>
167+
<StyledDetailRow>
168+
<strong>ID</strong>
169+
<span>{instance.instanceId}</span>
170+
</StyledDetailRow>
171+
<StyledDetailRow>
172+
<strong>Upstream</strong>
173+
<span>{instance.connectedVia || 'Unleash'}</span>
174+
</StyledDetailRow>
175+
<StyledDetailRow>
176+
<strong>Status</strong>
177+
<StyledBadge
178+
color={
179+
connectionStatus === 'Disconnected'
180+
? 'error'
181+
: connectionStatus === 'Stale'
182+
? 'warning'
183+
: 'success'
184+
}
185+
>
186+
{connectionStatus}
187+
</StyledBadge>
188+
</StyledDetailRow>
189+
<StyledDetailRow>
190+
<strong>Start</strong>
191+
<span>{start}</span>
192+
</StyledDetailRow>
193+
<StyledDetailRow>
194+
<strong>Last report</strong>
195+
<span>{lastReport}</span>
196+
</StyledDetailRow>
197+
<StyledDetailRow>
198+
<strong>App name</strong>
199+
<span>{instance.appName}</span>
200+
</StyledDetailRow>
201+
<StyledDetailRow>
202+
<strong>Region</strong>
203+
<span>{instance.region || 'Unknown'}</span>
204+
</StyledDetailRow>
205+
<StyledDetailRow>
206+
<strong>Version</strong>
207+
<span>{instance.edgeVersion}</span>
208+
</StyledDetailRow>
209+
<StyledDetailRow>
210+
<strong>CPU</strong>
211+
<span>
212+
{cpuPercentage}{' '}
213+
<HelpIcon
214+
tooltip={
215+
<>
216+
<p>
217+
CPU average usage since instance
218+
started
219+
</p>
220+
{archWarning}
221+
</>
222+
}
223+
size='16px'
224+
/>
225+
</span>
226+
</StyledDetailRow>
227+
<StyledDetailRow>
228+
<strong>Memory</strong>
229+
<span>
230+
{memory}{' '}
231+
<HelpIcon
232+
tooltip={
233+
<>
234+
<p>Current memory usage</p>
235+
{archWarning}
236+
</>
237+
}
238+
size='16px'
239+
/>
240+
</span>
241+
</StyledDetailRow>
242+
<StyledDetailRow>
243+
<strong>Stream clients</strong>
244+
<span>{instance.connectedStreamingClients}</span>
245+
</StyledDetailRow>
246+
<StyledDetailRow>
247+
<NetworkConnectedEdgeInstanceLatency
248+
instance={instance}
249+
/>
250+
</StyledDetailRow>
251+
</StyledAccordionDetails>
252+
</StyledAccordion>
253+
</StyledInstance>
254+
);
255+
};

0 commit comments

Comments
 (0)