1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +02:00

experiment with a network view and header warning for the hackathon

This commit is contained in:
Nuno Góis 2022-09-22 22:17:10 +02:00
parent d7b2874afd
commit 3c05559053
9 changed files with 10294 additions and 173 deletions

View File

@ -76,6 +76,7 @@
"react-chartjs-2": "4.3.1",
"react-dom": "17.0.2",
"react-hooks-global-state": "2.0.0",
"react-mermaid2": "^0.1.4",
"react-router-dom": "6.3.0",
"react-table": "7.8.0",
"react-test-renderer": "17.0.2",

View File

@ -150,6 +150,19 @@ function AdminMenu() {
}
/>
)}
<Tab
value="/admin/network"
label={
<NavLink
to="/admin/network"
style={({ isActive }) =>
createNavLinkStyle({ isActive, theme })
}
>
Network
</NavLink>
}
/>
</Tabs>
</Paper>
);

View File

@ -0,0 +1,42 @@
import AdminMenu from '../menu/AdminMenu';
import { PageContent } from 'component/common/PageContent/PageContent';
import { useContext, useEffect } from 'react';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import AccessContext from 'contexts/AccessContext';
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
import { NetworkDashboard } from './NetworkDashboard/NetworkDashboard';
export const Network = () => {
const { instanceStatus, refetchInstanceStatus, refresh, loading } =
useInstanceStatus();
const { hasAccess } = useContext(AccessContext);
useEffect(() => {
const hardRefresh = async () => {
await refresh();
refetchInstanceStatus();
};
hardRefresh();
}, [refetchInstanceStatus, refresh]);
return (
<div>
<AdminMenu />
<PageContent header="Network" isLoading={loading}>
<ConditionallyRender
condition={hasAccess(ADMIN)}
show={() => (
<>
<NetworkDashboard
instanceStatus={instanceStatus!}
/>
</>
)}
elseShow={() => <AdminAlert />}
/>
</PageContent>
</div>
);
};

View File

@ -0,0 +1,128 @@
import { FC } from 'react';
import { styled } from '@mui/material';
import { IInstanceStatus } from 'interfaces/instance';
import Mermaid from 'react-mermaid2';
const StyledMermaid = styled(Mermaid)(() => ({
// TODO: Not working :(
'& > svg': {
display: 'flex',
margin: 'auto',
},
}));
interface INetworkDashboardProps {
instanceStatus: IInstanceStatus;
}
type statusCode = 'OK' | 'WARN' | 'ERR';
interface IClientStatus {
id: string;
appName: string;
sdk: string;
status: statusCode;
edgeConnection: string;
}
interface IEdgeStatus {
id: string;
label: string;
lastFetch?: Date;
status: statusCode;
}
interface IUnleashStatus {
status: statusCode;
}
interface INetworkStatus {
unleash: IUnleashStatus;
clients: IClientStatus[];
edges: IEdgeStatus[];
}
const getDateString = (date?: Date) =>
date?.toISOString().replace('T', ' ').split('.')[0] || 'Never';
const buildGraph = (status: INetworkStatus): string => {
// TODO: Each node could be clickable to show more info.
const graph = ['graph LR'];
status.clients.forEach(client => {
graph.push(
`client${client.id}(("${client.appName}")):::${client.status}`
);
graph.push(`--${client.sdk}-->edge${client.edgeConnection}`);
});
status.edges.forEach(edge => {
graph.push(
`edge${edge.id}("${edge.label}"<br />${
edge.status
}<br />${getDateString(edge.lastFetch)}):::${edge.status}`
);
graph.push(`-->unleash{{Unleash<br />${status.unleash.status}}}`);
});
graph.push('classDef OK fill:#00bfa5,stroke:#00bfa5,stroke-width:1px');
graph.push('classDef WARN fill:#ff9800,stroke:#ff9800,stroke-width:1px');
graph.push('classDef ERR fill:#f44336,stroke:#f44336,stroke-width:1px');
return graph.join('\n');
};
export const NetworkDashboard: FC<INetworkDashboardProps> = ({
instanceStatus,
}) => {
// TODO: Grab this from something like instanceStatus?
const mockNetworkStatus: INetworkStatus = {
unleash: {
status: 'OK',
},
clients: [
{
id: 'app-1',
appName: 'my-app #1',
sdk: 'rust-sdk',
status: 'OK',
edgeConnection: 'edge-1',
},
{
id: 'app-2',
appName: 'my-app #2',
sdk: 'react-sdk',
status: 'WARN',
edgeConnection: 'edge-2',
},
{
id: 'app-3',
appName: 'my-app #3',
sdk: 'python-sdk',
status: 'ERR',
edgeConnection: 'edge-3',
},
],
edges: [
{
id: 'edge-1',
label: 'edge #1',
lastFetch: new Date(),
status: 'OK',
},
{
id: 'edge-2',
label: 'edge #2',
lastFetch: new Date(Date.now() - 2 * 60 * 60 * 1000),
status: 'WARN',
},
{
id: 'edge-3',
label: 'edge #3',
status: 'ERR',
},
],
};
return <StyledMermaid chart={buildGraph(mockNetworkStatus)} />;
};

