Feat: Implement endpoint dependency and metrics (#321)

diff --git a/src/assets/lang/en.ts b/src/assets/lang/en.ts
index 66d52ab..05ce744 100644
--- a/src/assets/lang/en.ts
+++ b/src/assets/lang/en.ts
@@ -52,9 +52,9 @@
   cpm: 'Cpm',
   sla: 'SLA',
   latency: 'Latency',
-  avgResponseTime: 'Avg Response Time',
-  avgThroughput: 'Avg Throughput',
-  avgSLA: 'Avg SLA',
+  avgResponseTime: 'Avg Response Time ( ms )',
+  avgThroughput: 'Load (CPM - calls per minute)',
+  avgSLA: 'Successful Rate ( % )',
   all: 'All',
   success: 'Success',
   error: 'Error',
@@ -100,7 +100,7 @@
   weekCutTip: 'Last 1 week',
   monthCutTip: 'Last 1 month',
   serverZone: 'Server Zone',
-  percentResponse: 'Response Time Percentile',
+  percentResponse: 'Response Time Percentile ( ms )',
   exportImage: 'Export image',
   queryData: 'Query',
   previousService: 'Previous Service',
@@ -148,6 +148,7 @@
   descendOrder: 'Descend Order',
   increaseOrder: 'Increase Order',
   chartType: 'Chart Type',
+  currentDepth: 'Current Depth',
 };
 
 export default m;
diff --git a/src/assets/lang/zh.ts b/src/assets/lang/zh.ts
index 6ca18a8..abeb7fb 100644
--- a/src/assets/lang/zh.ts
+++ b/src/assets/lang/zh.ts
@@ -148,6 +148,7 @@
   descendOrder: '递减顺序',
   increaseOrder: '递增顺序',
   chartType: '图表类型',
+  currentDepth: '当前深度',
 };
 
 export default m;
diff --git a/src/graph/fragments/topology.ts b/src/graph/fragments/topology.ts
index 3075faf..18288f9 100644
--- a/src/graph/fragments/topology.ts
+++ b/src/graph/fragments/topology.ts
@@ -228,6 +228,26 @@
     }
   }`,
 };
+export const endpointTopology = {
+  variable: ['$endpointId: ID!', '$duration: Duration!'],
+  query: `
+  endpointTopology: getEndpointDependencies(endpointId: $endpointId, duration: $duration) {
+    nodes {
+      id
+      name
+      serviceId
+      serviceName
+      type
+      isReal
+    }
+    calls {
+      id
+      source
+      target
+      detectPoints
+    }
+  }`,
+};
 export const TopoMetric = {
   variable: '$ids: [ID!]!',
   query: `
