initial set up with react-force-graph

This commit is contained in:
dhrubesh 2021-05-09 14:44:14 +05:30
parent 44666a4944
commit c7ed2daf4a
11 changed files with 21863 additions and 79 deletions

21596
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -88,6 +88,7 @@
"react-css-theme-switcher": "^0.1.6", "react-css-theme-switcher": "^0.1.6",
"react-dev-utils": "^11.0.0", "react-dev-utils": "^11.0.0",
"react-dom": "17.0.0", "react-dom": "17.0.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5", "react-graph-vis": "^1.0.5",
"react-modal": "^3.12.1", "react-modal": "^3.12.1",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",

View File

@ -1,71 +0,0 @@
import React from "react";
// import {useState} from "react";
import Graph from "react-graph-vis";
// import { graphEvents } from "react-graph-vis";
//PNOTE - types of react-graph-vis defined in typings folder.
//How is it imported directly?
// type definition for service graph - https://github.com/crubier/react-graph-vis/issues/80
// Set shapes - https://visjs.github.io/vis-network/docs/network/nodes.html#
// https://github.com/crubier/react-graph-vis/issues/93
const graph = {
nodes: [
{
id: 1,
label: "Catalogue",
shape: "box",
color: "green",
border: "black",
size: 100,
},
{ id: 2, label: "Users", shape: "box", color: "#FFFF00" },
{ id: 3, label: "Payment App", shape: "box", color: "#FB7E81" },
{ id: 4, label: "My Sql", shape: "box", size: 10, color: "#7BE141" },
{ id: 5, label: "Redis-db", shape: "box", color: "#6E6EFD" },
],
edges: [
{ from: 1, to: 2, color: { color: "red" }, size: { size: 20 } },
{ from: 2, to: 3, color: { color: "red" } },
{ from: 1, to: 3, color: { color: "red" } },
{ from: 3, to: 4, color: { color: "red" } },
{ from: 3, to: 5, color: { color: "red" } },
],
};
const options = {
layout: {
hierarchical: true,
},
edges: {
color: "#000000",
},
height: "500px",
};
// const events = {
// select: function(event:any) { //PNOTE - TO DO - Get rid of any type
// var { nodes, edges } = event;
// }
// };
const ServiceGraph = () => {
// const [network, setNetwork] = useState(null);
return (
<React.Fragment>
<div> Updated Service Graph module coming soon..</div>
<Graph
graph={graph}
options={options}
// events={events}
// getNetwork={network => {
// // if you want access to vis.js network api you can set the state in a parent component using this property
// }}
/>
</React.Fragment>
);
};
export default ServiceGraph;

View File

@ -1,14 +1,114 @@
import React from "react"; import React, { useEffect, useRef } from "react";
import ServiceGraph from "./ServiceGraph"; import { connect } from "react-redux";
import { RouteComponentProps } from "react-router-dom";
import {
GlobalTime,
serviceMapStore,
getServiceMapItems,
getDetailedServiceMapItems,
} from "Src/store/actions";
import { StoreState } from "../../store/reducers";
import { getGraphData } from "./utils";
import { ForceGraph2D } from "react-force-graph";
const ServiceMap = () => { interface ServiceMapProps extends RouteComponentProps<any> {
serviceMap: serviceMapStore;
globalTime: GlobalTime;
getServiceMapItems: Function;
getDetailedServiceMapItems: Function;
}
interface graphNode {
id: string;
group: number;
}
interface graphLink {
source: string;
target: string;
value: number;
}
export interface graphDataType {
nodes: graphNode[];
links: graphLink[];
}
const ServiceMap = (props: ServiceMapProps) => {
const fgRef = useRef();
const {
getDetailedServiceMapItems,
getServiceMapItems,
globalTime,
serviceMap,
} = props;
useEffect(() => {
getServiceMapItems(globalTime);
getDetailedServiceMapItems(globalTime);
}, []);
if (!serviceMap.items.length) {
return "loading";
}
const { nodes, links } = getGraphData(serviceMap.items);
const graphData = { nodes, links };
return ( return (
<div> <div>
{" "} <ForceGraph2D
Service Map module coming soon... ref={fgRef}
{/* <ServiceGraph /> */} cooldownTicks={100}
onEngineStop={() => fgRef.current.zoomToFit(100, 100)}
graphData={graphData}
nodeLabel="id"
nodeAutoColorBy={(d) => d.id}
linkAutoColorBy={(d) => d.target}
linkDirectionalParticles="value"
linkDirectionalParticleSpeed={(d) => d.value}
nodeCanvasObject={(node, ctx, globalScale) => {
const label = node.id;
const fontSize = 16 / globalScale;
ctx.font = `${fontSize}px Sans-Serif`;
const textWidth = ctx.measureText(label).width;
const bckgDimensions = [textWidth, fontSize].map((n) => n + fontSize); // some padding
ctx.fillStyle = node.color;
ctx.fillRect(
node.x - bckgDimensions[0] / 2,
node.y - bckgDimensions[1] / 2,
...bckgDimensions,
);
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "black";
ctx.fillText(label, node.x, node.y);
node.__bckgDimensions = bckgDimensions; // to re-use in nodePointerAreaPaint
}}
nodePointerAreaPaint={(node, color, ctx) => {
ctx.fillStyle = color;
const bckgDimensions = node.__bckgDimensions;
bckgDimensions &&
ctx.fillRect(
node.x - bckgDimensions[0] / 2,
node.y - bckgDimensions[1] / 2,
...bckgDimensions,
);
}}
/>
</div> </div>
); );
}; };
export default ServiceMap; const mapStateToProps = (
state: StoreState,
): {
serviceMap: serviceMapStore;
globalTime: GlobalTime;
} => {
return {
serviceMap: state.serviceMap,
globalTime: state.globalTime,
};
};
export default connect(mapStateToProps, {
getServiceMapItems: getServiceMapItems,
getDetailedServiceMapItems: getDetailedServiceMapItems,
})(ServiceMap);

