This commit is contained in:
Kia Lam
2022-01-19 14:43:33 -08:00
parent 826a069be0
commit 73a5802c11
2 changed files with 165 additions and 221 deletions

View File

@@ -1,102 +1,102 @@
import React, { useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro'; // import { t } from '@lingui/macro';
import * as d3 from 'd3'; import * as d3 from 'd3';
// function MeshGraph({ data }) { function MeshGraph({ data }) {
function MeshGraph() { // function MeshGraph() {
const data = { // const data = {
nodes: [ // nodes: [
{ // {
hostname: "aapc1.local", // hostname: 'aapc1.local',
node_state: "healthy", // node_state: 'healthy',
node_type: "control", // node_type: 'control',
id: 1 // id: 1,
}, // },
{ // {
hostname: "aapc2.local", // hostname: 'aapc2.local',
node_type: "control", // node_type: 'control',
node_state: "disabled", // node_state: 'disabled',
id: 2 // id: 2,
}, // },
{ // {
hostname: "aapc3.local", // hostname: 'aapc3.local',
node_type: "control", // node_type: 'control',
node_state: "healthy", // node_state: 'healthy',
id: 3 // id: 3,
}, // },
{ // {
hostname: "aape1.local", // hostname: 'aape1.local',
node_type: "execution", // node_type: 'execution',
node_state: "error", // node_state: 'error',
id: 4 // id: 4,
}, // },
{ // {
hostname: "aape2.local", // hostname: 'aape2.local',
node_type: "execution", // node_type: 'execution',
node_state: "error", // node_state: 'error',
id: 5 // id: 5,
}, // },
{ // {
hostname: "aape3.local", // hostname: 'aape3.local',
node_type: "execution", // node_type: 'execution',
node_state: "healthy", // node_state: 'healthy',
id: 6 // id: 6,
}, // },
{ // {
hostname: "aape4.local", // hostname: 'aape4.local',
node_type: "execution", // node_type: 'execution',
node_state: "healthy", // node_state: 'healthy',
id: 7 // id: 7,
}, // },
{ // {
hostname: "aaph1.local", // hostname: 'aaph1.local',
node_type: "hop", // node_type: 'hop',
node_state: "disabled", // node_state: 'disabled',
id: 8 // id: 8,
}, // },
{ // {
hostname: "aaph2.local", // hostname: 'aaph2.local',
node_type: "hop", // node_type: 'hop',
node_state: "healthy", // node_state: 'healthy',
id: 9 // id: 9,
}, // },
{ // {
hostname: "aaph3.local", // hostname: 'aaph3.local',
node_type: "hop", // node_type: 'hop',
node_state: "error", // node_state: 'error',
id: 10 // id: 10,
} // },
], // ],
links: [ // links: [
{ source: "aapc1.local", target: "aapc2.local" }, // { source: 'aapc1.local', target: 'aapc2.local' },
{ source: "aapc1.local", target: "aapc3.local" }, // { source: 'aapc1.local', target: 'aapc3.local' },
{ source: "aapc1.local", target: "aape1.local" }, // { source: 'aapc1.local', target: 'aape1.local' },
{ source: "aapc1.local", target: "aape2.local" }, // { source: 'aapc1.local', target: 'aape2.local' },
{ source: "aapc2.local", target: "aapc3.local" }, // { source: 'aapc2.local', target: 'aapc3.local' },
{ source: "aapc2.local", target: "aape1.local" }, // { source: 'aapc2.local', target: 'aape1.local' },
{ source: "aapc2.local", target: "aape2.local" }, // { source: 'aapc2.local', target: 'aape2.local' },
{ source: "aapc3.local", target: "aape1.local" }, // { source: 'aapc3.local', target: 'aape1.local' },
{ source: "aapc3.local", target: "aape2.local" }, // { source: 'aapc3.local', target: 'aape2.local' },
{ source: "aape3.local", target: "aaph1.local" }, // { source: 'aape3.local', target: 'aaph1.local' },
{ source: "aape3.local", target: "aaph2.local" }, // { source: 'aape3.local', target: 'aaph2.local' },
{ source: "aape4.local", target: "aaph3.local" }, // { source: 'aape4.local', target: 'aaph3.local' },
{ source: "aaph1.local", target: "aapc1.local" }, // { source: 'aaph1.local', target: 'aapc1.local' },
{ source: "aaph1.local", target: "aapc2.local" }, // { source: 'aaph1.local', target: 'aapc2.local' },
{ source: "aaph1.local", target: "aapc3.local" }, // { source: 'aaph1.local', target: 'aapc3.local' },
{ source: "aaph2.local", target: "aapc1.local" }, // { source: 'aaph2.local', target: 'aapc1.local' },
{ source: "aaph2.local", target: "aapc2.local" }, // { source: 'aaph2.local', target: 'aapc2.local' },
{ source: "aaph2.local", target: "aapc3.local" }, // { source: 'aaph2.local', target: 'aapc3.local' },
{ source: "aaph3.local", target: "aaph1.local" }, // { source: 'aaph3.local', target: 'aaph1.local' },
{ source: "aaph3.local", target: "aaph2.local" } // { source: 'aaph3.local', target: 'aaph2.local' },
] // ],
}; // };
const draw = useCallback(() => { const draw = useCallback(() => {
const margin = 80; const margin = 80;
@@ -122,7 +122,7 @@ function MeshGraph() {
const zoom = d3 const zoom = d3
.zoom() .zoom()
.scaleExtent([1, 8]) .scaleExtent([1, 8])
.on('zoom', function (event) { .on('zoom', (event) => {
svg.selectAll('.links, .nodes').attr('transform', event.transform); svg.selectAll('.links, .nodes').attr('transform', event.transform);
}); });
@@ -145,18 +145,14 @@ function MeshGraph() {
.forceSimulation() .forceSimulation()
.force( .force(
'link', 'link',
d3.forceLink().id(function (d) { d3.forceLink().id((d) => d.hostname)
return d.hostname;
})
) )
.force('charge', d3.forceManyBody().strength(-350)) .force('charge', d3.forceManyBody().strength(-350))
.force( .force(
'collide', 'collide',
d3.forceCollide(function (d) { d3.forceCollide((d) =>
return d.node_type === 'execution' || d.node_type === 'hop' d.node_type === 'execution' || d.node_type === 'hop' ? 75 : 100
? 75 )
: 100;
})
) )
.force('center', d3.forceCenter(width / 2, height / 2)); .force('center', d3.forceCenter(width / 2, height / 2));
@@ -172,19 +168,8 @@ function MeshGraph() {
.style('stroke', '#ccc') .style('stroke', '#ccc')
.style('stroke-width', '2px') .style('stroke-width', '2px')
.attr('pointer-events', 'none') .attr('pointer-events', 'none')
.on('mouseover', function (event, d) { .on('mouseover', function showPointer() {
// tooltip
// .html(`source: ${d.source.hostname} <br>target: ${d.target.hostname}`)
// .style('visibility', 'visible');
d3.select(this).transition().style('cursor', 'pointer'); d3.select(this).transition().style('cursor', 'pointer');
})
.on('mousemove', function () {
// tooltip
// .style('top', event.pageY - 10 + 'px')
// .style('left', event.pageX + 10 + 'px');
})
.on('mouseout', function () {
// tooltip.html(``).style('visibility', 'hidden');
}); });
const node = svg const node = svg
@@ -194,53 +179,42 @@ function MeshGraph() {
.data(graph.nodes) .data(graph.nodes)
.enter() .enter()
.append('g') .append('g')
.on('mouseenter', function (event, d) { .on('mouseenter', function handleNodeHover(_, d) {
d3.select(this).transition().style('cursor', 'pointer'); d3.select(this).transition().style('cursor', 'pointer');
highlightSiblings(d) highlightSiblings(d);
tooltip tooltip
.html( .html(
`<h3>Details</h3> <hr>name: ${d.hostname} <br>type: ${d.node_type} <br>status: ${d.node_state} <br> <a>Click on a node to view the details</a>` `<h3>Details</h3> <hr>name: ${d.hostname} <br>type: ${d.node_type} <br>status: ${d.node_state} <br> <a>Click on a node to view the details</a>`
) )
.style('visibility', 'visible') .style('visibility', 'visible');
// .style('visibility', 'visible');
// d3.select(this).transition().attr('r', 9).style('cursor', 'pointer');
}) })
.on('mousemove', function () { .on('mouseleave', (_, d) => {
// tooltip deselectSiblings(d);
// .style('top', event.pageY - 10 + 'px')
// .style('left', event.pageX + 10 + 'px');
})
.on('mouseleave', function (event, d) {
deselectSiblings(d)
tooltip.html(``).style('visibility', 'hidden'); tooltip.html(``).style('visibility', 'hidden');
// d3.select(this).attr('r', 6);
}); });
const healthRings = node // health rings on nodes
node
.append('circle') .append('circle')
.attr('r', 8) .attr('r', 8)
.attr('class', (d) => d.node_state) .attr('class', (d) => d.node_state)
.attr('stroke', d => renderHealthColor(d.node_state)) .attr('stroke', (d) => renderHealthColor(d.node_state))
.attr('fill', d => renderHealthColor(d.node_state)); .attr('fill', (d) => renderHealthColor(d.node_state));
const nodeRings = node // inner node ring
node
.append('circle') .append('circle')
.attr('r', defaultRadius) .attr('r', defaultRadius)
.attr('class', (d) => d.node_type) .attr('class', (d) => d.node_type)
.attr('class', (d) => `id-${d.id}`) .attr('class', (d) => `id-${d.id}`)
.attr('fill', function (d) { .attr('fill', (d) => color(d.node_type))
return color(d.node_type);
})
.attr('stroke', 'white'); .attr('stroke', 'white');
svg.call(expandGlow); svg.call(expandGlow);
const legend = svg // legend
.append('text') svg.append('text').attr('x', 10).attr('y', 20).text('Legend');
.attr('x', 10)
.attr('y', 20)
.text('Legend')
svg svg
.append('g') .append('g')
.selectAll('g') .selectAll('g')
.attr('class', 'chart-legend') .attr('class', 'chart-legend')
@@ -248,16 +222,13 @@ function MeshGraph() {
.enter() .enter()
.append('circle') .append('circle')
.attr('cx', 10) .attr('cx', 10)
.attr('cy', function (d, i) { .attr('cy', (d, i) => 50 + i * 25)
return 50 + i * 25;
})
.attr('r', defaultRadius) .attr('r', defaultRadius)
.attr('class', (d) => d.node_type) .attr('class', (d) => d.node_type)
.style('fill', function (d) { .style('fill', (d) => color(d.node_type));
return color(d.node_type);
});
const legend_text = svg // legend text
svg
.append('g') .append('g')
.attr('class', 'chart-text') .attr('class', 'chart-text')
.selectAll('g') .selectAll('g')
@@ -265,9 +236,7 @@ function MeshGraph() {
.enter() .enter()
.append('text') .append('text')
.attr('x', 20) .attr('x', 20)
.attr('y', function (d, i) { .attr('y', (d, i) => 50 + i * 25)
return 50 + i * 25;
})
.text((d) => `${d.hostname} - ${d.node_type}`) .text((d) => `${d.hostname} - ${d.node_type}`)
.attr('text-anchor', 'left') .attr('text-anchor', 'left')
.style('alignment-baseline', 'middle'); .style('alignment-baseline', 'middle');
@@ -292,11 +261,10 @@ function MeshGraph() {
.style('color', '#e6e6e') .style('color', '#e6e6e')
.text(''); .text('');
const labels = node // node labels
node
.append('text') .append('text')
.text(function (d) { .text((d) => d.hostname)
return d.hostname;
})
.attr('x', 16) .attr('x', 16)
.attr('y', 3); .attr('y', 3);
@@ -306,29 +274,14 @@ function MeshGraph() {
function ticked() { function ticked() {
link.attr('d', linkArc); link.attr('d', linkArc);
node.attr('transform', function (d) { node.attr('transform', (d) => `translate(${d.x},${d.y})`);
return 'translate(' + d.x + ',' + d.y + ')';
});
} }
function linkArc(d) { function linkArc(d) {
var dx = d.target.x - d.source.x, const dx = d.target.x - d.source.x;
dy = d.target.y - d.source.y, const dy = d.target.y - d.source.y;
dr = Math.sqrt(dx * dx + dy * dy); const dr = Math.sqrt(dx * dx + dy * dy);
return ( return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
'M' +
d.source.x +
',' +
d.source.y +
'A' +
dr +
',' +
dr +
' 0 0,1 ' +
d.target.x +
',' +
d.target.y
);
} }
function contractGlow() { function contractGlow() {
@@ -351,58 +304,54 @@ function MeshGraph() {
function renderHealthColor(nodeState) { function renderHealthColor(nodeState) {
const colorKey = { const colorKey = {
'disabled': '#c6c6c6', disabled: '#c6c6c6',
'healthy': '#50D050', healthy: '#50D050',
'error': '#ff6766' error: '#ff6766',
}; };
return colorKey[nodeState]; return colorKey[nodeState];
} }
function renderNodeClass(nodeState) { function highlightSiblings(n) {
const colorKey = { setTimeout(() => {
'disabled': 'node-disabled', svg.selectAll(`id-${n.id}`).attr('r', highlightRadius);
'healthy': 'node-healthy', const immediate = graph.links.filter(
'error': 'node-error' (l) =>
}; n.hostname === l.source.hostname || n.hostname === l.target.hostname
return colorKey[nodeState]; );
immediate.forEach((s) => {
svg
.selectAll(`.link-${s.index}`)
.transition()
.style('stroke', '#6e6e6e');
svg
.selectAll(`.id-${s.source.id}`)
.transition()
.attr('r', highlightRadius);
svg
.selectAll(`.id-${s.target.id}`)
.transition()
.attr('r', highlightRadius);
});
}, 0);
} }
function highlightSiblings(node) { function deselectSiblings(n) {
setTimeout(function() { svg.selectAll(`id-${n.id}`).attr('r', defaultRadius);
svg.selectAll(`id-${node.id}`) const immediate = graph.links.filter(
.attr('r', highlightRadius); (l) =>
const immediate = graph.links.filter(link => node.hostname === link.source.hostname || node.hostname === link.target.hostname); n.hostname === l.source.hostname || n.hostname === l.target.hostname
immediate.forEach(s => { );
const links = svg.selectAll(`.link-${s.index}`) immediate.forEach((s) => {
svg.selectAll(`.link-${s.index}`).transition().style('stroke', '#ccc');
svg
.selectAll(`.id-${s.source.id}`)
.transition() .transition()
.style('stroke', '#6e6e6e')
const sourceNodes = svg.selectAll(`.id-${s.source.id}`)
.transition()
.attr('r', highlightRadius)
const targetNodes = svg.selectAll(`.id-${s.target.id}`)
.transition()
.attr('r', highlightRadius)
})
}, 0)
}
function deselectSiblings(node) {
svg.selectAll(`id-${node.id}`)
.attr('r', defaultRadius); .attr('r', defaultRadius);
const immediate = graph.links.filter(link => node.hostname === link.source.hostname || node.hostname === link.target.hostname); svg
immediate.forEach(s => { .selectAll(`.id-${s.target.id}`)
const links = svg.selectAll(`.link-${s.index}`)
.transition() .transition()
.style('stroke', '#ccc') .attr('r', defaultRadius);
svg.selectAll(`.id-${s.source.id}`) });
.transition()
.attr('r', defaultRadius)
svg.selectAll(`.id-${s.target.id}`)
.transition()
.attr('r', defaultRadius)
})
} }
// const zoom = d3 // const zoom = d3
// .zoom() // .zoom()

View File

@@ -2,20 +2,15 @@ import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader'; import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import { import { PageSection, Card, CardBody } from '@patternfly/react-core';
PageSection,
Card,
CardHeader,
CardBody,
} from '@patternfly/react-core';
import MeshGraph from './MeshGraph';
import useRequest from 'hooks/useRequest'; import useRequest from 'hooks/useRequest';
import { MeshAPI } from 'api'; import { MeshAPI } from 'api';
import MeshGraph from './MeshGraph';
function TopologyView() { function TopologyView() {
const { const {
result: { meshData }, result: { meshData },
error: fetchInitialError, // error: fetchInitialError,
request: fetchMeshVisualizer, request: fetchMeshVisualizer,
} = useRequest( } = useRequest(
useCallback(async () => { useCallback(async () => {