Add health check toast notification for Instance list and detail views.

This commit is contained in:
Kia Lam
2022-09-13 08:53:31 -07:00
committed by Jeff Bradberry
parent 0510978516
commit 4a41098b24
7 changed files with 226 additions and 277 deletions

View File

@@ -1,7 +1,15 @@
import React from 'react'; import React from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Alert, Button, AlertActionCloseButton } from '@patternfly/react-core'; import {
Alert as PFAlert,
Button,
AlertActionCloseButton,
} from '@patternfly/react-core';
import styled from 'styled-components';
const Alert = styled(PFAlert)`
z-index: 1;
`;
function HealthCheckAlert({ onSetHealthCheckAlert }) { function HealthCheckAlert({ onSetHealthCheckAlert }) {
return ( return (
<Alert <Alert
@@ -15,7 +23,7 @@ function HealthCheckAlert({ onSetHealthCheckAlert }) {
<Button <Button
variant="link" variant="link"
isInline isInline
onClick={() => window.location.reload(false)} onClick={() => window.location.reload()}
>{t`Reload`}</Button> >{t`Reload`}</Button>
</> </>
} }

View File

@@ -28,6 +28,7 @@ import RoutedTabs from 'components/RoutedTabs';
import ContentError from 'components/ContentError'; import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading'; import ContentLoading from 'components/ContentLoading';
import { Detail, DetailList } from 'components/DetailList'; import { Detail, DetailList } from 'components/DetailList';
import HealthCheckAlert from 'components/HealthCheckAlert';
import StatusLabel from 'components/StatusLabel'; import StatusLabel from 'components/StatusLabel';
import useRequest, { import useRequest, {
useDeleteItems, useDeleteItems,
@@ -66,6 +67,7 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
const history = useHistory(); const history = useHistory();
const [healthCheck, setHealthCheck] = useState({}); const [healthCheck, setHealthCheck] = useState({});
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
const [forks, setForks] = useState(); const [forks, setForks] = useState();
const { const {
@@ -79,7 +81,6 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
data: { results }, data: { results },
} = await InstanceGroupsAPI.readInstances(instanceGroup.id); } = await InstanceGroupsAPI.readInstances(instanceGroup.id);
let instanceDetails; let instanceDetails;
let healthCheckDetails;
const isAssociated = results.some( const isAssociated = results.some(
({ id: instId }) => instId === parseInt(instanceId, 10) ({ id: instId }) => instId === parseInt(instanceId, 10)
); );
@@ -92,7 +93,7 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
]); ]);
instanceDetails = details; instanceDetails = details;
healthCheckDetails = healthCheckData; setHealthCheck(healthCheckData);
} else { } else {
throw new Error( throw new Error(
`This instance is not associated with this instance group` `This instance is not associated with this instance group`
@@ -100,7 +101,6 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
} }
setBreadcrumb(instanceGroup, instanceDetails); setBreadcrumb(instanceGroup, instanceDetails);
setHealthCheck(healthCheckDetails);
setForks( setForks(
computeForks( computeForks(
instanceDetails.mem_capacity, instanceDetails.mem_capacity,
@@ -121,8 +121,12 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
request: fetchHealthCheck, request: fetchHealthCheck,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const { data } = await InstancesAPI.healthCheck(instanceId); const { status } = await InstancesAPI.healthCheck(instanceId);
const { data } = await InstancesAPI.readHealthCheckDetail(instanceId);
setHealthCheck(data); setHealthCheck(data);
if (status === 200) {
setShowHealthCheckAlert(true);
}
}, [instanceId]) }, [instanceId])
); );
@@ -188,6 +192,9 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
return ( return (
<> <>
<RoutedTabs tabsArray={tabsArray} /> <RoutedTabs tabsArray={tabsArray} />
{showHealthCheckAlert ? (
<HealthCheckAlert onSetHealthCheckAlert={setShowHealthCheckAlert} />
) : null}
<CardBody> <CardBody>
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail <Detail

View File

@@ -298,58 +298,6 @@ describe('<InstanceDetails/>', () => {
expect(InstancesAPI.readDetail).not.toBeCalled(); expect(InstancesAPI.readDetail).not.toBeCalled();
}); });
test('Should make request for Health Check', async () => {
InstancesAPI.healthCheck.mockResolvedValue({
data: {
uuid: '00000000-0000-0000-0000-000000000000',
hostname: 'awx_1',
version: '19.1.0',
last_health_check: '2021-09-15T18:02:07.270664Z',
errors: '',
cpu: 8,
memory: 6232231936,
cpu_capacity: 32,
mem_capacity: 38,
capacity: 38,
},
});
InstanceGroupsAPI.readInstances.mockResolvedValue({
data: {
results: [
{
id: 1,
},
{
id: 2,
},
],
},
});
jest.spyOn(ConfigContext, 'useConfig').mockImplementation(() => ({
me: { is_superuser: true },
}));
await act(async () => {
wrapper = mountWithContexts(
<InstanceDetails
instanceGroup={instanceGroup}
setBreadcrumb={() => {}}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(
wrapper.find("Button[ouiaId='health-check-button']").prop('isDisabled')
).toBe(false);
await act(async () => {
wrapper.find("Button[ouiaId='health-check-button']").prop('onClick')();
});
expect(InstancesAPI.healthCheck).toBeCalledWith(1);
wrapper.update();
expect(
wrapper.find("Detail[label='Last Health Check']").prop('value')
).toBe('9/15/2021, 6:02:07 PM');
});
test('Should handle api error for health check', async () => { test('Should handle api error for health check', async () => {
InstancesAPI.healthCheck.mockRejectedValue( InstancesAPI.healthCheck.mockRejectedValue(
new Error({ new Error({

View File

@@ -23,6 +23,7 @@ import useSelected from 'hooks/useSelected';
import { InstanceGroupsAPI, InstancesAPI } from 'api'; import { InstanceGroupsAPI, InstancesAPI } from 'api';
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs'; import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
import HealthCheckButton from 'components/HealthCheckButton/HealthCheckButton'; import HealthCheckButton from 'components/HealthCheckButton/HealthCheckButton';
import HealthCheckAlert from 'components/HealthCheckAlert';
import InstanceListItem from './InstanceListItem'; import InstanceListItem from './InstanceListItem';
const QS_CONFIG = getQSConfig('instance', { const QS_CONFIG = getQSConfig('instance', {
@@ -33,6 +34,7 @@ const QS_CONFIG = getQSConfig('instance', {
function InstanceList({ instanceGroup }) { function InstanceList({ instanceGroup }) {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
const location = useLocation(); const location = useLocation();
const { id: instanceGroupId } = useParams(); const { id: instanceGroupId } = useParams();
@@ -86,9 +88,15 @@ function InstanceList({ instanceGroup }) {
isLoading: isHealthCheckLoading, isLoading: isHealthCheckLoading,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
await Promise.all(selected.map(({ id }) => InstancesAPI.healthCheck(id))); const [...response] = await Promise.all(
fetchInstances(); selected
}, [selected, fetchInstances]) .filter(({ node_type }) => node_type !== 'hop')
.map(({ id }) => InstancesAPI.healthCheck(id))
);
if (response) {
setShowHealthCheckAlert(true);
}
}, [selected])
); );
const handleHealthCheck = async () => { const handleHealthCheck = async () => {
@@ -171,6 +179,9 @@ function InstanceList({ instanceGroup }) {
return ( return (
<> <>
{showHealthCheckAlert ? (
<HealthCheckAlert onSetHealthCheckAlert={setShowHealthCheckAlert} />
) : null}
<PaginatedTable <PaginatedTable
contentError={contentError} contentError={contentError}
hasContentLoading={ hasContentLoading={

View File

@@ -32,6 +32,7 @@ import useRequest, {
useDeleteItems, useDeleteItems,
useDismissableError, useDismissableError,
} from 'hooks/useRequest'; } from 'hooks/useRequest';
import HealthCheckAlert from 'components/HealthCheckAlert';
import RemoveInstanceButton from '../Shared/RemoveInstanceButton'; import RemoveInstanceButton from '../Shared/RemoveInstanceButton';
const Unavailable = styled.span` const Unavailable = styled.span`
@@ -66,6 +67,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
const [forks, setForks] = useState(); const [forks, setForks] = useState();
const history = useHistory(); const history = useHistory();
const [healthCheck, setHealthCheck] = useState({}); const [healthCheck, setHealthCheck] = useState({});
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
const { const {
isLoading, isLoading,
@@ -119,8 +121,12 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
request: fetchHealthCheck, request: fetchHealthCheck,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {
const { data } = await InstancesAPI.healthCheck(id); const { status } = await InstancesAPI.healthCheck(id);
const { data } = await InstancesAPI.readHealthCheckDetail(id);
setHealthCheck(data); setHealthCheck(data);
if (status === 200) {
setShowHealthCheckAlert(true);
}
}, [id]) }, [id])
); );
@@ -175,6 +181,10 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
const isHopNode = instance.node_type === 'hop'; const isHopNode = instance.node_type === 'hop';
return ( return (
<>
{showHealthCheckAlert ? (
<HealthCheckAlert onSetHealthCheckAlert={setShowHealthCheckAlert} />
) : null}
<CardBody> <CardBody>
<DetailList gutter="sm"> <DetailList gutter="sm">
<Detail <Detail
@@ -361,6 +371,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
</AlertModal> </AlertModal>
)} )}
</CardBody> </CardBody>
</>
); );
} }

View File

@@ -165,41 +165,6 @@ describe('<InstanceDetail/>', () => {
expect(wrapper.find('InstanceToggle').length).toBe(1); expect(wrapper.find('InstanceToggle').length).toBe(1);
}); });
test('Should make request for Health Check', async () => {
InstancesAPI.healthCheck.mockResolvedValue({
data: {
uuid: '00000000-0000-0000-0000-000000000000',
hostname: 'awx_1',
version: '19.1.0',
last_health_check: '2021-09-15T18:02:07.270664Z',
errors: '',
cpu: 8,
memory: 6232231936,
cpu_capacity: 32,
mem_capacity: 38,
capacity: 38,
},
});
jest.spyOn(ConfigContext, 'useConfig').mockImplementation(() => ({
me: { is_superuser: true },
}));
await act(async () => {
wrapper = mountWithContexts(<InstanceDetail setBreadcrumb={() => {}} />);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(
wrapper.find("Button[ouiaId='health-check-button']").prop('isDisabled')
).toBe(false);
await act(async () => {
wrapper.find("Button[ouiaId='health-check-button']").prop('onClick')();
});
expect(InstancesAPI.healthCheck).toBeCalledWith(1);
wrapper.update();
expect(
wrapper.find("Detail[label='Last Health Check']").prop('value')
).toBe('9/15/2021, 6:02:07 PM');
});
test('Should handle api error for health check', async () => { test('Should handle api error for health check', async () => {
InstancesAPI.healthCheck.mockRejectedValue( InstancesAPI.healthCheck.mockRejectedValue(
new Error({ new Error({

View File

@@ -93,10 +93,9 @@ function InstanceList() {
if (response) { if (response) {
setShowHealthCheckAlert(true); setShowHealthCheckAlert(true);
} }
return response;
}, [selected]) }, [selected])
); );
const handleHealthCheck = async () => { const handleHealthCheck = async () => {
await fetchHealthCheck(); await fetchHealthCheck();
clearSelected(); clearSelected();