Skip to content

Commit

Permalink
Feat network overview (#2708)
Browse files Browse the repository at this point in the history
  • Loading branch information
nunogois committed Dec 16, 2022
1 parent 2979f21 commit a3ac96f
Show file tree
Hide file tree
Showing 14 changed files with 581 additions and 73 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Expand Up @@ -71,6 +71,7 @@
"jsdom": "20.0.3",
"lodash.clonedeep": "4.5.0",
"lodash.omit": "4.5.0",
"mermaid": "^9.3.0",
"millify": "^5.0.1",
"msw": "0.49.1",
"pkginfo": "0.4.1",
Expand Down
27 changes: 14 additions & 13 deletions frontend/src/component/admin/menu/AdminMenu.tsx
@@ -1,4 +1,3 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import { Paper, Tab, Tabs } from '@mui/material';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
Expand All @@ -11,6 +10,8 @@ function AdminMenu() {
const { isBilling } = useInstanceStatus();
const { flags } = uiConfig;

const activeTab = pathname.split('/')[2];

return (
<Paper
style={{
Expand All @@ -20,13 +21,13 @@ function AdminMenu() {
}}
>
<Tabs
value={pathname}
value={activeTab}
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
>
<Tab
value="/admin/users"
value="users"
label={
<CenteredNavLink to="/admin/users">
<span>Users</span>
Expand All @@ -35,7 +36,7 @@ function AdminMenu() {
/>
{flags.UG && (
<Tab
value="/admin/groups"
value="groups"
label={
<CenteredNavLink to="/admin/groups">
<span>Groups</span>
Expand All @@ -45,7 +46,7 @@ function AdminMenu() {
)}
{flags.RE && (
<Tab
value="/admin/roles"
value="roles"
label={
<CenteredNavLink to="/admin/roles">
<span>Project roles</span>
Expand All @@ -54,7 +55,7 @@ function AdminMenu() {
/>
)}
<Tab
value="/admin/api"
value="api"
label={
<CenteredNavLink to="/admin/api">
API access
Expand All @@ -63,7 +64,7 @@ function AdminMenu() {
/>
{uiConfig.flags.embedProxyFrontend && (
<Tab
value="/admin/cors"
value="cors"
label={
<CenteredNavLink to="/admin/cors">
CORS origins
Expand All @@ -72,15 +73,15 @@ function AdminMenu() {
/>
)}
<Tab
value="/admin/auth"
value="auth"
label={
<CenteredNavLink to="/admin/auth">
Single sign-on
</CenteredNavLink>
}
/>
<Tab
value="/admin/instance"
value="instance"
label={
<CenteredNavLink to="/admin/instance">
Instance stats
Expand All @@ -89,17 +90,17 @@ function AdminMenu() {
/>
{flags.networkView && (
<Tab
value="/admin/traffic"
value="network"
label={
<CenteredNavLink to="/admin/traffic">
Traffic
<CenteredNavLink to="/admin/network">
Network
</CenteredNavLink>
}
/>
)}
{isBilling && (
<Tab
value="/admin/billing"
value="billing"
label={
<CenteredNavLink to="/admin/billing">
Billing
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/component/admin/network/Network.tsx
@@ -0,0 +1,63 @@
import { NetworkOverview } from './NetworkOverview/NetworkOverview';
import { NetworkTraffic } from './NetworkTraffic/NetworkTraffic';
import AdminMenu from '../menu/AdminMenu';
import { styled, Tab, Tabs } from '@mui/material';
import { Route, Routes, useLocation } from 'react-router-dom';
import { CenteredNavLink } from '../menu/CenteredNavLink';
import { PageContent } from 'component/common/PageContent/PageContent';

const StyledPageContent = styled(PageContent)(() => ({
'.page-header': {
padding: 0,
},
}));

const tabs = [
{
label: 'Overview',
path: '/admin/network',
},
{
label: 'Traffic',
path: '/admin/network/traffic',
},
];

export const Network = () => {
const { pathname } = useLocation();

return (
<div>
<AdminMenu />
<StyledPageContent
headerClass="page-header"
header={
<Tabs
value={pathname}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
allowScrollButtonsMobile
>
{tabs.map(({ label, path }) => (
<Tab
key={label}
value={path}
label={
<CenteredNavLink to={path}>
<span>{label}</span>
</CenteredNavLink>
}
/>
))}
</Tabs>
}
>
<Routes>
<Route path="traffic" element={<NetworkTraffic />} />
<Route path="*" element={<NetworkOverview />} />
</Routes>
</StyledPageContent>
</div>
);
};
@@ -0,0 +1,68 @@
import { usePageTitle } from 'hooks/usePageTitle';
import { Mermaid } from 'component/common/Mermaid/Mermaid';
import { useInstanceMetrics } from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Alert, styled } from '@mui/material';

const StyledMermaid = styled(Mermaid)(({ theme }) => ({
'#mermaid .node rect': {
fill: theme.palette.secondary.light,
stroke: theme.palette.secondary.border,
},
}));

interface INetworkApp {
label?: string;
reqs: string;
type: string;
}

export const NetworkOverview = () => {
usePageTitle('Network - Overview');
const { metrics } = useInstanceMetrics();

const apps: INetworkApp[] = [];
if (Boolean(metrics)) {
Object.keys(metrics).forEach(metric => {
apps.push(
...(
metrics[metric].data?.result
?.map(result => ({
label: result.metric?.appName,
reqs: parseFloat(
result.values?.[
result.values?.length - 1
][1].toString() || '0'
).toFixed(2),
type: metric.split('Metrics')[0],
}))
.filter(app => app.label !== 'undefined') || []
).filter(app => app.reqs !== '0.00')
);
});
}

const graph = `
graph TD
subgraph _[ ]
direction BT
Unleash(<img src='https://www.getunleash.io/logos/unleash_glyph_pos.svg' width='60' height='60' /><br/>Unleash)
${apps
.map(
({ label, reqs, type }, i) =>
`app-${i}(${label}) -- ${reqs} req/s<br>${type} --> Unleash`
)
.join('\n')}
end
`;

return (
<ConditionallyRender
condition={apps.length === 0}
show={<Alert severity="warning">No data available.</Alert>}
elseShow={<StyledMermaid>{graph}</StyledMermaid>}
/>
);
};

export default NetworkOverview;
@@ -1,7 +1,7 @@
import {
InstanceMetrics,
useInstanceMetrics,
} from '../../../hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
} from 'hooks/api/getters/useInstanceMetrics/useInstanceMetrics';
import { useMemo, VFC } from 'react';
import { Line } from 'react-chartjs-2';
import {
Expand All @@ -21,18 +21,17 @@ import {
import {
ILocationSettings,
useLocationSettings,
} from '../../../hooks/useLocationSettings';
import theme from '../../../themes/theme';
import { formatDateHM } from '../../../utils/formatDate';
} from 'hooks/useLocationSettings';
import theme from 'themes/theme';
import { formatDateHM } from 'utils/formatDate';
import { RequestsPerSecondSchema } from 'openapi';
import 'chartjs-adapter-date-fns';
import { Alert, PaletteColor } from '@mui/material';
import { PageContent } from '../../common/PageContent/PageContent';
import { PageHeader } from '../../common/PageHeader/PageHeader';
import { Box } from '@mui/system';
import { current } from 'immer';
import { CyclicIterator } from 'utils/cyclicIterator';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { usePageTitle } from 'hooks/usePageTitle';

interface IPoint {
x: number;
y: number;
Expand Down Expand Up @@ -172,37 +171,38 @@ const createInstanceChartData = (metrics?: InstanceMetrics): ChartDataType => {
return { datasets: [] };
};

export const InstanceMetricsChart: VFC = () => {
export const NetworkTraffic: VFC = () => {
const { locationSettings } = useLocationSettings();
const { metrics } = useInstanceMetrics();
const options = useMemo(() => {
return createInstanceChartOptions(metrics, locationSettings);
}, [metrics, locationSettings]);

usePageTitle('Network - Traffic');

const data = useMemo(() => {
return createInstanceChartData(metrics);
}, [metrics, locationSettings]);

return (
<PageContent header={<PageHeader title="Requests per second" />}>
<ConditionallyRender
condition={data.datasets.length === 0}
show={<Alert severity="warning">No data available.</Alert>}
elseShow={
<Box sx={{ display: 'grid', gap: 4 }}>
<div style={{ height: 400 }}>
<Line
data={data}
options={options}
aria-label="An instance metrics line chart with two lines: requests per second for admin API and requests per second for client API"
/>
</div>
</Box>
}
/>
</PageContent>
<ConditionallyRender
condition={data.datasets.length === 0}
show={<Alert severity="warning">No data available.</Alert>}
elseShow={
<Box sx={{ display: 'grid', gap: 4 }}>
<div style={{ height: 400 }}>
<Line
data={data}
options={options}
aria-label="An instance metrics line chart with two lines: requests per second for admin API and requests per second for client API"
/>
</div>
</Box>
}
/>
);
};

// Register dependencies that we need to draw the chart.
ChartJS.register(
CategoryScale,
Expand All @@ -216,4 +216,4 @@ ChartJS.register(
);

// Use a default export to lazy-load the charting library.
export default InstanceMetricsChart;
export default NetworkTraffic;
11 changes: 0 additions & 11 deletions frontend/src/component/admin/traffic/Traffic.tsx

This file was deleted.

43 changes: 43 additions & 0 deletions frontend/src/component/common/Mermaid/Mermaid.tsx
@@ -0,0 +1,43 @@
import { styled } from '@mui/material';
import mermaid from 'mermaid';
import { useEffect } from 'react';

const StyledMermaid = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
'#mermaid .edgeLabel': {
backgroundColor: theme.palette.background.paper,
},
}));

mermaid.initialize({
theme: 'default',
themeCSS: `
.clusters #_ rect {
fill: transparent;
stroke: transparent;
}
`,
});

interface IMermaidProps {
className?: string;
children: string;
}

export const Mermaid = ({ className = '', children }: IMermaidProps) => {
useEffect(() => {
mermaid.render('mermaid', children, (svgCode: string) => {
const mermaidDiv = document.querySelector('.mermaid');
if (mermaidDiv) {
mermaidDiv.innerHTML = svgCode;
}
});
}, [children]);

return (
<StyledMermaid className={`mermaid ${className}`}>
{children}
</StyledMermaid>
);
};

0 comments on commit a3ac96f

Please sign in to comment.