View File

@ -0,0 +1 @@
export { default } from "./ServiceMap";

View File

@ -0,0 +1,28 @@
import { uniqBy, uniq, maxBy, cloneDeep } from "lodash";
import { servicesMapItem } from "Src/store/actions";
import { graphDataType } from "./ServiceMap";
export const getGraphData = (item: servicesMapItem[]): graphDataType => {
const highestNum = maxBy(item, (e) => e.callCount).callCount;
const divNum = Number(String(1).padEnd(highestNum.toString().length, "0"));
const links = cloneDeep(item).map((node) => {
const { parent, child, callCount } = node;
return {
source: parent,
target: child,
value: (100 - callCount / divNum) * 0.01,
};
});
const uniqParent = uniqBy(cloneDeep(item), "parent").map((e) => e.parent);
const uniqChild = uniqBy(cloneDeep(item), "child").map((e) => e.child);
const uniqNodes = uniq([...uniqParent, ...uniqChild]);
const nodes = uniqNodes.map((node, i) => ({
id: node,
group: i + 1,
}));
return {
nodes,
links,
};
};

View File

@ -1,5 +1,6 @@
export * from "./types"; export * from "./types";
export * from "./traceFilters"; export * from "./traceFilters";
export * from "./serviceMap";
export * from "./traces"; export * from "./traces";
export * from "./metrics"; export * from "./metrics";
export * from "./usage"; export * from "./usage";

View File

@ -0,0 +1,96 @@
import { Dispatch } from "redux";
import api, { apiV1 } from "../../api";
import { GlobalTime } from "./global";
import { ActionTypes } from "./types";
export interface serviceMapStore {
items: servicesMapItem[];
services: servicesItem[];
}
export interface servicesItem {
serviceName: string;
p99: number;
avgDuration: number;
numCalls: number;
callRate: number;
numErrors: number;
errorRate: number;
num4XX: number;
fourXXRate: number;
}
export interface servicesMapItem {
parent: string;
child: string;
callCount: number;
}
export interface serviceMapItemAction {
type: ActionTypes.getServiceMapItems;
payload: servicesMapItem[];
}
export interface servicesAction {
type: ActionTypes.getServices;
payload: servicesItem[];
}
export const getServiceMapItems = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string =
"/serviceMapDependencies?start=" +
globalTime.minTime +
"&end=" +
globalTime.maxTime;
// const response = await api.get<servicesMapItem[]>(apiV1 + request_string);
const response = {
data: [
{
parent: "driver",
child: "redis",
callCount: 17050,
},
{
parent: "frontend",
child: "driver",
callCount: 1263,
},
{
parent: "customer",
child: "mysql",
callCount: 1262,
},
{
parent: "frontend",
child: "customer",
callCount: 1262,
},
{
parent: "frontend",
child: "route",
callCount: 12636,
},
],
};
dispatch<serviceMapItemAction>({
type: ActionTypes.getServiceMapItems,
payload: response.data,
});
};
};
export const getDetailedServiceMapItems = (globalTime: GlobalTime) => {
return async (dispatch: Dispatch) => {
let request_string =
"/services?start=" + globalTime.minTime + "&end=" + globalTime.maxTime;
const response = await api.get<servicesItem[]>(apiV1 + request_string);
dispatch<servicesAction>({
type: ActionTypes.getServices,
payload: response.data,
});
};
};

