mirror of
https://github.com/ansible/awx.git
synced 2026-02-24 14:36:00 -03:30
Merge pull request #12974 from kialam/new-health-check-started
Update UI to support pending health checks.
This commit is contained in:
@@ -3,7 +3,12 @@ import { Plural, t } from '@lingui/macro';
|
|||||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||||
import { useKebabifiedMenu } from 'contexts/Kebabified';
|
import { useKebabifiedMenu } from 'contexts/Kebabified';
|
||||||
|
|
||||||
function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
|
function HealthCheckButton({
|
||||||
|
isDisabled,
|
||||||
|
onClick,
|
||||||
|
selectedItems,
|
||||||
|
healthCheckPending,
|
||||||
|
}) {
|
||||||
const { isKebabified } = useKebabifiedMenu();
|
const { isKebabified } = useKebabifiedMenu();
|
||||||
|
|
||||||
const selectedItemsCount = selectedItems.length;
|
const selectedItemsCount = selectedItems.length;
|
||||||
@@ -42,7 +47,11 @@ function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
ouiaId="health-check"
|
ouiaId="health-check"
|
||||||
onClick={onClick}
|
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>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Slider,
|
Slider,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
import { CaretLeftIcon, OutlinedClockIcon } from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { useConfig } from 'contexts/Config';
|
import { useConfig } from 'contexts/Config';
|
||||||
@@ -23,6 +23,7 @@ import ErrorDetail from 'components/ErrorDetail';
|
|||||||
import DisassociateButton from 'components/DisassociateButton';
|
import DisassociateButton from 'components/DisassociateButton';
|
||||||
import InstanceToggle from 'components/InstanceToggle';
|
import InstanceToggle from 'components/InstanceToggle';
|
||||||
import { CardBody, CardActionsRow } from 'components/Card';
|
import { CardBody, CardActionsRow } from 'components/Card';
|
||||||
|
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import RoutedTabs from 'components/RoutedTabs';
|
import RoutedTabs from 'components/RoutedTabs';
|
||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
@@ -62,7 +63,7 @@ function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
||||||
const { me = {} } = useConfig();
|
const config = useConfig();
|
||||||
const { id, instanceId } = useParams();
|
const { id, instanceId } = useParams();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -115,15 +116,9 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDetails();
|
fetchDetails();
|
||||||
}, [fetchDetails]);
|
}, [fetchDetails]);
|
||||||
const {
|
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
|
||||||
error: healthCheckError,
|
|
||||||
isLoading: isRunningHealthCheck,
|
|
||||||
request: fetchHealthCheck,
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const { status } = await InstancesAPI.healthCheck(instanceId);
|
const { status } = await InstancesAPI.healthCheck(instanceId);
|
||||||
const { data } = await InstancesAPI.readHealthCheckDetail(instanceId);
|
|
||||||
setHealthCheck(data);
|
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
setShowHealthCheckAlert(true);
|
setShowHealthCheckAlert(true);
|
||||||
}
|
}
|
||||||
@@ -161,6 +156,18 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatHealthCheckTimeStamp = (last) => (
|
||||||
|
<>
|
||||||
|
{formatDateString(last)}
|
||||||
|
{instance.health_check_pending ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<OutlinedClockIcon />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const { error, dismissError } = useDismissableError(
|
const { error, dismissError } = useDismissableError(
|
||||||
disassociateError || updateInstanceError || healthCheckError
|
disassociateError || updateInstanceError || healthCheckError
|
||||||
);
|
);
|
||||||
@@ -189,6 +196,8 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isExecutionNode = instance.node_type === 'execution';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RoutedTabs tabsArray={tabsArray} />
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
@@ -218,7 +227,22 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
|
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
|
||||||
<Detail
|
<Detail
|
||||||
label={t`Last Health Check`}
|
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 label={t`Node Type`} value={instance.node_type} />
|
||||||
<Detail
|
<Detail
|
||||||
@@ -237,7 +261,7 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={!me?.is_superuser || !instance.enabled}
|
isDisabled={!config?.me?.is_superuser || !instance.enabled}
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -274,19 +298,25 @@ function InstanceDetails({ setBreadcrumb, instanceGroup }) {
|
|||||||
)}
|
)}
|
||||||
</DetailList>
|
</DetailList>
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
<Tooltip content={t`Run a health check on the instance`}>
|
{isExecutionNode && (
|
||||||
<Button
|
<Tooltip content={t`Run a health check on the instance`}>
|
||||||
isDisabled={!me.is_superuser || isRunningHealthCheck}
|
<Button
|
||||||
variant="primary"
|
isDisabled={
|
||||||
ouiaId="health-check-button"
|
!config?.me?.is_superuser || instance.health_check_pending
|
||||||
onClick={fetchHealthCheck}
|
}
|
||||||
isLoading={isRunningHealthCheck}
|
variant="primary"
|
||||||
spinnerAriaLabel={t`Running health check`}
|
ouiaId="health-check-button"
|
||||||
>
|
onClick={fetchHealthCheck}
|
||||||
{t`Run health check`}
|
isLoading={instance.health_check_pending}
|
||||||
</Button>
|
spinnerAriaLabel={t`Running health check`}
|
||||||
</Tooltip>
|
>
|
||||||
{me.is_superuser && instance.node_type !== 'control' && (
|
{instance.health_check_pending
|
||||||
|
? t`Running health check`
|
||||||
|
: t`Run health check`}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{config?.me?.is_superuser && instance.node_type !== 'control' && (
|
||||||
<DisassociateButton
|
<DisassociateButton
|
||||||
verifyCannotDisassociate={instanceGroup.name === 'controlplane'}
|
verifyCannotDisassociate={instanceGroup.name === 'controlplane'}
|
||||||
key="disassociate"
|
key="disassociate"
|
||||||
|
|||||||
@@ -87,8 +87,9 @@ describe('<InstanceDetails/>', () => {
|
|||||||
mem_capacity: 38,
|
mem_capacity: 38,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
managed_by_policy: true,
|
managed_by_policy: true,
|
||||||
node_type: 'hybrid',
|
node_type: 'execution',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
|
health_check_pending: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InstancesAPI.readHealthCheckDetail.mockResolvedValue({
|
InstancesAPI.readHealthCheckDetail.mockResolvedValue({
|
||||||
@@ -347,6 +348,67 @@ describe('<InstanceDetails/>', () => {
|
|||||||
expect(wrapper.find('ErrorDetail')).toHaveLength(1);
|
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 () => {
|
test('Should call disassociate', async () => {
|
||||||
InstanceGroupsAPI.readInstances.mockResolvedValue({
|
InstanceGroupsAPI.readInstances.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ 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 [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
|
||||||
|
const [pendingHealthCheck, setPendingHealthCheck] = useState(false);
|
||||||
|
const [canRunHealthCheck, setCanRunHealthCheck] = useState(true);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { id: instanceGroupId } = useParams();
|
const { id: instanceGroupId } = useParams();
|
||||||
|
|
||||||
@@ -56,6 +58,10 @@ function InstanceList({ instanceGroup }) {
|
|||||||
InstanceGroupsAPI.readInstances(instanceGroupId, params),
|
InstanceGroupsAPI.readInstances(instanceGroupId, params),
|
||||||
InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
|
InstanceGroupsAPI.readInstanceOptions(instanceGroupId),
|
||||||
]);
|
]);
|
||||||
|
setPendingHealthCheck(
|
||||||
|
response?.data?.result?.some((i) => i.health_check_pending === true)
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
instances: response.data.results,
|
instances: response.data.results,
|
||||||
count: response.data.count,
|
count: response.data.count,
|
||||||
@@ -90,7 +96,7 @@ function InstanceList({ instanceGroup }) {
|
|||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [...response] = await Promise.all(
|
const [...response] = await Promise.all(
|
||||||
selected
|
selected
|
||||||
.filter(({ node_type }) => node_type !== 'hop')
|
.filter(({ node_type }) => node_type === 'execution')
|
||||||
.map(({ id }) => InstancesAPI.healthCheck(id))
|
.map(({ id }) => InstancesAPI.healthCheck(id))
|
||||||
);
|
);
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -99,6 +105,18 @@ function InstanceList({ instanceGroup }) {
|
|||||||
}, [selected])
|
}, [selected])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected) {
|
||||||
|
selected.forEach((i) => {
|
||||||
|
if (i.node_type === 'execution') {
|
||||||
|
setCanRunHealthCheck(true);
|
||||||
|
} else {
|
||||||
|
setCanRunHealthCheck(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
const handleHealthCheck = async () => {
|
const handleHealthCheck = async () => {
|
||||||
await fetchHealthCheck();
|
await fetchHealthCheck();
|
||||||
clearSelected();
|
clearSelected();
|
||||||
@@ -246,9 +264,10 @@ function InstanceList({ instanceGroup }) {
|
|||||||
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
|
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
|
||||||
/>,
|
/>,
|
||||||
<HealthCheckButton
|
<HealthCheckButton
|
||||||
isDisabled={!canAdd}
|
isDisabled={!canAdd || !canRunHealthCheck}
|
||||||
onClick={handleHealthCheck}
|
onClick={handleHealthCheck}
|
||||||
selectedItems={selected}
|
selectedItems={selected}
|
||||||
|
healthCheckPending={pendingHealthCheck}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
emptyStateControls={
|
emptyStateControls={
|
||||||
@@ -263,7 +282,10 @@ function InstanceList({ instanceGroup }) {
|
|||||||
)}
|
)}
|
||||||
headerRow={
|
headerRow={
|
||||||
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
<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="errors">{t`Status`}</HeaderCell>
|
||||||
<HeaderCell sortKey="node_type">{t`Node Type`}</HeaderCell>
|
<HeaderCell sortKey="node_type">{t`Node Type`}</HeaderCell>
|
||||||
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
|
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ describe('<InstanceList/>', () => {
|
|||||||
await act(async () =>
|
await act(async () =>
|
||||||
wrapper.find('Button[ouiaId="health-check"]').prop('onClick')()
|
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 () => {
|
test('should render health check error', async () => {
|
||||||
InstancesAPI.healthCheck.mockRejectedValue(
|
InstancesAPI.healthCheck.mockRejectedValue(
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import { OutlinedClockIcon } from '@patternfly/react-icons';
|
||||||
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||||
|
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
||||||
import InstanceToggle from 'components/InstanceToggle';
|
import InstanceToggle from 'components/InstanceToggle';
|
||||||
@@ -52,7 +54,7 @@ function InstanceListItem({
|
|||||||
fetchInstances,
|
fetchInstances,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
}) {
|
}) {
|
||||||
const { me = {} } = useConfig();
|
const config = useConfig();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [forks, setForks] = useState(
|
const [forks, setForks] = useState(
|
||||||
computeForks(
|
computeForks(
|
||||||
@@ -100,6 +102,18 @@ function InstanceListItem({
|
|||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatHealthCheckTimeStamp = (last) => (
|
||||||
|
<>
|
||||||
|
{formatDateString(last)}
|
||||||
|
{instance.health_check_pending ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<OutlinedClockIcon />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
@@ -154,7 +168,7 @@ function InstanceListItem({
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={!me?.is_superuser || !instance.enabled}
|
isDisabled={!config?.me?.is_superuser || !instance.enabled}
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -206,7 +220,22 @@ function InstanceListItem({
|
|||||||
<Detail
|
<Detail
|
||||||
data-cy="last-health-check"
|
data-cy="last-health-check"
|
||||||
label={t`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>
|
</DetailList>
|
||||||
</ExpandableRowContent>
|
</ExpandableRowContent>
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ describe('<InstanceListItem/>', () => {
|
|||||||
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
||||||
'Auto'
|
'Auto'
|
||||||
);
|
);
|
||||||
expect(
|
expect(wrapper.find('Detail[label="Last Health Check"]').text()).toBe(
|
||||||
wrapper.find('Detail[label="Last Health Check"]').prop('value')
|
'Last Health Check9/15/2021, 6:02:07 PM'
|
||||||
).toBe('9/15/2021, 6:02:07 PM');
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Label,
|
Label,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import { DownloadIcon } from '@patternfly/react-icons';
|
import { DownloadIcon, OutlinedClockIcon } from '@patternfly/react-icons';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { useConfig } from 'contexts/Config';
|
import { useConfig } from 'contexts/Config';
|
||||||
@@ -23,6 +23,7 @@ import AlertModal from 'components/AlertModal';
|
|||||||
import ErrorDetail from 'components/ErrorDetail';
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
import InstanceToggle from 'components/InstanceToggle';
|
import InstanceToggle from 'components/InstanceToggle';
|
||||||
import { CardBody, CardActionsRow } from 'components/Card';
|
import { CardBody, CardActionsRow } from 'components/Card';
|
||||||
|
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
import ContentLoading from 'components/ContentLoading';
|
import ContentLoading from 'components/ContentLoading';
|
||||||
@@ -62,7 +63,8 @@ function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function InstanceDetail({ setBreadcrumb, isK8s }) {
|
function InstanceDetail({ setBreadcrumb, isK8s }) {
|
||||||
const { me = {} } = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const [forks, setForks] = useState();
|
const [forks, setForks] = useState();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -85,8 +87,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
InstancesAPI.readDetail(id),
|
InstancesAPI.readDetail(id),
|
||||||
InstancesAPI.readInstanceGroup(id),
|
InstancesAPI.readInstanceGroup(id),
|
||||||
]);
|
]);
|
||||||
|
if (details.node_type === 'execution') {
|
||||||
if (details.node_type !== 'hop') {
|
|
||||||
const { data: healthCheckData } =
|
const { data: healthCheckData } =
|
||||||
await InstancesAPI.readHealthCheckDetail(id);
|
await InstancesAPI.readHealthCheckDetail(id);
|
||||||
setHealthCheck(healthCheckData);
|
setHealthCheck(healthCheckData);
|
||||||
@@ -115,15 +116,9 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
setBreadcrumb(instance);
|
setBreadcrumb(instance);
|
||||||
}
|
}
|
||||||
}, [instance, setBreadcrumb]);
|
}, [instance, setBreadcrumb]);
|
||||||
const {
|
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
|
||||||
error: healthCheckError,
|
|
||||||
isLoading: isRunningHealthCheck,
|
|
||||||
request: fetchHealthCheck,
|
|
||||||
} = useRequest(
|
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const { status } = await InstancesAPI.healthCheck(id);
|
const { status } = await InstancesAPI.healthCheck(id);
|
||||||
const { data } = await InstancesAPI.readHealthCheckDetail(id);
|
|
||||||
setHealthCheck(data);
|
|
||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
setShowHealthCheckAlert(true);
|
setShowHealthCheckAlert(true);
|
||||||
}
|
}
|
||||||
@@ -149,6 +144,18 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatHealthCheckTimeStamp = (last) => (
|
||||||
|
<>
|
||||||
|
{formatDateString(last)}
|
||||||
|
{instance.health_check_pending ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<OutlinedClockIcon />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const buildLinkURL = (inst) =>
|
const buildLinkURL = (inst) =>
|
||||||
inst.is_container_group
|
inst.is_container_group
|
||||||
? '/instance_groups/container_group/'
|
? '/instance_groups/container_group/'
|
||||||
@@ -179,6 +186,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
const isHopNode = instance.node_type === 'hop';
|
const isHopNode = instance.node_type === 'hop';
|
||||||
|
const isExecutionNode = instance.node_type === 'execution';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -242,7 +250,22 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
<Detail
|
<Detail
|
||||||
label={t`Last Health Check`}
|
label={t`Last Health Check`}
|
||||||
dataCy="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 && (
|
{instance.related?.install_bundle && (
|
||||||
<Detail
|
<Detail
|
||||||
@@ -280,7 +303,9 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={!me?.is_superuser || !instance.enabled}
|
isDisabled={
|
||||||
|
!config?.me?.is_superuser || !instance.enabled
|
||||||
|
}
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -324,7 +349,7 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
</DetailList>
|
</DetailList>
|
||||||
{!isHopNode && (
|
{!isHopNode && (
|
||||||
<CardActionsRow>
|
<CardActionsRow>
|
||||||
{me.is_superuser && isK8s && instance.node_type === 'execution' && (
|
{config?.me?.is_superuser && isK8s && isExecutionNode && (
|
||||||
<RemoveInstanceButton
|
<RemoveInstanceButton
|
||||||
dataCy="remove-instance-button"
|
dataCy="remove-instance-button"
|
||||||
itemsToRemove={[instance]}
|
itemsToRemove={[instance]}
|
||||||
@@ -332,18 +357,24 @@ function InstanceDetail({ setBreadcrumb, isK8s }) {
|
|||||||
onRemove={removeInstances}
|
onRemove={removeInstances}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Tooltip content={t`Run a health check on the instance`}>
|
{isExecutionNode && (
|
||||||
<Button
|
<Tooltip content={t`Run a health check on the instance`}>
|
||||||
isDisabled={!me.is_superuser || isRunningHealthCheck}
|
<Button
|
||||||
variant="primary"
|
isDisabled={
|
||||||
ouiaId="health-check-button"
|
!config?.me?.is_superuser || instance.health_check_pending
|
||||||
onClick={fetchHealthCheck}
|
}
|
||||||
isLoading={isRunningHealthCheck}
|
variant="primary"
|
||||||
spinnerAriaLabel={t`Running health check`}
|
ouiaId="health-check-button"
|
||||||
>
|
onClick={fetchHealthCheck}
|
||||||
{t`Run health check`}
|
isLoading={instance.health_check_pending}
|
||||||
</Button>
|
spinnerAriaLabel={t`Running health check`}
|
||||||
</Tooltip>
|
>
|
||||||
|
{instance.health_check_pending
|
||||||
|
? t`Running health check`
|
||||||
|
: t`Run health check`}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<InstanceToggle
|
<InstanceToggle
|
||||||
css="display: inline-flex;"
|
css="display: inline-flex;"
|
||||||
fetchInstances={fetchDetails}
|
fetchInstances={fetchDetails}
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ describe('<InstanceDetail/>', () => {
|
|||||||
mem_capacity: 38,
|
mem_capacity: 38,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
managed_by_policy: true,
|
managed_by_policy: true,
|
||||||
node_type: 'hybrid',
|
node_type: 'execution',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
|
health_check_pending: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
InstancesAPI.readInstanceGroup.mockResolvedValue({
|
InstancesAPI.readInstanceGroup.mockResolvedValue({
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ function InstanceList() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { me } = useConfig();
|
const { me } = useConfig();
|
||||||
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
|
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
|
||||||
|
const [pendingHealthCheck, setPendingHealthCheck] = useState(false);
|
||||||
|
const [canRunHealthCheck, setCanRunHealthCheck] = useState(true);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { instances, count, relatedSearchableKeys, searchableKeys, isK8s },
|
result: { instances, count, relatedSearchableKeys, searchableKeys, isK8s },
|
||||||
@@ -51,6 +53,9 @@ function InstanceList() {
|
|||||||
InstancesAPI.readOptions(),
|
InstancesAPI.readOptions(),
|
||||||
SettingsAPI.readCategory('system'),
|
SettingsAPI.readCategory('system'),
|
||||||
]);
|
]);
|
||||||
|
setPendingHealthCheck(
|
||||||
|
response?.data?.result?.some((i) => i.health_check_pending === true)
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
instances: response.data.results,
|
instances: response.data.results,
|
||||||
isK8s: sysSettings.data.IS_K8S,
|
isK8s: sysSettings.data.IS_K8S,
|
||||||
@@ -87,7 +92,7 @@ function InstanceList() {
|
|||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [...response] = await Promise.all(
|
const [...response] = await Promise.all(
|
||||||
selected
|
selected
|
||||||
.filter(({ node_type }) => node_type !== 'hop')
|
.filter(({ node_type }) => node_type === 'execution')
|
||||||
.map(({ id }) => InstancesAPI.healthCheck(id))
|
.map(({ id }) => InstancesAPI.healthCheck(id))
|
||||||
);
|
);
|
||||||
if (response) {
|
if (response) {
|
||||||
@@ -96,6 +101,18 @@ function InstanceList() {
|
|||||||
}, [selected])
|
}, [selected])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected) {
|
||||||
|
selected.forEach((i) => {
|
||||||
|
if (i.node_type === 'execution') {
|
||||||
|
setCanRunHealthCheck(true);
|
||||||
|
} else {
|
||||||
|
setCanRunHealthCheck(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
const handleHealthCheck = async () => {
|
const handleHealthCheck = async () => {
|
||||||
await fetchHealthCheck();
|
await fetchHealthCheck();
|
||||||
clearSelected();
|
clearSelected();
|
||||||
@@ -189,6 +206,8 @@ function InstanceList() {
|
|||||||
onClick={handleHealthCheck}
|
onClick={handleHealthCheck}
|
||||||
key="healthCheck"
|
key="healthCheck"
|
||||||
selectedItems={selected}
|
selectedItems={selected}
|
||||||
|
healthCheckPending={pendingHealthCheck}
|
||||||
|
isDisabled={!canRunHealthCheck}
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -196,7 +215,7 @@ function InstanceList() {
|
|||||||
headerRow={
|
headerRow={
|
||||||
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
|
||||||
<HeaderCell
|
<HeaderCell
|
||||||
tooltip={t`Cannot run health check on hop nodes.`}
|
tooltip={t`Health checks can only be run on execution nodes.`}
|
||||||
sortKey="hostname"
|
sortKey="hostname"
|
||||||
>{t`Name`}</HeaderCell>
|
>{t`Name`}</HeaderCell>
|
||||||
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
|
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const instances = [
|
|||||||
jobs_running: 0,
|
jobs_running: 0,
|
||||||
jobs_total: 68,
|
jobs_total: 68,
|
||||||
cpu: 6,
|
cpu: 6,
|
||||||
node_type: 'control',
|
node_type: 'execution',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
memory: 2087469056,
|
memory: 2087469056,
|
||||||
cpu_capacity: 24,
|
cpu_capacity: 24,
|
||||||
@@ -52,7 +52,7 @@ const instances = [
|
|||||||
jobs_running: 0,
|
jobs_running: 0,
|
||||||
jobs_total: 68,
|
jobs_total: 68,
|
||||||
cpu: 6,
|
cpu: 6,
|
||||||
node_type: 'hybrid',
|
node_type: 'execution',
|
||||||
node_state: 'ready',
|
node_state: 'ready',
|
||||||
memory: 2087469056,
|
memory: 2087469056,
|
||||||
cpu_capacity: 24,
|
cpu_capacity: 24,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
|
import { OutlinedClockIcon } from '@patternfly/react-icons';
|
||||||
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||||
|
import getDocsBaseUrl from 'util/getDocsBaseUrl';
|
||||||
import { formatDateString } from 'util/dates';
|
import { formatDateString } from 'util/dates';
|
||||||
import computeForks from 'util/computeForks';
|
import computeForks from 'util/computeForks';
|
||||||
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
|
||||||
@@ -52,7 +54,7 @@ function InstanceListItem({
|
|||||||
fetchInstances,
|
fetchInstances,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
}) {
|
}) {
|
||||||
const { me = {} } = useConfig();
|
const config = useConfig();
|
||||||
const [forks, setForks] = useState(
|
const [forks, setForks] = useState(
|
||||||
computeForks(
|
computeForks(
|
||||||
instance.mem_capacity,
|
instance.mem_capacity,
|
||||||
@@ -98,7 +100,21 @@ function InstanceListItem({
|
|||||||
);
|
);
|
||||||
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
debounceUpdateInstance({ capacity_adjustment: roundedValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatHealthCheckTimeStamp = (last) => (
|
||||||
|
<>
|
||||||
|
{formatDateString(last)}
|
||||||
|
{instance.health_check_pending ? (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<OutlinedClockIcon />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const isHopNode = instance.node_type === 'hop';
|
const isHopNode = instance.node_type === 'hop';
|
||||||
|
const isExecutionNode = instance.node_type === 'execution';
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
@@ -121,7 +137,7 @@ function InstanceListItem({
|
|||||||
rowIndex,
|
rowIndex,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
disable: isHopNode,
|
disable: !isExecutionNode,
|
||||||
}}
|
}}
|
||||||
dataLabel={t`Selected`}
|
dataLabel={t`Selected`}
|
||||||
/>
|
/>
|
||||||
@@ -164,7 +180,7 @@ function InstanceListItem({
|
|||||||
step={0.1}
|
step={0.1}
|
||||||
value={instance.capacity_adjustment}
|
value={instance.capacity_adjustment}
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
isDisabled={!me?.is_superuser || !instance.enabled}
|
isDisabled={!config?.me?.is_superuser || !instance.enabled}
|
||||||
data-cy="slider"
|
data-cy="slider"
|
||||||
/>
|
/>
|
||||||
</SliderForks>
|
</SliderForks>
|
||||||
@@ -221,7 +237,22 @@ function InstanceListItem({
|
|||||||
<Detail
|
<Detail
|
||||||
data-cy="last-health-check"
|
data-cy="last-health-check"
|
||||||
label={t`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>
|
</DetailList>
|
||||||
</ExpandableRowContent>
|
</ExpandableRowContent>
|
||||||
|
|||||||
@@ -272,9 +272,9 @@ describe('<InstanceListItem/>', () => {
|
|||||||
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
expect(wrapper.find('Detail[label="Policy Type"]').prop('value')).toBe(
|
||||||
'Auto'
|
'Auto'
|
||||||
);
|
);
|
||||||
expect(
|
expect(wrapper.find('Detail[label="Last Health Check"]').text()).toBe(
|
||||||
wrapper.find('Detail[label="Last Health Check"]').prop('value')
|
'Last Health Check9/15/2021, 6:02:07 PM'
|
||||||
).toBe('9/15/2021, 6:02:07 PM');
|
);
|
||||||
});
|
});
|
||||||
test('Hop should not render some things', async () => {
|
test('Hop should not render some things', async () => {
|
||||||
const onSelect = jest.fn();
|
const onSelect = jest.fn();
|
||||||
|
|||||||
Reference in New Issue
Block a user