/** **************************************************************************** * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. * Licensed under the Apache License Version 2.0 (the 'License'). You may not * use this file except in compliance with the License. A copy of the License * is located at * * http://www.apache.org/licenses/ * or in the 'license' file accompanying this file. This file is distributed on * an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or * implied. See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************** */ /* eslint-disable no-underscore-dangle */ import React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; import { withStyles } from '@material-ui/core/styles'; import Paper from '@material-ui/core/Paper'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; import { ResponsiveContainer, Line, Area, ReferenceArea, CartesianGrid, XAxis, YAxis, Tooltip, Legend, ComposedChart, Dot, } from 'recharts'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; dayjs.extend(utc); const styles = (theme) => ({ root: {}, header: { position: 'static', width: '100%', display: 'flex', zIndex: 1100, boxSizing: 'border-box', flexShrink: 0, flexDirection: 'column', padding: theme.spacing(1, 2), background: '#f7f7f7', }, headerInside: { position: 'relative', display: 'flex', alignItems: 'center', }, toggleReferenceArea: { marginLeft: 'auto', }, content: { paddingTop: theme.spacing(1), paddingBottom: theme.spacing(1), }, referenceArea: { opacity: '0.8', }, legend: { padding: 0, marginTop: '6px', marginBottom: 0, textAlign: 'center', color: '#212121', fontSize: '14px', '& li': { display: 'inline-block', marginRight: 10, }, '& .colorBox': { width: 10, height: 10, border: '1px #aaa solid', display: 'inline-block', marginRight: 2, }, }, tooltip: { border: '1px rgba(0, 0, 0, 0.35) solid', background: 'rgba(255, 255, 255, 0.96)', fontSize: '14px', padding: theme.spacing(0.5), '&$normal': { }, '&$abnormal': { border: '1px rgba(200, 0, 0, 0.35) solid', background: 'rgba(255, 235, 235, 0.96)', }, '& .date': { fontWeight: 'bold', textAlign: 'right', }, '& .alert': { color: 'red', }, '& p': { margin: 0, }, }, normal: {}, abnormal: {}, }); const colorMap = { Available: '#88ff88', PendingBusy: '#fff8a2', Busy: '#ffcc88', AfterCallWork: '#888888', FailedConnectAgent: '#ff8488', FailedConnectCustomer: '#ff8488', CallingCustomer: '#fff8a2', MissedCallAgent: '#bbbbff', __other: '#ffffff', }; class MetricsView extends React.PureComponent { constructor(props) { super(props); this.state = this.getInitialState(); this.handleToggleReferenceArea = this.handleToggleReferenceArea.bind(this); this.handleLegendMouseEnter = this.handleLegendMouseEnter.bind(this); this.handleLegendMouseLeave = this.handleLegendMouseLeave.bind(this); this.renderCustomAxisTick = this.renderCustomAxisTick.bind(this); this.renderCustomTooltip = this.renderCustomTooltip.bind(this); this.renderCustomLegend = this.renderCustomLegend.bind(this); } getInitialState() { return { skewThreshold: 10000, hideReferenceArea: false, hideLatencyGraph: false, hideSkewGraph: false, referenceAreaOpacities: Object.fromEntries(Object.keys(colorMap).map((name) => [name, 1])), }; } handleToggleReferenceArea() { const { hideReferenceArea } = this.state; this.setState({ hideReferenceArea: !hideReferenceArea }); } handleToggleLatencyGraph() { const { hideLatencyGraph } = this.state; this.setState({ hideLatencyGraph: !hideLatencyGraph }); } handleToggleSkewGraph() { const { hideSkewGraph } = this.state; this.setState({ hideSkewGraph: !hideSkewGraph }); } handleLegendMouseEnter(name) { const { referenceAreaOpacities: opacities } = this.state; const newOpacities = Object.fromEntries(Object.keys(opacities).map((n) => [n, 0.25])); this.setState({ referenceAreaOpacities: { ...newOpacities, [name]: 1 }, }); } handleLegendMouseLeave() { const { referenceAreaOpacities: opacities } = this.state; const newOpacities = Object.fromEntries(Object.keys(opacities).map((n) => [n, 1.0])); this.setState({ referenceAreaOpacities: { ...newOpacities }, }); } // eslint-disable-next-line class-methods-use-this renderCustomAxisTick({ x, y, payload, }) { return ( {dayjs(payload.value).utc().format('HH:mm:ss')} ); } // eslint-disable-next-line class-methods-use-this renderDots({ key, cx, cy, r, payload, }) { if (payload && payload.status && payload.status === 'failed') { return ( ); } return false; } // eslint-disable-next-line class-methods-use-this renderActiveDots({ key, cx, cy, r, fill, strokeWidth, payload, }) { if (payload && payload.status && payload.status === 'failed') { return ( ); } return (); } renderCustomTooltip({ payload, active }) { const { classes } = this.props; const { skewThreshold } = this.state; if (active && payload) { if (payload.length > 0 && payload[0].name === 'skew') { const skewTooLarge = Math.abs(payload[0].payload.skew) >= skewThreshold; return (

Server Time: {dayjs(payload[0].payload._snapshotTimestamp).toISOString()}

Local Time: {payload[0].payload.localTimestamp}

{payload[0].payload.state.name}

Local clock {' '} {Math.abs(payload[0].payload.skew)} {' '} ms {' '} {payload[0].payload.skew > 0 ? 'ahead' : 'behind'} {skewTooLarge &&  ⚠️}

); } if (payload.length > 0 && payload[0].name === 'latency') { return (

Local Time: {payload[0].payload.localTimestamp}

API: {payload[0].payload.apiName}

Latency: {payload[0].payload.latency} {' '} ms

{ payload[0].payload.status && (

Status: {payload[0].payload.status} {' '} {payload[0].payload.status === 'failed' ? '⚠️' : ''}

)}
); } } return null; } renderCustomLegend() { const { classes } = this.props; const { referenceAreaOpacities } = this.state; return (
    { Object.keys(colorMap).map((name) => (
  • this.handleLegendMouseEnter(name)} onMouseLeave={() => this.handleLegendMouseLeave()} style={{ opacity: referenceAreaOpacities[name] }} >
    { (name === '__other') ? 'Other' : name}
  • ))}
); } render() { const { classes, className: classNameProp, log, indexedLogs, } = this.props; const { skewThreshold, hideReferenceArea, referenceAreaOpacities, hideLatencyGraph, hideSkewGraph, } = this.state; const snapshots = log .filter((event) => (event.text === 'GET_AGENT_SNAPSHOT succeeded.')) .flatMap((event) => event.objects.map((object, idx) => ({ ...object.snapshot, _event: event, _key: `${event._key}-${idx}`, _date: object.snapshot.snapshotTimestamp.substring(0, 10), _time: object.snapshot.snapshotTimestamp.substring(11, 23), _timezone: object.snapshot.snapshotTimestamp.substring(23), _snapshotTimestamp: dayjs(object.snapshot.snapshotTimestamp).valueOf(), _localTimestamp: event._ts, localTimestamp: dayjs(event._ts).toISOString(), _type: 'SNAPSHOT', }))) .map((snapshot, idx, arr) => { const eventKeyFrom = snapshot._event._key; // eslint-disable-next-line max-len const eventKeyTo = (idx !== arr.length - 1) ? arr[idx + 1]._event._key : log[log.length - 1]._key; return { ...snapshot, // eslint-disable-next-line max-len _targetEventKeys: Array.from(Array(eventKeyTo - eventKeyFrom), (v, k) => (k + eventKeyFrom)), }; }); const seqSnapshots = snapshots // removing the duplications in states. .reduce((acc, x) => { if (acc.length > 0 && acc[acc.length - 1][0].state.name === x.state.name) { acc[acc.length - 1].push(x); } else { acc.push([x]); } return acc; }, []); const gradientOffset = () => { const dataMax = Math.max(...snapshots.map((s) => s.skew)); const dataMin = Math.min(...snapshots.map((s) => s.skew)); const y0 = Math.min(1, Math.max(0, (skewThreshold - dataMin) / (dataMax - dataMin))); const y1 = Math.min(1, Math.max(0, (-skewThreshold - dataMin) / (dataMax - dataMin))); return [ 1 - y0, 1 - y1, ]; }; const off = gradientOffset(); // filtering out the following APIs for a better representation of the latency const apiFilter = new Set([ 'getAgentSnapshot', ]); const latencies = log .filter((event) => (indexedLogs.has(event._key))) .flatMap((event) => ({ _localTimestamp: event._ts, localTimestamp: event.time, _type: 'LATENCY', ...indexedLogs.get(event._key), })) .filter((event) => !(apiFilter.has(event.apiName) || event.type === 'SEND')); const data = () => { if (hideLatencyGraph && !hideSkewGraph) { return snapshots; } if (!hideLatencyGraph && hideSkewGraph) { return latencies; } return snapshots.concat(latencies) .sort((a, b) => a._localTimestamp - b._localTimestamp); }; return (
Metrics {hideSkewGraph ? ( ) : ( )} {hideLatencyGraph ? ( ) : ( )} {hideReferenceArea ? ( ) : ( )}
{/* eslint-disable-next-line max-len */} {/* */} {!hideSkewGraph && } {!hideLatencyGraph && } {!hideReferenceArea && seqSnapshots.map((s, i, arr) => { const s0 = s[0]; const s1 = (i < arr.length - 1) ? arr[i + 1][0] : s[s.length - 1]; const stateHits = Object.keys(colorMap) .filter((name) => s0.state.name.includes(name)); const color = (stateHits.length > 0) ? colorMap[stateHits[0]] : colorMap.__other; // eslint-disable-next-line max-len const opacity = (stateHits.length > 0) ? referenceAreaOpacities[stateHits[0]] : referenceAreaOpacities.__other; return ( ); })} {!hideReferenceArea && }
); } } MetricsView.propTypes = { classes: PropTypes.object.isRequired, className: PropTypes.string, log: PropTypes.array.isRequired, indexedLogs: PropTypes.object.isRequired, }; MetricsView.defaultProps = { className: '', }; export default withStyles(styles)(MetricsView);