UX updates

This commit is contained in:
dhrubesh 2021-05-09 22:51:08 +05:30
parent 325ca434d4
commit 9ac2dece11
6 changed files with 200 additions and 165 deletions

View File

@ -46,16 +46,15 @@
} }
}, },
"@ant-design/icons": { "@ant-design/icons": {
"version": "4.5.0", "version": "4.6.2",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.5.0.tgz", "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-4.6.2.tgz",
"integrity": "sha512-ZAKJcmr4DBV3NWr8wm2dCxNKN4eFrX+qCaPsuFejP6FRsf+m5OKxvCVi9bSp1lmKWeOI5yECAx5s0uFm4QHuPw==", "integrity": "sha512-QsBG2BxBYU/rxr2eb8b2cZ4rPKAPBpzAR+0v6rrZLp/lnyvflLH3tw1vregK+M7aJauGWjIGNdFmUfpAOtw25A==",
"requires": { "requires": {
"@ant-design/colors": "^6.0.0", "@ant-design/colors": "^6.0.0",
"@ant-design/icons-svg": "^4.0.0", "@ant-design/icons-svg": "^4.0.0",
"@babel/runtime": "^7.11.2", "@babel/runtime": "^7.11.2",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"insert-css": "^2.0.0", "rc-util": "^5.9.4"
"rc-util": "^5.0.1"
} }
}, },
"@ant-design/icons-svg": { "@ant-design/icons-svg": {
@ -11076,11 +11075,6 @@
"css-in-js-utils": "^2.0.0" "css-in-js-utils": "^2.0.0"
} }
}, },
"insert-css": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/insert-css/-/insert-css-2.0.0.tgz",
"integrity": "sha1-610Ql7dUL0x56jBg067gfQU4gPQ="
},
"internal-ip": { "internal-ip": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz",

View File

@ -12,6 +12,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.2",
"@auth0/auth0-react": "^1.2.0", "@auth0/auth0-react": "^1.2.0",
"@babel/core": "7.12.3", "@babel/core": "7.12.3",
"@material-ui/core": "^4.0.0", "@material-ui/core": "^4.0.0",

View File

@ -1,8 +1,27 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { servicesItem } from "Src/store/actions"; import { servicesItem } from "Src/store/actions";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Select } from "antd"; import { Select } from "antd";
import styled from "styled-components";
const { Option } = Select; const { Option } = Select;
const Container = styled.div`
margin-top: 12px;
display: flex;
.info {
display:flex;
font-family: Roboto;
margin-left: auto;
margin-right: 12px;
color: #4F4F4F;
font-size: 14px;
.anticon-info-circle {
margin-top: 22px;
margin-right: 18px;
}
}
`;
interface SelectServiceProps { interface SelectServiceProps {
services: servicesItem[]; services: servicesItem[];
zoomToService: (arg0: string) => void; zoomToService: (arg0: string) => void;
@ -16,18 +35,28 @@ const SelectService = (props: SelectServiceProps) => {
zoomToService(value); zoomToService(value);
}; };
return ( return (
<Select <Container>
style={{ width: 270, marginBottom: "56px" }} <Select
placeholder="Select a service" style={{ width: 270, marginBottom: "56px" }}
onChange={handleSelect} placeholder="Select a service"
value={selectedVal} onChange={handleSelect}
> value={selectedVal}
{services.map(({ serviceName }) => ( >
<Option key={serviceName} value={serviceName}> {services.map(({ serviceName }) => (
{serviceName} <Option key={serviceName} value={serviceName}>
</Option> {serviceName}
))} </Option>
</Select> ))}
</Select>
<div className='info'>
<InfoCircleOutlined />
<div>
<div>-> Size of circles is proportial to the number of requests served by each node </div>
<div>-> Click on node name to reposition the node</div>
</div>
</div>
</Container>
); );
}; };

View File

