Files
awx/awx/ui/src/screens/InstanceGroup/InstanceDetails/InstanceDetails.js

355 lines
11 KiB
JavaScript

import React, { useCallback, useEffect, useState } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { t, Plural } from '@lingui/macro';
import {
Button,
Progress,
ProgressMeasureLocation,
ProgressSize,
CodeBlock,
CodeBlockCode,
Tooltip,
Slider,
} from '@patternfly/react-core';
import { CaretLeftIcon, OutlinedClockIcon } from '@patternfly/react-icons';
import styled from 'styled-components';
import { useConfig } from 'contexts/Config';
import { InstancesAPI, InstanceGroupsAPI } from 'api';
import useDebounce from 'hooks/useDebounce';
import AlertModal from 'components/AlertModal';
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';
import ContentLoading from 'components/ContentLoading';
import { Detail, DetailList } from 'components/DetailList';
import HealthCheckAlert from 'components/HealthCheckAlert';
import StatusLabel from 'components/StatusLabel';
import useRequest, {
useDeleteItems,
useDismissableError,
} from 'hooks/useRequest';
const Unavailable = styled.span`
color: var(--pf-global--danger-color--200);
`;
const SliderHolder = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
`;
const SliderForks = styled.div`
flex-grow: 1;
margin-right: 8px;
margin-left: 8px;
text-align: center;
`;
function computeForks(memCapacity, cpuCapacity, selectedCapacityAdjustment) {
const minCapacity = Math.min(memCapacity, cpuCapacity);
const maxCapacity = Math.max(memCapacity, cpuCapacity);
return Math.floor(
minCapacity + (maxCapacity - minCapacity) * selectedCapacityAdjustment
);
}
function InstanceDetails({ setBreadcrumb, instanceGroup }) {
const config = useConfig();
const { id, instanceId } = useParams();
const history = useHistory();
const [healthCheck, setHealthCheck] = useState({});
const [showHealthCheckAlert, setShowHealthCheckAlert] = useState(false);
const [forks, setForks] = useState();
const {
isLoading,
error: contentError,
request: fetchDetails,
result: { instance },
} = useRequest(
useCallback(async () => {
const {
data: { results },
} = await InstanceGroupsAPI.readInstances(instanceGroup.id);
let instanceDetails;
const isAssociated = results.some(
({ id: instId }) => instId === parseInt(instanceId, 10)
);
if (isAssociated) {
const [{ data: details }, { data: healthCheckData }] =
await Promise.all([
InstancesAPI.readDetail(instanceId),
InstancesAPI.readHealthCheckDetail(instanceId),
]);
instanceDetails = details;
setHealthCheck(healthCheckData);
} else {
throw new Error(
`This instance is not associated with this instance group`
);
}
setBreadcrumb(instanceGroup, instanceDetails);
setForks(
computeForks(
instanceDetails.mem_capacity,
instanceDetails.cpu_capacity,
instanceDetails.capacity_adjustment
)
);
return { instance: instanceDetails };
}, [instanceId, setBreadcrumb, instanceGroup]),
{ instance: {}, isLoading: true }
);
useEffect(() => {
fetchDetails();
}, [fetchDetails]);
const { error: healthCheckError, request: fetchHealthCheck } = useRequest(
useCallback(async () => {
const { status } = await InstancesAPI.healthCheck(instanceId);
if (status === 200) {
setShowHealthCheckAlert(true);
}
}, [instanceId])
);
const {
deleteItems: disassociateInstance,
deletionError: disassociateError,
} = useDeleteItems(
useCallback(async () => {
await InstanceGroupsAPI.disassociateInstance(
instanceGroup.id,
instance.id
);
history.push(`/instance_groups/${instanceGroup.id}/instances`);
}, [instanceGroup.id, instance.id, history])
);
const { error: updateInstanceError, request: updateInstance } = useRequest(
useCallback(
async (values) => {
await InstancesAPI.update(instance.id, values);
},
[instance]
)
);
const debounceUpdateInstance = useDebounce(updateInstance, 200);
const handleChangeValue = (value) => {
const roundedValue = Math.round(value * 100) / 100;
setForks(
computeForks(instance.mem_capacity, instance.cpu_capacity, roundedValue)
);
debounceUpdateInstance({ capacity_adjustment: roundedValue });
};
const formatHealthCheckTimeStamp = (last) => (
<>
{formatDateString(last)}
{instance.health_check_pending ? (
<>
{' '}
<OutlinedClockIcon />
</>
) : null}
</>
);
const { error, dismissError } = useDismissableError(
disassociateError || updateInstanceError || healthCheckError
);
const tabsArray = [
{
name: (
<>
<CaretLeftIcon />
{t`Back to Instances`}
</>
),
link: `/instance_groups/${id}/instances`,
id: 99,
},
{
name: t`Details`,
link: `/instance_groups/${id}/instances/${instanceId}/details`,
id: 0,
},
];
if (contentError) {
return <ContentError error={contentError} />;
}
if (isLoading) {
return <ContentLoading />;
}
const isExecutionNode = instance.node_type === 'execution';
return (
<>
<RoutedTabs tabsArray={tabsArray} />
{showHealthCheckAlert ? (
<HealthCheckAlert onSetHealthCheckAlert={setShowHealthCheckAlert} />
) : null}
<CardBody>
<DetailList gutter="sm">
<Detail
label={t`Host Name`}
value={instance.hostname}
dataCy="instance-detail-name"
/>
<Detail
label={t`Status`}
value={
instance.node_state ? (
<StatusLabel status={instance.node_state} />
) : null
}
/>
<Detail
label={t`Policy Type`}
value={instance.managed_by_policy ? t`Auto` : t`Manual`}
/>
<Detail label={t`Running Jobs`} value={instance.jobs_running} />
<Detail label={t`Total Jobs`} value={instance.jobs_total} />
<Detail
label={t`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`Capacity Adjustment`}
value={
<SliderHolder data-cy="slider-holder">
<div data-cy="cpu-capacity">{t`CPU ${instance.cpu_capacity}`}</div>
<SliderForks data-cy="slider-forks">
<div data-cy="number-forks">
<Plural value={forks} one="# fork" other="# forks" />
</div>
<Slider
areCustomStepsContinuous
max={1}
min={0}
step={0.1}
value={instance.capacity_adjustment}
onChange={handleChangeValue}
isDisabled={!config?.me?.is_superuser || !instance.enabled}
data-cy="slider"
/>
</SliderForks>
<div data-cy="mem-capacity">{t`RAM ${instance.mem_capacity}`}</div>
</SliderHolder>
}
/>
<Detail
label={t`Used Capacity`}
value={
instance.enabled ? (
<Progress
title={t`Used capacity`}
value={Math.round(100 - instance.percent_capacity_remaining)}
measureLocation={ProgressMeasureLocation.top}
size={ProgressSize.sm}
aria-label={t`Used capacity`}
/>
) : (
<Unavailable>{t`Unavailable`}</Unavailable>
)
}
/>
{healthCheck?.errors && (
<Detail
fullWidth
label={t`Errors`}
value={
<CodeBlock>
<CodeBlockCode>{healthCheck?.errors}</CodeBlockCode>
</CodeBlock>
}
/>
)}
</DetailList>
<CardActionsRow>
{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"
onDisassociate={disassociateInstance}
itemsToDisassociate={[instance]}
isProtectedInstanceGroup={instanceGroup.name === 'controlplane'}
modalTitle={t`Disassociate instance from instance group?`}
/>
)}
<InstanceToggle
css="display: inline-flex;"
fetchInstances={fetchDetails}
instance={instance}
/>
</CardActionsRow>
{error && (
<AlertModal
isOpen={error}
onClose={dismissError}
title={t`Error!`}
variant="error"
>
{updateInstanceError
? t`Failed to update capacity adjustment.`
: t`Failed to disassociate one or more instances.`}
<ErrorDetail error={error} />
</AlertModal>
)}
</CardBody>
</>
);
}
export default InstanceDetails;