Refactor the health check button

This commit is contained in:
Alex Corey 2022-02-09 11:23:50 -05:00
parent 14a99a7b9e
commit e2e80313ac
17 changed files with 3348 additions and 3190 deletions

View File

@ -5,37 +5,26 @@ import { useKebabifiedMenu } from 'contexts/Kebabified';
function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
const { isKebabified } = useKebabifiedMenu();
const hopNodeSelected =
selectedItems.filter((instance) => instance.node_type === 'hop').length > 0;
const hasSelectedItems = selectedItems.length > 0;
const buildTooltip = () => {
if (hopNodeSelected) {
return (
<Plural
value={hopNodeSelected}
one="Cannot run health check on a hop node. Deselect the hop node to run a health check."
other="Cannot run health check on hop nodes. Deselect the hop nodes to run health checks."
/>
);
}
return selectedItems.length ? (
const selectedItemsCount = selectedItems.length;
const buildTooltip = () =>
selectedItemsCount ? (
<Plural
value={selectedItems.length}
value={selectedItemsCount}
one="Click to run a health check on the selected instance."
other="Click to run a health check on the selected instances."
/>
) : (
t`Select an instance to run a health check.`
);
};
if (isKebabified) {
return (
<Tooltip data-cy="healthCheckTooltip" content={buildTooltip()}>
<DropdownItem
key="approve"
isDisabled={hopNodeSelected || isDisabled || !hasSelectedItems}
isDisabled={isDisabled || !selectedItemsCount}
component="button"
onClick={onClick}
ouiaId="health-check"
@ -49,7 +38,7 @@ function HealthCheckButton({ isDisabled, onClick, selectedItems }) {
<Tooltip data-cy="healthCheckTooltip" content={buildTooltip()}>
<div>
<Button
isDisabled={hopNodeSelected || isDisabled || !hasSelectedItems}
isDisabled={isDisabled || !selectedItemsCount}
variant="secondary"
ouiaId="health-check"
onClick={onClick}

View File

@ -69,6 +69,7 @@ export function HeaderCell({
idPrefix,
className,
children,
tooltip,
}) {
const sort = sortKey
? {
@ -79,6 +80,11 @@ export function HeaderCell({
: null;
return (
<Th
info={
tooltip && {
popover: <div>{tooltip}</div>,
}
}
id={sortKey ? `${idPrefix}-${sortKey}` : null}
className={className}
sort={sort}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -88,10 +88,14 @@ function InstanceList() {
useCallback(async () => {
await Promise.all(selected.map(({ id }) => InstancesAPI.healthCheck(id)));
fetchInstances();
clearSelected();
}, [selected, clearSelected, fetchInstances])
}, [selected, fetchInstances])
);
const handleHealthCheck = async () => {
await fetchHealthCheck();
clearSelected();
};
const {
isLoading: isDisassociateLoading,
deleteItems: disassociateInstances,
@ -231,7 +235,7 @@ function InstanceList() {
/>,
<HealthCheckButton
isDisabled={!canAdd}
onClick={fetchHealthCheck}
onClick={handleHealthCheck}
selectedItems={selected}
/>,
]}
@ -250,7 +254,7 @@ function InstanceList() {
<HeaderCell sortKey="hostname">{t`Name`}</HeaderCell>
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
<HeaderCell sortKey="node_type">{t`Node Type`}</HeaderCell>
<HeaderCell sortKey="capacity_adjustment">{t`Capacity Adjustment`}</HeaderCell>
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
<HeaderCell>{t`Used Capacity`}</HeaderCell>
<HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow>

View File

@ -19,6 +19,7 @@ import StatusLabel from 'components/StatusLabel';
import { Instance } from 'types';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import useDebounce from 'hooks/useDebounce';
import computeForks from 'util/computeForks';
import { InstancesAPI } from 'api';
import { useConfig } from 'contexts/Config';
import AlertModal from 'components/AlertModal';
@ -42,15 +43,6 @@ const SliderForks = styled.div`
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 InstanceListItem({
instance,
isExpanded,
@ -196,13 +188,23 @@ function InstanceListItem({
<Td colSpan={7}>
<ExpandableRowContent>
<DetailList>
<Detail value={instance.jobs_running} label={t`Running Jobs`} />
<Detail value={instance.jobs_total} label={t`Total Jobs`} />
<Detail
data-cy="running-jobs"
value={instance.jobs_running}
label={t`Running Jobs`}
/>
<Detail
data-cy="total-jobs"
value={instance.jobs_total}
label={t`Total Jobs`}
/>
<Detail
data-cy="policy-type"
label={t`Policy Type`}
value={instance.managed_by_policy ? t`Auto` : t`Manual`}
/>
<Detail
data-cy="last-health-check"
label={t`Last Health Check`}
value={formatDateString(instance.last_health_check)}
/>

View File

@ -33,7 +33,6 @@ function Instance({ setBreadcrumb }) {
<Route path="/instances/:id/details" key="details">
<InstanceDetail setBreadcrumb={setBreadcrumb} />
</Route>
,
<Route path="*" key="not-found">
<ContentError isNotFound>
{match.params.id && (
@ -49,5 +48,4 @@ function Instance({ setBreadcrumb }) {
);
}
export { Instance as _Instance };
export default Instance;

View File

@ -134,124 +134,122 @@ function InstanceDetail({ setBreadcrumb }) {
}
const isHopNode = instance.node_type === 'hop';
return (
<>
<CardBody>
<DetailList gutter="sm">
<Detail
label={t`Host Name`}
value={instance.hostname}
dataCy="instance-detail-name"
/>
<Detail
label={t`Status`}
value={
<StatusLabel status={healthCheck?.errors ? 'error' : 'healthy'} />
}
/>
<Detail label={t`Node Type`} value={instance.node_type} />
{!isHopNode && (
<>
<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`}
value={formatDateString(healthCheck?.last_health_check)}
/>
<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={!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 && (
<CardBody>
<DetailList gutter="sm">
<Detail
label={t`Host Name`}
value={instance.hostname}
dataCy="instance-detail-name"
/>
<Detail
label={t`Status`}
value={
<StatusLabel status={healthCheck?.errors ? 'error' : 'healthy'} />
}
/>
<Detail label={t`Node Type`} value={instance.node_type} />
{!isHopNode && (
<>
<Detail
fullWidth
label={t`Errors`}
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`}
value={formatDateString(healthCheck?.last_health_check)}
/>
<Detail
label={t`Capacity Adjustment`}
value={
<CodeBlock>
<CodeBlockCode>{healthCheck?.errors}</CodeBlockCode>
</CodeBlock>
<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={!me?.is_superuser || !instance.enabled}
data-cy="slider"
/>
</SliderForks>
<div data-cy="mem-capacity">{t`RAM ${instance.mem_capacity}`}</div>
</SliderHolder>
}
/>
)}
</DetailList>
{!isHopNode && (
<CardActionsRow>
<Tooltip content={t`Run a health check on the instance`}>
<Button
isDisabled={!me.is_superuser}
variant="primary"
ouiaId="health-check-button"
onClick={fetchHealthCheck}
>
{t`Health Check`}
</Button>
</Tooltip>
<InstanceToggle
css="display: inline-flex;"
fetchInstances={fetchDetails}
instance={instance}
<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>
)
}
/>
</CardActionsRow>
</>
)}
{healthCheck?.errors && (
<Detail
fullWidth
label={t`Errors`}
value={
<CodeBlock>
<CodeBlockCode>{healthCheck?.errors}</CodeBlockCode>
</CodeBlock>
}
/>
)}
</DetailList>
{!isHopNode && (
<CardActionsRow>
<Tooltip content={t`Run a health check on the instance`}>
<Button
isDisabled={!me.is_superuser}
variant="primary"
ouiaId="health-check-button"
onClick={fetchHealthCheck}
>
{t`Health Check`}
</Button>
</Tooltip>
<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>
</>
{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>
);
}

View File

@ -61,7 +61,7 @@ function InstanceList() {
);
const { selected, isAllSelected, handleSelect, clearSelected, selectAll } =
useSelected(instances);
useSelected(instances.filter((i) => i.node_type !== 'hop'));
useEffect(() => {
fetchInstances();
@ -79,15 +79,16 @@ function InstanceList() {
.map(({ id }) => InstancesAPI.healthCheck(id))
);
fetchInstances();
clearSelected();
}, [selected, clearSelected, fetchInstances])
}, [selected, fetchInstances])
);
const handleHealthCheck = async () => {
await fetchHealthCheck();
clearSelected();
};
const { error, dismissError } = useDismissableError(healthCheckError);
const { expanded, isAllExpanded, handleExpand, expandAll } =
useExpanded(instances);
return (
<>
<PageSection>
@ -135,7 +136,7 @@ function InstanceList() {
qsConfig={QS_CONFIG}
additionalControls={[
<HealthCheckButton
onClick={fetchHealthCheck}
onClick={handleHealthCheck}
selectedItems={selected}
/>,
]}
@ -143,10 +144,13 @@ function InstanceList() {
)}
headerRow={
<HeaderRow qsConfig={QS_CONFIG} isExpandable>
<HeaderCell sortKey="hostname">{t`Name`}</HeaderCell>
<HeaderCell
tooltip={t`Cannot run health check on hop nodes.`}
sortKey="hostname"
>{t`Name`}</HeaderCell>
<HeaderCell sortKey="errors">{t`Status`}</HeaderCell>
<HeaderCell sortKey="node_type">{t`Node Type`}</HeaderCell>
<HeaderCell sortKey="capacity_adjustment">{t`Capacity Adjustment`}</HeaderCell>
<HeaderCell>{t`Capacity Adjustment`}</HeaderCell>
<HeaderCell>{t`Used Capacity`}</HeaderCell>
<HeaderCell>{t`Actions`}</HeaderCell>
</HeaderRow>

View File

@ -152,13 +152,6 @@ describe('<InstanceList/>', () => {
wrapper.find('DataListToolbar').prop('onSelectAll')(instances)
);
wrapper.update();
// Ensures health check button is disabled because a hop node is among
// the selected.
expect(
wrapper.find('Button[ouiaId="health-check"]').prop('isDisabled')
).toBe(true);
await act(async () =>
wrapper.find('input[aria-label="Select row 3"]').prop('onChange')(false)
);
@ -197,17 +190,4 @@ describe('<InstanceList/>', () => {
wrapper.update();
expect(wrapper.find('AlertModal')).toHaveLength(1);
});
test('Health check button should remain disabled', async () => {
await act(async () =>
wrapper.find('input[aria-label="Select row 3"]').prop('onChange')(true)
);
wrapper.update();
expect(
wrapper.find('Button[ouiaId="health-check"]').prop('isDisabled')
).toBe(true);
expect(wrapper.find('Tooltip[data-cy="healthCheckTooltip"]').length).toBe(
1
);
});
});

View File

@ -13,6 +13,7 @@ import {
} from '@patternfly/react-core';
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
import { formatDateString } from 'util/dates';
import computeForks from 'util/computeForks';
import { ActionsTd, ActionItem } from 'components/PaginatedTable';
import InstanceToggle from 'components/InstanceToggle';
import StatusLabel from 'components/StatusLabel';
@ -42,15 +43,6 @@ const SliderForks = styled.div`
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 InstanceListItem({
instance,
isExpanded,
@ -120,7 +112,6 @@ function InstanceListItem({
expand={{
rowIndex,
isExpanded,
disabled: isHopNode,
onToggle: onExpand,
}}
/>
@ -130,6 +121,7 @@ function InstanceListItem({
rowIndex,
isSelected,
onSelect,
disable: isHopNode,
}}
dataLabel={t`Selected`}
/>
@ -138,21 +130,23 @@ function InstanceListItem({
<b>{instance.hostname}</b>
</Link>
</Td>
{!instance.node_type !== 'hop' && (
<Td dataLabel={t`Status`}>
<Tooltip
content={
<div>
{t`Last Health Check`}
&nbsp;
{formatDateString(instance.last_health_check)}
</div>
}
>
<StatusLabel status={instance.errors ? 'error' : 'healthy'} />
</Tooltip>
</Td>
)}
<Td dataLabel={t`Status`}>
<Tooltip
content={
<div>
{t`Last Health Check`}
&nbsp;
{formatDateString(
instance.last_health_check ?? instance.last_seen
)}
</div>
}
>
<StatusLabel status={instance.errors ? 'error' : 'healthy'} />
</Tooltip>
</Td>
<Td dataLabel={t`Node Type`}>{instance.node_type}</Td>
{!isHopNode && (
<>
@ -209,13 +203,23 @@ function InstanceListItem({
<Td colSpan={7}>
<ExpandableRowContent>
<DetailList>
<Detail value={instance.jobs_running} label={t`Running Jobs`} />
<Detail value={instance.jobs_total} label={t`Total Jobs`} />
<Detail
data-cy="running-jobs"
value={instance.jobs_running}
label={t`Running Jobs`}
/>
<Detail
data-cy="total-jobs"
value={instance.jobs_total}
label={t`Total Jobs`}
/>
<Detail
data-cy="policy-type"
label={t`Policy Type`}
value={instance.managed_by_policy ? t`Auto` : t`Manual`}
/>
<Detail
data-cy="last-health-check"
label={t`Last Health Check`}
value={formatDateString(instance.last_health_check)}
/>

View File

@ -0,0 +1,12 @@
export default function computeForks(
memCapacity,
cpuCapacity,
selectedCapacityAdjustment
) {
const minCapacity = Math.min(memCapacity, cpuCapacity);
const maxCapacity = Math.max(memCapacity, cpuCapacity);
return Math.floor(
minCapacity + (maxCapacity - minCapacity) * selectedCapacityAdjustment
);
}