WIP new mesh layout based on QE feedback.

This commit is contained in:
Kia Lam
2022-02-02 16:00:07 -08:00
parent cd54d560b3
commit 8090cd3032
4 changed files with 500 additions and 275 deletions

View File

@@ -0,0 +1,139 @@
/* eslint-disable i18next/no-literal-string */
import React from 'react';
import styled from 'styled-components';
import {
Button as PFButton,
DescriptionList as PFDescriptionList,
DescriptionListTerm,
DescriptionListGroup as PFDescriptionListGroup,
DescriptionListDescription as PFDescriptionListDescription,
Divider,
TextContent,
Text as PFText,
TextVariants,
} from '@patternfly/react-core';
import {
ExclamationIcon as PFExclamationIcon,
CheckIcon as PFCheckIcon,
} from '@patternfly/react-icons';
const Wrapper = styled.div`
position: absolute;
top: -20px;
left: 0;
padding: 10px;
width: 190px;
`;
const Button = styled(PFButton)`
width: 20px;
height: 20px;
border-radius: 10px;
padding: 0;
font-size: 11px;
`;
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;
`;
const DescriptionListGroup = styled(PFDescriptionListGroup)`
align-items: center;
`;
const Text = styled(PFText)`
margin: 10px 0 5px;
`;
function Legend() {
return (
<Wrapper class="legend" data-cy="legend">
<TextContent>
<Text
component={TextVariants.small}
style={{ 'font-weight': 'bold', color: 'black' }}
>
Legend
</Text>
<Divider component="div" />
<Text component={TextVariants.small}>Node types</Text>
</TextContent>
<DescriptionList isHorizontal isFluid>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="primary" isSmall>
C
</Button>
</DescriptionListTerm>
<DescriptionListDescription>Control node</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="primary" isSmall>
Ex
</Button>
</DescriptionListTerm>
<DescriptionListDescription>
Execution node
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="primary" isSmall>
H
</Button>
</DescriptionListTerm>
<DescriptionListDescription>Hybrid node</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="primary" isSmall>
h
</Button>
</DescriptionListTerm>
<DescriptionListDescription>Hop node</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
<TextContent>
<Text component={TextVariants.small}>Status types</Text>
</TextContent>
<DescriptionList isHorizontal isFluid>
<DescriptionListGroup>
<DescriptionListTerm>
<Button
icon={<CheckIcon />}
isSmall
style={{ border: '1px solid gray', backgroundColor: '#3E8635' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>Healthy</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="danger" icon={<ExclamationIcon />} isSmall />
</DescriptionListTerm>
<DescriptionListDescription>Error</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
<Button
isSmall
style={{ border: '1px solid gray', backgroundColor: '#e6e6e6' }}
/>
</DescriptionListTerm>
<DescriptionListDescription>Disabled</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</Wrapper>
);
}
export default Legend;

View File

@@ -1,90 +1,115 @@
import React, { useCallback, useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { InstancesAPI } from 'api';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
// import { t } from '@lingui/macro'; // import { t } from '@lingui/macro';
import * as d3 from 'd3'; import * as d3 from 'd3';
import Legend from './Legend';
import Tooltip from './Tooltip';
// function MeshGraph({ data }) { function MeshGraph({ data }) {
function MeshGraph({ redirectToDetailsPage }) { // function MeshGraph() {
const draw = useCallback(() => { const [isNodeSelected, setIsNodeSelected] = useState(false);
const data = { const [selectedNode, setSelectedNode] = useState(null);
nodes: [ const [nodeDetail, setNodeDetail] = useState(null);
{ const history = useHistory();
id: 1,
hostname: 'awx_1', const draw = () => {
node_type: 'hybrid', // const data = {
node_state: 'healthy', // nodes: [
}, // {
{ // id: 1,
id: 3, // hostname: 'awx_1',
hostname: 'receptor-1', // node_type: 'hybrid',
node_type: 'execution', // node_state: 'healthy',
node_state: 'healthy', // },
}, // {
{ // id: 3,
id: 4, // hostname: 'receptor-1',
hostname: 'receptor-2', // node_type: 'execution',
node_type: 'execution', // node_state: 'healthy',
node_state: 'healthy', // },
}, // {
{ // id: 4,
id: 2, // hostname: 'receptor-2',
hostname: 'receptor-hop', // node_type: 'execution',
node_type: 'hop', // node_state: 'healthy',
node_state: 'healthy', // },
}, // {
{ // id: 2,
id: 5, // hostname: 'receptor-hop',
hostname: 'receptor-hop-1', // node_type: 'hop',
node_type: 'hop', // node_state: 'healthy',
node_state: 'healthy', // },
}, // {
{ // id: 5,
id: 6, // hostname: 'receptor-hop-1',
hostname: 'receptor-hop-2', // node_type: 'hop',
node_type: 'hop', // node_state: 'healthy',
node_state: 'healthy', // },
}, // {
{ // id: 6,
id: 7, // hostname: 'receptor-hop-2',
hostname: 'receptor-hop-3', // node_type: 'hop',
node_type: 'hop', // node_state: 'disabled',
node_state: 'healthy', // },
}, // {
{ // id: 7,
id: 8, // hostname: 'receptor-hop-3',
hostname: 'receptor-hop-4', // node_type: 'hop',
node_type: 'hop', // node_state: 'error',
node_state: 'healthy', // },
}, // {
], // id: 8,
links: [ // hostname: 'receptor-hop-4',
{ // node_type: 'hop',
source: 'receptor-hop', // node_state: 'healthy',
target: 'awx_1', // },
}, // ],
{ // links: [
source: 'receptor-1', // {
target: 'receptor-hop', // source: 'receptor-hop',
}, // target: 'awx_1',
{ // },
source: 'receptor-2', // {
target: 'receptor-hop', // source: 'receptor-1',
}, // target: 'receptor-hop',
{ // },
source: 'receptor-hop-3', // {
target: 'receptor-hop', // source: 'receptor-2',
}, // target: 'receptor-hop',
// { // },
// "source": "receptor-2", // {
// "target": "receptor-hop-1" // source: 'receptor-hop-3',
// }, // target: 'receptor-hop',
// { // },
// "source": "receptor-2", // // {
// "target": "receptor-hop-2" // // "source": "receptor-hop",
// } // // "target": "receptor-hop-1"
], // // },
}; // // {
const margin = 80; // // "source": "receptor-1",
// // "target": "receptor-hop-2"
// // },
// // {
// // "source": "awx_1",
// // "target": "receptor-hop-4"
// // }
// ],
// };
const margin = 15;
const defaultRadius = 16;
const defaultCollisionFactor = 80;
const defaultForceStrength = -100;
const defaultForceBody = 75;
const defaultForceX = 0;
const defaultForceY = 0;
const height = 600;
const fallbackWidth = 700;
const defaultNodeColor = '#0066CC';
const defaultNodeHighlightColor = '#16407C';
const defaultNodeLabelColor = 'white';
const defaultFontSize = '12px';
const getWidth = () => { const getWidth = () => {
let width; let width;
// This is in an a try/catch due to an error from jest. // This is in an a try/catch due to an error from jest.
@@ -92,27 +117,25 @@ function MeshGraph({ redirectToDetailsPage }) {
// style function, it says it is null in the test // style function, it says it is null in the test
try { try {
width = width =
parseInt(d3.select(`#chart`).style('width'), 10) - margin || 700; parseInt(d3.select(`#chart`).style('width'), 10) - margin ||
fallbackWidth;
} catch (error) { } catch (error) {
width = 700; width = fallbackWidth;
} }
return width; return width;
}; };
const width = getWidth(); const width = getWidth();
const height = 600;
const defaultRadius = 6;
const highlightRadius = 9;
const zoom = d3 // const zoom = d3
.zoom() // .zoom()
// .scaleExtent([1, 8]) // // .scaleExtent([1, 8])
.on('zoom', (event) => { // .on('zoom', (event) => {
svg.attr('transform', event.transform); // svg.attr('transform', event.transform);
}); // });
/* Add SVG */ /* Add SVG */
d3.selectAll(`#chart > *`).remove(); d3.selectAll(`#chart > svg`).remove();
const svg = d3 const svg = d3
.select('#chart') .select('#chart')
@@ -120,48 +143,34 @@ function MeshGraph({ redirectToDetailsPage }) {
.attr('width', `${width + margin}px`) .attr('width', `${width + margin}px`)
.attr('height', `${height + margin}px`) .attr('height', `${height + margin}px`)
.append('g') .append('g')
.attr('transform', `translate(${margin}, ${margin})`) .attr('transform', `translate(${margin}, ${margin})`);
.call(zoom); // .call(zoom);
const graph = data; const graph = data;
const simulation = d3 const simulation = d3
.forceSimulation() .forceSimulation()
.force('charge', d3.forceManyBody(75).strength(-100)) .force(
'charge',
d3.forceManyBody(defaultForceBody).strength(defaultForceStrength)
)
.force( .force(
'link', 'link',
d3.forceLink().id((d) => d.hostname) d3.forceLink().id((d) => d.hostname)
) )
.force('collide', d3.forceCollide(80)) .force('collide', d3.forceCollide(defaultCollisionFactor))
.force('forceX', d3.forceX(0)) .force('forceX', d3.forceX(defaultForceX))
.force('forceY', d3.forceY(0)) .force('forceY', d3.forceY(defaultForceY))
.force('center', d3.forceCenter(width / 2, height / 2)); .force('center', d3.forceCenter(width / 2, height / 2));
// const simulation = d3
// .forceSimulation()
// .force(
// 'link',
// d3.forceLink().id((d) => d.hostname)
// )
// .force('charge', d3.forceManyBody().strength(-350))
// .force(
// 'collide',
// d3.forceCollide((d) =>
// d.node_type === 'execution' || d.node_type === 'hop' ? 75 : 100
// )
// )
// .force('center', d3.forceCenter(width / 2, height / 2));
const link = svg const link = svg
.append('g') .append('g')
.attr('class', `links`) .attr('class', `links`)
.attr('data-cy', 'links') .attr('data-cy', 'links')
// .selectAll('path')
.selectAll('line') .selectAll('line')
.data(graph.links) .data(graph.links)
.enter() .enter()
.append('line') .append('line')
// .append('path')
.attr('class', (d, i) => `link-${i}`) .attr('class', (d, i) => `link-${i}`)
.attr('data-cy', (d) => `${d.source}-${d.target}`) .attr('data-cy', (d) => `${d.source}-${d.target}`)
.style('fill', 'none') .style('fill', 'none')
@@ -183,99 +192,62 @@ function MeshGraph({ redirectToDetailsPage }) {
.on('mouseenter', function handleNodeHover(_, 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
.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>`
)
.style('visibility', 'visible');
}) })
.on('mouseleave', (_, d) => { .on('mouseleave', (_, d) => {
deselectSiblings(d); deselectSiblings(d);
tooltip.html(``).style('visibility', 'hidden');
}) })
.on('click', (_, d) => { .on('click', (_, d) => {
if (d.node_type !== 'hop') { setNodeDetail(d);
redirectToDetailsPage(d); highlightSelected(d);
}
}); });
// health rings on nodes // node circles
node
.append('circle')
.attr('r', 8)
.attr('class', (d) => d.node_state)
.attr('stroke', (d) => renderHealthColor(d.node_state))
.attr('fill', (d) => renderHealthColor(d.node_state));
// inner node ring
node 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', (d) => renderNodeColor(d.node_type)) .attr('fill', defaultNodeColor)
.attr('stroke', 'white'); .attr('stroke', defaultNodeLabelColor);
svg.call(expandGlow);
// legend // node type labels
svg.append('text').attr('x', 10).attr('y', 20).text('Legend');
svg
.append('g')
.selectAll('g')
.attr('class', 'chart-legend')
.attr('data-cy', 'chart-legend')
.data(graph.nodes)
.enter()
.append('circle')
.attr('cx', 10)
.attr('cy', (d, i) => 50 + i * 25)
.attr('r', defaultRadius)
.attr('class', (d) => d.node_type)
.style('fill', (d) => renderNodeColor(d.node_type));
// legend text
svg
.append('g')
.attr('class', 'chart-text')
.attr('data-cy', 'chart-text')
.selectAll('g')
.data(graph.nodes)
.enter()
.append('text')
.attr('x', 20)
.attr('y', (d, i) => 50 + i * 25)
.text((d) => `${d.hostname} - ${d.node_type}`)
.attr('text-anchor', 'left')
.style('alignment-baseline', 'middle');
const tooltip = d3
.select('#chart')
.append('div')
.attr('class', 'd3-tooltip')
.attr('data-cy', 'd3-tooltip')
.style('position', 'absolute')
.style('top', '200px')
.style('right', '40px')
.style('z-index', '10')
.style('visibility', 'hidden')
.style('padding', '15px')
// .style('border', '1px solid #e6e6e6')
// .style('box-shadow', '5px 5px 5px #e6e6e6')
.style('max-width', '15%')
// .style('background', 'rgba(0,0,0,0.6)')
// .style('border-radius', '5px')
// .style('color', '#fff')
.style('font-family', 'sans-serif')
.style('color', '#e6e6e')
.text('');
// node labels
node node
.append('text') .append('text')
.text((d) => d.hostname) .text((d) => renderNodeType(d.node_type))
.attr('x', 16) .attr('text-anchor', 'middle')
.attr('y', 3); .attr('alignment-baseline', 'central')
.attr('fill', defaultNodeLabelColor);
// node hostname labels
const hostNames = node.append('g');
hostNames
.append('text')
.text((d) => renderLabelText(d.node_state, d.hostname))
.attr('fill', defaultNodeLabelColor)
.attr('text-anchor', 'middle')
.attr('y', 40)
.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', bbox.x)
.attr('y', bbox.y)
.attr('width', bbox.width)
.attr('height', bbox.height)
.attr('rx', 8)
.attr('ry', 8)
.style('fill', (d) => renderStateColor(d.node_state));
});
hostNames
.append('text')
.text((d) => renderLabelText(d.node_state, d.hostname))
.attr('font-size', defaultFontSize)
.attr('fill', defaultNodeLabelColor)
.attr('text-anchor', 'middle')
.attr('y', 38);
simulation.nodes(graph.nodes).on('tick', ticked); simulation.nodes(graph.nodes).on('tick', ticked);
simulation.force('link').links(graph.links); simulation.force('link').links(graph.links);
@@ -291,54 +263,37 @@ function MeshGraph({ redirectToDetailsPage }) {
node.attr('transform', (d) => `translate(${d.x},${d.y})`); node.attr('transform', (d) => `translate(${d.x},${d.y})`);
} }
// function linkArc(d) { function renderStateColor(nodeState) {
// const dx = d.target.x - d.source.x;
// const dy = d.target.y - d.source.y;
// const dr = Math.sqrt(dx * dx + dy * dy);
// return `M${d.source.x},${d.source.y}A${dr},${dr} 0 0,1 ${d.target.x},${d.target.y}`;
// }
function contractGlow() {
svg
.selectAll('.healthy')
.transition()
.duration(1000)
.attr('stroke-width', '1px')
.on('end', expandGlow);
}
function expandGlow() {
svg
.selectAll('.healthy')
.transition()
.duration(1000)
.attr('stroke-width', '4.5px')
.on('end', contractGlow);
}
function renderHealthColor(nodeState) {
const colorKey = { const colorKey = {
disabled: '#c6c6c6', disabled: '#6A6E73',
healthy: '#50D050', healthy: '#3E8635',
error: '#ff6766', error: '#C9190B',
}; };
return colorKey[nodeState]; return colorKey[nodeState];
} }
function renderLabelText(nodeState, name) {
const stateKey = {
disabled: '\u25EF',
healthy: '\u2713',
error: '\u0021',
};
return `${stateKey[nodeState]} ${name}`;
}
function renderNodeColor(nodeType) { function renderNodeType(nodeType) {
const colorKey = { const typeKey = {
hop: '#C46100', hop: 'h',
execution: '#F0AB00', execution: 'Ex',
hybrid: '#0066CC', hybrid: 'Hy',
control: '#005F60', control: 'C',
}; };
return colorKey[nodeType]; return typeKey[nodeType];
} }
function highlightSiblings(n) { function highlightSiblings(n) {
setTimeout(() => { setTimeout(() => {
svg.selectAll(`id-${n.id}`).attr('r', highlightRadius); svg.select(`circle.id-${n.id}`).attr('fill', defaultNodeHighlightColor);
const immediate = graph.links.filter( const immediate = graph.links.filter(
(l) => (l) =>
n.hostname === l.source.hostname || n.hostname === l.target.hostname n.hostname === l.source.hostname || n.hostname === l.target.hostname
@@ -347,50 +302,91 @@ function MeshGraph({ redirectToDetailsPage }) {
svg svg
.selectAll(`.link-${s.index}`) .selectAll(`.link-${s.index}`)
.transition() .transition()
.style('stroke', '#6e6e6e'); .style('stroke', '#0066CC')
svg .style('stroke-width', '3px');
.selectAll(`.id-${s.source.id}`)
.transition()
.attr('r', highlightRadius);
svg
.selectAll(`.id-${s.target.id}`)
.transition()
.attr('r', highlightRadius);
}); });
}, 0); }, 0);
} }
function deselectSiblings(n) { function deselectSiblings(n) {
svg.selectAll(`id-${n.id}`).attr('r', defaultRadius); svg.select(`circle.id-${n.id}`).attr('fill', defaultNodeColor);
const immediate = graph.links.filter( const immediate = graph.links.filter(
(l) => (l) =>
n.hostname === l.source.hostname || n.hostname === l.target.hostname n.hostname === l.source.hostname || n.hostname === l.target.hostname
); );
immediate.forEach((s) => { immediate.forEach((s) => {
svg.selectAll(`.link-${s.index}`).transition().style('stroke', '#ccc');
svg svg
.selectAll(`.id-${s.source.id}`) .selectAll(`.link-${s.index}`)
.transition() .transition()
.attr('r', defaultRadius); .style('stroke', '#ccc')
svg .style('stroke-width', '2px');
.selectAll(`.id-${s.target.id}`)
.transition()
.attr('r', defaultRadius);
}); });
} }
}, []); // eslint-disable-line react-hooks/exhaustive-deps
function highlightSelected(n) {
if (svg.select(`circle.id-${n.id}`).attr('stroke-width') !== null) {
// toggle rings
svg.select(`circle.id-${n.id}`).attr('stroke-width', null);
// show default empty state of tooltip
setIsNodeSelected(false);
setSelectedNode(null);
return;
}
svg.selectAll('circle').attr('stroke-width', null);
svg
.select(`circle.id-${n.id}`)
.attr('stroke-width', '5px')
.attr('stroke', '#D2D2D2');
setIsNodeSelected(true);
setSelectedNode(n);
}
};
async function redirectToDetailsPage() {
const { id: nodeId } = selectedNode;
const {
data: { results },
} = await InstancesAPI.readInstanceGroup(nodeId);
const { id: instanceGroupId } = results[0];
const constructedURL = `/instance_groups/${instanceGroupId}/instances/${nodeId}/details`;
history.push(constructedURL);
}
function renderNodeIcon() {
if (selectedNode) {
const { node_type: nodeType } = selectedNode;
const typeKey = {
hop: 'h',
execution: 'Ex',
hybrid: 'Hy',
control: 'C',
};
return typeKey[nodeType];
}
return false;
}
useEffect(() => { useEffect(() => {
function handleResize() { function handleResize() {
draw(); draw();
} }
window.addEventListener('resize', debounce(handleResize, 500)); window.addEventListener('resize', debounce(handleResize, 500));
draw(); handleResize();
return () => window.removeEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);
}, [draw]); }, []); // eslint-disable-line react-hooks/exhaustive-deps
return <div id="chart" />; return (
<div id="chart" style={{ position: 'relative' }}>
<Legend />
<Tooltip
isNodeSelected={isNodeSelected}
renderNodeIcon={renderNodeIcon}
nodeDetail={nodeDetail}
redirectToDetailsPage={redirectToDetailsPage}
/>
</div>
);
} }
export default MeshGraph; export default MeshGraph;

View File

@@ -0,0 +1,107 @@
/* eslint-disable i18next/no-literal-string,
jsx-a11y/anchor-is-valid,
jsx-a11y/click-events-have-key-events,
jsx-a11y/no-static-element-interactions */
import React from 'react';
import styled from 'styled-components';
import {
Button as PFButton,
DescriptionList as PFDescriptionList,
DescriptionListTerm,
DescriptionListGroup as PFDescriptionListGroup,
DescriptionListDescription,
Divider,
TextContent,
Text as PFText,
TextVariants,
} from '@patternfly/react-core';
import StatusLabel from 'components/StatusLabel';
const Wrapper = styled.div`
position: absolute;
top: -20px;
right: 0;
padding: 10px;
width: 20%;
`;
const Button = styled(PFButton)`
width: 20px;
height: 20px;
border-radius: 10px;
padding: 0;
font-size: 11px;
`;
const DescriptionList = styled(PFDescriptionList)`
gap: 0;
`;
const DescriptionListGroup = styled(PFDescriptionListGroup)`
align-items: center;
margin-top: 10px;
`;
const Text = styled(PFText)`
margin: 10px 0 5px;
`;
function Tooltip({
isNodeSelected,
renderNodeIcon,
nodeDetail,
redirectToDetailsPage,
}) {
return (
<Wrapper class="tooltip" data-cy="tooltip">
{isNodeSelected === false ? (
<TextContent>
<Text
component={TextVariants.small}
style={{ 'font-weight': 'bold', color: 'black' }}
>
Details
</Text>
<Divider component="div" />
<Text component={TextVariants.small}>
Click on a node icon to display the details.
</Text>
</TextContent>
) : (
<>
<TextContent>
<Text
component={TextVariants.small}
style={{ 'font-weight': 'bold', color: 'black' }}
>
Details
</Text>
<Divider component="div" />
</TextContent>
<DescriptionList isHorizontal isFluid>
<DescriptionListGroup>
<DescriptionListTerm>
<Button variant="primary" isSmall>
{renderNodeIcon()}
</Button>
</DescriptionListTerm>
<DescriptionListDescription>
<a onClick={redirectToDetailsPage}>{nodeDetail.hostname}</a>
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Type</DescriptionListTerm>
<DescriptionListDescription>
{nodeDetail.node_type} node
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Status</DescriptionListTerm>
<DescriptionListDescription>
<StatusLabel status={nodeDetail.node_state} />
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</>
)}
</Wrapper>
);
}
export default Tooltip;

View File

@@ -1,15 +1,14 @@
import React, { useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import ScreenHeader from 'components/ScreenHeader/ScreenHeader'; import ScreenHeader from 'components/ScreenHeader/ScreenHeader';
import { PageSection, Card, CardBody } from '@patternfly/react-core'; import { PageSection, Card, CardBody } from '@patternfly/react-core';
import useRequest from 'hooks/useRequest'; import useRequest from 'hooks/useRequest';
import { MeshAPI, InstancesAPI } from 'api'; import { MeshAPI } from 'api';
import MeshGraph from './MeshGraph'; import MeshGraph from './MeshGraph';
function TopologyView() { function TopologyView() {
const { const {
isLoading,
result: { meshData }, result: { meshData },
// error: fetchInitialError, // error: fetchInitialError,
request: fetchMeshVisualizer, request: fetchMeshVisualizer,
@@ -22,15 +21,6 @@ function TopologyView() {
}, []), }, []),
{ meshData: { nodes: [], links: [] } } { meshData: { nodes: [], links: [] } }
); );
async function RedirectToDetailsPage({ id: nodeId }) {
const history = useHistory();
const {
data: { results },
} = await InstancesAPI.readInstanceGroup(nodeId);
const { id: instanceGroupId } = results[0];
const constructedURL = `/instance_groups/${instanceGroupId}/instances/${nodeId}/details`;
history.push(constructedURL);
}
useEffect(() => { useEffect(() => {
fetchMeshVisualizer(); fetchMeshVisualizer();
}, [fetchMeshVisualizer]); }, [fetchMeshVisualizer]);
@@ -40,14 +30,7 @@ function TopologyView() {
<PageSection> <PageSection>
<Card> <Card>
<CardBody> <CardBody>{!isLoading && <MeshGraph data={meshData} />}</CardBody>
{meshData && (
<MeshGraph
data={meshData}
redirectToDetailsPage={RedirectToDetailsPage}
/>
)}
</CardBody>
</Card> </Card>
</PageSection> </PageSection>
</> </>