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:
parent
227abd8bba
commit
ae257d5957
@ -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
|
environments that have received traffic for this application
|
||||||
application will be shown here.
|
will be shown here.
|
||||||
</legend>
|
</Box>
|
||||||
{allEnvironmentsSorted.map((env) => {
|
<ToggleButtonGroup
|
||||||
return (
|
color='primary'
|
||||||
<label key={env.name}>
|
value={currentEnvironment}
|
||||||
{env.name}
|
exclusive
|
||||||
|
onChange={(event, value) => {
|
||||||
<ConditionallyRender
|
if (value !== null) {
|
||||||
condition={env.problemsDetected}
|
setCurrentEnvironment(value);
|
||||||
show={
|
|
||||||
<WarningAmber titleAccess='Problems detected' />
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
<input
|
|
||||||
defaultChecked={
|
|
||||||
currentEnvironment === env.name
|
|
||||||
}
|
|
||||||
className='visually-hidden'
|
|
||||||
type='radio'
|
|
||||||
name='active-environment'
|
|
||||||
onClick={() => {
|
|
||||||
setCurrentEnvironment(env.name);
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</label>
|
{allEnvironmentsSorted.map((env) => {
|
||||||
);
|
return <ToggleButton value={env}>{env}</ToggleButton>;
|
||||||
})}
|
})}
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 }] }),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user