Add new node details; update legend.

This commit is contained in:
Kia Lam 2022-08-21 20:10:40 -07:00 committed by Jeff Bradberry
parent 7e627e1d1e
commit 89a6162dcd
3 changed files with 447 additions and 79 deletions

View File

@ -14,8 +14,12 @@ import {
} from '@patternfly/react-core';
import {
ExclamationIcon as PFExclamationIcon,
CheckIcon as PFCheckIcon,
ExclamationIcon,
CheckIcon,
OutlinedClockIcon,
PlusIcon,
MinusIcon,
ResourcesEmptyIcon,
} from '@patternfly/react-icons';
const Wrapper = styled.div`
@ -27,23 +31,20 @@ const Wrapper = styled.div`
background-color: rgba(255, 255, 255, 0.85);
`;
const Button = styled(PFButton)`
width: 20px;
height: 20px;
border-radius: 10px;
padding: 0;
font-size: 11px;
&&& {
width: 20px;
height: 20px;
border-radius: 10px;
padding: 0;
font-size: 11px;
background-color: white;
border: 1px solid #ccc;
color: black;
}
`;
const DescriptionListDescription = styled(PFDescriptionListDescription)`
font-size: 11px;
`;
const ExclamationIcon = styled(PFExclamationIcon)`
fill: white;
margin-left: 2px;
`;
const CheckIcon = styled(PFCheckIcon)`
fill: white;
margin-left: 2px;
`;
const DescriptionList = styled(PFDescriptionList)`
gap: 7px;
`;
@ -70,9 +71,7 @@ function Legend() {
<DescriptionList isHorizontal isFluid>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="primary" isSmall>
{t`C`}
</Button>
<Button isSmall>{t`C`}</Button>
</DescriptionListTerm>
<DescriptionListDescription>{t`Control node`}</DescriptionListDescription>
</DescriptionListGroup>
@ -110,27 +109,133 @@ function Legend() {
<DescriptionListGroup>
<DescriptionListTerm>
<Button
icon={<CheckIcon />}
icon={
<CheckIcon
style={{ fill: 'white', marginLeft: '2px', marginTop: '3px' }}
/>
}
isSmall
style={{ border: '1px solid gray', backgroundColor: '#3E8635' }}
style={{ backgroundColor: '#3E8635' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>{t`Healthy`}</DescriptionListDescription>
<DescriptionListDescription>{t`Ready`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="danger" icon={<ExclamationIcon />} isSmall />
<Button
icon={
<OutlinedClockIcon
style={{ fill: 'white', marginLeft: '3px', marginTop: '3px' }}
/>
}
isSmall
style={{ backgroundColor: '#0066CC' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>{t`Installed`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button
icon={
<PlusIcon
style={{ fill: 'white', marginLeft: '3px', marginTop: '3px' }}
/>
}
isSmall
style={{ backgroundColor: '#6A6E73' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>{t`Provisioning`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button
icon={
<MinusIcon
style={{ fill: 'white', marginLeft: '3px', marginTop: '3px' }}
/>
}
isSmall
style={{ backgroundColor: '#6A6E73' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>{t`Deprovisioning`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button
icon={
<ResourcesEmptyIcon
style={{ fill: 'white', marginLeft: '3px', marginTop: '3px' }}
/>
}
isSmall
style={{ backgroundColor: '#F0AB00' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>{t`Unavailable`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button
icon={
<ExclamationIcon
style={{ fill: 'white', marginLeft: '3px', marginTop: '3px' }}
/>
}
isSmall
style={{ backgroundColor: '#C9190B' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>{t`Error`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button
isSmall
style={{ border: '1px solid gray', backgroundColor: '#e6e6e6' }}
/>
<svg width="20" height="15" xmlns="http://www.w3.org/2000/svg">
<line
x1="0"
y1="9"
x2="20"
y2="9"
stroke="#666"
strokeWidth="4"
/>
</svg>
</DescriptionListTerm>
<DescriptionListDescription>{t`Disabled`}</DescriptionListDescription>
<DescriptionListDescription>{t`Established`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<svg width="20" height="15" xmlns="http://www.w3.org/2000/svg">
<line
x1="0"
y1="9"
x2="20"
y2="9"
stroke="#666"
strokeWidth="4"
strokeDasharray="6"
/>
</svg>
</DescriptionListTerm>
<DescriptionListDescription>{t`Adding`}</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<svg width="20" height="15" xmlns="http://www.w3.org/2000/svg">
<line
x1="0"
y1="9"
x2="20"
y2="9"
stroke="#C9190B"
strokeWidth="4"
strokeDasharray="6"
/>
</svg>
</DescriptionListTerm>
<DescriptionListDescription>{t`Removing`}</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</Wrapper>

View File

@ -1,8 +1,13 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { t } from '@lingui/macro';
import styled from 'styled-components';
import debounce from 'util/debounce';
import * as d3 from 'd3';
import { InstancesAPI } from 'api';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail';
import Legend from './Legend';
import Tooltip from './Tooltip';
import ContentLoading from './ContentLoading';
@ -38,9 +43,38 @@ const Loader = styled(ContentLoading)`
function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
const [isNodeSelected, setIsNodeSelected] = useState(false);
const [selectedNode, setSelectedNode] = useState(null);
const [nodeDetail, setNodeDetail] = useState(null);
const [simulationProgress, setSimulationProgress] = useState(null);
const history = useHistory();
const {
result: { instance, instanceGroups },
error: fetchError,
isLoading,
request: fetchDetails,
} = useRequest(
useCallback(async () => {
const { data: instanceData } = await InstancesAPI.readDetail(
selectedNode.id
);
const { data: instanceGroupsData } = await InstancesAPI.readInstanceGroup(
selectedNode.id
);
return {
instance: instanceData,
instanceGroups: instanceGroupsData,
};
}, [selectedNode]),
{
result: {},
}
);
const { error: fetchInstanceError, dismissError } =
useDismissableError(fetchError);
useEffect(() => {
fetchDetails();
}, [selectedNode, fetchDetails]);
const draw = () => {
let width;
@ -134,7 +168,9 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
.attr('class', (_, i) => `link-${i}`)
.attr('data-cy', (d) => `${d.source.hostname}-${d.target.hostname}`)
.style('fill', 'none')
.style('stroke', '#ccc')
.style('stroke', (d) =>
d.link_state === 'removing' ? '#C9190B' : '#CCC'
)
.style('stroke-width', '2px')
.style('stroke-dasharray', (d) => renderLinkState(d.link_state))
.attr('pointer-events', 'none')
@ -158,7 +194,6 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
deselectSiblings(d);
})
.on('click', (_, d) => {
setNodeDetail(d);
highlightSelected(d);
});
@ -272,7 +307,9 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
.selectAll(`.link-${s.index}`)
.transition()
.duration(50)
.style('stroke', '#ccc')
.style('stroke', (d) =>
d.link_state === 'removing' ? '#C9190B' : '#CCC'
)
.style('stroke-width', '2px')
.attr('marker-end', 'url(#end)');
});
@ -319,14 +356,33 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
return (
<div id="chart" style={{ position: 'relative', height: '100%' }}>
{showLegend && <Legend />}
<Tooltip
isNodeSelected={isNodeSelected}
renderNodeIcon={renderNodeIcon(selectedNode)}
nodeDetail={nodeDetail}
redirectToDetailsPage={() =>
redirectToDetailsPage(selectedNode, history)
}
/>
{instance && (
<>
{fetchInstanceError && (
<AlertModal
variant="error"
title={t`Error!`}
isOpen
onClose={dismissError}
>
{t`Failed to update instance.`}
<ErrorDetail error={fetchInstanceError} />
</AlertModal>
)}
<Tooltip
isNodeSelected={isNodeSelected}
renderNodeIcon={renderNodeIcon(selectedNode)}
selectedNode={selectedNode}
fetchInstance={fetchDetails}
instanceGroups={instanceGroups}
instanceDetail={instance}
isLoading={isLoading}
redirectToDetailsPage={() =>
redirectToDetailsPage(selectedNode, history)
}
/>
</>
)}
<Loader className="simulation-loader" progress={simulationProgress} />
</div>
);

