mirror of https://github.com/linkerd/linkerd2.git
279 lines
8.9 KiB
JavaScript
279 lines
8.9 KiB
JavaScript
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 PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import { StyledProgress } from './util/Progress.jsx';
|
|
import Table from '@material-ui/core/Table';
|
|
import TableBody from '@material-ui/core/TableBody';
|
|
import TableCell from '@material-ui/core/TableCell';
|
|
import TableRow from '@material-ui/core/TableRow';
|
|
import Typography from '@material-ui/core/Typography';
|
|
import _ceil from 'lodash/ceil';
|
|
import _floor from 'lodash/floor';
|
|
import _get from 'lodash/get';
|
|
import _isEmpty from 'lodash/isEmpty';
|
|
import _isNil from 'lodash/isNil';
|
|
import _orderBy from 'lodash/orderBy';
|
|
import _size from 'lodash/size';
|
|
import _slice from 'lodash/slice';
|
|
import _take from 'lodash/take';
|
|
import _times from 'lodash/times';
|
|
import { getSuccessRateClassification } from './util/MetricUtils.jsx' ;
|
|
import { withStyles } from "@material-ui/core/styles";
|
|
|
|
const maxNumNeighbors = 6; // max number of neighbor nodes to show in the octopus graph
|
|
|
|
const styles = () => ({
|
|
graphContainer: {
|
|
overflowX: "scroll",
|
|
padding: "16px 0"
|
|
},
|
|
graph: {
|
|
maxWidth: "974px",
|
|
minWidth: "974px",
|
|
marginLeft: "auto",
|
|
marginRight: "auto"
|
|
},
|
|
centerNode: {
|
|
width: "244px"
|
|
},
|
|
neighborNode: {
|
|
width: "220px"
|
|
}
|
|
});
|
|
class Octopus extends React.Component {
|
|
static defaultProps = {
|
|
neighbors: {},
|
|
resource: {},
|
|
unmeshedSources: []
|
|
}
|
|
|
|
static propTypes = {
|
|
classes: PropTypes.shape({}).isRequired,
|
|
neighbors: PropTypes.shape({}),
|
|
resource: PropTypes.shape({}),
|
|
unmeshedSources: PropTypes.arrayOf(PropTypes.shape({})),
|
|
}
|
|
|
|
getNeighborDisplayData = neighbors => {
|
|
// only display maxNumNeighbors neighboring nodes in the octopus graph,
|
|
// otherwise it will be really tall
|
|
let upstreams = _orderBy(neighbors.upstream, n => n.successRate);
|
|
let downstreams = _orderBy(neighbors.downstream, n => n.successRate);
|
|
|
|
let display = {
|
|
upstreams: {
|
|
displayed: upstreams,
|
|
collapsed: []
|
|
},
|
|
downstreams: {
|
|
displayed: downstreams,
|
|
collapsed: []
|
|
}
|
|
};
|
|
|
|
if (_size(upstreams) > maxNumNeighbors) {
|
|
display.upstreams.displayed = _take(upstreams, maxNumNeighbors);
|
|
display.upstreams.collapsed = _slice(upstreams, maxNumNeighbors, _size(upstreams));
|
|
}
|
|
|
|
if (_size(downstreams) > maxNumNeighbors) {
|
|
display.downstreams.displayed = _take(downstreams, maxNumNeighbors);
|
|
display.downstreams.collapsed = _slice(downstreams, maxNumNeighbors, _size(downstreams));
|
|
}
|
|
|
|
return display;
|
|
}
|
|
|
|
linkedResourceTitle = (resource, display) => {
|
|
return _isNil(resource.namespace) ? display :
|
|
<this.props.api.ResourceLink
|
|
resource={resource}
|
|
linkText={display} />;
|
|
}
|
|
|
|
renderResourceCard(resource, type) {
|
|
const { classes } = this.props;
|
|
let display = displayName(resource);
|
|
let classification = getSuccessRateClassification(resource.successRate);
|
|
let Progress = StyledProgress(classification);
|
|
|
|
return (
|
|
<Grid item key={resource.name} >
|
|
<Card className={type === "neighbor" ? classes.neighborNode : classes.centerNode} title={display}>
|
|
<CardContent>
|
|
|
|
<Typography variant={type === "neighbor" ? "subtitle1" : "h6"} align="center">
|
|
{ this.linkedResourceTitle(resource, display) }
|
|
</Typography>
|
|
|
|
<Progress variant="determinate" value={resource.successRate * 100} />
|
|
|
|
<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>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell><Typography>P99</Typography></TableCell>
|
|
<TableCell numeric={true}><Typography>{metricToFormatter["LATENCY"](_get(resource, "latency.P99"))}</Typography></TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
);
|
|
}
|
|
|
|
renderUnmeshedResources = unmeshedResources => {
|
|
const { classes } = this.props;
|
|
return (
|
|
<Grid item>
|
|
<Card key="unmeshed-resources" className={classes.neighborNode}>
|
|
<CardContent>
|
|
<Typography variant="subtitle1">Unmeshed</Typography>
|
|
{
|
|
unmeshedResources.map(r => {
|
|
let display = displayName(r);
|
|
return <Typography key={display} variant="body2" title={display}>{display}</Typography>;
|
|
})
|
|
}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
);
|
|
}
|
|
|
|
renderCollapsedNeighbors = neighbors => {
|
|
const { classes } = this.props;
|
|
return (
|
|
<Grid item>
|
|
<Card className={classes.neighborNode}>
|
|
<CardContent>
|
|
{
|
|
neighbors.map(r => {
|
|
let display = displayName(r);
|
|
return <Typography key={display}>{this.linkedResourceTitle(r, display)}</Typography>;
|
|
})
|
|
}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
);
|
|
}
|
|
|
|
renderArrowCol = (numNeighbors, isOutbound) => {
|
|
let width = 80;
|
|
let showArrow = numNeighbors > 0;
|
|
let isEven = numNeighbors % 2 === 0;
|
|
let middleElementIndex = isEven ? ((numNeighbors - 1) / 2) : _floor(numNeighbors / 2);
|
|
|
|
let arrowTypes = _times(numNeighbors, i => i).map(i => {
|
|
if (i < middleElementIndex) {
|
|
let height = (_ceil(middleElementIndex - i) - 1) * baseHeight + (baseHeight / 2);
|
|
return { type: "up", inboundType: "down", height };
|
|
} else if (i === middleElementIndex) {
|
|
return { type: "flat", inboundType: "flat", height: baseHeight };
|
|
} else {
|
|
let height = (_ceil(i - middleElementIndex) - 1) * baseHeight + (baseHeight / 2);
|
|
return { type: "down", inboundType: "up", height };
|
|
}
|
|
});
|
|
|
|
let height = numNeighbors * baseHeight;
|
|
let svg = (
|
|
<svg height={height} width={width} version="1.1" viewBox={`0 0 ${width} ${height}`}>
|
|
<defs />
|
|
{
|
|
arrowTypes.map(arrow => {
|
|
let arrowType = isOutbound ? arrow.type : arrow.inboundType;
|
|
return OctopusArms[arrowType](width, height, arrow.height, isOutbound, isEven);
|
|
})
|
|
}
|
|
</svg>
|
|
);
|
|
|
|
return !showArrow ? null : svg;
|
|
}
|
|
|
|
render() {
|
|
let { resource, neighbors, unmeshedSources, classes} = this.props;
|
|
|
|
if (_isEmpty(resource)) {
|
|
return null;
|
|
}
|
|
|
|
let display = this.getNeighborDisplayData(neighbors);
|
|
|
|
let numUpstreams = _size(display.upstreams.displayed) + (_isEmpty(unmeshedSources) ? 0 : 1) +
|
|
(_isEmpty(display.upstreams.collapsed) ? 0 : 1);
|
|
|
|
let numDownstreams = _size(display.downstreams.displayed) + (_isEmpty(display.downstreams.collapsed) ? 0 : 1);
|
|
|
|
|
|
return (
|
|
<div className={classes.graphContainer}>
|
|
<div className={classes.graph}>
|
|
<Grid
|
|
container
|
|
direction="row"
|
|
justify="center"
|
|
alignItems="center">
|
|
|
|
|
|
<Grid
|
|
container
|
|
spacing={24}
|
|
direction="column"
|
|
justify="center"
|
|
alignItems="center"
|
|
item
|
|
xs={3}>
|
|
{display.upstreams.displayed.map(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}>
|
|
{this.renderArrowCol(numUpstreams, false)}
|
|
</Grid>
|
|
|
|
|
|
<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}>
|
|
{ display.downstreams.displayed.map(n => this.renderResourceCard(n, "neighbor"))}
|
|
{_isEmpty(display.downstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.downstreams.collapsed)}
|
|
</Grid>
|
|
</Grid>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withStyles(styles)(Octopus);
|