TRA_4623 - methods view on timeline stats with coordination to the pie stats (#1175)

* Add select protocol → when selected, the view will be on commands of that exact protocol

* CR fixes

* added const instead of free string

* remove redundant sass file
This commit is contained in:
AmitUp9 2022-06-30 13:35:08 +03:00 committed by GitHub
parent 3a9236a381
commit 3b0b311e1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 135 additions and 81 deletions

View File

@ -1,5 +1,5 @@
import styles from "./TimelineBarChart.module.sass";
import { StatsMode } from "../TrafficStatsModal"
import { ALL_PROTOCOLS, StatsMode } from "../TrafficStatsModal"
import React, { useEffect, useMemo, useState } from "react";
import {
BarChart,
@ -7,31 +7,33 @@ import {
XAxis,
YAxis,
Tooltip,
Legend
} from "recharts";
import { Utils } from "../../../../helpers/Utils";
interface TimelineBarChartProps {
timeLineBarChartMode: string;
data: any;
selectedProtocol: string;
}
export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarChartMode, data }) => {
export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarChartMode, data, selectedProtocol }) => {
const [protocolStats, setProtocolStats] = useState([]);
const [protocolsNamesAndColors, setProtocolsNamesAndColors] = useState([]);
const [commandStats, setCommandStats] = useState(null);
const [commandNames, setcommandNames] = useState(null);
useEffect(() => {
if (!data) return;
const protocolsBarsData = [];
const prtcNames = [];
data.forEach(protocolObj => {
let obj: { [k: string]: any } = {};
obj.timestamp = Utils.getHoursAndMinutes(protocolObj.timestamp);
let newProtocolbj: { [k: string]: any } = {};
newProtocolbj.timestamp = Utils.getHoursAndMinutes(protocolObj.timestamp);
protocolObj.protocols.forEach(protocol => {
obj[`${protocol.name}`] = protocol[StatsMode[timeLineBarChartMode]];
newProtocolbj[`${protocol.name}`] = protocol[StatsMode[timeLineBarChartMode]];
prtcNames.push({ name: protocol.name, color: protocol.color });
})
protocolsBarsData.push(obj);
protocolsBarsData.push(newProtocolbj);
})
const uniqueObjArray = Utils.creatUniqueObjArrayByProp(prtcNames, "name")
protocolsBarsData.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1);
@ -39,16 +41,39 @@ export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarC
setProtocolsNamesAndColors(uniqueObjArray);
}, [data, timeLineBarChartMode])
const bars = useMemo(() => protocolsNamesAndColors.map((protocolToDIsplay) => {
return <Bar key={protocolToDIsplay.name} dataKey={protocolToDIsplay.name} stackId="a" fill={protocolToDIsplay.color} />
}), [protocolsNamesAndColors])
useEffect(() => {
if (selectedProtocol === ALL_PROTOCOLS) {
setCommandStats(null);
setcommandNames(null);
return;
}
const commandsNames = [];
const protocolsCommands = [];
data.forEach(protocolObj => {
let newCommandlbj: { [k: string]: any } = {};
newCommandlbj.timestamp = Utils.getHoursAndMinutes(protocolObj.timestamp);
protocolObj.protocols.find(protocol => protocol.name === selectedProtocol)?.methods.forEach(command => {
newCommandlbj[`${command.name}`] = command[StatsMode[timeLineBarChartMode]]
if (commandsNames.indexOf(command.name) === -1)
commandsNames.push(command.name);
})
protocolsCommands.push(newCommandlbj);
})
protocolsCommands.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1);
setcommandNames(commandsNames);
setCommandStats(protocolsCommands);
}, [data, timeLineBarChartMode, selectedProtocol])
const bars = useMemo(() => (commandNames || protocolsNamesAndColors).map((entry) => {
return <Bar key={entry.name || entry} dataKey={entry.name || entry} stackId="a" fill={entry.color || Utils.stringToColor(entry)} />
}), [protocolsNamesAndColors, commandNames])
return (
<div className={styles.barChartContainer}>
{protocolStats.length > 0 && <BarChart
width={730}
height={250}
data={protocolStats}
data={commandStats || protocolStats}
margin={{
top: 20,
right: 30,
@ -59,7 +84,6 @@ export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarC
<XAxis dataKey="timestamp" />
<YAxis tickFormatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value} />
<Tooltip formatter={(value) => timeLineBarChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"} />
<Legend />
{bars}
</BarChart>}
</div>

View File

@ -1,16 +0,0 @@
.breadCrumbsContainer
margin-top: 15px
height: 15px
.breadCrumbs
color: #494677
text-align: left
.clickableTag
margin-right: 5px
border-bottom: 1px black solid
cursor: pointer
.nonClickableTag
margin-left: 5px
font-weight: 600

View File

@ -1,21 +1,18 @@
import React, {useEffect, useMemo, useState} from "react";
import styles from "./TrafficPieChart.module.sass";
import {Cell, Legend, Pie, PieChart, Tooltip} from "recharts";
import {Utils} from "../../../../helpers/Utils";
import {StatsMode as PieChartMode} from "../TrafficStatsModal"
const COLORS = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000'];
import React, { useEffect, useMemo, useState } from "react";
import { Cell, Legend, Pie, PieChart, Tooltip } from "recharts";
import { Utils } from "../../../../helpers/Utils";
import { ALL_PROTOCOLS, StatsMode as PieChartMode } from "../TrafficStatsModal"
const RADIAN = Math.PI / 180;
const renderCustomizedLabel = ({
cx,
cy,
midAngle,
innerRadius,
outerRadius,
percent,
index
}: any) => {
cx,
cy,
midAngle,
innerRadius,
outerRadius,
percent,
index
}: any) => {
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
@ -38,13 +35,13 @@ const renderCustomizedLabel = ({
interface TrafficPieChartProps {
pieChartMode: string;
data: any;
selectedProtocol: string;
}
export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode , data}) => {
export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({ pieChartMode, data, selectedProtocol }) => {
const [protocolsStats, setProtocolsStats] = useState([]);
const [commandStats, setCommandStats] = useState(null);
const [selectedProtocol, setSelectedProtocol] = useState(null as string);
useEffect(() => {
if (!data) return;
@ -59,11 +56,11 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
}, [data, pieChartMode])
useEffect(() => {
if (!selectedProtocol) {
if (selectedProtocol === ALL_PROTOCOLS) {
setCommandStats(null);
return;
}
const commandsPieData = data.find(protocol => protocol.name === selectedProtocol).methods.map(command => {
const commandsPieData = data.find(protocol => protocol.name === selectedProtocol)?.methods.map(command => {
return {
name: command.name,
value: command[PieChartMode[pieChartMode]]
@ -75,18 +72,18 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
const pieLegend = useMemo(() => {
if (!data) return;
let legend;
if (!selectedProtocol) {
legend = data.map(protocol => <div style={{marginBottom: 5, display: "flex"}}>
<div style={{height: 15, width: 30, background: protocol?.color}}/>
<span style={{marginLeft: 5}}>
if (selectedProtocol === ALL_PROTOCOLS) {
legend = data.map(protocol => <div style={{ marginBottom: 5, display: "flex" }}>
<div style={{ height: 15, width: 30, background: protocol?.color }} />
<span style={{ marginLeft: 5 }}>
{protocol.name}
</span>
</div>)
} else {
legend = data.find(protocol => protocol.name === selectedProtocol).methods.map((method, index) => <div
style={{marginBottom: 5, display: "flex"}}>
<div style={{height: 15, width: 30, background: COLORS[index % COLORS.length]}}/>
<span style={{marginLeft: 5}}>
legend = data.find(protocol => protocol.name === selectedProtocol)?.methods.map((method) => <div
style={{ marginBottom: 5, display: "flex" }}>
<div style={{ height: 15, width: 30, background: Utils.stringToColor(method.name)}} />
<span style={{ marginLeft: 5 }}>
{method.name}
</span>
</div>)
@ -96,15 +93,7 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
return (
<div>
<div className={styles.breadCrumbsContainer}>
{selectedProtocol && <div className={styles.breadCrumbs}>
<span className={styles.clickableTag} onClick={() => setSelectedProtocol(null)}>protocols</span>
<span>/</span>
<span className={styles.nonClickableTag}>{selectedProtocol}</span>
</div>}
</div>
{protocolsStats?.length > 0 && <div style={{width: "100%", display: "flex", justifyContent: "center"}}>
{protocolsStats?.length > 0 && <div style={{ width: "100%", display: "flex", justifyContent: "center" }}>
<PieChart width={300} height={300}>
<Pie
data={commandStats || protocolsStats}
@ -114,14 +103,13 @@ export const TrafficPieChart: React.FC<TrafficPieChartProps> = ({pieChartMode ,
labelLine={false}
label={renderCustomizedLabel}
outerRadius={125}
fill="#8884d8"
onClick={(section) => !commandStats && setSelectedProtocol(section.name)}>
fill="#8884d8">
{(commandStats || protocolsStats).map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color || COLORS[index % COLORS.length]}/>)
<Cell key={`cell-${index}`} fill={entry.color || Utils.stringToColor(entry.name)} />)
)}
</Pie>
<Legend wrapperStyle={{position: "absolute", width: "auto", height: "auto", right: -150, top: 0}} content={pieLegend}/>
<Tooltip formatter={(value) => pieChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"}/>
<Legend wrapperStyle={{ position: "absolute", width: "auto", height: "auto", right: -150, top: 0 }} content={pieLegend} />
<Tooltip formatter={(value) => pieChartMode === "VOLUME" ? Utils.humanFileSize(value) : value + " Requests"} />
</PieChart>
</div>}
</div>

View File

@ -1,3 +1,6 @@
.headlineContainer
display: flex
.title
color: #494677
font-family: Source Sans Pro,Lucida Grande,Tahoma,sans-serif
@ -13,6 +16,11 @@
padding: 30px
text-align: center
.selectContainer
display: flex
justify-content: space-evenly
margin-bottom: 4%
.select
border: none
border-bottom: 1px black solid

View File

@ -1,10 +1,12 @@
import React, { useEffect, useState } from "react";
import { Backdrop, Box, Fade, Modal } from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import { Backdrop, Box, Button, debounce, Fade, Modal } from "@mui/material";
import styles from "./TrafficStatsModal.module.sass";
import closeIcon from "assets/close.svg";
import { TrafficPieChart } from "./TrafficPieChart/TrafficPieChart";
import { TimelineBarChart } from "./TimelineBarChart/TimelineBarChart";
import spinnerImg from "assets/spinner.svg";
import refreshIcon from "assets/refresh.svg";
import { useCommonStyles } from "../../../helpers/commonStyle";
const modalStyle = {
position: 'absolute',
@ -32,15 +34,20 @@ interface TrafficStatsModalProps {
getTimelineStatsDataApi: () => Promise<any>
}
export const PROTOCOLS = ["ALL PROTOCOLS","gRPC", "REDIS", "HTTP", "GQL", "AMQP", "KFAKA"];
export const ALL_PROTOCOLS = PROTOCOLS[0];
export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, onClose, getPieStatsDataApi, getTimelineStatsDataApi }) => {
const modes = Object.keys(StatsMode).filter(x => !(parseInt(x) >= 0));
const [statsMode, setStatsMode] = useState(modes[0]);
const [selectedProtocol, setSelectedProtocol] = useState("ALL PROTOCOLS");
const [pieStatsData, setPieStatsData] = useState(null);
const [timelineStatsData, setTimelineStatsData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const commonClasses = useCommonStyles();
useEffect(() => {
const getTrafficStats = useCallback(async () => {
if (isOpen && getPieStatsDataApi) {
(async () => {
try {
@ -58,6 +65,14 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
}
}, [isOpen, getPieStatsDataApi, getTimelineStatsDataApi, setPieStatsData, setTimelineStatsData])
useEffect(() => {
getTrafficStats();
}, [getTrafficStats])
const refreshStats = debounce(() => {
getTrafficStats();
}, 500);
return (
<Modal
aria-labelledby="transition-modal-title"
@ -72,21 +87,40 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
<div className={styles.closeIcon}>
<img src={closeIcon} alt="close" onClick={() => onClose()} style={{ cursor: "pointer", userSelect: "none" }} />
</div>
<div className={styles.title}>Traffic Statistics</div>
<div className={styles.headlineContainer}>
<div className={styles.title}>Traffic Statistics</div>
<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
startIcon={<img src={refreshIcon} className="custom" alt="refresh"></img>}
size="medium"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={refreshStats}
>
Refresh
</Button>
</div>
<div className={styles.mainContainer}>
<div>
<span style={{ marginRight: 15 }}>Breakdown By</span>
<select className={styles.select} value={statsMode} onChange={(e) => setStatsMode(e.target.value)}>
{modes.map(mode => <option key={mode} value={mode}>{mode}</option>)}
</select>
<div className={styles.selectContainer}>
<div>
<span style={{ marginRight: 15 }}>Breakdown By</span>
<select className={styles.select} value={statsMode} onChange={(e) => setStatsMode(e.target.value)}>
{modes.map(mode => <option key={mode} value={mode}>{mode}</option>)}
</select>
</div>
<div>
<span style={{ marginRight: 15 }}>Protocol</span>
<select className={styles.select} value={selectedProtocol} onChange={(e) => setSelectedProtocol(e.target.value)}>
{PROTOCOLS.map(protocol => <option key={protocol} value={protocol}>{protocol}</option>)}
</select>
</div>
</div>
<div>
{isLoading ? <div style={{ textAlign: "center", marginTop: 20 }}>
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
</div> :
</div> :
<div>
<TrafficPieChart pieChartMode={statsMode} data={pieStatsData} />
<TimelineBarChart timeLineBarChartMode={statsMode} data={timelineStatsData} />
<TrafficPieChart pieChartMode={statsMode} data={pieStatsData} selectedProtocol={selectedProtocol}/>
<TimelineBarChart timeLineBarChartMode={statsMode} data={timelineStatsData} selectedProtocol={selectedProtocol}/>
</div>}
</div>
</div>

View File

@ -0,0 +1,3 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.8337 11.9167H7.69308L7.69416 11.907C7.83561 11.2143 8.11247 10.5564 8.50883 9.97105C9.09865 9.10202 9.92598 8.42097 10.8922 8.00913C11.2193 7.87046 11.5606 7.7643 11.9083 7.69388C12.6297 7.54762 13.3731 7.54762 14.0945 7.69388C15.1312 7.90631 16.0825 8.41908 16.8299 9.1683L18.3639 7.63863C17.6725 6.94707 16.8546 6.39501 15.9546 6.01255C15.4956 5.81823 15.0184 5.67016 14.53 5.57055C13.5223 5.36581 12.4838 5.36581 11.4761 5.57055C10.9873 5.67057 10.5098 5.819 10.0504 6.01363C8.69682 6.58791 7.53808 7.54123 6.71374 8.7588C6.15895 9.5798 5.77099 10.5019 5.57191 11.4725C5.54158 11.6188 5.52533 11.7683 5.50366 11.9167H2.16699L6.50033 16.25L10.8337 11.9167ZM15.167 14.0834H18.3076L18.3065 14.092C18.0234 15.4806 17.205 16.7019 16.0282 17.4915C15.443 17.8882 14.7851 18.1651 14.0923 18.3062C13.3713 18.4525 12.6283 18.4525 11.9072 18.3062C11.2146 18.1648 10.5567 17.8879 9.97133 17.4915C9.68383 17.2971 9.41541 17.0758 9.16966 16.8307L7.63783 18.3625C8.32954 19.0539 9.14791 19.6056 10.0482 19.9875C10.5076 20.1825 10.9875 20.331 11.4728 20.4295C12.4801 20.6344 13.5184 20.6344 14.5257 20.4295C16.4676 20.0265 18.1757 18.8819 19.2869 17.2391C19.8412 16.4187 20.2288 15.4974 20.4277 14.5275C20.4569 14.3813 20.4742 14.2318 20.4959 14.0834H23.8337L19.5003 9.75005L15.167 14.0834Z" fill="#205CF5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -42,4 +42,17 @@ export class Utils {
return Array.from(map);
}
static stringToColor = (str) => {
let colors = ["#e51c23", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#5677fc", "#03a9f4", "#00bcd4", "#009688", "#259b24", "#8bc34a", "#afb42b", "#ff9800", "#ff5722", "#795548", "#607d8b"]
let hash = 0;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
}
}