View File

@ -0,0 +1,85 @@
import { styled, Button, Typography } from '@mui/material';
import { IInstanceStatus } from 'interfaces/instance';
import { INSTANCE_STATUS_BAR_ID } from 'utils/testIds';
import { WarningAmber } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { useContext } from 'react';
import AccessContext from 'contexts/AccessContext';
import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
const StyledWarningBar = styled('aside')(({ theme }) => ({
position: 'relative',
zIndex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(1),
gap: theme.spacing(1),
borderBottom: '1px solid',
borderColor: theme.palette.warning.border,
background: theme.palette.warning.light,
color: theme.palette.warning.dark,
}));
const StyledButton = styled(Button)(({ theme }) => ({
whiteSpace: 'nowrap',
minWidth: '8rem',
marginLeft: theme.spacing(2),
}));
const StyledWarningIcon = styled(WarningAmber)(({ theme }) => ({
color: theme.palette.warning.main,
}));
const isMisbehaving = (instanceStatus: IInstanceStatus) => {
// TODO: Implement.
return true;
};
interface IInstanceStatusBarProps {
instanceStatus: IInstanceStatus;
}
export const NetworkStatus = () => {
const { instanceStatus } = useInstanceStatus();
if (instanceStatus && isMisbehaving(instanceStatus)) {
return <StatusBarNetworkWarning instanceStatus={instanceStatus} />;
}
return null;
};
const StatusBarNetworkWarning = ({
instanceStatus,
}: IInstanceStatusBarProps) => {
return (
<StyledWarningBar data-testid={INSTANCE_STATUS_BAR_ID}>
<StyledWarningIcon />
<Typography sx={theme => ({ fontSize: theme.fontSizes.smallBody })}>
<strong>Heads up!</strong> It seems like one of your client
instances might be misbehaving.
</Typography>
<NetworkLink />
</StyledWarningBar>
);
};
const NetworkLink = () => {
const { hasAccess } = useContext(AccessContext);
const navigate = useNavigate();
if (!hasAccess(ADMIN)) {
return null;
}
return (
<StyledButton
onClick={() => navigate('/admin/network')}
variant="outlined"
>
View Network
</StyledButton>
);
};

View File

@ -52,6 +52,7 @@ import { SegmentTable } from 'component/segments/SegmentTable/SegmentTable';
import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
import { Billing } from 'component/admin/billing/Billing';
import { Network } from 'component/admin/network/Network';
import { Group } from 'component/admin/groups/Group/Group';
import { CreateGroup } from 'component/admin/groups/CreateGroup/CreateGroup';
import { EditGroup } from 'component/admin/groups/EditGroup/EditGroup';
@ -507,6 +508,14 @@ export const routes: IRoute[] = [
type: 'protected',
menu: {},
},
{
path: '/admin/network',
parent: '/admin',
title: 'Network',
component: Network,
type: 'protected',
menu: { adminSettings: true },
},
{
path: '/admin-invoices',
parent: '/admin',

View File

@ -12,6 +12,7 @@ import { basePath } from 'utils/formatPath';
import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/FeedbackCESProvider';
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
import { NetworkStatus } from 'component/common/NetworkStatus/NetworkStatus';
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
ReactDOM.render(
@ -22,6 +23,7 @@ ReactDOM.render(
<AnnouncerProvider>
<FeedbackCESProvider>
<InstanceStatus>
<NetworkStatus />
<ScrollTop />
<App />
</InstanceStatus>

View File

@ -1,2 +1,4 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
// TODO: react-mermaid2 doesn't have type declarations, but there's probably better alternatives anyways...
declare module 'react-mermaid2';

File diff suppressed because it is too large Load Diff