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:
Risha Mars 2018-11-01 12:30:19 -07:00 committed by GitHub
parent f777f87924
commit 6d8911090d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 165 additions and 131 deletions

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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;