mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-10 17:53:36 +02:00
experiment with a network view and header warning for the hackathon
This commit is contained in:
parent
d7b2874afd
commit
3c05559053
@ -76,6 +76,7 @@
|
|||||||
"react-chartjs-2": "4.3.1",
|
"react-chartjs-2": "4.3.1",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-hooks-global-state": "2.0.0",
|
"react-hooks-global-state": "2.0.0",
|
||||||
|
"react-mermaid2": "^0.1.4",
|
||||||
"react-router-dom": "6.3.0",
|
"react-router-dom": "6.3.0",
|
||||||
"react-table": "7.8.0",
|
"react-table": "7.8.0",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
|
@ -150,6 +150,19 @@ function AdminMenu() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<Tab
|
||||||
|
value="/admin/network"
|
||||||
|
label={
|
||||||
|
<NavLink
|
||||||
|
to="/admin/network"
|
||||||
|
style={({ isActive }) =>
|
||||||
|
createNavLinkStyle({ isActive, theme })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Network
|
||||||
|
</NavLink>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
42
frontend/src/component/admin/network/Network.tsx
Normal file
42
frontend/src/component/admin/network/Network.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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)} />;
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -52,6 +52,7 @@ import { SegmentTable } from 'component/segments/SegmentTable/SegmentTable';
|
|||||||
import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
|
import FlaggedBillingRedirect from 'component/admin/billing/FlaggedBillingRedirect/FlaggedBillingRedirect';
|
||||||
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
|
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
|
||||||
import { Billing } from 'component/admin/billing/Billing';
|
import { Billing } from 'component/admin/billing/Billing';
|
||||||
|
import { Network } from 'component/admin/network/Network';
|
||||||
import { Group } from 'component/admin/groups/Group/Group';
|
import { Group } from 'component/admin/groups/Group/Group';
|
||||||
import { CreateGroup } from 'component/admin/groups/CreateGroup/CreateGroup';
|
import { CreateGroup } from 'component/admin/groups/CreateGroup/CreateGroup';
|
||||||
import { EditGroup } from 'component/admin/groups/EditGroup/EditGroup';
|
import { EditGroup } from 'component/admin/groups/EditGroup/EditGroup';
|
||||||
@ -507,6 +508,14 @@ export const routes: IRoute[] = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/network',
|
||||||
|
parent: '/admin',
|
||||||
|
title: 'Network',
|
||||||
|
component: Network,
|
||||||
|
type: 'protected',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin-invoices',
|
path: '/admin-invoices',
|
||||||
parent: '/admin',
|
parent: '/admin',
|
||||||
|
@ -12,6 +12,7 @@ import { basePath } from 'utils/formatPath';
|
|||||||
import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/FeedbackCESProvider';
|
import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/FeedbackCESProvider';
|
||||||
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
|
import { AnnouncerProvider } from 'component/common/Announcer/AnnouncerProvider/AnnouncerProvider';
|
||||||
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
|
import { InstanceStatus } from 'component/common/InstanceStatus/InstanceStatus';
|
||||||
|
import { NetworkStatus } from 'component/common/NetworkStatus/NetworkStatus';
|
||||||
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
|
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
@ -22,6 +23,7 @@ ReactDOM.render(
|
|||||||
<AnnouncerProvider>
|
<AnnouncerProvider>
|
||||||
<FeedbackCESProvider>
|
<FeedbackCESProvider>
|
||||||
<InstanceStatus>
|
<InstanceStatus>
|
||||||
|
<NetworkStatus />
|
||||||
<ScrollTop />
|
<ScrollTop />
|
||||||
<App />
|
<App />
|
||||||
</InstanceStatus>
|
</InstanceStatus>
|
||||||
|
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@ -1,2 +1,4 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
/// <reference types="vite-plugin-svgr/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';
|
||||||
|
10185
frontend/yarn.lock
10185
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user