{
+ 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 (
- {" "}
- Service Map module coming soon...
- {/* */}
+ 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,
+ );
+ }}
+ />
);
};
-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);
diff --git a/frontend/src/modules/Servicemap/index.ts b/frontend/src/modules/Servicemap/index.ts
new file mode 100644
index 0000000000..c4c0baa7e3
--- /dev/null
+++ b/frontend/src/modules/Servicemap/index.ts
@@ -0,0 +1 @@
+export { default } from "./ServiceMap";
diff --git a/frontend/src/modules/Servicemap/utils.ts b/frontend/src/modules/Servicemap/utils.ts
new file mode 100644
index 0000000000..d06961fde9
--- /dev/null
+++ b/frontend/src/modules/Servicemap/utils.ts
@@ -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,
+ };
+};
diff --git a/frontend/src/store/actions/index.ts b/frontend/src/store/actions/index.ts
index 240adcf773..28fb2a715f 100644
--- a/frontend/src/store/actions/index.ts
+++ b/frontend/src/store/actions/index.ts
@@ -1,5 +1,6 @@
export * from "./types";
export * from "./traceFilters";
+export * from "./serviceMap";
export * from "./traces";
export * from "./metrics";
export * from "./usage";
diff --git a/frontend/src/store/actions/serviceMap.ts b/frontend/src/store/actions/serviceMap.ts
new file mode 100644
index 0000000000..c82a43c64d
--- /dev/null
+++ b/frontend/src/store/actions/serviceMap.ts
@@ -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(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({
+ 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(apiV1 + request_string);
+
+ dispatch({
+ type: ActionTypes.getServices,
+ payload: response.data,
+ });
+ };
+};
diff --git a/frontend/src/store/actions/types.ts b/frontend/src/store/actions/types.ts
index 013a4ff77d..bf05b41d70 100644
--- a/frontend/src/store/actions/types.ts
+++ b/frontend/src/store/actions/types.ts
@@ -10,6 +10,7 @@ import {
getFilteredTraceMetricsAction,
getDbOverViewMetricsAction,
} from "./metrics";
+import { serviceMapItemAction, servicesAction } from "./serviceMap";
import { getUsageDataAction } from "./usage";
import { updateTimeIntervalAction } from "./global";
@@ -28,6 +29,8 @@ export enum ActionTypes {
getUsageData = "GET_USAGE_DATE",
updateTimeInterval = "UPDATE_TIME_INTERVAL",
getFilteredTraceMetrics = "GET_FILTERED_TRACE_METRICS",
+ getServiceMapItems = "GET_SERVICE_MAP_ITEMS",
+ getServices = "GET_SERVICES",
}
export type Action =
@@ -44,4 +47,6 @@ export type Action =
| getExternalMetricsAction
| externalErrCodeMetricsActions
| getDbOverViewMetricsAction
+ | servicesAction
+ | serviceMapItemAction
| externalMetricsAvgDurationAction;
diff --git a/frontend/src/store/reducers/index.ts b/frontend/src/store/reducers/index.ts
index 57abab376c..8c33fde3d5 100644
--- a/frontend/src/store/reducers/index.ts
+++ b/frontend/src/store/reducers/index.ts
@@ -10,6 +10,7 @@ import {
usageDataItem,
GlobalTime,
externalErrCodeMetricsItem,
+ serviceMapStore,
customMetricsItem,
TraceFilters,
} from "../actions";
@@ -27,7 +28,7 @@ import {
import { traceFiltersReducer, inputsReducer } from "./traceFilters";
import { traceItemReducer, tracesReducer } from "./traces";
import { usageDataReducer } from "./usage";
-
+import { ServiceMapReducer } from "./serviceMap";
export interface StoreState {
traceFilters: TraceFilters;
inputTag: string;
@@ -43,6 +44,7 @@ export interface StoreState {
usageDate: usageDataItem[];
globalTime: GlobalTime;
filteredTraceMetrics: customMetricsItem[];
+ serviceMap: serviceMapStore;
}
const reducers = combineReducers({
@@ -60,6 +62,7 @@ const reducers = combineReducers({
usageDate: usageDataReducer,
globalTime: updateGlobalTimeReducer,
filteredTraceMetrics: filteredTraceMetricsReducer,
+ serviceMap: ServiceMapReducer,
});
export default reducers;
diff --git a/frontend/src/store/reducers/serviceMap.ts b/frontend/src/store/reducers/serviceMap.ts
new file mode 100644
index 0000000000..aef22e17f1
--- /dev/null
+++ b/frontend/src/store/reducers/serviceMap.ts
@@ -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;
+ }
+};