mirror of https://github.com/linkerd/linkerd2.git
Rewrite octopus arm code to be more parameterized and flexible. (#1834)
As a result, displays better in the material UI version of the dashboard. Also adds Success rate to data displayed on neighbour nodes. * Rewrite octopus arm code to be more parameterized and flexible. As a result, displays better in the material UI version of the dashboard. * Add Success rate to data displayed on neighbour nodes * Fix variablilty in grid spacing by fixing the max and min widths of the chart, and by scrolling the overflow * Center the octopus graph so it looks better at full width * Also add padding, so that the drop shadows aren't cut off
This commit is contained in:
parent
f777f87924
commit
6d8911090d
|
@ -52,3 +52,22 @@ div.success-rate-dot {
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
div.octopus-graph-container {
|
||||
overflow-x: scroll;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
div.octopus-graph {
|
||||
max-width: 974px;
|
||||
min-width: 974px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.octopus-body-node.neighbor {
|
||||
width: 220px;
|
||||
}
|
||||
.octopus-body-node.main {
|
||||
width: 244px;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { OctopusArms, baseHeight } from './util/OctopusArms.jsx';
|
||||
import { displayName, metricToFormatter } from './util/Utils.js';
|
||||
|
||||
import Card from '@material-ui/core/Card';
|
||||
import CardContent from '@material-ui/core/CardContent';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import OctopusArms from './util/OctopusArms.jsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { StyledProgress } from './util/Progress.jsx';
|
||||
|
@ -17,7 +17,6 @@ import { getSuccessRateClassification } from './util/MetricUtils.jsx' ;
|
|||
|
||||
const maxNumNeighbors = 6; // max number of neighbor nodes to show in the octopus graph
|
||||
|
||||
|
||||
export default class Octopus extends React.Component {
|
||||
static defaultProps = {
|
||||
neighbors: {},
|
||||
|
@ -75,17 +74,21 @@ export default class Octopus extends React.Component {
|
|||
|
||||
return (
|
||||
<Grid item key={resource.name} >
|
||||
<Card className={`octopus-body ${type}`} title={display}>
|
||||
<Card className={`octopus-body-node ${type}`} title={display}>
|
||||
<CardContent>
|
||||
|
||||
<Typography variant={type === "neighbor" ? "h6" : "h4"} align="center">
|
||||
<Typography variant={type === "neighbor" ? "subtitle1" : "h6"} align="center">
|
||||
{ this.linkedResourceTitle(resource, display) }
|
||||
</Typography>
|
||||
|
||||
<Progress variant="determinate" value={resource.successRate * 100} />
|
||||
|
||||
<Table>
|
||||
<Table padding="dense">
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell><Typography>SR</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{metricToFormatter["SUCCESS_RATE"](resource.successRate)}</Typography></TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell><Typography>RPS</Typography></TableCell>
|
||||
<TableCell numeric={true}><Typography>{metricToFormatter["NO_UNIT"](resource.requestRate)}</Typography></TableCell>
|
||||
|
@ -105,15 +108,15 @@ export default class Octopus extends React.Component {
|
|||
renderUnmeshedResources = unmeshedResources => {
|
||||
return (
|
||||
<Grid item>
|
||||
<Card key="unmeshed-resources">
|
||||
<Card key="unmeshed-resources" className="octopus-body-node neighbor">
|
||||
<CardContent>
|
||||
<Typography variant="h6">Unmeshed</Typography>
|
||||
<Typography variant="subtitle1">Unmeshed</Typography>
|
||||
{
|
||||
_.map(unmeshedResources, r => {
|
||||
let display = displayName(r);
|
||||
return <Typography key={display} variant="body2" title={display}>{display}</Typography>;
|
||||
})
|
||||
}
|
||||
_.map(unmeshedResources, r => {
|
||||
let display = displayName(r);
|
||||
return <Typography key={display} variant="body2" title={display}>{display}</Typography>;
|
||||
})
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
@ -122,20 +125,23 @@ export default class Octopus extends React.Component {
|
|||
|
||||
renderCollapsedNeighbors = neighbors => {
|
||||
return (
|
||||
<Grid item key="unmeshed-resources">
|
||||
{
|
||||
_.map(neighbors, r => {
|
||||
let display = displayName(r);
|
||||
return <div key={display}>{this.linkedResourceTitle(r, display)}</div>;
|
||||
})
|
||||
}
|
||||
<Grid item>
|
||||
<Card className="octopus-body-node neighbor">
|
||||
<CardContent>
|
||||
{
|
||||
_.map(neighbors, r => {
|
||||
let display = displayName(r);
|
||||
return <Typography key={display}>{this.linkedResourceTitle(r, display)}</Typography>;
|
||||
})
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
renderArrowCol = (numNeighbors, isOutbound) => {
|
||||
let baseHeight = 180;
|
||||
let width = 75;
|
||||
let width = 80;
|
||||
let showArrow = numNeighbors > 0;
|
||||
let isEven = numNeighbors % 2 === 0;
|
||||
let middleElementIndex = isEven ? ((numNeighbors - 1) / 2) : _.floor(numNeighbors / 2);
|
||||
|
@ -159,7 +165,7 @@ export default class Octopus extends React.Component {
|
|||
{
|
||||
_.map(arrowTypes, arrow => {
|
||||
let arrowType = isOutbound ? arrow.type : arrow.inboundType;
|
||||
return OctopusArms[arrowType](width, height, arrow.height, isOutbound);
|
||||
return OctopusArms[arrowType](width, height, arrow.height, isOutbound, isEven);
|
||||
})
|
||||
}
|
||||
</svg>
|
||||
|
@ -179,57 +185,59 @@ export default class Octopus extends React.Component {
|
|||
|
||||
let numUpstreams = _.size(display.upstreams.displayed) + (_.isEmpty(unmeshedSources) ? 0 : 1) +
|
||||
(_.isEmpty(display.upstreams.collapsed) ? 0 : 1);
|
||||
let hasUpstreams = numUpstreams > 0;
|
||||
|
||||
let numDownstreams = _.size(display.downstreams.displayed) + (_.isEmpty(display.downstreams.collapsed) ? 0 : 1);
|
||||
let hasDownstreams = numDownstreams > 0;
|
||||
|
||||
|
||||
return (
|
||||
<div className="octopus-graph">
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justify="center"
|
||||
alignItems="center">
|
||||
|
||||
<div className="octopus-graph-container">
|
||||
<div className="octopus-graph">
|
||||
<Grid
|
||||
container
|
||||
spacing={24}
|
||||
direction="column"
|
||||
direction="row"
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
item
|
||||
xs={3}
|
||||
className={`octopus-col ${hasUpstreams ? "resource-col" : ""}`}>
|
||||
{_.map(display.upstreams.displayed, n => this.renderResourceCard(n, "neighbor"))}
|
||||
{_.isEmpty(unmeshedSources) ? null : this.renderUnmeshedResources(unmeshedSources)}
|
||||
{_.isEmpty(display.upstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.upstreams.collapsed)}
|
||||
</Grid>
|
||||
alignItems="center">
|
||||
|
||||
<Grid item xs={1} className="octopus-col">
|
||||
{this.renderArrowCol(numUpstreams, false)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={4} className="octopus-col resource-col">
|
||||
{this.renderResourceCard(resource, "main")}
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
spacing={24}
|
||||
direction="column"
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
item
|
||||
xs={3}>
|
||||
{_.map(display.upstreams.displayed, n => this.renderResourceCard(n, "neighbor"))}
|
||||
{_.isEmpty(unmeshedSources) ? null : this.renderUnmeshedResources(unmeshedSources)}
|
||||
{_.isEmpty(display.upstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.upstreams.collapsed)}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1} className="octopus-col">
|
||||
{this.renderArrowCol(numDownstreams, true)}
|
||||
</Grid>
|
||||
<Grid item xs={1}>
|
||||
{this.renderArrowCol(numUpstreams, false)}
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={24}
|
||||
direction="column"
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
item
|
||||
xs={3}
|
||||
className={`octopus-col ${hasDownstreams ? "resource-col" : ""}`}>
|
||||
{_.map(display.downstreams.displayed, n => this.renderResourceCard(n, "neighbor"))}
|
||||
{_.isEmpty(display.downstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.downstreams.collapsed)}
|
||||
|
||||
<Grid item xs={3}>
|
||||
{this.renderResourceCard(resource, "main")}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1}>
|
||||
{this.renderArrowCol(numDownstreams, true)}
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={24}
|
||||
direction="column"
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
item
|
||||
xs={3}>
|
||||
{_.map(display.downstreams.displayed, n => this.renderResourceCard(n, "neighbor"))}
|
||||
{_.isEmpty(display.downstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.downstreams.collapsed)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,68 +1,90 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import grey from '@material-ui/core/colors/grey';
|
||||
|
||||
const stroke = "#000000";
|
||||
const strokeOpacity = "0.2";
|
||||
const strokeOpacity = "0.7";
|
||||
const arrowColor = grey[500];
|
||||
|
||||
const svgArrow = (x1, y1, width, height, direction) => {
|
||||
let segmentWidth = width / 2 - 10;
|
||||
export const baseHeight = 220; // the height of the neighbor node box
|
||||
const halfBoxHeight = baseHeight / 2;
|
||||
const controlPoint = 10; // width and height of the control points for the bezier curves
|
||||
const inboundAlignment = controlPoint * 2;
|
||||
|
||||
let x2 = x1 + segmentWidth + 10;
|
||||
let y2 = y1 + 10;
|
||||
const generateSvgComponents = (y1, width, height) => {
|
||||
let segmentWidth = width / 2 - controlPoint; // width of each horizontal arrow segment
|
||||
|
||||
let x3 = x2 + 10;
|
||||
let y3 = y2 + height + 10;
|
||||
let x1 = 0;
|
||||
|
||||
let horizLine1 = `M ${x1},${y1} L ${x1+segmentWidth},${y1}`;
|
||||
let curve1 = `C ${x1+segmentWidth+7},${y1} ${x2},${y1+3} ${x2},${y2}`;
|
||||
let verticalLine = `L ${x2}, ${y2+height}`;
|
||||
let curve2 = `C ${x2},${y2+height+6} ${x2+2},${y3} ${x3},${y3}`;
|
||||
let horizLine2 = `L ${x3 + segmentWidth},${y3}`;
|
||||
let arrow = `${horizLine1} ${curve1} ${verticalLine} ${curve2} ${horizLine2}`;
|
||||
let x2 = x1 + segmentWidth;
|
||||
let x3 = x2 + controlPoint;
|
||||
|
||||
let y2 = y1 - controlPoint;
|
||||
let y3 = y2 - height;
|
||||
let y4 = y3 - controlPoint;
|
||||
|
||||
let x4 = x3 + controlPoint;
|
||||
let x5 = x4 + segmentWidth;
|
||||
|
||||
let start = `M ${x1},${y1}`;
|
||||
let horizLine1 = `L ${x2},${y1}`;
|
||||
let curve1 = `C ${x3},${y1} ${x3},${y1}`;
|
||||
let curve1End = `${x3},${y2}`;
|
||||
let verticalLineEnd = `L ${x3},${y3}`;
|
||||
let curve2 = `C ${x3},${y4} ${x3},${y4}`;
|
||||
let curve2End = `${x4},${y4}`;
|
||||
let horizLine2 = `L ${x5},${y4}`;
|
||||
|
||||
let arrowPath = _.join([start, horizLine1, curve1, curve1End, verticalLineEnd, curve2, curve2End, horizLine2], " ");
|
||||
|
||||
let arrowEndX = width;
|
||||
let arrowEndY = direction === "up" ? y1 : y3;
|
||||
let arrowEndY = y4;
|
||||
let arrowHead = `${arrowEndX - 4} ${arrowEndY - 4} ${arrowEndX} ${arrowEndY} ${arrowEndX - 4} ${arrowEndY + 4}`;
|
||||
|
||||
let circle = {
|
||||
cx: x1,
|
||||
cy: direction === "up" ? y3 : y1
|
||||
};
|
||||
let circle = { cx: x1, cy: y1 };
|
||||
|
||||
return {
|
||||
arrow,
|
||||
arrowHead,
|
||||
circle
|
||||
arrowPath,
|
||||
circle,
|
||||
arrowHead
|
||||
};
|
||||
};
|
||||
|
||||
const up = (width, svgHeight, arrowHeight, isOutbound) => {
|
||||
let height = (svgHeight / 2) - arrowHeight;
|
||||
|
||||
let x1 = 0;
|
||||
let y1;
|
||||
|
||||
if (isOutbound) {
|
||||
y1 = arrowHeight - 20; // I don't know where we intoduced that 20 but we need to match the other arrow
|
||||
} else {
|
||||
y1 = svgHeight / 2 ;
|
||||
}
|
||||
|
||||
let svgPaths = svgArrow(x1, y1, width, height, "up");
|
||||
|
||||
const arrowG = (id, arm, transform) => {
|
||||
return (
|
||||
<g key={`up-arrow-${height}`} id="downstream-up" fill="none" stroke="none" strokeWidth="1">
|
||||
<g key={id} id={id} fill="none" strokeWidth="1">
|
||||
<path
|
||||
d={svgPaths.arrow}
|
||||
stroke={stroke}
|
||||
strokeOpacity={strokeOpacity}
|
||||
transform="translate(31, 54) scale(-1, 1) translate(-44, -54) " />
|
||||
<circle cx={svgPaths.circle.cx} cy={svgPaths.circle.cy} fill="#CCCCCC" r="4" />
|
||||
<polyline points={svgPaths.arrowHead} stroke="#CCCCCC" strokeLinecap="round" />
|
||||
d={arm.arrowPath}
|
||||
stroke={arrowColor}
|
||||
transform={transform}
|
||||
strokeOpacity={strokeOpacity} />
|
||||
<circle
|
||||
cx={arm.circle.cx}
|
||||
cy={arm.circle.cy}
|
||||
transform={transform}
|
||||
fill={arrowColor}
|
||||
r="4" />
|
||||
<polyline
|
||||
points={arm.arrowHead}
|
||||
stroke={arrowColor}
|
||||
strokeLinecap="round"
|
||||
transform={transform} />
|
||||
</g>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
const up = (width, svgHeight, arrowHeight, isOutbound, isEven) => {
|
||||
let height = arrowHeight + (isEven ? 0 : halfBoxHeight);
|
||||
|
||||
// up arrows start and the center of the middle node for outbound arms,
|
||||
// and at the noce position for inbound arms
|
||||
let y1 = isOutbound ? svgHeight / 2 : arrowHeight;
|
||||
let arm = generateSvgComponents(y1, width, height);
|
||||
|
||||
let translate = isOutbound ? null : `translate(0, ${svgHeight / 2 + (isEven ? 0 : halfBoxHeight) + inboundAlignment})`;
|
||||
|
||||
return arrowG(`up-arrow-${height}`, arm, translate);
|
||||
};
|
||||
|
||||
const flat = (width, height) => {
|
||||
let arrowY = height / 2;
|
||||
let arrowEndX = width;
|
||||
|
@ -72,10 +94,10 @@ const flat = (width, height) => {
|
|||
<g key="flat-arrow" id="downstream-flat" fill="none" stroke="none" strokeWidth="1">
|
||||
<path
|
||||
d={`M0,${arrowY} L${arrowEndX},${arrowY}`}
|
||||
stroke={stroke}
|
||||
stroke={arrowColor}
|
||||
strokeOpacity={strokeOpacity} />
|
||||
<circle cx="0" cy={arrowY} fill="#CCCCCC" r="4" />
|
||||
<polyline points={polylinePoints} stroke="#CCCCCC" strokeLinecap="round" />
|
||||
<circle cx="0" cy={arrowY} fill={arrowColor} r="4" />
|
||||
<polyline points={polylinePoints} stroke={arrowColor} strokeLinecap="round" />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
@ -85,36 +107,21 @@ const down = (width, svgHeight, arrowHeight, isOutbound) => {
|
|||
// have end of block n at (1/2 block height) + (block height * n-1)
|
||||
let height = (svgHeight / 2) - arrowHeight;
|
||||
|
||||
let x1 = 0;
|
||||
let y1;
|
||||
|
||||
// inbound arrows start at the offset of the card, and end in the center of the middle card
|
||||
// outbound arrows start in the center of the middle card, and end at the card's height
|
||||
if (isOutbound) {
|
||||
y1 = svgHeight / 2;
|
||||
} else {
|
||||
y1 = arrowHeight - 20; // I don't know where we intoduced that 20 but we need to match the other arrow
|
||||
}
|
||||
let y1 = isOutbound ? svgHeight / 2 : halfBoxHeight;
|
||||
|
||||
let svg = svgArrow(x1, y1, width, height, "down");
|
||||
let arm = generateSvgComponents(y1, width, height);
|
||||
|
||||
return (
|
||||
<g key={`down-arrow-${height}`} id="downstream-down" fill="none" stroke="none" strokeWidth="1">
|
||||
<path
|
||||
d={svg.arrow}
|
||||
stroke={stroke}
|
||||
strokeOpacity={strokeOpacity} />
|
||||
<circle cx={svg.circle.cx} cy={svg.circle.cy} fill="#CCCCCC" r="4" />
|
||||
<polyline points={svg.arrowHead} stroke="#CCCCCC" strokeLinecap="round" />
|
||||
</g>
|
||||
let translate = `translate(0, ${isOutbound ? svgHeight : svgHeight / 2 - height + halfBoxHeight - inboundAlignment})`;
|
||||
let reflect = "scale(1, -1)";
|
||||
let transform = `${translate} ${reflect}`;
|
||||
|
||||
);
|
||||
return arrowG(`down-arrow-${height}`, arm, transform);
|
||||
};
|
||||
|
||||
const OctopusArms = {
|
||||
export const OctopusArms = {
|
||||
up,
|
||||
flat,
|
||||
down
|
||||
};
|
||||
|
||||
export default OctopusArms;
|
||||
|
|
Loading…
Reference in New Issue