summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSarah Akus <sakus@redhat.com>2022-10-12 17:37:57 +0200
committerGitHub <noreply@github.com>2022-10-12 17:37:57 +0200
commita2be320605d02a3cd3ca14efeba716da6a8cb267 (patch)
tree8483883fb3db18882794796cc3d43c7b271d983f
parentAdd developer documentation for project signing work (diff)
parentLink out to docs; use `some` in place of `forEach` when looping through results. (diff)
downloadawx-a2be320605d02a3cd3ca14efeba716da6a8cb267.tar.xz
awx-a2be320605d02a3cd3ca14efeba716da6a8cb267.zip
Merge pull request #12974 from kialam/new-health-check-started
Update UI to support pending health checks.
-rw-r--r--awx/ui/src/components/HealthCheckButton/HealthCheckButton.js13
-rw-r--r--awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.js78
-rw-r--r--awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.test.js64
-rw-r--r--awx/ui/src/screens/InstanceGroup/Instances/InstanceList.js28
-rw-r--r--awx/ui/src/screens/InstanceGroup/Instances/InstanceList.test.js2
-rw-r--r--awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.js35
-rw-r--r--awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.test.js6
-rw-r--r--awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.js83
-rw-r--r--awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.test.js3
-rw-r--r--awx/ui/src/screens/Instances/InstanceList/InstanceList.js23
-rw-r--r--awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js4
-rw-r--r--awx/ui/src/screens/Instances/InstanceList/InstanceListItem.js39
-rw-r--r--awx/ui/src/screens/Instances/InstanceList/InstanceListItem.test.js6
13 files changed, 309 insertions, 75 deletions
diff --git a/awx/ui/src/components/HealthCheckButton/HealthCheckButton.js b/awx/ui/src/components/HealthCheckButton/HealthCheckButton.js
index 034f94d83b..464521d68c 100644
--- a/awx/ui/src/components/HealthCheckButton/HealthCheckButton.js
+++ b/awx/ui/src/components/HealthCheckButton/HealthCheckButton.js
@@ -3,7 +3,12 @@ import { Plural, t } from '@lingui/macro';
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
import { useKebabifiedMenu } from 'contexts/Kebabified';
-function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
+function HealthCheckButton({
+ isDisabled,
+ onClick,
+ selectedItems,
+ healthCheckPending,
+}) {
const { isKebabified } = useKebabifiedMenu();
const selectedItemsCount = selectedItems.length;
@@ -42,7 +47,11 @@ function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
variant="secondary"
ouiaId="health-check"
onClick={onClick}
- >{t`Run health check`}</Button>
+ isLoading={healthCheckPending}
+ spinnerAriaLabel={t`Running health check`}
+ >
+ {healthCheckPending ? t`Running health check` : t`Run health check`}
+ </Button>
</div>
</Tooltip>
);
diff --git a/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.js b/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.js
index eb24f1f9aa..b4ab4dc81f 100644
--- a/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.js
+++ b/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.js
@@ -12,7 +12,7 @@ import {
Tooltip,
Slider,
} from '@patternfly/react-core';
-import { CaretLeftIcon } from '@patternfly/react-icons';
+import { CaretLeftIcon, OutlinedClockIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import { useConfig } from 'contexts/Config';
@@ -23,6 +23,7 @@ import ErrorDetail from 'components/ErrorDetail';
import DisassociateButton from 'components/DisassociateButton';
import InstanceToggle from 'components/InstanceToggle';
import { CardBody, CardActionsRow } from 'components/Card';
+import getDocsBaseUrl from 'util/getDocsBaseUrl';
import { formatDateString } from 'util/dates';
import RoutedTabs from 'components/RoutedTabs';
import ContentError from 'components/ContentError';
@@ -62,7 +63,7 @@ function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
}
function InstanceDetails({ setBreadcrumb, instanceGroup }) {
- const { me = {} } = useConfig();
+ const config = useConfig();
const { id, instanceId } = useParams();
const history = useHistory();
@@ -115,15 +116,9 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
useEffect(() => {
fetchDetails();
}, [fetchDetails]);
- const {
- error: healthCheckError,
- isLoading: isRunningHealthCheck,
- request: fetchHealthCheck,
- } = useRequest(
+ const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
useCallback(async () => {
const { status } = await InstancesAPI.healthCheck(instanceId);
- const { data } = await InstancesAPI.readHealthCheckDetail(instanceId);
- setHealthCheck(data);
if (status === 200) {
setShowHealthCheckAlert(true);
}
@@ -161,6 +156,18 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
debounceUpdateInstance({ capacity_adjustment: roundedValue });
};
+ const formatHealthCheckTimeStamp = (last) => (
+ <>
+ {formatDateString(last)}
+ {instance.health_check_pending ? (
+ <>
+ {' '}
+ <OutlinedClockIcon />
+ </>
+ ) : null}
+ </>
+ );
+
const { error, dismissError } = useDismissableError(
disassociateError || updateInstanceError || healthCheckError
);
@@ -189,6 +196,8 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
return <ContentLoading />;
}
+ const isExecutionNode = instance.node_type === 'execution';
+
return (
<>
<RoutedTabs tabsArray={tabsArray} />
@@ -218,7 +227,22 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
<Detail
label={t`Last Health Check`}
- value={formatDateString(healthCheck?.last_health_check)}
+ helpText={
+ <>
+ {t`Health checks are asynchronous tasks. See the`}{' '}
+ <a
+ href={`${getDocsBaseUrl(
+ config
+ )}/html/administration/instances.html#health-check`}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {t`documentation`}
+ </a>{' '}
+ {t`for more info.`}
+ </>
+ }
+ value={formatHealthCheckTimeStamp(instance.last_health_check)}
/>
<Detail label={t`Node Type`} value={instance.node_type} />
<Detail
@@ -237,7 +261,7 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
step={0.1}
value={instance.capacity_adjustment}
onChange={handleChangeValue}
- isDisabled={!me?.is_superuser || !instance.enabled}
+ isDisabled={!config?.me?.is_superuser || !instance.enabled}
data-cy="slider"
/>
</SliderForks>
@@ -274,19 +298,25 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
)}
</DetailList>
<CardActionsRow>
- <Tooltip content={t`Run a health check on the instance`}>
- <Button
- isDisabled={!me.is_superuser || isRunningHealthCheck}
- variant="primary"
- ouiaId="health-check-button"
- onClick={fetchHealthCheck}
- isLoading={isRunningHealthCheck}
- spinnerAriaLabel={t`Running health check`}
- >
- {t`Run health check`}
- </Button>
- </Tooltip>
- {me.is_superuser && instance.node_type !== 'control' && (
+ {isExecutionNode && (
+ <Tooltip content={t`Run a health check on the instance`}>
+ <Button
+ isDisabled={
+ !config?.me?.is_superuser || instance.health_check_pending
+ }
+ variant="primary"
+ ouiaId="health-check-button"
+ onClick={fetchHealthCheck}
+ isLoading={instance.health_check_pending}
+ spinnerAriaLabel={t`Running health check`}
+ >
+ {instance.health_check_pending
+ ? t`Running health check`
+ : t`Run health check`}
+ </Button>
+ </Tooltip>
+ )}
+ {config?.me?.is_superuser && instance.node_type !== 'control' && (
<DisassociateButton
verifyCannotDisassociate={instanceGroup.name === 'controlplane'}
key="disassociate"
diff --git a/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.test.js b/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.test.js
index 2f01894f44..d40d49cd7c 100644
--- a/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.test.js
+++ b/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.test.js
@@ -87,8 +87,9 @@ describe('<InstanceDetails/>', () => {
mem_capacity: 38,
enabled: true,
managed_by_policy: true,
- node_type: 'hybrid',
+ node_type: 'execution',
node_state: 'ready',
+ health_check_pending: false,
},
});
InstancesAPI.readHealthCheckDetail.mockResolvedValue({
@@ -347,6 +348,67 @@ describe('<InstanceDetails/>', () => {
expect(wrapper.find('ErrorDetail')).toHaveLength(1);
});
+ test.each([
+ [1, 'hybrid', 0],
+ [2, 'hop', 0],
+ [3, 'control', 0],
+ ])(
+ 'hide health check button for non-execution type nodes',
+ async (a, b, expected) => {
+ InstancesAPI.readDetail.mockResolvedValue({
+ data: {
+ id: a,
+ type: 'instance',
+ url: '/api/v2/instances/1/',
+ related: {
+ named_url: '/api/v2/instances/awx_1/',
+ jobs: '/api/v2/instances/1/jobs/',
+ instance_groups: '/api/v2/instances/1/instance_groups/',
+ health_check: '/api/v2/instances/1/health_check/',
+ },
+ uuid: '00000000-0000-0000-0000-000000000000',
+ hostname: 'awx_1',
+ created: '2021-09-08T17:10:34.484569Z',
+ modified: '2021-09-09T13:55:44.219900Z',
+ last_seen: '2021-09-09T20:20:31.623148Z',
+ last_health_check: '2021-09-09T20:20:31.623148Z',
+ errors: '',
+ capacity_adjustment: '1.00',
+ version: '19.1.0',
+ capacity: 38,
+ consumed_capacity: 0,
+ percent_capacity_remaining: 100.0,
+ jobs_running: 0,
+ jobs_total: 0,
+ cpu: 8,
+ memory: 6232231936,
+ cpu_capacity: 32,
+ mem_capacity: 38,
+ enabled: true,
+ managed_by_policy: true,
+ node_type: b,
+ node_state: 'ready',
+ health_check_pending: false,
+ },
+ });
+ 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']")).toHaveLength(
+ expected
+ );
+ }
+ );
+
test('Should call disassociate', async () => {
InstanceGroupsAPI.readInstances.mockResolvedValue({
data: {
diff --git a/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.js b/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.js
index c42cbbda51..8479591673 100644
--- a/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.js
+++ b/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.js
@@ -35,6 +35,8 @@ const QS_CONFIG = getQSConfig('instance', {
function InstanceList({ instanceGroup }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
+ const [pendingHealthCheck, setPendingHealthCheck] = useState(false);
+ const [canRunHealthCheck, setCanRunHealthCheck] = useState(true);
const location = useLocation();
const { id: instanceGroupId } = useParams();
@@ -56,6 +58,10 @@ function InstanceList({ instanceGroup }) {
InstanceGroupsAPI.readInstances(instanceGroupId, params),
InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
]);
+ setPendingHealthCheck(
+ response?.data?.result?.some((i) => i.health_check_pending === true)
+ );
+
return {
instances: response.data.results,
count: response.data.count,
@@ -90,7 +96,7 @@ function InstanceList({ instanceGroup }) {
useCallback(async () => {
const [...response] = await Promise.all(
selected
- .filter(({ node_type }) => node_type !== 'hop')
+ .filter(({ node_type }) => node_type === 'execution')
.map(({ id }) => InstancesAPI.healthCheck(id))
);
if (response) {
@@ -99,6 +105,18 @@ function InstanceList({ instanceGroup }) {
}, [selected])
);
+ useEffect(() => {
+ if (selected) {
+ selected.forEach((i) => {
+ if (i.node_type === 'execution') {
+ setCanRunHealthCheck(true);
+ } else {
+ setCanRunHealthCheck(false);
+ }
+ });
+ }
+ }, [selected]);
+
const handleHealthCheck = async () => {
await fetchHealthCheck();
clearSelected();
@@ -246,9 +264,10 @@ function InstanceList({ instanceGroup }) {
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
/>,
<HealthCheckButton
- isDisabled={!canAdd}
+ isDisabled={!canAdd || !canRunHealthCheck}
onClick={handleHealthCheck}
selectedItems={selected}
+ healthCheckPending={pendingHealthCheck}
/>,
]}
emptyStateControls={
@@ -263,7 +282,10 @@ function InstanceList({ instanceGroup }) {
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
- <HeaderCell sortKey="hostname">{t`Name`}</HeaderCell>
+ <HeaderCell
+ tooltip={t`Health checks can only be run on execution nodes.`}
+ sortKey="hostname"
+ >{t`Name`}</HeaderCell>
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
<HeaderCell sortKey="node_type">{t`Node Type`}</HeaderCell>
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
diff --git a/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.test.js b/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.test.js
index 5e2c0b10c0..9aa3ce0bda 100644
--- a/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.test.js
+++ b/awx/ui/src/screens/InstanceGroup/Instances/InstanceList.test.js
@@ -172,7 +172,7 @@ describe('<InstanceList/>', () => {
await act(async () =>
wrapper.find('Button[ouiaId="health-check"]').prop('onClick')()
);
- expect(InstancesAPI.healthCheck).toBeCalledTimes(3);
+ expect(InstancesAPI.healthCheck).toBeCalledTimes(1);
});
test('should render health check error', async () => {
InstancesAPI.healthCheck.mockRejectedValue(
diff --git a/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.js b/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.js
index 60a1cc49a5..7a45003064 100644
--- a/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.js
+++ b/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.js
@@ -11,7 +11,9 @@ import {
Slider,
Tooltip,
} from '@patternfly/react-core';
+import { OutlinedClockIcon } from '@patternfly/react-icons';
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
+import getDocsBaseUrl from 'util/getDocsBaseUrl';
import { formatDateString } from 'util/dates';
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
import InstanceToggle from 'components/InstanceToggle';
@@ -52,7 +54,7 @@ function InstanceListItem({
fetchInstances,
rowIndex,
}) {
- const { me = {} } = useConfig();
+ const config = useConfig();
const { id } = useParams();
const [forks, setForks] = useState(
computeForks(
@@ -100,6 +102,18 @@ function InstanceListItem({
debounceUpdateInstance({ capacity_adjustment: roundedValue });
};
+ const formatHealthCheckTimeStamp = (last) => (
+ <>
+ {formatDateString(last)}
+ {instance.health_check_pending ? (
+ <>
+ {' '}
+ <OutlinedClockIcon />
+ </>
+ ) : null}
+ </>
+ );
+
return (
<>
<Tr
@@ -154,7 +168,7 @@ function InstanceListItem({
step={0.1}
value={instance.capacity_adjustment}
onChange={handleChangeValue}
- isDisabled={!me?.is_superuser || !instance.enabled}
+ isDisabled={!config?.me?.is_superuser || !instance.enabled}
data-cy="slider"
/>
</SliderForks>
@@ -206,7 +220,22 @@ function InstanceListItem({
<Detail
data-cy="last-health-check"
label={t`Last Health Check`}
- value={formatDateString(instance.last_health_check)}
+ helpText={
+ <>
+ {t`Health checks are asynchronous tasks. See the`}{' '}
+ <a
+ href={`${getDocsBaseUrl(
+ config
+ )}/html/administration/instances.html#health-check`}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {t`documentation`}
+ </a>{' '}
+ {t`for more info.`}
+ </>
+ }
+ value={formatHealthCheckTimeStamp(instance.last_health_check)}
/>
</DetailList>
</ExpandableRowContent>
diff --git a/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.test.js b/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.test.js
index 334b6f07fe..9bed322d6d 100644
--- a/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.test.js
+++ b/awx/ui/src/screens/InstanceGroup/Instances/InstanceListItem.test.js
@@ -281,8 +281,8 @@ describe('<InstanceListItem/>', () => {
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');
+ expect(wrapper.find('Detail[label="Last Health Check"]').text()).toBe(
+ 'Last Health Check9/15/2021, 6:02:07 PM'
+ );
});
});
diff --git a/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.js b/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.js
index 3871837e2f..1b96fb30e9 100644
--- a/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.js
+++ b/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.js
@@ -13,7 +13,7 @@ import {
Slider,
Label,
} from '@patternfly/react-core';
-import { DownloadIcon } from '@patternfly/react-icons';
+import { DownloadIcon, OutlinedClockIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import { useConfig } from 'contexts/Config';
@@ -23,6 +23,7 @@ import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail';
import InstanceToggle from 'components/InstanceToggle';
import { CardBody, CardActionsRow } from 'components/Card';
+import getDocsBaseUrl from 'util/getDocsBaseUrl';
import { formatDateString } from 'util/dates';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
@@ -62,7 +63,8 @@ function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
}
function InstanceDetail({ setBreadcrumb, isK8s }) {
- const { me = {} } = useConfig();
+ const config = useConfig();
+
const { id } = useParams();
const [forks, setForks] = useState();
const history = useHistory();
@@ -85,8 +87,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
InstancesAPI.readDetail(id),
InstancesAPI.readInstanceGroup(id),
]);
-
- if (details.node_type !== 'hop') {
+ if (details.node_type === 'execution') {
const { data: healthCheckData } =
await InstancesAPI.readHealthCheckDetail(id);
setHealthCheck(healthCheckData);
@@ -115,15 +116,9 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
setBreadcrumb(instance);
}
}, [instance, setBreadcrumb]);
- const {
- error: healthCheckError,
- isLoading: isRunningHealthCheck,
- request: fetchHealthCheck,
- } = useRequest(
+ const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
useCallback(async () => {
const { status } = await InstancesAPI.healthCheck(id);
- const { data } = await InstancesAPI.readHealthCheckDetail(id);
- setHealthCheck(data);
if (status === 200) {
setShowHealthCheckAlert(true);
}
@@ -149,6 +144,18 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
debounceUpdateInstance({ capacity_adjustment: roundedValue });
};
+ const formatHealthCheckTimeStamp = (last) => (
+ <>
+ {formatDateString(last)}
+ {instance.health_check_pending ? (
+ <>
+ {' '}
+ <OutlinedClockIcon />
+ </>
+ ) : null}
+ </>
+ );
+
const buildLinkURL = (inst) =>
inst.is_container_group
? '/instance_groups/container_group/'
@@ -179,6 +186,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
return <ContentLoading />;
}
const isHopNode = instance.node_type === 'hop';
+ const isExecutionNode = instance.node_type === 'execution';
return (
<>
@@ -242,7 +250,22 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
<Detail
label={t`Last Health Check`}
dataCy="last-health-check"
- value={formatDateString(healthCheck?.last_health_check)}
+ helpText={
+ <>
+ {t`Health checks are asynchronous tasks. See the`}{' '}
+ <a
+ href={`${getDocsBaseUrl(
+ config
+ )}/html/administration/instances.html#health-check`}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {t`documentation`}
+ </a>{' '}
+ {t`for more info.`}
+ </>
+ }
+ value={formatHealthCheckTimeStamp(instance.last_health_check)}
/>
{instance.related?.install_bundle && (
<Detail
@@ -280,7 +303,9 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
step={0.1}
value={instance.capacity_adjustment}
onChange={handleChangeValue}
- isDisabled={!me?.is_superuser || !instance.enabled}
+ isDisabled={
+ !config?.me?.is_superuser || !instance.enabled
+ }
data-cy="slider"
/>
</SliderForks>
@@ -324,7 +349,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
</DetailList>
{!isHopNode && (
<CardActionsRow>
- {me.is_superuser && isK8s && instance.node_type === 'execution' && (
+ {config?.me?.is_superuser && isK8s && isExecutionNode && (
<RemoveInstanceButton
dataCy="remove-instance-button"
itemsToRemove={[instance]}
@@ -332,18 +357,24 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
onRemove={removeInstances}
/>
)}
- <Tooltip content={t`Run a health check on the instance`}>
- <Button
- isDisabled={!me.is_superuser || isRunningHealthCheck}
- variant="primary"
- ouiaId="health-check-button"
- onClick={fetchHealthCheck}
- isLoading={isRunningHealthCheck}
- spinnerAriaLabel={t`Running health check`}
- >
- {t`Run health check`}
- </Button>
- </Tooltip>
+ {isExecutionNode && (
+ <Tooltip content={t`Run a health check on the instance`}>
+ <Button
+ isDisabled={
+ !config?.me?.is_superuser || instance.health_check_pending
+ }
+ variant="primary"
+ ouiaId="health-check-button"
+ onClick={fetchHealthCheck}
+ isLoading={instance.health_check_pending}
+ spinnerAriaLabel={t`Running health check`}
+ >
+ {instance.health_check_pending
+ ? t`Running health check`
+ : t`Run health check`}
+ </Button>
+ </Tooltip>
+ )}
<InstanceToggle
css="display: inline-flex;"
fetchInstances={fetchDetails}
diff --git a/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.test.js b/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.test.js
index cc038c6624..baa1ab3e54 100644
--- a/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.test.js
+++ b/awx/ui/src/screens/Instances/InstanceDetail/InstanceDetail.test.js
@@ -49,8 +49,9 @@ describe('<InstanceDetail/>', () => {
mem_capacity: 38,
enabled: true,
managed_by_policy: true,
- node_type: 'hybrid',
+ node_type: 'execution',
node_state: 'ready',
+ health_check_pending: false,
},
});
InstancesAPI.readInstanceGroup.mockResolvedValue({
diff --git a/awx/ui/src/screens/Instances/InstanceList/InstanceList.js b/awx/ui/src/screens/Instances/InstanceList/InstanceList.js
index fdebb58833..ad7d13bb5c 100644
--- a/awx/ui/src/screens/Instances/InstanceList/InstanceList.js
+++ b/awx/ui/src/screens/Instances/InstanceList/InstanceList.js
@@ -37,6 +37,8 @@ function InstanceList() {
const location = useLocation();
const { me } = useConfig();
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
+ const [pendingHealthCheck, setPendingHealthCheck] = useState(false);
+ const [canRunHealthCheck, setCanRunHealthCheck] = useState(true);
const {
result: { instances, count, relatedSearchableKeys, searchableKeys, isK8s },
@@ -51,6 +53,9 @@ function InstanceList() {
InstancesAPI.readOptions(),
SettingsAPI.readCategory('system'),
]);
+ setPendingHealthCheck(
+ response?.data?.result?.some((i) => i.health_check_pending === true)
+ );
return {
instances: response.data.results,
isK8s: sysSettings.data.IS_K8S,
@@ -87,7 +92,7 @@ function InstanceList() {
useCallback(async () => {
const [...response] = await Promise.all(
selected
- .filter(({ node_type }) => node_type !== 'hop')
+ .filter(({ node_type }) => node_type === 'execution')
.map(({ id }) => InstancesAPI.healthCheck(id))
);
if (response) {
@@ -96,6 +101,18 @@ function InstanceList() {
}, [selected])
);
+ useEffect(() => {
+ if (selected) {
+ selected.forEach((i) => {
+ if (i.node_type === 'execution') {
+ setCanRunHealthCheck(true);
+ } else {
+ setCanRunHealthCheck(false);
+ }
+ });
+ }
+ }, [selected]);
+
const handleHealthCheck = async () => {
await fetchHealthCheck();
clearSelected();
@@ -189,6 +206,8 @@ function InstanceList() {
onClick={handleHealthCheck}
key="healthCheck"
selectedItems={selected}
+ healthCheckPending={pendingHealthCheck}
+ isDisabled={!canRunHealthCheck}
/>,
]}
/>
@@ -196,7 +215,7 @@ function InstanceList() {
headerRow={
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
<HeaderCell
- tooltip={t`Cannot run health check on hop nodes.`}
+ tooltip={t`Health checks can only be run on execution nodes.`}
sortKey="hostname"
>{t`Name`}</HeaderCell>
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
diff --git a/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js b/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js
index 4542287b07..db6ad983a0 100644
--- a/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js
+++ b/awx/ui/src/screens/Instances/InstanceList/InstanceList.test.js
@@ -32,7 +32,7 @@ const instances = [
jobs_running: 0,
jobs_total: 68,
cpu: 6,
- node_type: 'control',
+ node_type: 'execution',
node_state: 'ready',
memory: 2087469056,
cpu_capacity: 24,
@@ -52,7 +52,7 @@ const instances = [
jobs_running: 0,
jobs_total: 68,
cpu: 6,
- node_type: 'hybrid',
+ node_type: 'execution',
node_state: 'ready',
memory: 2087469056,
cpu_capacity: 24,
diff --git a/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.js b/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.js
index f01134f266..6b27f3a225 100644
--- a/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.js
+++ b/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.js
@@ -11,7 +11,9 @@ import {
Slider,
Tooltip,
} from '@patternfly/react-core';
+import { OutlinedClockIcon } from '@patternfly/react-icons';
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
+import getDocsBaseUrl from 'util/getDocsBaseUrl';
import { formatDateString } from 'util/dates';
import computeForks from 'util/computeForks';
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
@@ -52,7 +54,7 @@ function InstanceListItem({
fetchInstances,
rowIndex,
}) {
- const { me = {} } = useConfig();
+ const config = useConfig();
const [forks, setForks] = useState(
computeForks(
instance.mem_capacity,
@@ -98,7 +100,21 @@ function InstanceListItem({
);
debounceUpdateInstance({ capacity_adjustment: roundedValue });
};
+
+ const formatHealthCheckTimeStamp = (last) => (
+ <>
+ {formatDateString(last)}
+ {instance.health_check_pending ? (
+ <>
+ {' '}
+ <OutlinedClockIcon />
+ </>
+ ) : null}
+ </>
+ );
+
const isHopNode = instance.node_type === 'hop';
+ const isExecutionNode = instance.node_type === 'execution';
return (
<>
<Tr
@@ -121,7 +137,7 @@ function InstanceListItem({
rowIndex,
isSelected,
onSelect,
- disable: isHopNode,
+ disable: !isExecutionNode,
}}
dataLabel={t`Selected`}
/>
@@ -164,7 +180,7 @@ function InstanceListItem({
step={0.1}
value={instance.capacity_adjustment}
onChange={handleChangeValue}
- isDisabled={!me?.is_superuser || !instance.enabled}
+ isDisabled={!config?.me?.is_superuser || !instance.enabled}
data-cy="slider"
/>
</SliderForks>
@@ -221,7 +237,22 @@ function InstanceListItem({
<Detail
data-cy="last-health-check"
label={t`Last Health Check`}
- value={formatDateString(instance.last_health_check)}
+ helpText={
+ <>
+ {t`Health checks are asynchronous tasks. See the`}{' '}
+ <a
+ href={`${getDocsBaseUrl(
+ config
+ )}/html/administration/instances.html#health-check`}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {t`documentation`}
+ </a>{' '}
+ {t`for more info.`}
+ </>
+ }
+ value={formatHealthCheckTimeStamp(instance.last_health_check)}
/>
</DetailList>
</ExpandableRowContent>
diff --git a/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.test.js b/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.test.js
index 7f5b8359b4..add1ffbe12 100644
--- a/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.test.js
+++ b/awx/ui/src/screens/Instances/InstanceList/InstanceListItem.test.js
@@ -272,9 +272,9 @@ describe('<InstanceListItem/>', () => {
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');
+ expect(wrapper.find('Detail[label="Last Health Check"]').text()).toBe(
+ 'Last Health Check9/15/2021, 6:02:07 PM'
+ );
});
test('Hop should not render some things', async () => {
const onSelect = jest.fn();