mirror of
https://github.com/ansible/awx.git
synced 2026-05-23 08:37:48 -02:30
Add health check toast notification for Instance list and detail views.
This commit is contained in:
@@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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,192 +181,197 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
const isHopNode = instance.node_type === 'hop';
|
const isHopNode = instance.node_type === 'hop';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardBody>
|
<>
|
||||||
<DetailList gutter="sm">
|
{showHealthCheckAlert ? (
|
||||||
<Detail
|
<HealthCheckAlert onSetHealthCheckAlert={setShowHealthCheckAlert} />
|
||||||
label={t`Host Name`}
|
) : null}
|
||||||
value={instance.hostname}
|
<CardBody>
|
||||||
dataCy="instance-detail-name"
|
<DetailList gutter="sm">
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={t`Status`}
|
|
||||||
value={
|
|
||||||
instance.node_state ? (
|
|
||||||
<StatusLabel status={instance.node_state} />
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Detail label={t`Node Type`} value={instance.node_type} />
|
|
||||||
{!isHopNode && (
|
|
||||||
<>
|
|
||||||
<Detail
|
|
||||||
label={t`Policy Type`}
|
|
||||||
value={instance.managed_by_policy ? t`Auto` : t`Manual`}
|
|
||||||
/>
|
|
||||||
<Detail label={t`Host`} value={instance.ip_address} />
|
|
||||||
<Detail label={t`Running Jobs`} value={instance.jobs_running} />
|
|
||||||
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
|
|
||||||
{instanceGroups && (
|
|
||||||
<Detail
|
|
||||||
fullWidth
|
|
||||||
label={t`Instance Groups`}
|
|
||||||
helpText={t`The Instance Groups to which this instance belongs.`}
|
|
||||||
value={instanceGroups.map((ig) => (
|
|
||||||
<React.Fragment key={ig.id}>
|
|
||||||
<Label
|
|
||||||
color="blue"
|
|
||||||
isTruncated
|
|
||||||
render={({ className, content, componentRef }) => (
|
|
||||||
<Link
|
|
||||||
to={`${buildLinkURL(ig)}${ig.id}/details`}
|
|
||||||
className={className}
|
|
||||||
innerRef={componentRef}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{ig.name}
|
|
||||||
</Label>{' '}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
isEmpty={instanceGroups.length === 0}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Detail
|
|
||||||
label={t`Last Health Check`}
|
|
||||||
value={formatDateString(healthCheck?.last_health_check)}
|
|
||||||
/>
|
|
||||||
{instance.related?.install_bundle && (
|
|
||||||
<Detail
|
|
||||||
label={t`Install Bundle`}
|
|
||||||
value={
|
|
||||||
<Tooltip content={t`Click to download bundle`}>
|
|
||||||
<Button
|
|
||||||
component="a"
|
|
||||||
isSmall
|
|
||||||
href={`${instance.related?.install_bundle}`}
|
|
||||||
target="_blank"
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Detail
|
|
||||||
label={t`Capacity Adjustment`}
|
|
||||||
value={
|
|
||||||
<SliderHolder data-cy="slider-holder">
|
|
||||||
<div data-cy="cpu-capacity">{t`CPU ${instance.cpu_capacity}`}</div>
|
|
||||||
<SliderForks data-cy="slider-forks">
|
|
||||||
<div data-cy="number-forks">
|
|
||||||
<Plural value={forks} one="# fork" other="# forks" />
|
|
||||||
</div>
|
|
||||||
<Slider
|
|
||||||
areCustomStepsContinuous
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
step={0.1}
|
|
||||||
value={instance.capacity_adjustment}
|
|
||||||
onChange={handleChangeValue}
|
|
||||||
isDisabled={!me?.is_superuser || !instance.enabled}
|
|
||||||
data-cy="slider"
|
|
||||||
/>
|
|
||||||
</SliderForks>
|
|
||||||
<div data-cy="mem-capacity">{t`RAM ${instance.mem_capacity}`}</div>
|
|
||||||
</SliderHolder>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Detail
|
|
||||||
label={t`Used Capacity`}
|
|
||||||
value={
|
|
||||||
instance.enabled ? (
|
|
||||||
<Progress
|
|
||||||
title={t`Used capacity`}
|
|
||||||
value={Math.round(
|
|
||||||
100 - instance.percent_capacity_remaining
|
|
||||||
)}
|
|
||||||
measureLocation={ProgressMeasureLocation.top}
|
|
||||||
size={ProgressSize.sm}
|
|
||||||
aria-label={t`Used capacity`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Unavailable>{t`Unavailable`}</Unavailable>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{healthCheck?.errors && (
|
|
||||||
<Detail
|
<Detail
|
||||||
fullWidth
|
label={t`Host Name`}
|
||||||
label={t`Errors`}
|
value={instance.hostname}
|
||||||
|
dataCy="instance-detail-name"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Status`}
|
||||||
value={
|
value={
|
||||||
<CodeBlock>
|
instance.node_state ? (
|
||||||
<CodeBlockCode>{healthCheck?.errors}</CodeBlockCode>
|
<StatusLabel status={instance.node_state} />
|
||||||
</CodeBlock>
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
<Detail label={t`Node Type`} value={instance.node_type} />
|
||||||
</DetailList>
|
{!isHopNode && (
|
||||||
{!isHopNode && (
|
<>
|
||||||
<CardActionsRow>
|
<Detail
|
||||||
{me.is_superuser && isK8s && instance.node_type === 'execution' && (
|
label={t`Policy Type`}
|
||||||
<RemoveInstanceButton
|
value={instance.managed_by_policy ? t`Auto` : t`Manual`}
|
||||||
itemsToRemove={[instance]}
|
/>
|
||||||
isK8s={isK8s}
|
<Detail label={t`Host`} value={instance.ip_address} />
|
||||||
onRemove={removeInstances}
|
<Detail label={t`Running Jobs`} value={instance.jobs_running} />
|
||||||
|
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
|
||||||
|
{instanceGroups && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={t`Instance Groups`}
|
||||||
|
helpText={t`The Instance Groups to which this instance belongs.`}
|
||||||
|
value={instanceGroups.map((ig) => (
|
||||||
|
<React.Fragment key={ig.id}>
|
||||||
|
<Label
|
||||||
|
color="blue"
|
||||||
|
isTruncated
|
||||||
|
render={({ className, content, componentRef }) => (
|
||||||
|
<Link
|
||||||
|
to={`${buildLinkURL(ig)}${ig.id}/details`}
|
||||||
|
className={className}
|
||||||
|
innerRef={componentRef}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{ig.name}
|
||||||
|
</Label>{' '}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
isEmpty={instanceGroups.length === 0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Detail
|
||||||
|
label={t`Last Health Check`}
|
||||||
|
value={formatDateString(healthCheck?.last_health_check)}
|
||||||
|
/>
|
||||||
|
{instance.related?.install_bundle && (
|
||||||
|
<Detail
|
||||||
|
label={t`Install Bundle`}
|
||||||
|
value={
|
||||||
|
<Tooltip content={t`Click to download bundle`}>
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
isSmall
|
||||||
|
href={`${instance.related?.install_bundle}`}
|
||||||
|
target="_blank"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Detail
|
||||||
|
label={t`Capacity Adjustment`}
|
||||||
|
value={
|
||||||
|
<SliderHolder data-cy="slider-holder">
|
||||||
|
<div data-cy="cpu-capacity">{t`CPU ${instance.cpu_capacity}`}</div>
|
||||||
|
<SliderForks data-cy="slider-forks">
|
||||||
|
<div data-cy="number-forks">
|
||||||
|
<Plural value={forks} one="# fork" other="# forks" />
|
||||||
|
</div>
|
||||||
|
<Slider
|
||||||
|
areCustomStepsContinuous
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
value={instance.capacity_adjustment}
|
||||||
|
onChange={handleChangeValue}
|
||||||
|
isDisabled={!me?.is_superuser || !instance.enabled}
|
||||||
|
data-cy="slider"
|
||||||
|
/>
|
||||||
|
</SliderForks>
|
||||||
|
<div data-cy="mem-capacity">{t`RAM ${instance.mem_capacity}`}</div>
|
||||||
|
</SliderHolder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Used Capacity`}
|
||||||
|
value={
|
||||||
|
instance.enabled ? (
|
||||||
|
<Progress
|
||||||
|
title={t`Used capacity`}
|
||||||
|
value={Math.round(
|
||||||
|
100 - instance.percent_capacity_remaining
|
||||||
|
)}
|
||||||
|
measureLocation={ProgressMeasureLocation.top}
|
||||||
|
size={ProgressSize.sm}
|
||||||
|
aria-label={t`Used capacity`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Unavailable>{t`Unavailable`}</Unavailable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{healthCheck?.errors && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={t`Errors`}
|
||||||
|
value={
|
||||||
|
<CodeBlock>
|
||||||
|
<CodeBlockCode>{healthCheck?.errors}</CodeBlockCode>
|
||||||
|
</CodeBlock>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={t`Run a health check on the instance`}>
|
</DetailList>
|
||||||
<Button
|
{!isHopNode && (
|
||||||
isDisabled={!me.is_superuser || isRunningHealthCheck}
|
<CardActionsRow>
|
||||||
variant="primary"
|
{me.is_superuser && isK8s && instance.node_type === 'execution' && (
|
||||||
ouiaId="health-check-button"
|
<RemoveInstanceButton
|
||||||
onClick={fetchHealthCheck}
|
itemsToRemove={[instance]}
|
||||||
isLoading={isRunningHealthCheck}
|
isK8s={isK8s}
|
||||||
spinnerAriaLabel={t`Running health check`}
|
onRemove={removeInstances}
|
||||||
>
|
/>
|
||||||
{t`Run health check`}
|
)}
|
||||||
</Button>
|
<Tooltip content={t`Run a health check on the instance`}>
|
||||||
</Tooltip>
|
<Button
|
||||||
<InstanceToggle
|
isDisabled={!me.is_superuser || isRunningHealthCheck}
|
||||||
css="display: inline-flex;"
|
variant="primary"
|
||||||
fetchInstances={fetchDetails}
|
ouiaId="health-check-button"
|
||||||
instance={instance}
|
onClick={fetchHealthCheck}
|
||||||
/>
|
isLoading={isRunningHealthCheck}
|
||||||
</CardActionsRow>
|
spinnerAriaLabel={t`Running health check`}
|
||||||
)}
|
>
|
||||||
|
{t`Run health check`}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<InstanceToggle
|
||||||
|
css="display: inline-flex;"
|
||||||
|
fetchInstances={fetchDetails}
|
||||||
|
instance={instance}
|
||||||
|
/>
|
||||||
|
</CardActionsRow>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={error}
|
isOpen={error}
|
||||||
onClose={dismissError}
|
onClose={dismissError}
|
||||||
title={t`Error!`}
|
title={t`Error!`}
|
||||||
variant="error"
|
variant="error"
|
||||||
>
|
>
|
||||||
{updateInstanceError
|
{updateInstanceError
|
||||||
? t`Failed to update capacity adjustment.`
|
? t`Failed to update capacity adjustment.`
|
||||||
: t`Failed to disassociate one or more instances.`}
|
: t`Failed to disassociate one or more instances.`}
|
||||||
<ErrorDetail error={error} />
|
<ErrorDetail error={error} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{removeError && (
|
{removeError && (
|
||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={removeError}
|
isOpen={removeError}
|
||||||
variant="error"
|
variant="error"
|
||||||
aria-label={t`Removal Error`}
|
aria-label={t`Removal Error`}
|
||||||
title={t`Error!`}
|
title={t`Error!`}
|
||||||
onClose={clearDeletionError}
|
onClose={clearDeletionError}
|
||||||
>
|
>
|
||||||
{t`Failed to remove one or more instances.`}
|
{t`Failed to remove one or more instances.`}
|
||||||
<ErrorDetail error={removeError} />
|
<ErrorDetail error={removeError} />
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
)}
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user