1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-08 01:15:49 +02:00

feat: connected instance ui api integration (#6343)

This commit is contained in:
Mateusz Kwasniewski 2024-02-27 08:30:31 +01:00 committed by GitHub
parent 227abd8bba
commit ae257d5957
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 91 additions and 121 deletions

View File

@ -1,99 +1,38 @@
import { useMemo } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import useApplication from 'hooks/api/getters/useApplication/useApplication'; import useApplication from 'hooks/api/getters/useApplication/useApplication';
import { WarningAmber } from '@mui/icons-material';
import { formatDateYMDHMS } from 'utils/formatDate'; import { formatDateYMDHMS } from 'utils/formatDate';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useConnectedInstancesTable } from './useConnectedInstancesTable'; import { useConnectedInstancesTable } from './useConnectedInstancesTable';
import { ConnectedInstancesTable } from './ConnectedInstancesTable'; import { ConnectedInstancesTable } from './ConnectedInstancesTable';
import { IApplication } from 'interfaces/application'; import { IApplication } from 'interfaces/application';
import { useQueryParam } from 'use-query-params'; import { Box, ToggleButton, ToggleButtonGroup } from '@mui/material';
import { styled } from '@mui/material'; import { useApplicationOverview } from 'hooks/api/getters/useApplicationOverview/useApplicationOverview';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useConnectedInstances } from 'hooks/api/getters/useConnectedInstances/useConnectedInstances';
const Container = styled('div')(({ theme }) => ({ export const ConnectedInstances: FC = () => {
'* + *': {
marginBlockStart: theme.spacing(2),
},
}));
const EnvironmentSelectionContainer = styled('div')(({ theme }) => ({
label: {
'--padding-horizontal': theme.spacing(3),
'--padding-vertical': theme.spacing(1),
color: theme.palette.primary.main,
background: theme.palette.background,
paddingInline: 'var(--padding-horizontal)',
paddingBlock: 'var(--padding-vertical)',
border: `1px solid ${theme.palette.background.alternative}`,
borderInlineStart: 'none',
fontWeight: 'bold',
position: 'relative',
svg: {
color: theme.palette.warning.main,
position: 'absolute',
fontSize: theme.fontSizes.bodySize,
top: 'calc(var(--padding-horizontal) * .12)',
right: 'calc(var(--padding-horizontal) * .2)',
},
},
'label:first-of-type': {
borderInlineStart: `1px solid ${theme.palette.background.alternative}`,
borderRadius: `${theme.shape.borderRadiusMedium}px 0 0 ${theme.shape.borderRadiusMedium}px`,
},
'label:last-of-type': {
borderRadius: `0 ${theme.shape.borderRadiusMedium}px ${theme.shape.borderRadiusMedium}px 0`,
},
'label:has(input:checked)': {
background: theme.palette.background.alternative,
color: theme.palette.primary.contrastText,
svg: {
color: 'inherit',
},
},
'label:focus-within': {
outline: `2px solid ${theme.palette.background.alternative}`,
outlineOffset: theme.spacing(0.5),
},
fieldset: {
border: 'none',
padding: 0,
margin: 0,
},
legend: {
marginBlockEnd: theme.spacing(3),
},
'.visually-hidden': {
border: 0,
clip: 'rect(0 0 0 0)',
height: 'auto',
margin: 0,
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
whiteSpace: 'nowrap',
},
}));
export const ConnectedInstances = () => {
const name = useRequiredPathParam('name'); const name = useRequiredPathParam('name');
const { application } = useApplication(name); const { application } = useApplication(name);
const [currentEnvironment, setCurrentEnvironment] = const { data: applicationOverview } = useApplicationOverview(name);
useQueryParam('environment');
const availableEnvironments = new Set( const availableEnvironments = applicationOverview.environments.map(
application?.instances.map( (env) => env.name,
// @ts-expect-error: the type definition here is incomplete. It
// should be updated as part of this project.
(instance) => instance.environment,
),
); );
const allEnvironmentsSorted = Array.from(availableEnvironments) const allEnvironmentsSorted = Array.from(availableEnvironments).sort(
.sort((a, b) => a.localeCompare(b)) (a, b) => a.localeCompare(b),
.map((env) => ({ name: env, problemsDetected: false })); );
const [currentEnvironment, setCurrentEnvironment] = useState(
allEnvironmentsSorted[0],
);
const { data: connectedInstances } = useConnectedInstances(
name,
currentEnvironment,
);
useEffect(() => {
if (!currentEnvironment && availableEnvironments.length > 0) {
setCurrentEnvironment(availableEnvironments[0]);
}
}, [JSON.stringify(availableEnvironments)]);
const tableData = useMemo(() => { const tableData = useMemo(() => {
const map = ({ const map = ({
@ -123,41 +62,28 @@ export const ConnectedInstances = () => {
useConnectedInstancesTable(tableData); useConnectedInstancesTable(tableData);
return ( return (
<Container> <Box>
<EnvironmentSelectionContainer> <Box sx={{ mb: 3 }}>
<fieldset> <Box sx={{ mb: 2 }}>
<legend> Select which environment to display data for. Only
Select which environment to display data for. Only environments that have received traffic for this application
environments that have received traffic for this will be shown here.
application will be shown here. </Box>
</legend> <ToggleButtonGroup
color='primary'
value={currentEnvironment}
exclusive
onChange={(event, value) => {
if (value !== null) {
setCurrentEnvironment(value);
}
}}
>
{allEnvironmentsSorted.map((env) => { {allEnvironmentsSorted.map((env) => {
return ( return <ToggleButton value={env}>{env}</ToggleButton>;
<label key={env.name}>
{env.name}
<ConditionallyRender
condition={env.problemsDetected}
show={
<WarningAmber titleAccess='Problems detected' />
}
/>
<input
defaultChecked={
currentEnvironment === env.name
}
className='visually-hidden'
type='radio'
name='active-environment'
onClick={() => {
setCurrentEnvironment(env.name);
}}
/>
</label>
);
})} })}
</fieldset> </ToggleButtonGroup>
</EnvironmentSelectionContainer> </Box>
<ConnectedInstancesTable <ConnectedInstancesTable
loading={false} loading={false}
headerGroups={headerGroups} headerGroups={headerGroups}
@ -166,6 +92,6 @@ export const ConnectedInstances = () => {
getTableProps={getTableProps} getTableProps={getTableProps}
rows={rows} rows={rows}
/> />
</Container> </Box>
); );
}; };

View File

@ -14,7 +14,7 @@ export const useConnectedInstancesTable = (
instanceData: ConnectedInstancesTableData[], instanceData: ConnectedInstancesTableData[],
) => { ) => {
const initialState = useMemo( const initialState = useMemo(
() => ({ sortBy: [{ id: 'instanceId' }] }), () => ({ sortBy: [{ id: 'lastSeen', desc: true }] }),
[], [],
); );

View File

@ -0,0 +1,44 @@
import { SWRConfiguration } from 'swr';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
type ConnectedInstancesSchema = {
instances: {
instanceId: string;
sdk: string;
clientIp: string;
lastSeen: string;
}[];
};
export const useConnectedInstances = (
application: string,
environment?: string,
options: SWRConfiguration = {},
) => {
const path = formatApiPath(
`api/admin/metrics/instances/${application}/${environment}`,
);
const { data, error } = useConditionalSWR<ConnectedInstancesSchema>(
Boolean(environment),
{ instances: [] },
path,
fetcher,
options,
);
return {
data: data || { instances: [] },
error,
loading: !error && !data,
};
};
const fetcher = async (path: string): Promise<ConnectedInstancesSchema> => {
const res = await fetch(path).then(
handleErrorResponses('Connected instances'),
);
const data = await res.json();
return data;
};