import { OctopusArms, inboundAlignment } 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 RootRef from '@material-ui/core/RootRef'; 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 _get from 'lodash/get'; import _isEmpty from 'lodash/isEmpty'; import _isNil from 'lodash/isNil'; import _size from 'lodash/size'; import _slice from 'lodash/slice'; import _sortBy from 'lodash/sortBy'; import _take from 'lodash/take'; 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: 'auto', padding: '16px 0', }, graph: { maxWidth: '974px', minWidth: '974px', marginLeft: 'auto', marginRight: 'auto', }, centerNode: { width: '244px', }, neighborNode: { width: '220px', }, collapsedNeighborName: { paddingTop: '10px', }, }); class Octopus extends React.Component { constructor(props) { super(props); this.upstreamsContainer = React.createRef(); this.downstreamsContainer = React.createRef(); this.upstreamsRefs = []; this.downstreamsRefs = []; } getNeighborDisplayData = neighbors => { // only display maxNumNeighbors neighboring nodes in the octopus graph, // otherwise it will be really tall // even though _sortBy is a stable sort, the order that this data is returned by the API // can change, so the original order of items can change; this means we have to sort by // name and by SR to ensure an actual stable sort const upstreams = _sortBy(_sortBy(neighbors.upstream, displayName), n => n.successRate); const downstreams = _sortBy(_sortBy(neighbors.downstream, displayName), n => n.successRate); const 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; } addElementToRefsList = (element, index, isOutbound, type = 'neighbor') => { if (element && (type !== 'main')) { if (isOutbound) { this.downstreamsRefs[index] = element; } else { this.upstreamsRefs[index] = element; } } } linkedResourceTitle = (resource, display) => { // trafficsplit leaf resources cannot be linked if (_isNil(resource.namespace) || resource.isLeafService) { return display; } const { api: { ResourceLink } } = this.props; return ; } renderResourceCard(resource, type, index, isOutbound) { const { classes } = this.props; const display = displayName(resource); const classification = getSuccessRateClassification(resource.successRate); const Progress = StyledProgress(classification); // if the resource only has TCP stats, display those instead let showTcp = false; // trafficsplit leaf with zero traffic should still show HTTP stats if (_isNil(resource.successRate) && _isNil(resource.requestRate) && resource.type !== 'service') { showTcp = true; } return ( this.addElementToRefsList(el, index, isOutbound, type)} key={`${resource.type}-${resource.name}`}> { this.linkedResourceTitle(resource, display) } {showTcp ? this.renderTCPStats(resource) : this.renderHttpStats(resource)}
); } renderHttpStats = resource => { return ( {resource.isLeafService && Weight {resource.tsStats.weight} } SR {metricToFormatter.SUCCESS_RATE(resource.successRate)} RPS {metricToFormatter.NO_UNIT(resource.requestRate)} {!resource.isApexService && P99 {metricToFormatter.LATENCY(_get(resource, 'latency.P99'))} } ); } renderTCPStats = resource => { const { tcp } = resource; return ( Conn {metricToFormatter.NO_UNIT(tcp.openConnections)} Read {metricToFormatter.BYTES(tcp.readRate)} Write {metricToFormatter.BYTES(tcp.writeRate)} ); } renderUnmeshedResources = (unmeshedResources, index, isOutbound) => { const { classes } = this.props; return ( this.addElementToRefsList(el, index, isOutbound)}> Unmeshed { unmeshedResources.map(r => { const display = displayName(r); return {display}; }) } ); } renderCollapsedNeighbors = (neighbors, index, isOutbound) => { const { classes } = this.props; const Progress = StyledProgress(); return ( this.addElementToRefsList(el, index, isOutbound)}> + { neighbors.length } more... { neighbors.map(r => { const display = displayName(r); return {this.linkedResourceTitle(r, display)}; }) } ); } renderArrowCol = (numNeighbors, isOutbound) => { const container = !isOutbound ? this.upstreamsContainer : this.downstreamsContainer; const refs = !isOutbound ? this.upstreamsRefs : this.downstreamsRefs; if (refs.length === 0 || container.current === undefined) { return null; } const width = 80; const fullHeight = container.current.offsetHeight; let arrowTypes = []; arrowTypes = refs.slice(0, numNeighbors).map(element => { const elementTop = element.offsetTop - container.current.offsetTop; const elementHeight = element.offsetHeight; const halfElement = elementHeight / 2; // Middle element if (Math.round(fullHeight / 2) === Math.round(elementTop + halfElement)) { const height = elementTop + halfElement; return { type: 'flat', inboundType: 'flat', height }; // Elements underneath main element } else if (elementTop + halfElement >= fullHeight / 2) { const height = !isOutbound ? elementTop - fullHeight / 2 + halfElement - inboundAlignment : fullHeight - elementTop - halfElement + inboundAlignment; return { type: 'down', inboundType: 'up', height, elementHeight }; // Elements over main element } else { const height = !isOutbound ? elementTop + halfElement + inboundAlignment : fullHeight / 2 - elementTop - halfElement - inboundAlignment; return { type: 'up', inboundType: 'down', height, elementHeight }; } }); const svg = ( { arrowTypes.map(arrow => { const arrowType = isOutbound ? arrow.type : arrow.inboundType; return OctopusArms[arrowType](width, fullHeight, arrow.height, isOutbound, arrow.elementHeight); }) } ); return svg; } render() { const { resource, neighbors, unmeshedSources, classes } = this.props; if (_isEmpty(resource)) { return null; } const display = this.getNeighborDisplayData(neighbors); const numUpstreams = _size(display.upstreams.displayed) + (_isEmpty(unmeshedSources) ? 0 : 1) + (_isEmpty(display.upstreams.collapsed) ? 0 : 1); const numDownstreams = _size(display.downstreams.displayed) + (_isEmpty(display.downstreams.collapsed) ? 0 : 1); return (
{display.upstreams.displayed.map((n, index) => this.renderResourceCard(n, 'neighbor', index, false))} {_isEmpty(unmeshedSources) ? null : this.renderUnmeshedResources(unmeshedSources, display.upstreams.displayed.length, false)} {_isEmpty(display.upstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.upstreams.collapsed, _isEmpty(unmeshedSources) ? display.upstreams.displayed.length : display.upstreams.displayed.length + 1, false)} {this.renderArrowCol(numUpstreams, false)} {this.renderResourceCard(resource, 'main')} {this.renderArrowCol(numDownstreams, true)} {display.downstreams.displayed.map((n, index) => this.renderResourceCard(n, 'neighbor', index, true))} {_isEmpty(display.downstreams.collapsed) ? null : this.renderCollapsedNeighbors(display.downstreams.collapsed, display.downstreams.displayed.length, true)}
); } } Octopus.propTypes = { api: PropTypes.shape({ ResourceLink: PropTypes.element, }), neighbors: PropTypes.shape({}), resource: PropTypes.shape({}), unmeshedSources: PropTypes.arrayOf(PropTypes.shape({})), }; Octopus.defaultProps = { api: null, neighbors: {}, resource: {}, unmeshedSources: [], }; export default withStyles(styles)(Octopus);