View File

@ -1,6 +1,10 @@
import React from 'react';
import { t } from '@lingui/macro';
import React, { useState, useCallback, useEffect } from 'react';
import { t, Plural } from '@lingui/macro';
import styled from 'styled-components';
import useRequest, { useDismissableError } from 'hooks/useRequest';
import useDebounce from 'hooks/useDebounce';
import { InstancesAPI } from 'api';
import computeForks from 'util/computeForks';
import {
Button as PFButton,
DescriptionList as PFDescriptionList,
@ -8,26 +12,41 @@ import {
DescriptionListGroup as PFDescriptionListGroup,
DescriptionListDescription,
Divider,
Progress,
ProgressMeasureLocation,
ProgressSize,
Slider,
TextContent,
Text as PFText,
TextVariants,
} from '@patternfly/react-core';
import { DownloadIcon } from '@patternfly/react-icons';
import ContentLoading from 'components/ContentLoading';
import InstanceToggle from 'components/InstanceToggle';
import StatusLabel from 'components/StatusLabel';
import AlertModal from 'components/AlertModal';
import ErrorDetail from 'components/ErrorDetail';
import { formatDateString } from 'util/dates';
const Wrapper = styled.div`
position: absolute;
top: -20px;
right: 0;
padding: 10px;
width: 20%;
width: 25%;
background-color: rgba(255, 255, 255, 0.85);
`;
const Button = styled(PFButton)`
width: 20px;
height: 20px;
border-radius: 10px;
padding: 0;
font-size: 11px;
&&& {
width: 30px;
height: 30px;
border-radius: 15px;
padding: 0;
font-size: 16px;
background-color: white;
border: 1px solid #ccc;
color: black;
}
`;
const DescriptionList = styled(PFDescriptionList)`
gap: 0;
@ -39,12 +58,95 @@ const DescriptionListGroup = styled(PFDescriptionListGroup)`
const Text = styled(PFText)`
margin: 10px 0 5px;
`;
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 renderInstanceGroups(instanceGroups) {
return instanceGroups.map((g) => <StatusLabel status={g.name} />);
}
function usedCapacity(instance) {
if (instance.enabled) {
return (
<Progress
value={Math.round(100 - instance.percent_capacity_remaining)}
measureLocation={ProgressMeasureLocation.top}
size={ProgressSize.sm}
title={t`Used capacity`}
/>
);
}
return <Unavailable>{t`Unavailable`}</Unavailable>;
}
function Tooltip({
fetchInstance,
isNodeSelected,
renderNodeIcon,
nodeDetail,
instanceDetail,
instanceGroups,
isLoading,
redirectToDetailsPage,
}) {
const [forks, setForks] = useState(
computeForks(
instanceDetail.mem_capacity,
instanceDetail.cpu_capacity,
instanceDetail.capacity_adjustment
)
);
const { error: updateInstanceError, request: updateInstance } = useRequest(
useCallback(
async (values) => {
await InstancesAPI.update(instanceDetail.id, values);
},
[instanceDetail]
)
);
const debounceUpdateInstance = useDebounce(updateInstance, 100);
const { error: updateError, dismissError: dismissUpdateError } =
useDismissableError(updateInstanceError);
const handleChangeValue = (value) => {
const roundedValue = Math.round(value * 100) / 100;
setForks(
computeForks(
instanceDetail.mem_capacity,
instanceDetail.cpu_capacity,
roundedValue
)
);
debounceUpdateInstance({ capacity_adjustment: roundedValue });
};
useEffect(() => {
setForks(
computeForks(
instanceDetail.mem_capacity,
instanceDetail.cpu_capacity,
instanceDetail.capacity_adjustment
)
);
}, [instanceDetail]);
return (
<Wrapper className="tooltip" data-cy="tooltip">
{isNodeSelected === false ? (
@ -62,6 +164,17 @@ function Tooltip({
</TextContent>
) : (
<>
{updateError && (
<AlertModal
variant="error"
title={t`Error!`}
isOpen
onClose={dismissUpdateError}
>
{t`Failed to update instance.`}
<ErrorDetail error={updateError} />
</AlertModal>
)}
<TextContent>
<Text
component={TextVariants.small}
@ -71,36 +184,130 @@ function Tooltip({
</Text>
<Divider component="div" />
</TextContent>
<DescriptionList isHorizontal isFluid>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="primary" isSmall>
{renderNodeIcon}
</Button>
</DescriptionListTerm>
<DescriptionListDescription>
<PFButton
variant="link"
isInline
onClick={redirectToDetailsPage}
>
{nodeDetail.hostname}
</PFButton>
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{t`Type`}</DescriptionListTerm>
<DescriptionListDescription>
{nodeDetail.node_type} {t`node`}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{t`Status`}</DescriptionListTerm>
<DescriptionListDescription>
<StatusLabel status={nodeDetail.node_state} />
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
{isLoading && <ContentLoading />}
{!isLoading && (
<DescriptionList>
<DescriptionListGroup>
<DescriptionListDescription>
<Button>{renderNodeIcon}</Button>{' '}
<PFButton
variant="link"
isInline
onClick={redirectToDetailsPage}
>
{instanceDetail.hostname}
</PFButton>
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{t`Instance status`}</DescriptionListTerm>
<DescriptionListDescription>
<StatusLabel status={instanceDetail.node_state} />
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{t`Instance type`}</DescriptionListTerm>
<DescriptionListDescription>
{instanceDetail.node_type}
</DescriptionListDescription>
</DescriptionListGroup>
{instanceDetail.related?.install_bundle && (
<DescriptionListGroup>
<DescriptionListTerm>{t`Download bundle`}</DescriptionListTerm>
<DescriptionListDescription>
<a href={`${instanceDetail.related.install_bundle}`}>
<PFButton
ouiaId="job-output-download-button"
variant="plain"
aria-label={t`Download Bundle`}
>
<DownloadIcon />
</PFButton>
</a>
</DescriptionListDescription>
</DescriptionListGroup>
)}
{instanceDetail.ip_address && (
<DescriptionListGroup>
<DescriptionListTerm>{t`IP address`}</DescriptionListTerm>
<DescriptionListDescription>
{instanceDetail.ip_address}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{instanceGroups && (
<DescriptionListGroup>
<DescriptionListTerm>{t`Instance groups`}</DescriptionListTerm>
<DescriptionListDescription>
{renderInstanceGroups(instanceGroups.results)}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{instanceDetail.node_type !== 'hop' && (
<>
<DescriptionListGroup>
<DescriptionListTerm>{t`Forks`}</DescriptionListTerm>
<DescriptionListDescription>
<SliderHolder data-cy="slider-holder">
<div data-cy="cpu-capacity">{t`CPU ${instanceDetail.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={instanceDetail.capacity_adjustment}
onChange={handleChangeValue}
isDisabled={!instanceDetail.enabled}
// isDisabled={!me?.is_superuser || !instance.enabled}
data-cy="slider"
/>
</SliderForks>
<div data-cy="mem-capacity">{t`RAM ${instanceDetail.mem_capacity}`}</div>
</SliderHolder>
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{t`Capacity`}</DescriptionListTerm>
<DescriptionListDescription>
{usedCapacity(instanceDetail)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListDescription>
<InstanceToggle
css="display: inline-flex;"
fetchInstances={fetchInstance}
instance={instanceDetail}
/>
</DescriptionListDescription>
</DescriptionListGroup>
</>
)}
<DescriptionListGroup>
<DescriptionListTerm>{t`Last modified`}</DescriptionListTerm>
<DescriptionListDescription>
{formatDateString(instanceDetail.modified)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{t`Last seen`}</DescriptionListTerm>
<DescriptionListDescription>
{instanceDetail.last_seen
? formatDateString(instanceDetail.last_seen)
: `not found`}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
)}
</>
)}
</Wrapper>