feature hop node topology updates (#14142)

This commit is contained in:
kialam 2023-07-20 07:57:04 -07:00 committed by Seth Foster
parent ed2a59c1a3
commit fdb359a67b
9 changed files with 144 additions and 95 deletions

View File

@ -148,6 +148,7 @@ function InstanceForm({
node_state: instance.node_state || 'installed',
listener_port: instance.listener_port || 27199,
enabled: instance.enabled || true,
managed_by_policy: instance.managed_by_policy || true,
peers_from_control_nodes: instance.peers_from_control_nodes
? true
: !isEdit,

View File

@ -33,7 +33,8 @@ function RemoveInstanceButton({ itemsToRemove, onRemove, isK8s }) {
const [removeDetails, setRemoveDetails] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const cannotRemove = (item) => !(item.node_type === 'execution' || item.node_type === 'hop');
const cannotRemove = (item) =>
!(item.node_type === 'execution' || item.node_type === 'hop');
const toggleModal = async (isOpen) => {
setRemoveDetails(null);

View File

@ -16,6 +16,7 @@ import {
SearchPlusIcon,
ExpandArrowsAltIcon,
ExpandIcon,
RedoAltIcon,
} from '@patternfly/react-icons';
const Header = ({
@ -26,6 +27,7 @@ const Header = ({
zoomOut,
resetZoom,
zoomFit,
refresh,
showZoomControls,
}) => {
const { light } = PageSectionVariants;
@ -48,6 +50,18 @@ const Header = ({
</Title>
</div>
<div>
<Tooltip content={t`Refresh`} position="top">
<Button
ouiaId="refresh-button"
aria-label={t`Refresh`}
variant="plain"
icon={<RedoAltIcon />}
onClick={refresh}
isDisabled={!showZoomControls}
>
<RedoAltIcon />
</Button>
</Tooltip>
<Tooltip content={t`Zoom in`} position="top">
<Button
ouiaId="zoom-in-button"

View File

@ -245,7 +245,7 @@ function Legend() {
y1="9"
x2="20"
y2="9"
stroke="#666"
stroke="#6A6E73"
strokeWidth="4"
/>
</svg>
@ -260,8 +260,9 @@ function Legend() {
y1="9"
x2="20"
y2="9"
stroke="#c9700b"
stroke="#ccc"
strokeWidth="4"
strokeDasharray="6"
/>
</svg>
</DescriptionListTerm>
@ -275,7 +276,7 @@ function Legend() {
y1="9"
x2="20"
y2="9"
stroke="#666"
stroke="#3E8635"
strokeWidth="4"
strokeDasharray="6"
/>

View File

@ -13,6 +13,7 @@ import Tooltip from './Tooltip';
import ContentLoading from './ContentLoading';
import {
renderStateColor,
renderLinkStatusColor,
renderLabelText,
renderNodeType,
renderNodeIcon,
@ -177,7 +178,13 @@ function MeshGraph({
mesh
.append('defs')
.selectAll('marker')
.data(['end', 'end-active'])
.data([
'end',
'end-active',
'end-disconnected',
'end-adding',
'end-removing',
])
.join('marker')
.attr('id', String)
.attr('viewBox', '0 -5 10 10')
@ -187,8 +194,10 @@ function MeshGraph({
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5');
mesh.select('#end').attr('refX', 23).attr('fill', '#ccc');
mesh.select('#end').attr('refX', 23).attr('fill', '#6A6E73');
mesh.select('#end-removing').attr('refX', 23).attr('fill', '#C9190B');
mesh.select('#end-adding').attr('refX', 23).attr('fill', '#3E8635');
mesh.select('#end-disconnected').attr('refX', 23).attr('fill', '#CCC');
mesh.select('#end-active').attr('refX', 18).attr('fill', '#0066CC');
// Add links
@ -204,24 +213,27 @@ function MeshGraph({
.attr('y1', (d) => d.source.y)
.attr('x2', (d) => d.target.x)
.attr('y2', (d) => d.target.y)
.attr('marker-end', 'url(#end)')
.attr('marker-end', (d) => {
if (d.link_state === 'disconnected') {
return 'url(#end-disconnected)';
}
if (d.link_state === 'adding') {
return 'url(#end-adding)';
}
if (d.link_state === 'removing') {
return 'url(#end-removing)';
}
return 'url(#end)';
})
.attr('class', (_, i) => `link-${i}`)
.attr('data-cy', (d) => `${d.source.hostname}-${d.target.hostname}`)
.style('fill', 'none')
.style('stroke', (d) => {
if (d.link_state === 'removing') {
return '#C9190B';
}
if (d.link_state === 'disconnected') {
return '#c9700b';
}
return '#CCC';
})
.style('stroke', (d) => renderLinkStatusColor(d.link_state))
.style('stroke-width', '2px')
.style('stroke-dasharray', (d) => renderLinkState(d.link_state))
.attr('pointer-events', 'none')
.on('mouseover', function showPointer() {
d3.select(this).transition().style('cursor', 'pointer');
d3.select(this).style('cursor', 'pointer');
});
// add nodes
const node = mesh
@ -234,7 +246,7 @@ function MeshGraph({
.append('g')
.attr('data-cy', (d) => `node-${d.id}`)
.on('mouseenter', function handleNodeHover(_, d) {
d3.select(this).transition().style('cursor', 'pointer');
d3.select(this).style('cursor', 'pointer');
highlightSiblings(d);
})
.on('mouseleave', (_, d) => {
@ -245,7 +257,8 @@ function MeshGraph({
});
// node circles
node
const nodeCircles = node.append('g');
nodeCircles
.append('circle')
.attr('r', DEFAULT_RADIUS)
.attr('cx', (d) => d.x)
@ -254,7 +267,8 @@ function MeshGraph({
.attr('class', (d) => `id-${d.id}`)
.attr('fill', DEFAULT_NODE_COLOR)
.attr('stroke-dasharray', (d) => (d.enabled ? `1 0` : `5`))
.attr('stroke', DEFAULT_NODE_STROKE_COLOR);
.attr('stroke', (d) => renderStateColor(d.node_state));
// node type labels
node
.append('text')
@ -265,64 +279,62 @@ function MeshGraph({
.attr('dominant-baseline', 'central')
.attr('fill', DEFAULT_NODE_SYMBOL_TEXT_COLOR);
const placeholder = node.append('g').attr('class', 'placeholder');
placeholder
.append('text')
.text((d) => renderLabelText(d.node_state, d.hostname))
.attr('x', (d) => d.x)
.attr('y', (d) => d.y + 40)
.attr('fill', 'black')
.attr('font-size', '18px')
.attr('text-anchor', 'middle')
.each(function calculateLabelWidth() {
// eslint-disable-next-line react/no-this-in-sfc
const bbox = this.getBBox();
// eslint-disable-next-line react/no-this-in-sfc
d3.select(this.parentNode)
.append('path')
.attr('d', (d) => renderLabelIcons(d.node_state))
.attr('transform', (d) => renderIconPosition(d.node_state, bbox))
.style('fill', 'black');
});
placeholder.each(function calculateLabelWidth() {
// eslint-disable-next-line react/no-this-in-sfc
const bbox = this.getBBox();
// eslint-disable-next-line react/no-this-in-sfc
d3.select(this.parentNode)
.append('rect')
.attr('x', (d) => d.x - bbox.width / 2)
.attr('y', bbox.y + 5)
.attr('width', bbox.width)
.attr('height', bbox.height)
.attr('rx', 8)
.attr('ry', 8)
.style('fill', (d) => renderStateColor(d.node_state));
});
const hostNames = node.append('g');
// node hostname labels
const hostNames = node.append('g').attr('class', 'node-state-label');
hostNames
.append('text')
.attr('x', (d) => d.x)
.attr('y', (d) => d.y + 40)
.text((d) => renderLabelText(d.node_state, d.hostname))
.attr('x', (d) => d.x + 6)
.attr('y', (d) => d.y + 42)
.attr('class', 'placeholder')
.attr('fill', 'white')
.attr('font-size', DEFAULT_FONT_SIZE)
.attr('text-anchor', 'middle')
.each(function calculateLabelWidth() {
// eslint-disable-next-line react/no-this-in-sfc
const bbox = this.getBBox();
const padding = 10;
// eslint-disable-next-line react/no-this-in-sfc
d3.select(this.parentNode)
.append('path')
.attr('class', (d) => `icon-${d.node_state}`)
.attr('d', (d) => renderLabelIcons(d.node_state))
.attr('transform', (d) => renderIconPosition(d.node_state, bbox))
.attr('fill', 'white');
.append('rect')
.attr('x', bbox.x - padding / 2)
.attr('y', bbox.y)
.attr('width', bbox.width + padding)
.attr('height', bbox.height)
.style('stroke-width', 1)
.attr('rx', 4)
.attr('ry', 4)
.attr('fill', 'white')
.style('stroke', DEFAULT_NODE_STROKE_COLOR);
});
svg.selectAll('g.placeholder').remove();
svg.selectAll('text.placeholder').remove();
hostNames
.append('text')
.attr('x', (d) => d.x)
.attr('y', (d) => d.y + 38)
.text((d) => renderLabelText(d.node_state, d.hostname))
.attr('font-size', DEFAULT_FONT_SIZE)
.attr('fill', 'black')
.attr('text-anchor', 'middle');
// add badge icons
const badges = nodeCircles.append('g').attr('class', 'node-state-badge');
badges.each(function drawStateBadge() {
// eslint-disable-next-line react/no-this-in-sfc
const bbox = this.parentNode.getBBox();
// eslint-disable-next-line react/no-this-in-sfc
d3.select(this)
.append('circle')
.attr('r', 9)
.attr('cx', bbox.x)
.attr('cy', bbox.y)
.attr('fill', (d) => renderStateColor(d.node_state));
d3.select(this)
.append('path')
.attr('class', (d) => `icon-${d.node_state}`)
.attr('d', (d) => renderLabelIcons(d.node_state))
.attr('transform', (d) => renderIconPosition(d.node_state, bbox))
.attr('fill', 'white');
});
svg.call(zoom);
function highlightSiblings(n) {
@ -336,7 +348,6 @@ function MeshGraph({
immediate.forEach((s) => {
svg
.selectAll(`.link-${s.index}`)
.transition()
.style('stroke', '#0066CC')
.style('stroke-width', '3px')
.attr('marker-end', 'url(#end-active)');
@ -352,19 +363,20 @@ function MeshGraph({
immediate.forEach((s) => {
svg
.selectAll(`.link-${s.index}`)
.transition()
.duration(50)
.style('stroke', (d) => {
if (d.link_state === 'removing') {
return '#C9190B';
}
if (d.link_state === 'disconnected') {
return '#c9700b';
}
return '#CCC';
})
.style('stroke', (d) => renderLinkStatusColor(d.link_state))
.style('stroke-width', '2px')
.attr('marker-end', 'url(#end)');
.attr('marker-end', (d) => {
if (d.link_state === 'disconnected') {
return 'url(#end-disconnected)';
}
if (d.link_state === 'adding') {
return 'url(#end-adding)';
}
if (d.link_state === 'removing') {
return 'url(#end-removing)';
}
return 'url(#end)';
});
});
}
@ -373,7 +385,7 @@ function MeshGraph({
// toggle rings
svg
.select(`circle.id-${n.id}`)
.attr('stroke', '#ccc')
.attr('stroke', (d) => renderStateColor(d.node_state))
.attr('stroke-width', null);
// show default empty state of tooltip
setIsNodeSelected(false);
@ -382,7 +394,7 @@ function MeshGraph({
}
svg
.selectAll('circle')
.attr('stroke', '#ccc')
.attr('stroke', (d) => renderStateColor(d.node_state))
.attr('stroke-width', null);
svg
.select(`circle.id-${n.id}`)

View File

@ -45,6 +45,7 @@ function TopologyView() {
zoomIn={zoomIn}
zoomOut={zoomOut}
zoomFit={zoomFit}
refresh={fetchMeshVisualizer}
resetZoom={resetZoom}
showZoomControls={showZoomControls}
/>

View File

@ -13,7 +13,7 @@ export const DEFAULT_NODE_COLOR = 'white';
export const DEFAULT_NODE_HIGHLIGHT_COLOR = '#eee';
export const DEFAULT_NODE_LABEL_TEXT_COLOR = 'white';
export const DEFAULT_NODE_SYMBOL_TEXT_COLOR = 'black';
export const DEFAULT_NODE_STROKE_COLOR = '#ccc';
export const DEFAULT_NODE_STROKE_COLOR = '#6A6E73';
export const DEFAULT_FONT_SIZE = '12px';
export const LABEL_TEXT_MAX_LENGTH = 15;
export const MARGIN = 15;
@ -27,6 +27,13 @@ export const NODE_STATE_COLOR_KEY = {
deprovisioning: '#666',
};
export const LINK_STATE_COLOR_KEY = {
established: '#6A6E73',
disconnected: '#CCC',
adding: '#3E8635',
removing: '#C9190B',
};
export const NODE_TYPE_SYMBOL_KEY = {
hop: 'h',
execution: 'Ex',

View File

@ -4,6 +4,7 @@ import { truncateString } from '../../../util/strings';
import {
NODE_STATE_COLOR_KEY,
NODE_TYPE_SYMBOL_KEY,
LINK_STATE_COLOR_KEY,
LABEL_TEXT_MAX_LENGTH,
ICONS,
} from '../constants';
@ -20,6 +21,12 @@ export function renderStateColor(nodeState) {
return NODE_STATE_COLOR_KEY[nodeState] ? NODE_STATE_COLOR_KEY[nodeState] : '';
}
export function renderLinkStatusColor(linkState) {
return LINK_STATE_COLOR_KEY[linkState]
? LINK_STATE_COLOR_KEY[linkState]
: '#ccc';
}
export function renderLabelText(nodeState, name) {
if (typeof nodeState === 'string' && typeof name === 'string') {
return `${truncateString(name, LABEL_TEXT_MAX_LENGTH)}`;
@ -45,8 +52,8 @@ export function renderLabelIcons(nodeState) {
ready: 'checkmark',
installed: 'clock',
unavailable: 'exclaimation',
'provision-fail': 'exclaimation',
'deprovision-fail': 'exclaimation',
'provision-fail': 'exclaimation',
provisioning: 'plus',
deprovisioning: 'minus',
};
@ -59,15 +66,17 @@ export function renderLabelIcons(nodeState) {
export function renderIconPosition(nodeState, bbox) {
if (nodeState) {
const iconPositionMapper = {
ready: `translate(${bbox.x - 15}, ${bbox.y + 3}), scale(0.02)`,
installed: `translate(${bbox.x - 18}, ${bbox.y + 1}), scale(0.03)`,
unavailable: `translate(${bbox.x - 9}, ${bbox.y + 3}), scale(0.02)`,
'provision-fail': `translate(${bbox.x - 9}, ${bbox.y + 3}), scale(0.02)`,
'deprovision-fail': `translate(${bbox.x - 9}, ${
bbox.y + 3
ready: `translate(${bbox.x - 4.5}, ${bbox.y - 4.5}), scale(0.02)`,
installed: `translate(${bbox.x - 6.5}, ${bbox.y - 6.5}), scale(0.025)`,
unavailable: `translate(${bbox.x - 2}, ${bbox.y - 4.4}), scale(0.02)`,
'provision-fail': `translate(${bbox.x - 2}, ${bbox.y - 4}), scale(0.02)`,
'deprovision-fail': `translate(${bbox.x - 2}, ${
bbox.y - 4
}), scale(0.02)`,
provisioning: `translate(${bbox.x - 4.5}, ${bbox.y - 4.5}), scale(0.02)`,
deprovisioning: `translate(${bbox.x - 4.5}, ${
bbox.y - 4.5
}), scale(0.02)`,
provisioning: `translate(${bbox.x - 12}, ${bbox.y + 3}), scale(0.02)`,
deprovisioning: `translate(${bbox.x - 12}, ${bbox.y + 3}), scale(0.02)`,
};
return iconPositionMapper[nodeState] ? iconPositionMapper[nodeState] : ``;
}
@ -86,6 +95,7 @@ export function redirectToDetailsPage(selectedNode, history) {
export function renderLinkState(linkState) {
const linkPattern = {
established: null,
disconnected: 3,
adding: 3,
removing: 3,
};
@ -101,7 +111,9 @@ export function getRandomInt(min, max) {
const generateRandomLinks = (n, r) => {
const links = [];
function getRandomLinkState() {
return ['established', 'adding', 'removing'][getRandomInt(0, 2)];
return ['established', 'adding', 'removing', 'disconnected'][
getRandomInt(0, 3)
];
}
for (let i = 0; i < r; i++) {
const link = {
@ -142,7 +154,7 @@ export const generateRandomNodes = (n) => {
hostname: `node-${id}`,
node_type: randomType,
node_state: randomState,
enabled: Math.random() < 0.5,
enabled: Math.random() < 0.9,
};
nodes.push(node);
}

View File

@ -73,7 +73,7 @@ describe('renderIconPosition', () => {
const bbox = { x: 400, y: 400, width: 10, height: 20 };
test('returns correct label icon', () => {
expect(renderIconPosition('ready', bbox)).toBe(
`translate(${bbox.x - 15}, ${bbox.y + 3}), scale(0.02)`
`translate(${bbox.x - 4.5}, ${bbox.y - 4.5}), scale(0.02)`
);
});
test('returns empty string if state is not found', () => {