@ -8,11 +8,28 @@ import {
getDetailedServiceMapItems, getDetailedServiceMapItems,
} from "Src/store/actions"; } from "Src/store/actions";
import { Spin } from "antd"; import { Spin } from "antd";
import styled from "styled-components";
import { StoreState } from "../../store/reducers"; import { StoreState } from "../../store/reducers";
import { getGraphData } from "./utils"; import { getGraphData } from "./utils";
import SelectService from "./SelectService"; import SelectService from "./SelectService";
import { ForceGraph2D } from "react-force-graph"; import { ForceGraph2D } from "react-force-graph";
const Container = styled.div`
.force-graph-container .graph-tooltip {
background: black;
padding: 1px;
.keyval {
display: flex;
.key {
margin-right: 4px;
}
.val {
margin-left: auto;
}
}
}
`;
interface ServiceMapProps extends RouteComponentProps<any> { interface ServiceMapProps extends RouteComponentProps<any> {
serviceMap: serviceMapStore; serviceMap: serviceMapStore;
globalTime: GlobalTime; globalTime: GlobalTime;
@ -52,13 +69,13 @@ const ServiceMap = (props: ServiceMapProps) => {
} }
const zoomToService = (value: string) => { const zoomToService = (value: string) => {
fgRef && fgRef.current.zoomToFit(700, 480, (e) => e.id === value); fgRef && fgRef.current.zoomToFit(700, 280, (e) => e.id === value);
}; };
const { nodes, links } = getGraphData(serviceMap); const { nodes, links } = getGraphData(serviceMap);
const graphData = { nodes, links }; const graphData = { nodes, links };
return ( return (
<div> <Container>
<SelectService <SelectService
services={serviceMap.services} services={serviceMap.services}
zoomToService={zoomToService} zoomToService={zoomToService}
@ -66,7 +83,7 @@ const ServiceMap = (props: ServiceMapProps) => {
<ForceGraph2D <ForceGraph2D
ref={fgRef} ref={fgRef}
cooldownTicks={100} cooldownTicks={100}
onEngineStop={() => fgRef.current.zoomToFit(100, 200)} onEngineStop={() => fgRef.current.zoomToFit(100, 120)}
graphData={graphData} graphData={graphData}
nodeLabel="id" nodeLabel="id"
linkAutoColorBy={(d) => d.target} linkAutoColorBy={(d) => d.target}
@ -75,46 +92,46 @@ const ServiceMap = (props: ServiceMapProps) => {
nodeCanvasObject={(node, ctx, globalScale) => { nodeCanvasObject={(node, ctx, globalScale) => {
const label = node.id; const label = node.id;
const fontSize = node.fontSize; const fontSize = node.fontSize;
ctx.font = `${fontSize}px Sans-Serif`; ctx.font = `${fontSize}px Roboto`;
const textWidth = ctx.measureText(label).width; const width = node.width;
const width = textWidth > node.width ? textWidth : node.width;
const bckgDimensions = [width, node.height].map((n) => n + fontSize); // some padding
ctx.fillStyle = node.color; ctx.fillStyle = node.color;
ctx.fillRect( ctx.beginPath();
node.x - bckgDimensions[0] / 2, ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
node.y - bckgDimensions[1] / 2, ctx.fill();
...bckgDimensions,
);
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.textBaseline = "middle"; ctx.textBaseline = "middle";
ctx.fillStyle = "black"; ctx.fillStyle = "#333333";
ctx.fillText(label, node.x, node.y); ctx.fillText(label, node.x, node.y);
node.__bckgDimensions = bckgDimensions; // to re-use in nodePointerAreaPaint
}} }}
onNodeClick={(node) => { onNodeClick={(node) => {
const tooltip = document.querySelector(".graph-tooltip"); const tooltip = document.querySelector(".graph-tooltip");
if (tooltip && node) { if (tooltip && node) {
tooltip.innerHTML = `<div style="padding:12px;background: black;border: 1px solid #BDBDBD;border-radius: 2px;"> tooltip.innerHTML = `<div style="color:#333333;padding:12px;background: white;border-radius: 2px;">
<div style="color:white; font-weight:bold; margin-bottom:8px;">${node.id}</div> <div style="font-weight:bold; margin-bottom:16px;">${node.id}</div>
<div style="color:white">P99 latency: ${node.p99 / 1000000}</div> <div class="keyval">
<div style="color:white">Error Rate: ${node.errorRate}%</div> <div class="key">P99 latency:</div>
<div style="color:white">Request Per Sec: ${node.callRate}</div> <div class="val">${node.p99 / 1000000}</div>
</div>
<div class="keyval">
<div class="key">Request:</div>
<div class="val">${node.callRate}/sec</div>
</div>
<div class="keyval">
<div class="key">Error Rate:</div>
<div class="val">${node.errorRate}%</div>
</div>
</div>`; </div>`;
} }
}} }}
nodePointerAreaPaint={(node, color, ctx) => { nodePointerAreaPaint={(node, color, ctx) => {
ctx.fillStyle = color; ctx.fillStyle = color;
const bckgDimensions = node.__bckgDimensions; ctx.beginPath();
bckgDimensions && ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
ctx.fillRect( ctx.fill();
node.x - bckgDimensions[0] / 2,
node.y - bckgDimensions[1] / 2,
...bckgDimensions,
);
}} }}
/> />
</div> </Container>
); );
}; };

View File

@ -2,20 +2,16 @@ import { uniqBy, uniq, maxBy, cloneDeep, find } from "lodash";
import { serviceMapStore } from "Src/store/actions"; import { serviceMapStore } from "Src/store/actions";
import { graphDataType } from "./ServiceMap"; import { graphDataType } from "./ServiceMap";
const MIN_WIDTH = 25; const MIN_WIDTH = 12;
const MAX_WIDTH = 50; const MAX_WIDTH = 20;
const MIN_HEIGHT = 10; const DEFAULT_FONT_SIZE = 6;
const MAX_HEIGHT = 15;
const DEFAULT_FONT_SIZE = 4;
export const getDimensions = (num, highest) => { export const getDimensions = (num, highest) => {
const percentage = (num / highest) * 100; const percentage = (num / highest) * 100;
const width = (percentage * (MAX_WIDTH - MIN_WIDTH)) / 100 + MIN_WIDTH; const width = (percentage * (MAX_WIDTH - MIN_WIDTH)) / 100 + MIN_WIDTH;
const height = (percentage * (MAX_HEIGHT - MIN_HEIGHT)) / 100 + MIN_HEIGHT;
const fontSize = DEFAULT_FONT_SIZE; const fontSize = DEFAULT_FONT_SIZE;
return { return {
fontSize, fontSize,
width, width,
height,
}; };
}; };
@ -40,14 +36,13 @@ export const getGraphData = (serviceMap: serviceMapStore): graphDataType => {
const uniqNodes = uniq([...uniqParent, ...uniqChild]); const uniqNodes = uniq([...uniqParent, ...uniqChild]);
const nodes = uniqNodes.map((node, i) => { const nodes = uniqNodes.map((node, i) => {
const service = find(services, (service) => service.serviceName === node); const service = find(services, (service) => service.serviceName === node);
let color = "#84ff00"; let color = "#88CEA5";
if (!service) { if (!service) {
return { return {
id: node, id: node,
group: i + 1, group: i + 1,
fontSize: DEFAULT_FONT_SIZE, fontSize: DEFAULT_FONT_SIZE,
width: MIN_WIDTH, width: MIN_WIDTH,
height: MIN_HEIGHT,
color, color,
nodeVal: MIN_WIDTH, nodeVal: MIN_WIDTH,
callRate: 0, callRate: 0,
@ -56,17 +51,16 @@ export const getGraphData = (serviceMap: serviceMapStore): graphDataType => {
}; };
} }
if (service.errorRate > 0) { if (service.errorRate > 0) {
color = "#f00a0a"; color = "#F98989";
} else if (service.fourXXRate > 0) { } else if (service.fourXXRate > 0) {
color = "#ebeb15"; color = "#F9DA7B";
} }
const { fontSize, width, height } = getDimensions(service.callRate, highestCallRate); const { fontSize, width } = getDimensions(service.callRate, highestCallRate);
return { return {
id: node, id: node,
group: i + 1, group: i + 1,
fontSize, fontSize,
width, width,
height,
color, color,
nodeVal: width, nodeVal: width,
callRate: service.callRate, callRate: service.callRate,

View File

@ -44,36 +44,36 @@ export const getServiceMapItems = (globalTime: GlobalTime) => {
"&end=" + "&end=" +
globalTime.maxTime; globalTime.maxTime;
const response = await api.get<servicesMapItem[]>(apiV1 + request_string); // const response = await api.get<servicesMapItem[]>(apiV1 + request_string);
// const response = { const response = {
// data: [ data: [
// { {
// parent: "driver", parent: "driver",
// child: "redis", child: "redisredisredisredisredisredisredisredisre",
// callCount: 17050, callCount: 17050,
// }, },
// { {
// parent: "frontend", parent: "frontend",
// child: "driver", child: "driver",
// callCount: 1263, callCount: 1263,
// }, },
// { {
// parent: "customer", parent: "customer",
// child: "mysql", child: "mysql",
// callCount: 1262, callCount: 1262,
// }, },
// { {
// parent: "frontend", parent: "frontend",
// child: "customer", child: "customer",
// callCount: 1262, callCount: 1262,
// }, },
// { {
// parent: "frontend", parent: "frontend",
// child: "route", child: "route",
// callCount: 12636, callCount: 12636,
// }, },
// ], ],
// }; };
dispatch<serviceMapItemAction>({ dispatch<serviceMapItemAction>({
type: ActionTypes.getServiceMapItems, type: ActionTypes.getServiceMapItems,
payload: response.data, payload: response.data,
@ -86,77 +86,77 @@ export const getDetailedServiceMapItems = (globalTime: GlobalTime) => {
let request_string = let request_string =
"/services?start=" + globalTime.minTime + "&end=" + globalTime.maxTime; "/services?start=" + globalTime.minTime + "&end=" + globalTime.maxTime;
const response = await api.get<servicesItem[]>(apiV1 + request_string); // const response = await api.get<servicesItem[]>(apiV1 + request_string);
// const response = { const response = {
// data: [ data: [
// { {
// serviceName: "redis", serviceName: "redisredisredisredisredisredisredisredisre",
// p99: 1179518000, p99: 1179518000,
// avgDuration: 742158850, avgDuration: 742158850,
// numCalls: 1898, numCalls: 1898,
// callRate: 0.019097411, callRate: 0.019097411,
// numErrors: 0, numErrors: 0,
// errorRate: 3, errorRate: 3,
// num4XX: 0, num4XX: 0,
// fourXXRate: 0, fourXXRate: 0,
// }, },
// { {
// serviceName: "mysql", serviceName: "mysql",
// p99: 1179518000, p99: 1179518000,
// avgDuration: 742158850, avgDuration: 742158850,
// numCalls: 1898, numCalls: 1898,
// callRate: 0.019097411, callRate: 0.019097411,
// numErrors: 0, numErrors: 0,
// errorRate: 0, errorRate: 0,
// num4XX: 0, num4XX: 0,
// fourXXRate: 0, fourXXRate: 0,
// }, },
// { {
// serviceName: "frontend", serviceName: "frontend",
// p99: 1179518000, p99: 1179518000,
// avgDuration: 742158850, avgDuration: 742158850,
// numCalls: 1898, numCalls: 1898,
// callRate: 0.000019097411, callRate: 0.000019097411,
// numErrors: 0, numErrors: 0,
// errorRate: 0, errorRate: 0,
// num4XX: 0, num4XX: 0,
// fourXXRate: 0, fourXXRate: 0,
// }, },
// { {
// serviceName: "customer", serviceName: "customer",
// p99: 728385000, p99: 728385000,
// avgDuration: 342475680, avgDuration: 342475680,
// numCalls: 1897, numCalls: 1897,
// callRate: 0.000019087349, callRate: 0.000019087349,
// numErrors: 0, numErrors: 0,
// errorRate: 0, errorRate: 0,
// num4XX: 0, num4XX: 0,
// fourXXRate: 0.6325778, fourXXRate: 0.6325778,
// }, },
// { {
// serviceName: "driver", serviceName: "driver",
// p99: 243808000, p99: 243808000,
// avgDuration: 204640670, avgDuration: 204640670,
// numCalls: 1898, numCalls: 1898,
// callRate: 0.000019097411, callRate: 0.000019097411,
// numErrors: 0, numErrors: 0,
// errorRate: 0, errorRate: 0,
// num4XX: 0, num4XX: 0,
// fourXXRate: 0.63224447, fourXXRate: 0.63224447,
// }, },
// { {
// serviceName: "route", serviceName: "route",
// p99: 79419000, p99: 79419000,
// avgDuration: 50748804, avgDuration: 50748804,
// numCalls: 18979, numCalls: 18979,
// callRate: 0.00019096404, callRate: 0.00019096404,
// numErrors: 0, numErrors: 0,
// errorRate: 3, errorRate: 3,
// num4XX: 0, num4XX: 0,
// fourXXRate: 0, fourXXRate: 0,
// }, },
// ], ],
// }; };
dispatch<servicesAction>({ dispatch<servicesAction>({
type: ActionTypes.getServices, type: ActionTypes.getServices,
payload: response.data, payload: response.data,