mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 10:30:03 -03:30
Merge pull request #11090 from AlexSCorey/10952-AddHealthCheckOnInstancesList
Adds Health Check functionality to instance list
This commit is contained in:
commit
b41f90e7d4
@ -6,10 +6,10 @@ class Instances extends Base {
|
||||
this.baseUrl = '/api/v2/instances/';
|
||||
|
||||
this.readHealthCheckDetail = this.readHealthCheckDetail.bind(this);
|
||||
this.createHealthCheck = this.createHealthCheck.bind(this);
|
||||
this.healthCheck = this.healthCheck.bind(this);
|
||||
}
|
||||
|
||||
createHealthCheck(instanceId) {
|
||||
healthCheck(instanceId) {
|
||||
return this.http.post(`${this.baseUrl}${instanceId}/health_check/`);
|
||||
}
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ function InstanceToggle({ className, fetchInstances, instance, onToggle }) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
content={t`Set the instance online or offline. If offline, jobs will not be assigned to this instance.`}
|
||||
content={t`Set the instance enabled or disabled. If disabled, jobs will not be assigned to this instance.`}
|
||||
position="top"
|
||||
>
|
||||
<Switch
|
||||
|
||||
@ -118,7 +118,7 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
||||
|
||||
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await InstancesAPI.createHealthCheck(instanceId);
|
||||
const { data } = await InstancesAPI.healthCheck(instanceId);
|
||||
setHealthCheck(data);
|
||||
}, [instanceId])
|
||||
);
|
||||
|
||||
@ -298,7 +298,7 @@ describe('<InstanceDetails/>', () => {
|
||||
});
|
||||
|
||||
test('Should make request for Health Check', async () => {
|
||||
InstancesAPI.createHealthCheck.mockResolvedValue({
|
||||
InstancesAPI.healthCheck.mockResolvedValue({
|
||||
data: {
|
||||
uuid: '00000000-0000-0000-0000-000000000000',
|
||||
hostname: 'awx_1',
|
||||
@ -342,7 +342,7 @@ describe('<InstanceDetails/>', () => {
|
||||
await act(async () => {
|
||||
wrapper.find("Button[ouiaId='health-check-button']").prop('onClick')();
|
||||
});
|
||||
expect(InstancesAPI.createHealthCheck).toBeCalledWith(1);
|
||||
expect(InstancesAPI.healthCheck).toBeCalledWith(1);
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find("Detail[label='Last Health Check']").prop('value')
|
||||
@ -350,7 +350,7 @@ describe('<InstanceDetails/>', () => {
|
||||
});
|
||||
|
||||
test('Should handle api error for health check', async () => {
|
||||
InstancesAPI.createHealthCheck.mockRejectedValue(
|
||||
InstancesAPI.healthCheck.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
@ -392,7 +392,7 @@ describe('<InstanceDetails/>', () => {
|
||||
await act(async () => {
|
||||
wrapper.find("Button[ouiaId='health-check-button']").prop('onClick')();
|
||||
});
|
||||
expect(InstancesAPI.createHealthCheck).toBeCalledWith(1);
|
||||
expect(InstancesAPI.healthCheck).toBeCalledWith(1);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal')).toHaveLength(1);
|
||||
expect(wrapper.find('ErrorDetail')).toHaveLength(1);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Plural, t } from '@lingui/macro';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import 'styled-components/macro';
|
||||
|
||||
import useExpanded from 'hooks/useExpanded';
|
||||
import DataListToolbar from 'components/DataListToolbar';
|
||||
import PaginatedTable, {
|
||||
HeaderRow,
|
||||
@ -24,6 +25,7 @@ import useSelected from 'hooks/useSelected';
|
||||
import { InstanceGroupsAPI, InstancesAPI } from 'api';
|
||||
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
|
||||
|
||||
import { Button, Tooltip } from '@patternfly/react-core';
|
||||
import InstanceListItem from './InstanceListItem';
|
||||
|
||||
const QS_CONFIG = getQSConfig('instance', {
|
||||
@ -81,6 +83,13 @@ function InstanceList() {
|
||||
fetchInstances();
|
||||
}, [fetchInstances]);
|
||||
|
||||
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
|
||||
useCallback(async () => {
|
||||
await Promise.all(selected.map(({ id }) => InstancesAPI.healthCheck(id)));
|
||||
fetchInstances();
|
||||
}, [selected, fetchInstances])
|
||||
);
|
||||
|
||||
const {
|
||||
isLoading: isDisassociateLoading,
|
||||
deleteItems: disassociateInstances,
|
||||
@ -129,7 +138,7 @@ function InstanceList() {
|
||||
};
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
associateError || disassociateError
|
||||
associateError || disassociateError || healthCheckError
|
||||
);
|
||||
|
||||
const canAdd =
|
||||
@ -148,6 +157,9 @@ function InstanceList() {
|
||||
[instanceGroupId]
|
||||
);
|
||||
|
||||
const { expanded, isAllExpanded, handleExpand, expandAll } =
|
||||
useExpanded(instances);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaginatedTable
|
||||
@ -178,6 +190,8 @@ function InstanceList() {
|
||||
{...props}
|
||||
isAllSelected={isAllSelected}
|
||||
onSelectAll={selectAll}
|
||||
isAllExpanded={isAllExpanded}
|
||||
onExpandAll={expandAll}
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd
|
||||
@ -198,6 +212,28 @@ function InstanceList() {
|
||||
)}
|
||||
modalTitle={t`Disassociate instance from instance group?`}
|
||||
/>,
|
||||
<Tooltip
|
||||
content={
|
||||
selected.length ? (
|
||||
<Plural
|
||||
value={selected.length}
|
||||
one="Click to run a health check on the selected instance."
|
||||
other="Click to run a health check on the selected instances."
|
||||
/>
|
||||
) : (
|
||||
t`Select an instance to run a health check.`
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<Button
|
||||
isDisabled={!canAdd || !selected.length}
|
||||
variant="secondary"
|
||||
ouiaId="health-check"
|
||||
onClick={fetchHealthCheck}
|
||||
>{t`Health Check`}</Button>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
]}
|
||||
emptyStateControls={
|
||||
canAdd ? (
|
||||
@ -210,10 +246,9 @@ function InstanceList() {
|
||||
/>
|
||||
)}
|
||||
headerRow={
|
||||
<HeaderRow qsConfig={QS_CONFIG}>
|
||||
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
||||
<HeaderCell sortKey="hostname">{t`Name`}</HeaderCell>
|
||||
<HeaderCell>{t`Node Type`}</HeaderCell>
|
||||
<HeaderCell>{t`Policy Type`}</HeaderCell>
|
||||
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
|
||||
<HeaderCell>{t`Running Jobs`}</HeaderCell>
|
||||
<HeaderCell>{t`Total Jobs`}</HeaderCell>
|
||||
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
|
||||
@ -223,6 +258,8 @@ function InstanceList() {
|
||||
}
|
||||
renderRow={(instance, index) => (
|
||||
<InstanceListItem
|
||||
isExpanded={expanded.some((row) => row.id === instance.id)}
|
||||
onExpand={() => handleExpand(instance)}
|
||||
key={instance.id}
|
||||
value={instance.hostname}
|
||||
instance={instance}
|
||||
@ -252,9 +289,11 @@ function InstanceList() {
|
||||
title={t`Error!`}
|
||||
variant="error"
|
||||
>
|
||||
{associateError
|
||||
? t`Failed to associate.`
|
||||
: t`Failed to disassociate one or more instances.`}
|
||||
{associateError && t`Failed to associate.`}
|
||||
{disassociateError &&
|
||||
t`Failed to disassociate one or more instances.`}
|
||||
{healthCheckError &&
|
||||
t`Failed to run a health check on one or more instances.`}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
)}
|
||||
|
||||
@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import { InstanceGroupsAPI } from 'api';
|
||||
import { InstancesAPI, InstanceGroupsAPI } from 'api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -152,4 +152,49 @@ describe('<InstanceList/>', () => {
|
||||
wrapper.find('ModalBoxCloseButton').simulate('click');
|
||||
expect(wrapper.find('AssociateModal').length).toBe(0);
|
||||
});
|
||||
test('should run health check', async () => {
|
||||
expect(
|
||||
wrapper.find('Button[ouiaId="health-check"]').prop('isDisabled')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('DataListToolbar').prop('onSelectAll')(instances)
|
||||
);
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('Button[ouiaId="health-check"]').prop('isDisabled')
|
||||
).toBe(false);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[ouiaId="health-check"]').prop('onClick')()
|
||||
);
|
||||
expect(InstancesAPI.healthCheck).toBeCalledTimes(3);
|
||||
});
|
||||
test('should render health check error', async () => {
|
||||
InstancesAPI.healthCheck.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'create',
|
||||
url: '/api/v2/instances',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(
|
||||
wrapper.find('Button[ouiaId="health-check"]').prop('isDisabled')
|
||||
).toBe(true);
|
||||
await act(async () =>
|
||||
wrapper.find('DataListToolbar').prop('onSelectAll')(instances)
|
||||
);
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('Button[ouiaId="health-check"]').prop('isDisabled')
|
||||
).toBe(false);
|
||||
await act(async () =>
|
||||
wrapper.find('Button[ouiaId="health-check"]').prop('onClick')()
|
||||
);
|
||||
wrapper.update();
|
||||
expect(wrapper.find('AlertModal')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,11 +9,13 @@ import {
|
||||
ProgressMeasureLocation,
|
||||
ProgressSize,
|
||||
Slider,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { Tr, Td } from '@patternfly/react-table';
|
||||
|
||||
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||
import { formatDateString } from 'util/dates';
|
||||
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
||||
import InstanceToggle from 'components/InstanceToggle';
|
||||
import StatusLabel from 'components/StatusLabel';
|
||||
import { Instance } from 'types';
|
||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
@ -21,6 +23,7 @@ import { InstancesAPI } from 'api';
|
||||
import { useConfig } from 'contexts/Config';
|
||||
import AlertModal from 'components/AlertModal';
|
||||
import ErrorDetail from 'components/ErrorDetail';
|
||||
import { Detail, DetailList } from 'components/DetailList';
|
||||
|
||||
const Unavailable = styled.span`
|
||||
color: var(--pf-global--danger-color--200);
|
||||
@ -50,6 +53,8 @@ function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
|
||||
|
||||
function InstanceListItem({
|
||||
instance,
|
||||
isExpanded,
|
||||
onExpand,
|
||||
isSelected,
|
||||
onSelect,
|
||||
fetchInstances,
|
||||
@ -106,6 +111,13 @@ function InstanceListItem({
|
||||
return (
|
||||
<>
|
||||
<Tr id={`instance-row-${instance.id}`}>
|
||||
<Td
|
||||
expand={{
|
||||
rowIndex,
|
||||
isExpanded,
|
||||
onToggle: onExpand,
|
||||
}}
|
||||
/>
|
||||
<Td
|
||||
select={{
|
||||
rowIndex,
|
||||
@ -117,12 +129,17 @@ function InstanceListItem({
|
||||
/>
|
||||
<Td id={labelId} dataLabel={t`Name`}>
|
||||
<Link to={`/instance_groups/${id}/instances/${instance.id}/details`}>
|
||||
{instance.hostname}
|
||||
<b>{instance.hostname}</b>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td dataLabel={t`Node Type`}>{instance.node_type}</Td>
|
||||
<Td dataLabel={t`Policy Type`}>
|
||||
{instance.managed_by_policy ? t`Auto` : t`Manual`}
|
||||
<Td dataLabel={t`Status`}>
|
||||
<Tooltip
|
||||
content={t`Last Health Check ${formatDateString(
|
||||
instance.last_health_check
|
||||
)}`}
|
||||
>
|
||||
<StatusLabel status={instance.errors ? 'error' : 'healthy'} />
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td dataLabel={t`Running Jobs`}>{instance.jobs_running}</Td>
|
||||
<Td dataLabel={t`Total Jobs`}>{instance.jobs_total}</Td>
|
||||
@ -166,6 +183,24 @@ function InstanceListItem({
|
||||
</ActionItem>
|
||||
</ActionsTd>
|
||||
</Tr>
|
||||
<Tr isExpanded={isExpanded}>
|
||||
<Td colSpan={2} />
|
||||
<Td colSpan={7}>
|
||||
<ExpandableRowContent>
|
||||
<DetailList>
|
||||
<Detail label={t`Node Type`} value={instance.node_type} />
|
||||
<Detail
|
||||
label={t`Policy Type`}
|
||||
value={instance.managed_by_policy ? t`Auto` : t`Manual`}
|
||||
/>
|
||||
<Detail
|
||||
label={t`Last Health Check`}
|
||||
value={formatDateString(instance.last_health_check)}
|
||||
/>
|
||||
</DetailList>
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
</Tr>
|
||||
{updateError && (
|
||||
<AlertModal
|
||||
variant="error"
|
||||
|
||||
@ -36,6 +36,7 @@ const instance = [
|
||||
percent_capacity_remaining: 60.0,
|
||||
jobs_running: 0,
|
||||
jobs_total: 68,
|
||||
last_health_check: '2021-09-15T18:02:07.270664Z',
|
||||
cpu: 6,
|
||||
memory: 2087469056,
|
||||
cpu_capacity: 24,
|
||||
@ -58,6 +59,7 @@ const instance = [
|
||||
modified: '2020-08-12T20:08:02.836748Z',
|
||||
capacity_adjustment: '0.40',
|
||||
version: '13.0.0',
|
||||
last_health_check: '2021-09-15T18:02:07.270664Z',
|
||||
capacity: 10,
|
||||
consumed_capacity: 0,
|
||||
percent_capacity_remaining: 60.0,
|
||||
@ -162,10 +164,8 @@ describe('<InstanceListItem/>', () => {
|
||||
expect(wrapper.find('Td[dataLabel="Name"]').find('Link').prop('to')).toBe(
|
||||
'/instance_groups/1/instances/1/details'
|
||||
);
|
||||
expect(wrapper.find('Td').at(1).text()).toBe('awx');
|
||||
expect(wrapper.find('Td').at(2).text()).toBe('awx');
|
||||
expect(wrapper.find('Progress').prop('value')).toBe(40);
|
||||
expect(wrapper.find('Td').at(2).text()).toBe('hybrid');
|
||||
expect(wrapper.find('Td').at(3).text()).toBe('Auto');
|
||||
expect(
|
||||
wrapper
|
||||
.find('Td')
|
||||
@ -198,9 +198,7 @@ describe('<InstanceListItem/>', () => {
|
||||
</table>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('Td').first().prop('select').onSelect).toEqual(
|
||||
onSelect
|
||||
);
|
||||
expect(wrapper.find('Td').at(1).prop('select').onSelect).toEqual(onSelect);
|
||||
});
|
||||
|
||||
test('should disable checkbox', async () => {
|
||||
@ -218,7 +216,7 @@ describe('<InstanceListItem/>', () => {
|
||||
</table>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('Td').first().prop('select').disable).toEqual(true);
|
||||
expect(wrapper.find('Td').at(1).prop('select').disable).toEqual(true);
|
||||
});
|
||||
|
||||
test('should display instance toggle', () => {
|
||||
@ -276,4 +274,32 @@ describe('<InstanceListItem/>', () => {
|
||||
});
|
||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||
});
|
||||
|
||||
test('Should render expanded row with the correct data points', async () => {
|
||||
const onSelect = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<table>
|
||||
<tbody>
|
||||
<InstanceListItem
|
||||
instance={instance[0]}
|
||||
onSelect={onSelect}
|
||||
fetchInstances={() => {}}
|
||||
isExpanded
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('InstanceListItem').prop('isExpanded')).toBe(true);
|
||||
expect(wrapper.find('Detail[label="Node Type"]').prop('value')).toBe(
|
||||
'hybrid'
|
||||
);
|
||||
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
||||
'Auto'
|
||||
);
|
||||
expect(
|
||||
wrapper.find('Detail[label="Last Health Check"]').prop('value')
|
||||
).toBe('9/15/2021, 6:02:07 PM');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user