View File

@ -10,6 +10,7 @@ import {
getFilteredTraceMetricsAction, getFilteredTraceMetricsAction,
getDbOverViewMetricsAction, getDbOverViewMetricsAction,
} from "./metrics"; } from "./metrics";
import { serviceMapItemAction, servicesAction } from "./serviceMap";
import { getUsageDataAction } from "./usage"; import { getUsageDataAction } from "./usage";
import { updateTimeIntervalAction } from "./global"; import { updateTimeIntervalAction } from "./global";
@ -28,6 +29,8 @@ export enum ActionTypes {
getUsageData = "GET_USAGE_DATE", getUsageData = "GET_USAGE_DATE",
updateTimeInterval = "UPDATE_TIME_INTERVAL", updateTimeInterval = "UPDATE_TIME_INTERVAL",
getFilteredTraceMetrics = "GET_FILTERED_TRACE_METRICS", getFilteredTraceMetrics = "GET_FILTERED_TRACE_METRICS",
getServiceMapItems = "GET_SERVICE_MAP_ITEMS",
getServices = "GET_SERVICES",
} }
export type Action = export type Action =
@ -44,4 +47,6 @@ export type Action =
| getExternalMetricsAction | getExternalMetricsAction
| externalErrCodeMetricsActions | externalErrCodeMetricsActions
| getDbOverViewMetricsAction | getDbOverViewMetricsAction
| servicesAction
| serviceMapItemAction
| externalMetricsAvgDurationAction; | externalMetricsAvgDurationAction;

View File

@ -10,6 +10,7 @@ import {
usageDataItem, usageDataItem,
GlobalTime, GlobalTime,
externalErrCodeMetricsItem, externalErrCodeMetricsItem,
serviceMapStore,
customMetricsItem, customMetricsItem,
TraceFilters, TraceFilters,
} from "../actions"; } from "../actions";
@ -27,7 +28,7 @@ import {
import { traceFiltersReducer, inputsReducer } from "./traceFilters"; import { traceFiltersReducer, inputsReducer } from "./traceFilters";
import { traceItemReducer, tracesReducer } from "./traces"; import { traceItemReducer, tracesReducer } from "./traces";
import { usageDataReducer } from "./usage"; import { usageDataReducer } from "./usage";
import { ServiceMapReducer } from "./serviceMap";
export interface StoreState { export interface StoreState {
traceFilters: TraceFilters; traceFilters: TraceFilters;
inputTag: string; inputTag: string;
@ -43,6 +44,7 @@ export interface StoreState {
usageDate: usageDataItem[]; usageDate: usageDataItem[];
globalTime: GlobalTime; globalTime: GlobalTime;
filteredTraceMetrics: customMetricsItem[]; filteredTraceMetrics: customMetricsItem[];
serviceMap: serviceMapStore;
} }
const reducers = combineReducers<StoreState>({ const reducers = combineReducers<StoreState>({
@ -60,6 +62,7 @@ const reducers = combineReducers<StoreState>({
usageDate: usageDataReducer, usageDate: usageDataReducer,
globalTime: updateGlobalTimeReducer, globalTime: updateGlobalTimeReducer,
filteredTraceMetrics: filteredTraceMetricsReducer, filteredTraceMetrics: filteredTraceMetricsReducer,
serviceMap: ServiceMapReducer,
}); });
export default reducers; export default reducers;

View File

@ -0,0 +1,24 @@
import { ActionTypes, Action, serviceMapStore } from "../actions";
export const ServiceMapReducer = (
state: serviceMapStore = {
items: [],
services: [],
},
action: Action,
) => {
switch (action.type) {
case ActionTypes.getServiceMapItems:
return {
...state,
items: action.payload,
};
case ActionTypes.getServices:
return {
...state,
services: action.payload,
};
default:
return state;
}
};