diff options
author | Sarah Akus <sakus@redhat.com> | 2022-10-12 17:37:57 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-12 17:37:57 +0200 |
commit | a2be320605d02a3cd3ca14efeba716da6a8cb267 (patch) | |
tree | 8483883fb3db18882794796cc3d43c7b271d983f | |
parent | Add developer documentation for project signing work (diff) | |
parent | Link out to docs; use `some` in place of `forEach` when looping through results. (diff) | |
download | awx-a2be320605d02a3cd3ca14efeba716da6a8cb267.tar.xz awx-a2be320605d02a3cd3ca14efeba716da6a8cb267.zip |
Merge pull request #12974 from kialam/new-health-check-started
Update UI to support pending health checks.
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(); |