@@ -371,3 +391,82 @@
     }
 `,
 };
+export const TopoEndpointDependencyMetrics = {
+  variable: [
+    '$serviceName: String',
+    '$endpointName: String',
+    '$destServiceName: String',
+    '$destEndpointName: String',
+    '$duration: Duration!',
+  ],
+  query: `
+    endpointRelationPercentile: readLabeledMetricsValues(condition: {
+      name: "endpoint_relation_percentile"
+      entity: {
+        scope: EndpointRelation
+        serviceName: $serviceName
+        normal: true
+        endpointName: $endpointName
+        destNormal: true
+        destServiceName:  $destServiceName
+        destEndpointName: $destEndpointName
+      }
+    }, labels: ["0", "1", "2", "3", "4"], duration: $duration) {
+      label
+      values {
+        values {value}
+      }
+    }
+    endpointRelationCpm: readMetricsValues(condition: {
+      name: "endpoint_relation_cpm"
+      entity: {
+        scope: EndpointRelation
+        serviceName: $serviceName
+        normal: true
+        endpointName: $endpointName
+        destNormal: true
+        destServiceName:  $destServiceName
+        destEndpointName: $destEndpointName
+      }
+    }, duration: $duration) {
+      label
+      values {
+        values {value}
+      }
+    }
+    endpointRelationRespTime: readMetricsValues(condition: {
+      name: "endpoint_relation_resp_time"
+      entity: {
+        scope: EndpointRelation
+        serviceName: $serviceName
+        normal: true
+        endpointName: $endpointName
+        destNormal: true
+        destServiceName:  $destServiceName
+        destEndpointName: $destEndpointName
+      }
+    }, duration: $duration) {
+      label
+      values {
+        values {value}
+      }
+    }
+    endpointRelationSla: readMetricsValues(condition: {
+      name: "endpoint_relation_sla"
+      entity: {
+        scope: EndpointRelation
+        serviceName: $serviceName
+        normal: true
+        endpointName: $endpointName
+        destNormal: true
+        destServiceName:  $destServiceName
+        destEndpointName: $destEndpointName
+      }
+    }, duration: $duration) {
+      label
+      values {
+        values {value}
+      }
+    }
+`,
+};
diff --git a/src/graph/query/topology.ts b/src/graph/query/topology.ts
index e92126a..d1bf631 100644
--- a/src/graph/query/topology.ts
+++ b/src/graph/query/topology.ts
@@ -30,6 +30,8 @@
   DependencyInstanceServerMetric,
   DependencyInstanceClientMetric,
   TopoServiceDetail,
+  endpointTopology,
+  TopoEndpointDependencyMetrics,
 } from '../fragments/topology';
 
 export const queryTopo = `query queryTopo(${Topo.variable}) {${Topo.query}}`;
@@ -72,3 +74,8 @@
 
 export const queryTopoServiceDetail = `query queryTopoServiceDetail(
   ${TopoServiceDetail.variable}) {${TopoServiceDetail.query}}`;
+
+export const queryEndpointTopology = `query queryEndpointTopology(${endpointTopology.variable}) {${endpointTopology.query}}`;
+
+export const queryTopoEndpointDependencyMetrics = `query queryTopoEndpointDependencyMetrics(
+  ${TopoEndpointDependencyMetrics.variable}) {${TopoEndpointDependencyMetrics.query}}`;
diff --git a/src/store/modules/topology/index.ts b/src/store/modules/topology/index.ts
index bdf4b22..28d4ecb 100644
--- a/src/store/modules/topology/index.ts
+++ b/src/store/modules/topology/index.ts
@@ -18,13 +18,19 @@
 import { Commit, ActionTree, Dispatch } from 'vuex';
 import graph from '@/graph';
 import * as types from '../../mutation-types';
-import { AxiosResponse } from 'axios';
+import axios, { AxiosPromise, AxiosResponse } from 'axios';
+import { cancelToken } from '@/utils/cancelToken';
 
 interface Option {
   key: string;
   label: string;
 }
-interface Call {
+export interface Duration {
+  start: string;
+  end: string;
+  step: string;
+}
+export interface Call {
   avgResponseTime: number;
   cpm: number;
   isAlert: boolean;
@@ -47,12 +53,18 @@
   type: string;
 }
 
+export interface EndpointDependencyConidition {
+  serviceName: string;
+  endpointName: string;
+  destServiceName: string;
+  destEndpointName: string;
+  duration: Duration;
+}
+
 export interface State {
   callback: any;
   calls: Call[];
   nodes: Node[];
-  _calls: Call[];
-  _nodes: Node[];
   detectPoints: string[];
   selectedServiceCall: Call | null;
   currentNode: any;
@@ -67,8 +79,14 @@
     calls: Call[];
     nodes: Node[];
   };
+  endpointDependency: {
+    calls: Call[];
+    nodes: Node[];
+  };
   selectedInstanceCall: Call | null;
   instanceDependencyMetrics: { [key: string]: any };
+  endpointDependencyMetrics: { [key: string]: any };
+  currentEndpointDepth: { key: number; label: string };
   queryInstanceMetricsType: string;
   serviceThroughput: { Throughput: number[] };
   serviceSLA: { SLA: number[] };
@@ -88,8 +106,6 @@
   selectedServiceCall: null,
   calls: [],
   nodes: [],
-  _calls: [],
-  _nodes: [],
   currentNode: {},
   currentLink: {},
   current: {
@@ -104,8 +120,14 @@
     calls: [],
     nodes: [],
   },
+  endpointDependency: {
+    calls: [],
+    nodes: [],
+  },
   selectedInstanceCall: null,
   instanceDependencyMetrics: {},
+  endpointDependencyMetrics: {},
+  currentEndpointDepth: { key: 2, label: '2' },
   queryInstanceMetricsType: '',
   serviceThroughput: { Throughput: [] },
   serviceSLA: { SLA: [] },
@@ -150,10 +172,6 @@
     state.calls = data.calls;
     state.nodes = data.nodes;
   },
-  [types.SET_TOPO_COPY](state: State, data: any) {
-    state._calls = data.calls;
-    state._nodes = data.nodes;
-  },
   [types.SET_SELECTED_CALL](state: State, data: any) {
     state.selectedServiceCall = data;
   },
@@ -196,6 +214,26 @@
       state.instanceDependencyMetrics.percentResponse[PercentileItem[index]] = item.values.map((i: any) => i.value);
     });
   },
+  [types.SET_ENDPOINT_DEPENDENCY_METRICS](state: State, data: { [key: string]: any }) {
+    state.endpointDependencyMetrics.cpm = data.endpointRelationCpm
+      ? data.endpointRelationCpm.values.values.map((i: any) => i.value)
+      : [];
+    state.endpointDependencyMetrics.respTime = data.endpointRelationRespTime
+      ? data.endpointRelationRespTime.values.values.map((i: any) => i.value)
+      : [];
+    state.endpointDependencyMetrics.sla = data.endpointRelationSla
+      ? data.endpointRelationSla.values.values.map((i: any) => i.value)
+      : [];
+    state.endpointDependencyMetrics.percentile = {};
+    if (!data.endpointRelationPercentile) {
+      return;
+    }
+    for (const item of data.endpointRelationPercentile) {
+      state.endpointDependencyMetrics.percentile[PercentileItem[Number(item.label)]] = item.values.values.map(
+        (i: any) => i.value,
+      );
+    }
+  },
   [types.SET_INSTANCE_DEPEDENCE_TYPE](state: State, data: string) {
     state.queryInstanceMetricsType = data;
   },
@@ -263,41 +301,43 @@
     state.topoEndpoints.push(comp);
     window.localStorage.setItem('topologyEndpoints', JSON.stringify(state.topoEndpoints));
   },
+  [types.SET_ENDPOINT_DEPENDENCY](state: State, data: { calls: Call[]; nodes: Node[] }) {
+    state.endpointDependency = data;
+  },
+  [types.SET_ENDPOINT_DEPTH](state: State, data: { key: number; label: string }) {
+    state.currentEndpointDepth = data;
+  },
 };
 
 // actions
 const actions: ActionTree<State, any> = {
-  FILTER_TOPO(context: { commit: Commit; state: State }, params: { services: string[]; group: string }) {
-    const tempCalls = [...context.state._calls];
-    const tempNodes = [...context.state._nodes];
-    if (params.group === 'all') {
-      context.commit(types.SET_TOPO, { calls: context.state._calls, nodes: context.state._nodes });
-      return;
+  GET_SERVICES(context: { commit: Commit }, params: { duration: Duration; keyword: string }) {
+    if (!params.keyword) {
+      params.keyword = '';
     }
-    const nodeInCalls: string[] = [];
-    const resultNodes: Node[] = [];
-    const resultCalls: Call[] = [];
-    tempCalls.forEach((call: any) => {
-      if (
-        params.services.some((i: string) => call.source.id === i) ||
-        params.services.some((i: string) => call.target.id === i)
-      ) {
-        nodeInCalls.push(call.source.id);
-        nodeInCalls.push(call.target.id);
-        resultCalls.push(call);
-      }
-    });
-    const setNodes: string[] = Array.from(new Set(nodeInCalls));
-    tempNodes.forEach((node: any) => {
-      if (setNodes.some((i: string) => node.id === i)) {
-        resultNodes.push(node);
-      }
-    });
-    context.commit(types.SET_TOPO, { calls: resultCalls, nodes: resultNodes });
+    return graph
+      .query('queryServices')
+      .params(params)
+      .then((res: AxiosResponse) => {
+        return res.data.data.services || [];
+      });
+  },
+  GET_SERVICE_ENDPOINTS(context: { commit: Commit }, params: { serviceId: string; keyword: string }) {
+    if (!params.serviceId) {
+      return new Promise((resolve) => resolve());
+    }
+    if (!params.keyword) {
+      params.keyword = '';
+    }
+    return graph
+      .query('queryEndpoints')
+      .params(params)
+      .then((res: AxiosResponse) => {
+        return res.data.data.getEndpoints || [];
+      });
   },
   CLEAR_TOPO(context: { commit: Commit; state: State }) {
     context.commit(types.SET_TOPO, { calls: [], nodes: [] });
-    context.commit(types.SET_TOPO_COPY, { calls: [], nodes: [] });
   },
   CLEAR_TOPO_INFO(context: { commit: Commit; state: State }) {
     context.commit(types.SET_TOPO_RELATION, {});
@@ -316,7 +356,7 @@
       context.dispatch('INSTANCE_RELATION_INFO', params);
     }
   },
-  GET_TOPO_SERVICE_INFO(context: { commit: Commit; state: State }, params: any) {
+  GET_TOPO_SERVICE_INFO(context: { commit: Commit; state: State }, params: { id: string; duration: Duration }) {
     if (!params.id) {
       return;
     }
@@ -346,7 +386,10 @@
         context.commit(types.SET_SELECTED_CALL, params);
       });
   },
-  GET_TOPO_SERVICE_DETAIL(context: { commit: Commit; state: State }, params: any) {
+  GET_TOPO_SERVICE_DETAIL(
+    context: { commit: Commit; state: State },
+    params: { serviceId: string; duration: Duration },
+  ) {
     return graph
       .query('queryTopoServiceDetail')
       .params({
@@ -387,7 +430,6 @@
           .then((info: AxiosResponse) => {
             const resInfo = info.data.data;
             if (!resInfo.sla) {
-              context.commit(types.SET_TOPO_COPY, { calls, nodes });
               return context.commit(types.SET_TOPO, { calls, nodes });
             }
             for (let i = 0; i < resInfo.sla.values.length; i += 1) {
@@ -404,7 +446,6 @@
               }
             }
             if (!resInfo.cpmC) {
-              context.commit(types.SET_TOPO_COPY, { calls, nodes });
               return context.commit(types.SET_TOPO, { calls, nodes });
             }
             for (let i = 0; i < resInfo.cpmC.values.length; i += 1) {
@@ -420,7 +461,6 @@
               }
             }
             if (!resInfo.cpmS) {
-              context.commit(types.SET_TOPO_COPY, { calls, nodes });
               return context.commit(types.SET_TOPO, { calls, nodes });
             }
             for (let i = 0; i < resInfo.cpmS.values.length; i += 1) {
@@ -434,17 +474,153 @@
                 }
               }
             }
-            context.commit(types.SET_TOPO_COPY, { calls, nodes });
             context.commit(types.SET_TOPO, { calls, nodes });
           });
       });
   },
+  // todo sync
+  GET_ALL_ENDPOINT_DEPENDENCY(
+    context: { commit: Commit; state: State; dispatch: Dispatch },
+    params: { endpointIds: string[]; duration: Duration },
+  ) {
+    context.dispatch('GET_ENDPOINT_TOPO', params).then((res) => {
+      if (context.state.currentEndpointDepth.key > 1) {
+        const endpointIds = res.nodes.map((item: Node) => item.id);
+
+        context.dispatch('GET_ENDPOINT_TOPO', { endpointIds, duration: params.duration }).then((json) => {
+          if (context.state.currentEndpointDepth.key > 2) {
+            const ids = json.nodes.map((item: Node) => item.id);
+
+            context.dispatch('GET_ENDPOINT_TOPO', { endpointIds: ids, duration: params.duration }).then((topo) => {
+              if (context.state.currentEndpointDepth.key > 3) {
+                const endpoints = topo.nodes.map((item: Node) => item.id);
+                context
+                  .dispatch('GET_ENDPOINT_TOPO', { endpointIds: endpoints, duration: params.duration })
+                  .then((data) => {
+                    if (context.state.currentEndpointDepth.key > 4) {
+                      context
+                        .dispatch('GET_ENDPOINT_TOPO', { endpointIds: endpoints, duration: params.duration })
+                        .then((topos) => {
+                          context.commit(types.SET_ENDPOINT_DEPENDENCY, topos);
+                        });
+                    } else {
+                      context.commit(types.SET_ENDPOINT_DEPENDENCY, data);
+                    }
+                  });
+              } else {
+                context.commit(types.SET_ENDPOINT_DEPENDENCY, topo);
+              }
+            });
+          } else {
+            context.commit(types.SET_ENDPOINT_DEPENDENCY, json);
+          }
+        });
+      } else {
+        context.commit(types.SET_ENDPOINT_DEPENDENCY, res);
+      }
+    });
+  },
+  GET_ENDPOINT_TOPO(context: { commit: Commit; state: State }, params: { endpointIds: string[]; duration: Duration }) {
+    const variables = ['$duration: Duration!'];
+    const fragment = params.endpointIds.map((id: string, index: number) => {
+      return `endpointTopology${index}: getEndpointDependencies(endpointId: "${id}", duration: $duration) {
+        nodes {
+          id
+          name
+          serviceId
+          serviceName
+          type
+          isReal
+        }
+        calls {
+          id
+          source
+          target
+          detectPoints
+        }
+      }`;
+    });
+    const querys = `query queryData(${variables}) {${fragment}}`;
+    return axios
+      .post('/graphql', { query: querys, variables: { duration: params.duration } }, { cancelToken: cancelToken() })
+      .then((res: AxiosResponse) => {
+        if (res.data.errors) {
+          context.commit(types.SET_ENDPOINT_DEPENDENCY, { calls: [], nodes: [] });
+          return;
+        }
+        const topo = res.data.data;
+        const calls = [] as any;
+        let nodes = [] as any;
+        for (const key of Object.keys(topo)) {
+          calls.push(...topo[key].calls);
+          nodes.push(...topo[key].nodes);
+        }
+        const obj = {} as any;
+        nodes = nodes.reduce((prev: Node[], next: Node) => {
+          if (!obj[next.id]) {
+            obj[next.id] = true;
+            prev.push(next);
+          }
+          return prev;
+        }, []);
+        const queryVariables = ['$duration: Duration!'];
+        const fragments = calls
+          .map((call: Call & EndpointDependencyConidition, index: number) => {
+            let source = {} as any;
+            let target = {} as any;
+            for (const node of nodes) {
+              if (node.id === call.source) {
+                source = node;
+                call.serviceName = node.serviceName;
+                call.endpointName = node.name;
+              }
+              if (node.id === call.target) {
+                target = node;
+                call.destServiceName = node.serviceName;
+                call.destEndpointName = node.name;
+              }
+            }
+            return `cpm_${index}: readMetricsValue(condition: {
+            name: "endpoint_relation_cpm"
+            entity: {
+              scope: EndpointRelation
+              serviceName: "${source.serviceName}"
+              normal: true
+              endpointName: "${source.name}"
+              destNormal: true
+              destServiceName: "${target.serviceName}"
+              destEndpointName: "${target.name}"
+            }
+          }, duration: $duration)`;
+          })
+          .join(' ');
+        const query = `query queryData(${queryVariables}) {${fragments}}`;
+        return axios
+          .post('/graphql', { query, variables: { duration: params.duration } }, { cancelToken: cancelToken() })
+          .then((json: AxiosResponse<any>) => {
+            if (json.data.errors) {
+              context.commit(types.SET_ENDPOINT_DEPENDENCY, { calls: [], nodes: [] });
+              return;
+            }
+            const cpms = json.data.data;
+            const keys = Object.keys(cpms);
+            for (const key of keys) {
+              const index = Number(key.split('_')[1]);
+              calls[index].value = cpms[key] || 0.01;
+            }
+            return { calls, nodes };
+          })
+          .catch(() => {
+            context.commit(types.SET_ENDPOINT_DEPENDENCY, { calls: [], nodes: [] });
+          });
+      });
+  },
   async GET_TOPO_INSTANCE_DEPENDENCY(
     context: { commit: Commit; state: State },
     params: {
       clientServiceId: string;
       serverServiceId: string;
-      duration: string;
+      duration: Duration;
     },
   ) {
     graph
@@ -511,7 +687,7 @@
   },
   INSTANCE_RELATION_INFO(
     context: { commit: Commit; state: State },
-    params: Call & { mode: string; queryType: string; durationTime: string },
+    params: Call & { mode: string; queryType: string; durationTime: Duration },
   ) {
     graph
       .query(params.queryType)
@@ -528,6 +704,17 @@
         context.commit(types.SET_INSTANCE_DEPEDENCE_METRICS, res.data.data);
       });
   },
+  GET_ENDPOINT_DEPENDENCY_METRICS(context: { commit: Commit; state: State }, params: EndpointDependencyConidition) {
+    return graph
+      .query('queryTopoEndpointDependencyMetrics')
+      .params(params)
+      .then((res: AxiosResponse) => {
+        if (!(res.data && res.data.data)) {
+          return;
+        }
+        context.commit(types.SET_ENDPOINT_DEPENDENCY_METRICS, res.data.data);
+      });
+  },
 };
 
 export default {
diff --git a/src/store/mutation-types.ts b/src/store/mutation-types.ts
index 0d7d7b0..dc05d2d 100644
--- a/src/store/mutation-types.ts
+++ b/src/store/mutation-types.ts
@@ -79,7 +79,6 @@
 export const SET_HONEYCOMB_NODE = 'SET_HONEYCOMB_NODE';
 export const SET_SHOW_DIALOG = 'SET_SHOW_DIALOG';
 export const SET_INSTANCE_DEPENDENCY = 'SET_INSTANCE_DEPENDENCY';
-export const SET_TOPO_COPY = 'SET_TOPO_COPY';
 export const SET_LINK = 'SET_LINK';
 export const SET_SERVICE_DETAIL = 'SET_SERVICE_DETAIL';
 export const SET_SERVICE_TOPOLOGY = 'GET_SERVICE_TOPOLOGY';
@@ -95,6 +94,9 @@
 export const DELETE_TOPO_ENDPOINT = 'DELETE_TOPO_ENDPOINT';
 export const ADD_TOPO_INSTANCE_COMP = 'ADD_TOPO_INSTANCE_COMP';
 export const ADD_TOPO_ENDPOINT_COMP = 'ADD_TOPO_ENDPOINT_COMP';
+export const SET_ENDPOINT_DEPENDENCY = 'SET_ENDPOINT_DEPENDENCY';
+export const SET_ENDPOINT_DEPENDENCY_METRICS = 'SET_ENDPOINT_DEPENDENCY_METRICS';
+export const SET_ENDPOINT_DEPTH = 'SET_ENDPOINT_DEPTH';
 
 // profile
 export const SET_TASK_OPTIONS = 'SET_TASK_OPTIONS';
diff --git a/src/views/components/dashboard/charts/chart-line.vue b/src/views/components/dashboard/charts/chart-line.vue
index 3527efe..407dac7 100644
--- a/src/views/components/dashboard/charts/chart-line.vue
+++ b/src/views/components/dashboard/charts/chart-line.vue
@@ -43,7 +43,9 @@
           },
         };
         if (this.type === 'areaChart') {
-          serie.areaStyle = {};
+          serie.areaStyle = {
+            opacity: 0.4,
+          };
         }
         return serie;
       });
diff --git a/src/views/components/topology/chart-line.vue b/src/views/components/topology/chart-line.vue
index 7a9bf61..eb9cd80 100644
--- a/src/views/components/topology/chart-line.vue
+++ b/src/views/components/topology/chart-line.vue
@@ -16,7 +16,7 @@
 <template>
   <div>
     <div class="grey sm mb-5">{{ title }}</div>
-    <RkEcharts height="90px" :option="option" />
+    <RkEcharts height="120px" :option="option" />
   </div>
 </template>
 <script lang="ts">
diff --git a/src/views/components/topology/chart/topo.vue b/src/views/components/topology/chart/topo.vue
index a2a0762..6fc604b 100644
--- a/src/views/components/topology/chart/topo.vue
+++ b/src/views/components/topology/chart/topo.vue
@@ -69,7 +69,7 @@
         {icon: 'INSTANCE', click: this.handleGoInstance},
         {icon: 'TRACE', click: this.handleGoTrace},
         {icon: 'ALARM', click: this.handleGoAlarm},
-        {icon: ''},
+        {icon: 'ENDPOINT', click: this.handleGoEndpointDependency},
         {icon: ''},
       ]);
       this.svg.on('click', (d, i) => {
@@ -106,6 +106,10 @@
         });
         this.$emit('setDialog', 'endpoint');
       },
+      // endpoint dependency hexagon
+      handleGoEndpointDependency() {
+        this.$emit('setDialog', 'endpoint_dependency');
+      },
       handleNodeClick(d) {
         this.$emit('setCurrent', { key: d.id, label: d.name });
         const {x, y, vx, vy, fx, fy, index, ...rest} = d;
diff --git a/src/views/components/topology/chart/utils/tool/ENDPOINT.png b/src/views/components/topology/chart/utils/tool/ENDPOINT.png
new file mode 100644
index 0000000..de26135
--- /dev/null
+++ b/src/views/components/topology/chart/utils/tool/ENDPOINT.png
Binary files differ
diff --git a/src/views/components/topology/dependency-sankey.vue b/src/views/components/topology/dependency-sankey.vue
index 22a88ab..406242f 100644
--- a/src/views/components/topology/dependency-sankey.vue
+++ b/src/views/components/topology/dependency-sankey.vue
@@ -63,11 +63,7 @@
 
     private clickLinks(params: any) {
       if (params.dataType === 'edge' && params.data) {
-        this.GET_INSTANCE_DEPENDENCY_METRICS({
-          ...params.data,
-          durationTime: this.durationTime,
-          mode: params.data.detectPoints[0],
-        });
+        this.$emit('showMetrics', params.data);
       }
     }
   }
diff --git a/src/views/components/topology/topo-chart.vue b/src/views/components/topology/topo-chart.vue
index 9b18346..8e017f8 100644
--- a/src/views/components/topology/topo-chart.vue
+++ b/src/views/components/topology/topo-chart.vue
@@ -15,7 +15,7 @@
 <template>
   <div>
     <div class="grey sm mb-5">{{ title }}</div>
-    <h5 class="mt-0 mb-0">{{ content.toFixed(2) }} {{ unit }}</h5>
+    <h5 class="grey mt-0 mb-0">{{ content.toFixed(2) }} {{ unit }}</h5>
     <RkEcharts height="100px" :option="responseConfig" />
   </div>
 </template>
@@ -78,8 +78,9 @@
         },
         series: [
           {
-            data: this.data.map((i: any, index: number) => [this.intervalTime[index], i]),
-            type: this.precent ? 'bar' : 'line',
+            data: this.data.map((i: any, index: number) => [this.intervalTime[index], this.precent ? i / 100 : i]),
+            // type: this.precent ? 'bar' : 'line',
+            type: 'line',
             symbol: 'none',
             barMaxWidth: 5,
             lineStyle: {
diff --git a/src/views/components/topology/topo-endpoint-dependency.vue b/src/views/components/topology/topo-endpoint-dependency.vue
new file mode 100644
index 0000000..3bcb97d
--- /dev/null
+++ b/src/views/components/topology/topo-endpoint-dependency.vue
@@ -0,0 +1,126 @@
+<!-- Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. -->
+<template>
+  <div class="rk-endpoint-dependency">
+    <div class="endpoint-dependency-chart">
+      <DependencySankey
+        v-if="stateTopo.endpointDependency.nodes.length"
+        :data="stateTopo.endpointDependency"
+        @showMetrics="showEndpointMetrics"
+      />
+      <div v-else class="endpoint-dependency-empty">
+        No Endpoint Dependency
+      </div>
+    </div>
+    <div class="endpoint-dependency-metrics">
+      <div v-if="respTime.length">
+        <TopoChart :data="respTime" :intervalTime="intervalTime" :title="$t('avgResponseTime')" unit="ms" />
+      </div>
+      <div v-if="cpm.length">
+        <TopoChart :data="cpm" :intervalTime="intervalTime" :title="$t('avgThroughput')" unit="cpm" />
+      </div>
+      <div v-if="sla.length">
+        <TopoChart :data="sla" :intervalTime="intervalTime" :precent="true" :title="$t('avgSLA')" unit="%" />
+      </div>
+      <div v-if="percentile.p50">
+        <ChartLine :data="percentile" :intervalTime="intervalTime" :title="$t('percentResponse')" />
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts">
+  import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
+  import { State, Action, Getter, Mutation } from 'vuex-class';
+  import { State as topoState, EndpointDependencyConidition, Call, Duration } from '@/store/modules/topology';
+  import TopoChart from './topo-chart.vue';
+  import DependencySankey from './dependency-sankey.vue';
+  import ChartLine from './chart-line.vue';
+
+  @Component({
+    components: {
+      ChartLine,
+      TopoChart,
+      DependencySankey,
+    },
+  })
+  export default class TopoEndpointDependency extends Vue {
+    @Getter('durationTime') private durationTime!: Duration;
+    @Getter('intervalTime') private intervalTime: any;
+    @State('rocketTopo') private stateTopo!: topoState;
+    @Action('rocketTopo/GET_ENDPOINT_DEPENDENCY_METRICS') private GET_ENDPOINT_DEPENDENCY_METRICS: any;
+
+    private respTime: number[] = [];
+    private cpm: number[] = [];
+    private sla: number[] = [];
+    private percentile: { [key: string]: number[] } = {};
+
+    private showEndpointMetrics(data: EndpointDependencyConidition & Call) {
+      this.GET_ENDPOINT_DEPENDENCY_METRICS({
+        serviceName: data.serviceName,
+        endpointName: data.endpointName,
+        destServiceName: data.destServiceName,
+        destEndpointName: data.destEndpointName,
+        duration: this.durationTime,
+      }).then(() => {
+        this.updateMetrics();
+      });
+    }
+
+    @Watch('stateTopo.endpointDependency.nodes')
+    private updateMetrics() {
+      this.respTime = this.stateTopo.endpointDependencyMetrics.respTime;
+      this.cpm = this.stateTopo.endpointDependencyMetrics.cpm;
+      this.sla = this.stateTopo.endpointDependencyMetrics.sla;
+      this.percentile = this.stateTopo.endpointDependencyMetrics.percentile;
+    }
+
+    private beforeDestroy() {
+      this.stateTopo.endpointDependency = {
+        calls: [],
+        nodes: [],
+      };
+    }
+  }
+</script>
+<style lang="scss">
+  .rk-endpoint-dependency {
+    background: #333840;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow: scroll;
+    .endpoint-dependency-chart {
+      height: 80%;
+      min-height: 500px;
+    }
+    .endpoint-dependency-metrics {
+      height: 20%;
+      min-height: 100px;
+      display: flex;
+      flex-direction: row;
+      padding-left: 10px;
+      > div {
+        width: 25%;
+        height: 100%;
+      }
+    }
+    .endpoint-dependency-empty {
+      color: #fff;
+      text-align: center;
+      height: 500px;
+      line-height: 500px;
+    }
+  }
+</style>
diff --git a/src/views/components/topology/topo-group/index.vue b/src/views/components/topology/topo-group/index.vue
index 2f4fb23..6ace529 100644
--- a/src/views/components/topology/topo-group/index.vue
+++ b/src/views/components/topology/topo-group/index.vue
@@ -1,24 +1,15 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the
+NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses
+this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance
+with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless
+required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS
+IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific
+language governing permissions and * limitations under the License. */
 
 <template>
   <div class="topo-group">
     <div class="topo-group-wrapper">
-      <div v-for="i in pagedData">
+      <div v-for="i in pagedData" :key="i.id">
         <GroupItem
           :servicesMap="servicesMap"
           :active="rocketTopoGroup.groupId === i.id"
@@ -62,7 +53,6 @@
     @Mutation('rocketTopoGroup/DELETE_GROUP') private DELETE_GROUP: any;
     @Mutation('rocketTopoGroup/SELECT_GROUP') private SELECT_GROUP: any;
     @Mutation('SET_EVENTS') private SET_EVENTS: any;
-    @Action('rocketTopo/FILTER_TOPO') private FILTER_TOPO: any;
     @Action('rocketTopo/GET_TOPO') private GET_TOPO: any;
     private servicesMap = [];
     private currentPage = 1;
diff --git a/src/views/components/topology/topo-instance-dependency.vue b/src/views/components/topology/topo-instance-dependency.vue
index ef706f4..b4858b7 100644
--- a/src/views/components/topology/topo-instance-dependency.vue
+++ b/src/views/components/topology/topo-instance-dependency.vue
@@ -15,7 +15,7 @@
 <template>
   <div class="rk-topo-instance-dependency">
     <div class="rk-dependency-chart">
-      <DependencySankey :data="stateTopo.instanceDependency" />
+      <DependencySankey :data="stateTopo.instanceDependency" @showMetrics="showDependencyMetrics" />
     </div>
     <div class="rk-instance-metric-box">
       <div v-if="!stateTopo.instanceDependency.nodes.length">
@@ -108,6 +108,13 @@
         mode,
       });
     }
+    private showDependencyMetrics(data: any) {
+      this.GET_INSTANCE_DEPENDENCY_METRICS({
+        ...data,
+        durationTime: this.durationTime,
+        mode: data.detectPoints[0],
+      });
+    }
   }
 </script>
 <style lang="scss">
diff --git a/src/views/components/topology/topo-services.vue b/src/views/components/topology/topo-services.vue
index 0e31b7e..c9b0f2c 100644
--- a/src/views/components/topology/topo-services.vue
+++ b/src/views/components/topology/topo-services.vue
@@ -20,7 +20,6 @@
 <script lang="ts">
   import { DurationTime } from '@/types/global';
   import compareObj from '@/utils/comparison';
-  import Axios, { AxiosResponse } from 'axios';
   import { Component, Vue, Watch } from 'vue-property-decorator';
   import { Action, Getter, Mutation } from 'vuex-class';
   import TopoSelect from './topo-select.vue';
@@ -29,26 +28,14 @@
   export default class TopoServices extends Vue {
     @Getter('durationTime') public durationTime: any;
     @Action('rocketTopo/GET_TOPO') public GET_TOPO: any;
+    @Action('rocketTopo/GET_SERVICES') private GET_SERVICES: any;
     @Mutation('rocketTopoGroup/UNSELECT_GROUP') private UNSELECT_GROUP: any;
     private services = [{ key: 0, label: 'All services' }];
     private service = { key: 0, label: 'All services' };
 
     private fetchData() {
-      Axios.post('/graphql', {
-        query: `
-      query queryServices($duration: Duration!) {
-        services: getAllServices(duration: $duration) {
-          key: id
-          label: name
-        }
-      }`,
-        variables: {
-          duration: this.durationTime,
-        },
-      }).then((res: AxiosResponse) => {
-        this.services = res.data.data.services
-          ? [{ key: 0, label: 'All services' }, ...res.data.data.services]
-          : [{ key: 0, label: 'All services' }];
+      this.GET_SERVICES({ duration: this.durationTime }).then((json: any[]) => {
+        this.services = [...this.services, ...json];
       });
     }
 
diff --git a/src/views/containers/topology/endpoint-dependency/index.vue b/src/views/containers/topology/endpoint-dependency/index.vue
new file mode 100644
index 0000000..51a5a43
--- /dev/null
+++ b/src/views/containers/topology/endpoint-dependency/index.vue
@@ -0,0 +1,109 @@
+<!-- Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License. -->
+<template>
+  <div class="endpoint-dependency-page">
+    <div class="rk-dashboard-bar flex-h">
+      <ToolBarSelect :selectable="false" :title="this.$t('currentService')" :current="current" icon="package" />
+      <ToolBarEndpointSelect
+        @onChoose="selectEndpoint"
+        :title="$t('currentEndpoint')"
+        :current="stateDashboardOption.currentEndpoint"
+        :data="stateDashboardOption.endpoints"
+        icon="code"
+      />
+      <ToolBarEndpointSelect
+        @onChoose="selectDepth"
+        :title="$t('currentDepth')"
+        :current="rocketTopo.currentEndpointDepth"
+        :data="depths"
+        icon="code"
+      />
+    </div>
+    <TopoEndpointDependency />
+  </div>
+</template>
+
+<script lang="ts">
+  import Vue from 'vue';
+  import { Component, Watch, Prop } from 'vue-property-decorator';
+  import { Action, Getter, State, Mutation } from 'vuex-class';
+  import ToolBarSelect from '@/views/components/dashboard/tool-bar-select.vue';
+  import ToolBarEndpointSelect from '@/views/components/dashboard/tool-bar-endpoint-select.vue';
+  import TopoEndpointDependency from '@/views/components/topology/topo-endpoint-dependency.vue';
+
+  @Component({
+    components: {
+      ToolBarSelect,
+      ToolBarEndpointSelect,
+      TopoEndpointDependency,
+    },
+  })
+  export default class WindowEndpointDependency extends Vue {
+    @State('rocketOption') private stateDashboardOption!: any;
+    @State('rocketTopo') private rocketTopo!: any;
+    @Getter('durationTime') private durationTime: any;
+    @Mutation('SET_CURRENT_SERVICE') private SET_CURRENT_SERVICE: any;
+    @Mutation('SET_EDIT') private SET_EDIT: any;
+    @Mutation('rocketTopo/SET_ENDPOINT_DEPENDENCY_METRICS') private SET_ENDPOINT_DEPENDENCY_METRICS: any;
+    @Mutation('rocketTopo/SET_ENDPOINT_DEPTH') private SET_ENDPOINT_DEPTH: any;
+    @Action('GET_SERVICE_ENDPOINTS') private GET_SERVICE_ENDPOINTS: any;
+    @Action('MIXHANDLE_CHANGE_GROUP_WITH_CURRENT') private MIXHANDLE_CHANGE_GROUP_WITH_CURRENT: any;
+    @Action('SELECT_ENDPOINT') private SELECT_ENDPOINT: any;
+    @Action('rocketTopo/GET_ALL_ENDPOINT_DEPENDENCY') private GET_ALL_ENDPOINT_DEPENDENCY: any;
+    @Prop() private current!: { key: number | string; label: string };
+
+    private depths: Array<{ key: number; label: string }> = [{ key: 2, label: '2' }];
+
+    private beforeMount() {
+      this.SET_CURRENT_SERVICE(this.current);
+      this.MIXHANDLE_CHANGE_GROUP_WITH_CURRENT({ index: 0, current: 2 });
+      this.GET_SERVICE_ENDPOINTS({ duration: this.durationTime, serviceId: this.current.key, keyword: '' }).then(() => {
+        this.selectEndpoint(this.stateDashboardOption.endpoints[0]);
+      });
+      this.depths = [1, 2, 3, 4, 5].map((item: number) => ({ key: item, label: String(item) }));
+    }
+
+    private selectEndpoint(i: any) {
+      this.SELECT_ENDPOINT({ endpoint: i, duration: this.durationTime });
+      this.GET_ALL_ENDPOINT_DEPENDENCY({ endpointIds: [i.key], duration: this.durationTime });
+      this.SET_ENDPOINT_DEPENDENCY_METRICS({ respTime: [], sla: [], cpm: [], percentile: {} });
+    }
+
+    private selectDepth(i: { key: number; label: string }) {
+      this.SET_ENDPOINT_DEPTH(i);
+      this.GET_ALL_ENDPOINT_DEPENDENCY({
+        endpointIds: [this.stateDashboardOption.currentEndpoint.key],
+        duration: this.durationTime,
+      });
+
+      this.SET_ENDPOINT_DEPENDENCY_METRICS({ respTime: [], sla: [], cpm: [], percentile: {} });
+    }
+
+    private beforeDestroy() {
+      this.SET_EDIT(false);
+      this.SET_ENDPOINT_DEPTH({ key: 2, label: '2' });
+      this.$emit('changeEndpointComps', { type: '' });
+    }
+  }
+</script>
+
+<style lang="scss">
+  .endpoint-dependency-page {
+    height: calc(100% - 48px);
+    .rk-dashboard-bar {
+      border-bottom: 1px solid #252a2f;
+    }
+  }
+</style>
diff --git a/src/views/containers/topology/topology.vue b/src/views/containers/topology/topology.vue
index 0271a66..1da0430 100644
--- a/src/views/containers/topology/topology.vue
+++ b/src/views/containers/topology/topology.vue
@@ -24,7 +24,7 @@
     />
     <TopoAside />
     <TopoGroup />
-    <rk-sidebox :show="dialog.length" @update:show="dialog = ''" :fixed="true" width="80%">
+    <rk-sidebox :show="dialog.length" @update:show="dialog = ''" :fixed="true" width="100%">
       <window-endpoint
         v-if="dialog === 'endpoint'"
         :current="this.current"
@@ -41,6 +41,11 @@
       />
       <window-trace v-if="dialog === 'trace'" :current="this.current" />
       <window-alarm v-if="dialog === 'alarm'" :current="this.current" />
+      <window-endpoint-dependency
+        v-if="dialog === 'endpoint_dependency'"
+        @changeEndpointComps="changeEndpointComps"
+        :current="this.current"
+      />
     </rk-sidebox>
   </div>
 </template>
@@ -57,6 +62,7 @@
   import Topo from '../../components/topology/chart/topo.vue';
   import TopoAside from '../../components/topology/topo-aside.vue';
   import TopoGroup from '../../components/topology/topo-group/index.vue';
+  import WindowEndpointDependency from '@/views/containers/topology/endpoint-dependency/index.vue';
 
   @Component({
     components: {
@@ -67,6 +73,7 @@
       WindowInstance,
       WindowTrace,
       WindowAlarm,
+      WindowEndpointDependency,
     },
   })
   export default class Topology extends Vue {