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', node_state: instance.node_state || 'installed',
listener_port: instance.listener_port || 27199, listener_port: instance.listener_port || 27199,
enabled: instance.enabled || true, enabled: instance.enabled || true,
managed_by_policy: instance.managed_by_policy || true,
peers_from_control_nodes: instance.peers_from_control_nodes peers_from_control_nodes: instance.peers_from_control_nodes
? true ? true
: !isEdit, : !isEdit,

View File

@@ -33,7 +33,8 @@ function RemoveInstanceButton({ itemsToRemove, onRemove, isK8s }) {
const [removeDetails, setRemoveDetails] = useState(null); const [removeDetails, setRemoveDetails] = useState(null);
const [isLoading, setIsLoading] = useState(false); 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) => { const toggleModal = async (isOpen) => {
setRemoveDetails(null); setRemoveDetails(null);

View File

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

View File

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

View File

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

View File

@@ -45,6 +45,7 @@ function TopologyView() {
zoomIn={zoomIn} zoomIn={zoomIn}
zoomOut={zoomOut} zoomOut={zoomOut}
zoomFit={zoomFit} zoomFit={zoomFit}
refresh={fetchMeshVisualizer}
resetZoom={resetZoom} resetZoom={resetZoom}
showZoomControls={showZoomControls} 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_HIGHLIGHT_COLOR = '#eee';
export const DEFAULT_NODE_LABEL_TEXT_COLOR = 'white'; export const DEFAULT_NODE_LABEL_TEXT_COLOR = 'white';
export const DEFAULT_NODE_SYMBOL_TEXT_COLOR = 'black'; 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 DEFAULT_FONT_SIZE = '12px';
export const LABEL_TEXT_MAX_LENGTH = 15; export const LABEL_TEXT_MAX_LENGTH = 15;
export const MARGIN = 15; export const MARGIN = 15;
@@ -27,6 +27,13 @@ export const NODE_STATE_COLOR_KEY = {
deprovisioning: '#666', deprovisioning: '#666',
}; };
export const LINK_STATE_COLOR_KEY = {
established: '#6A6E73',
disconnected: '#CCC',
adding: '#3E8635',
removing: '#C9190B',
};
export const NODE_TYPE_SYMBOL_KEY = { export const NODE_TYPE_SYMBOL_KEY = {
hop: 'h', hop: 'h',
execution: 'Ex', execution: 'Ex',

View File

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

View File

@@ -73,7 +73,7 @@ describe('renderIconPosition', () => {
const bbox = { x: 400, y: 400, width: 10, height: 20 }; const bbox = { x: 400, y: 400, width: 10, height: 20 };
test('returns correct label icon', () => { test('returns correct label icon', () => {
expect(renderIconPosition('ready', bbox)).toBe( 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', () => { test('returns empty string if state is not found', () => {