You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by qi...@apache.org on 2023/03/22 09:00:30 UTC

[skywalking-booster-ui] branch main updated: refactor: redesign and implement new topology (#243)

This is an automated email from the ASF dual-hosted git repository.

qiuxiafan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-booster-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new 449dccd  refactor: redesign and implement new topology (#243)
449dccd is described below

commit 449dccdf366fbcbefdd56f36c13db197ed5bd9d9
Author: Fine0830 <fa...@gmail.com>
AuthorDate: Wed Mar 22 17:00:24 2023 +0800

    refactor: redesign and implement new topology (#243)
---
 package-lock.json                                  |  22 +
 package.json                                       |   1 +
 src/{types/topology.d.ts => mock/index.ts}         |  55 +-
 src/types/{topology.d.ts => mock.d.ts}             |  22 +-
 src/types/topology.d.ts                            |   8 +
 src/views/dashboard/controls/Topology.vue          |   2 +-
 .../related/topology/components/Graph.vue          | 600 ++++++++++++---------
 .../related/topology/components/Settings.vue       |   7 +-
 .../related/topology/components/utils/layout.ts    | 122 +++++
 .../topology/components/utils/linkElement.ts       |  60 ---
 .../topology/components/utils/nodeElement.ts       |  85 ---
 .../topology/components/utils/simulation.ts        |  46 --
 12 files changed, 540 insertions(+), 490 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 49fe044..ebf1e90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -49,6 +49,7 @@
         "husky": "^8.0.2",
         "jsdom": "^20.0.3",
         "lint-staged": "^12.1.3",
+        "mockjs": "^1.1.0",
         "node-sass": "^8.0.0",
         "npm-run-all": "^4.1.5",
         "postcss-html": "^1.3.0",
@@ -10168,6 +10169,18 @@
         "node": ">=10"
       }
     },
+    "node_modules/mockjs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz",
+      "integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
+      "dev": true,
+      "dependencies": {
+        "commander": "*"
+      },
+      "bin": {
+        "random": "bin/random"
+      }
+    },
     "node_modules/moment": {
       "version": "2.29.4",
       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
@@ -23559,6 +23572,15 @@
       "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
       "dev": true
     },
+    "mockjs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz",
+      "integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
+      "dev": true,
+      "requires": {
+        "commander": "*"
+      }
+    },
     "moment": {
       "version": "2.29.4",
       "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
diff --git a/package.json b/package.json
index b15fa53..87fb147 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
     "husky": "^8.0.2",
     "jsdom": "^20.0.3",
     "lint-staged": "^12.1.3",
+    "mockjs": "^1.1.0",
     "node-sass": "^8.0.0",
     "npm-run-all": "^4.1.5",
     "postcss-html": "^1.3.0",
diff --git a/src/types/topology.d.ts b/src/mock/index.ts
similarity index 51%
copy from src/types/topology.d.ts
copy to src/mock/index.ts
index bacce26..3481867 100644
--- a/src/types/topology.d.ts
+++ b/src/mock/index.ts
@@ -14,24 +14,37 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export interface Call {
-  source: string | any;
-  target: string | any;
-  id: string;
-  detectPoints: string[];
-  type?: string;
-  sourceObj?: any;
-  targetObj?: any;
-  value?: number;
-  lowerArc?: boolean;
-  sourceComponents: string[];
-  targetComponents: string[];
-}
-export interface Node {
-  id: string;
-  name: string;
-  type: string;
-  isReal: boolean;
-  layer?: string;
-  serviceName?: string;
-}
+import Mock from "mockjs";
+
+const Random = Mock.Random;
+const nodes = Mock.mock({
+  "nodes|500": [
+    {
+      //id
+      id: "@guid",
+      name: "@name",
+      "type|1": ["ActiveMQ", "activemq-consumer", "H2", "APISIX", "Express", "USER", "Flash"],
+      "isReal|1": [true, false],
+    },
+  ],
+});
+const calls = Mock.mock({
+  "links|500": [
+    {
+      //id
+      id: "@guid",
+      detectPoints: ["SERVER", "CLIENT"],
+      source: function () {
+        const d = Random.integer(0, 250);
+        return nodes.nodes[d].id;
+      },
+      target: function () {
+        const d = Random.integer(250, 499);
+        return nodes.nodes[d].id;
+      },
+    },
+  ],
+});
+const callsMock = calls.links;
+const nodesMock = nodes.nodes;
+export { callsMock, nodesMock };
diff --git a/src/types/topology.d.ts b/src/types/mock.d.ts
similarity index 66%
copy from src/types/topology.d.ts
copy to src/types/mock.d.ts
index bacce26..c575679 100644
--- a/src/types/topology.d.ts
+++ b/src/types/mock.d.ts
@@ -14,24 +14,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export interface Call {
-  source: string | any;
-  target: string | any;
-  id: string;
-  detectPoints: string[];
-  type?: string;
-  sourceObj?: any;
-  targetObj?: any;
-  value?: number;
-  lowerArc?: boolean;
-  sourceComponents: string[];
-  targetComponents: string[];
-}
-export interface Node {
-  id: string;
-  name: string;
-  type: string;
-  isReal: boolean;
-  layer?: string;
-  serviceName?: string;
-}
+declare module "mockjs";
diff --git a/src/types/topology.d.ts b/src/types/topology.d.ts
index bacce26..91df945 100644
--- a/src/types/topology.d.ts
+++ b/src/types/topology.d.ts
@@ -26,6 +26,10 @@ export interface Call {
   lowerArc?: boolean;
   sourceComponents: string[];
   targetComponents: string[];
+  sourceX?: number;
+  sourceY?: number;
+  targetY?: number;
+  targetX?: number;
 }
 export interface Node {
   id: string;
@@ -34,4 +38,8 @@ export interface Node {
   isReal: boolean;
   layer?: string;
   serviceName?: string;
+  height?: number;
+  x?: number;
+  y?: number;
+  level?: number;
 }
diff --git a/src/views/dashboard/controls/Topology.vue b/src/views/dashboard/controls/Topology.vue
index 594d01b..9766ab1 100644
--- a/src/views/dashboard/controls/Topology.vue
+++ b/src/views/dashboard/controls/Topology.vue
@@ -59,7 +59,7 @@ limitations under the License. -->
 </script>
 <style lang="scss" scoped>
   .topology {
-    background-color: #333840;
+    // background-color: #333840;
     width: 100%;
     height: 100%;
     font-size: 12px;
diff --git a/src/views/dashboard/related/topology/components/Graph.vue b/src/views/dashboard/related/topology/components/Graph.vue
index 9d7ac03..606060b 100644
--- a/src/views/dashboard/related/topology/components/Graph.vue
+++ b/src/views/dashboard/related/topology/components/Graph.vue
@@ -20,6 +20,69 @@ limitations under the License. -->
     element-loading-background="rgba(0, 0, 0, 0)"
     :style="`height: ${height}px`"
   >
+    <svg class="svg-topology" :width="width - 100" :height="height" style="background-color: #fff" @click="svgEvent">
+      <g class="svg-graph" :transform="`translate(${diff[0]}, ${diff[1]})`">
+        <g
+          class="topo-node"
+          v-for="(n, index) in topologyLayout.nodes"
+          :key="index"
+          @mouseout="hideTip"
+          @mouseover="showNodeTip($event, n)"
+          @click="handleNodeClick($event, n)"
+          @mousedown="startMoveNode($event, n)"
+          @mouseup="stopMoveNode($event)"
+        >
+          <image width="36" height="36" :x="n.x - 15" :y="n.y - 18" :href="getNodeStatus(n)" />
+          <!-- <circle :cx="n.x" :cy="n.y" r="12" fill="none" stroke="red"/> -->
+          <image width="28" height="25" :x="n.x - 14" :y="n.y - 43" :href="icons.LOCAL" style="opacity: 0.8" />
+          <image
+            width="12"
+            height="12"
+            :x="n.x - 6"
+            :y="n.y - 38"
+            :href="!n.type || n.type === `N/A` ? icons.UNDEFINED : icons[n.type.toUpperCase().replace('-', '')]"
+          />
+          <text :x="n.x - (n.name.length * 6) / 2 + 6" :y="n.y + n.height + 8" style="pointer-events: none">
+            {{ n.name.length > 20 ? `${n.name.substring(0, 20)}...` : n.name }}
+          </text>
+        </g>
+        <g v-for="(l, index) in topologyLayout.calls" :key="index">
+          <path
+            class="topo-line"
+            :d="`M${l.sourceX} ${l.sourceY} L${l.targetX} ${l.targetY}`"
+            stroke="#97B0F8"
+            marker-end="url(#arrow)"
+          />
+          <circle
+            class="topo-line-anchor"
+            :cx="(l.sourceX + l.targetX) / 2"
+            :cy="(l.sourceY + l.targetY) / 2"
+            r="4"
+            fill="#97B0F8"
+            @click="handleLinkClick($event, l)"
+            @mouseover="showLinkTip($event, l)"
+            @mouseout="hideTip"
+          />
+        </g>
+        <g class="arrows">
+          <defs v-for="(_, index) in topologyLayout.calls" :key="index">
+            <marker
+              id="arrow"
+              markerUnits="strokeWidth"
+              markerWidth="16"
+              markerHeight="16"
+              viewBox="0 0 12 12"
+              refX="10"
+              refY="6"
+              orient="auto"
+            >
+              <path d="M2,2 L10,6 L2,10 L6,6 L2,2" fill="#97B0F8" />
+            </marker>
+          </defs>
+        </g>
+      </g>
+    </svg>
+    <div id="tooltip"></div>
     <div class="legend">
       <div>
         <img :src="icons.CUBE" />
@@ -53,8 +116,8 @@ limitations under the License. -->
       class="operations-list"
       v-if="topologyStore.node"
       :style="{
-        top: operationsPos.y + 'px',
-        left: operationsPos.x + 'px',
+        top: operationsPos.y + 5 + 'px',
+        left: operationsPos.x + 5 + 'px',
       }"
     >
       <span v-for="(item, index) of items" :key="index" @click="item.func(item.dashboard)">
@@ -68,11 +131,6 @@ limitations under the License. -->
   import { ref, onMounted, onBeforeUnmount, reactive, watch, computed, nextTick } from "vue";
   import { useI18n } from "vue-i18n";
   import * as d3 from "d3";
-  import d3tip from "d3-tip";
-  import zoom from "../../components/utils/zoom";
-  import { simulationInit, simulationSkip } from "./utils/simulation";
-  import nodeElement from "./utils/nodeElement";
-  import { linkElement, anchorElement, arrowMarker } from "./utils/linkElement";
   import type { Node, Call } from "@/types/topology";
   import { useSelectorStore } from "@/store/modules/selectors";
   import { useTopologyStore } from "@/store/modules/topology";
@@ -89,6 +147,8 @@ limitations under the License. -->
   import { aggregation } from "@/hooks/useMetricsProcessor";
   import icons from "@/assets/img/icons";
   import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
+  import { layout, circleIntersection, computeCallPos } from "./utils/layout";
+  import zoom from "../../components/utils/zoom";
 
   /*global Nullable, defineProps */
   const props = defineProps({
@@ -105,70 +165,186 @@ limitations under the License. -->
   const height = ref<number>(100);
   const width = ref<number>(100);
   const loading = ref<boolean>(false);
-  const simulation = ref<any>(null);
   const svg = ref<Nullable<any>>(null);
+  const graph = ref<Nullable<any>>(null);
   const chart = ref<Nullable<HTMLDivElement>>(null);
-  const tip = ref<Nullable<HTMLDivElement>>(null);
-  const graph = ref<any>(null);
-  const node = ref<any>(null);
-  const link = ref<any>(null);
-  const anchor = ref<any>(null);
-  const arrow = ref<any>(null);
   const showSetting = ref<boolean>(false);
   const settings = ref<any>(props.config);
   const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
   const items = ref<{ id: string; title: string; func: any; dashboard?: string }[]>([]);
   const graphConfig = computed(() => props.config.graph || {});
   const depth = ref<number>(graphConfig.value.depth || 2);
+  const topologyLayout = ref<any>({});
+  const tooltip = ref<Nullable<any>>(null);
+  const graphWidth = ref<number>(100);
+  const currentNode = ref<Nullable<Node>>();
+  const diff = computed(() => [(width.value - graphWidth.value - 130) / 2, 100]);
+  const radius = 8;
 
   onMounted(async () => {
     await nextTick();
+    init();
+  });
+  async function init() {
     const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
       height: 40,
       width: 0,
     };
     height.value = dom.height - 40;
     width.value = dom.width;
-
+    svg.value = d3.select(".svg-topology");
+    graph.value = d3.select(".svg-graph");
     loading.value = true;
     const json = await selectorStore.fetchServices(dashboardStore.layerId);
     if (json.errors) {
       ElMessage.error(json.errors);
       return;
     }
+    await freshNodes();
+    svg.value.call(zoom(d3, graph.value, diff.value));
+  }
+  async function freshNodes() {
+    topologyStore.setNode(null);
+    topologyStore.setLink(null);
     const resp = await getTopology();
     loading.value = false;
 
     if (resp && resp.errors) {
       ElMessage.error(resp.errors);
     }
+    await update();
+  }
+
+  async function update() {
+    topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
     topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
     topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
-    topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
     window.addEventListener("resize", resize);
-    svg.value = d3.select(chart.value).append("svg").attr("class", "topo-svg");
     await initLegendMetrics();
-    await init();
-    update();
+    draw();
+    tooltip.value = d3.select("#tooltip");
     setNodeTools(settings.value.nodeDashboard);
-  });
-  async function init() {
-    tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
-    graph.value = svg.value.append("g").attr("class", "topo-svg-graph").attr("transform", `translate(-100, -100)`);
-    graph.value.call(tip.value);
-    simulation.value = simulationInit(d3, topologyStore.nodes, topologyStore.calls, ticked);
-    node.value = graph.value.append("g").selectAll(".topo-node");
-    link.value = graph.value.append("g").selectAll(".topo-line");
-    anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
-    arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
-    svg.value.call(zoom(d3, graph.value, [-100, -100]));
-    svg.value.on("click", (event: PointerEvent) => {
-      event.stopPropagation();
-      event.preventDefault();
-      topologyStore.setNode(null);
-      topologyStore.setLink(null);
-      dashboardStore.selectWidget(props.config);
+  }
+  function draw() {
+    const node = findMostFrequent(topologyStore.calls);
+    const levels = [];
+    const nodes = topologyStore.nodes.sort((a: Node, b: Node) => {
+      if (a.name.toLowerCase() < b.name.toLowerCase()) {
+        return -1;
+      }
+      if (a.name.toLowerCase() > b.name.toLowerCase()) {
+        return 1;
+      }
+      return 0;
     });
+    const index = nodes.findIndex((n: Node) => n.type === "USER");
+    let key = index;
+    if (index < 0) {
+      const idx = nodes.findIndex((n: Node) => n.id === node.id);
+      key = idx;
+    }
+    levels.push([nodes[key]]);
+    nodes.splice(key, 1);
+    for (const level of levels) {
+      const a = [];
+      for (const l of level) {
+        for (const n of topologyStore.calls) {
+          if (n.target === l.id) {
+            const i = nodes.findIndex((d: Node) => d.id === n.source);
+            if (i > -1) {
+              a.push(nodes[i]);
+              nodes.splice(i, 1);
+            }
+          }
+          if (n.source === l.id) {
+            const i = nodes.findIndex((d: Node) => d.id === n.target);
+            if (i > -1) {
+              a.push(nodes[i]);
+              nodes.splice(i, 1);
+            }
+          }
+        }
+      }
+      if (a.length) {
+        levels.push(a);
+      }
+    }
+    topologyLayout.value = layout(levels, topologyStore.calls, radius);
+    graphWidth.value = topologyLayout.value.layout.width;
+    const drag: any = d3.drag().on("drag", (d: { x: number; y: number }) => {
+      moveNode(d);
+    });
+    setTimeout(() => {
+      d3.selectAll(".topo-node").call(drag);
+    }, 1000);
+  }
+
+  function moveNode(d: { x: number; y: number }) {
+    if (!currentNode.value) {
+      return;
+    }
+    for (const node of topologyLayout.value.nodes) {
+      if (node.id === currentNode.value.id) {
+        node.x = d.x;
+        node.y = d.y;
+      }
+    }
+    for (const call of topologyLayout.value.calls) {
+      if (call.sourceObj.id === currentNode.value.id) {
+        call.sourceObj.x = d.x;
+        call.sourceObj.y = d.y;
+      }
+      if (call.targetObj.id === currentNode.value.id) {
+        call.targetObj.x = d.x;
+        call.targetObj.y = d.y;
+      }
+      if (call.targetObj.id === currentNode.value.id || call.sourceObj.id === currentNode.value.id) {
+        const pos: any = circleIntersection(
+          call.sourceObj.x,
+          call.sourceObj.y,
+          radius,
+          call.targetObj.x,
+          call.targetObj.y,
+          radius,
+        );
+        call.sourceX = pos[0].x;
+        call.sourceY = pos[0].y;
+        call.targetX = pos[1].x;
+        call.targetY = pos[1].y;
+      }
+    }
+    topologyLayout.value.calls = computeCallPos(topologyLayout.value.calls, radius);
+  }
+
+  function startMoveNode(event: MouseEvent, d: Node) {
+    event.stopPropagation();
+    currentNode.value = d;
+  }
+  function stopMoveNode(event: MouseEvent) {
+    event.stopPropagation();
+    currentNode.value = null;
+  }
+
+  function findMostFrequent(arr: Call[]) {
+    let count: any = {};
+    let maxCount = 0;
+    let maxItem = null;
+
+    for (let i = 0; i < arr.length; i++) {
+      let item = arr[i];
+      count[item.sourceObj.id] = (count[item.sourceObj.id] || 0) + 1;
+      if (count[item.sourceObj.id] > maxCount) {
+        maxCount = count[item.sourceObj.id];
+        maxItem = item.sourceObj;
+      }
+      count[item.targetObj.id] = (count[item.targetObj.id] || 0) + 1;
+      if (count[item.targetObj.id] > maxCount) {
+        maxCount = count[item.targetObj.id];
+        maxItem = item.targetObj;
+      }
+    }
+
+    return maxItem;
   }
 
   async function initLegendMetrics() {
@@ -182,43 +358,91 @@ limitations under the License. -->
       }
     }
   }
-  function ticked() {
-    link.value.attr(
-      "d",
-      (d: Call | any) =>
-        `M${d.source.x} ${d.source.y} Q ${(d.source.x + d.target.x) / 2} ${
-          (d.target.y + d.source.y) / 2 - d.loopFactor * 90
-        } ${d.target.x} ${d.target.y}`,
-    );
-    anchor.value.attr(
-      "transform",
-      (d: Call | any) =>
-        `translate(${(d.source.x + d.target.x) / 2}, ${(d.target.y + d.source.y) / 2 - d.loopFactor * 45})`,
-    );
-    node.value.attr("transform", (d: Node | any) => `translate(${d.x - 22},${d.y - 22})`);
-  }
-  function dragstart(d: any) {
-    node.value._groups[0].forEach((g: any) => {
-      g.__data__.fx = g.__data__.x;
-      g.__data__.fy = g.__data__.y;
-    });
-    if (!d.active) {
-      simulation.value.alphaTarget(0.1).restart();
+  function getNodeStatus(d: any) {
+    const legend = settings.value.legend;
+    if (!legend) {
+      return icons.CUBE;
     }
-    d.subject.fx = d.subject.x;
-    d.subject.fy = d.subject.y;
-    d.sourceEvent.stopPropagation();
+    if (!legend.length) {
+      return icons.CUBE;
+    }
+    let c = true;
+    for (const l of legend) {
+      if (l.condition === "<") {
+        c = c && d[l.name] < Number(l.value);
+      } else {
+        c = c && d[l.name] > Number(l.value);
+      }
+    }
+    return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
   }
-  function dragged(d: any) {
-    d.subject.fx = d.x;
-    d.subject.fy = d.y;
+  function showNodeTip(event: MouseEvent, data: Node) {
+    const nodeMetrics: string[] = settings.value.nodeMetrics || [];
+    const nodeMetricConfig = settings.value.nodeMetricConfig || [];
+    const html = nodeMetrics.map((m, index) => {
+      const metric =
+        topologyStore.nodeMetricValue[m].values.find((val: { id: string; value: unknown }) => val.id === data.id) || {};
+      const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
+      const v = aggregation(metric.value, opt);
+      return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || "unknown"}</div>`;
+    });
+    const tipHtml = [
+      `<div class="mb-5"><span class="grey">name: </span>${
+        data.name
+      }</div><div class="mb-5"><span class="grey">type: </span>${data.type || "UNKNOWN"}</div>`,
+      ...html,
+    ].join(" ");
+
+    tooltip.value
+      .style("top", event.offsetY + 10 + "px")
+      .style("left", event.offsetX + 10 + "px")
+      .style("visibility", "visible")
+      .html(tipHtml);
   }
-  function dragended(d: any) {
-    if (!d.active) {
-      simulation.value.alphaTarget(0);
-    }
+  function showLinkTip(event: MouseEvent, data: Call) {
+    const linkClientMetrics: string[] = settings.value.linkClientMetrics || [];
+    const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
+    const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
+    const linkServerMetrics: string[] = settings.value.linkServerMetrics || [];
+    const htmlServer = linkServerMetrics.map((m, index) => {
+      const metric = topologyStore.linkServerMetrics[m].values.find(
+        (val: { id: string; value: unknown }) => val.id === data.id,
+      );
+      if (metric) {
+        const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
+        const v = aggregation(metric.value, opt);
+        return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
+      }
+    });
+    const htmlClient = linkClientMetrics.map((m: string, index: number) => {
+      const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
+      const metric = topologyStore.linkClientMetrics[m].values.find(
+        (val: { id: string; value: unknown }) => val.id === data.id,
+      );
+      if (metric) {
+        const v = aggregation(metric.value, opt);
+        return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
+      }
+    });
+    const html = [
+      ...htmlServer,
+      ...htmlClient,
+      `<div><span class="grey">${t("detectPoint")}:</span>${data.detectPoints.join(" | ")}</div>`,
+    ].join(" ");
+
+    tooltip.value
+      .style("top", event.offsetY + "px")
+      .style("left", event.offsetX + "px")
+      .style("visibility", "visible")
+      .html(html);
   }
-  function handleNodeClick(event: PointerEvent, d: Node & { x: number; y: number }) {
+
+  function hideTip() {
+    tooltip.value.style("visibility", "hidden");
+  }
+  function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
+    event.stopPropagation();
+    hideTip();
     topologyStore.setNode(d);
     topologyStore.setLink(null);
     operationsPos.x = event.offsetX;
@@ -231,11 +455,11 @@ limitations under the License. -->
       { id: "alerting", title: "Alerting", func: handleGoAlerting },
     ];
   }
-  function handleLinkClick(event: PointerEvent, d: Call) {
-    if (d.source.layer !== dashboardStore.layerId || d.target.layer !== dashboardStore.layerId) {
+  function handleLinkClick(event: MouseEvent, d: Call) {
+    event.stopPropagation();
+    if (d.sourceObj.layer !== dashboardStore.layerId || d.targetObj.layer !== dashboardStore.layerId) {
       return;
     }
-    event.stopPropagation();
     topologyStore.setNode(null);
     topologyStore.setLink(d);
     if (!settings.value.linkDashboard) {
@@ -253,132 +477,22 @@ limitations under the License. -->
       return;
     }
     dashboardStore.setEntity(dashboard.entity);
-    const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.source.id}/${d.target.id}/${dashboard.name}`;
+    const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.sourceObj.id}/${d.targetObj.id}/${dashboard.name}`;
     const routeUrl = router.resolve({ path });
     window.open(routeUrl.href, "_blank");
     dashboardStore.setEntity(origin);
   }
-  function update() {
-    // node element
-    if (!node.value || !link.value) {
-      return;
-    }
-    node.value = node.value.data(topologyStore.nodes, (d: Node) => d.id);
-    node.value.exit().remove();
-    node.value = nodeElement(
-      d3,
-      node.value.enter(),
-      {
-        dragstart: dragstart,
-        dragged: dragged,
-        dragended: dragended,
-        handleNodeClick: handleNodeClick,
-        tipHtml: (data: Node) => {
-          const nodeMetrics: string[] = settings.value.nodeMetrics || [];
-          const nodeMetricConfig = settings.value.nodeMetricConfig || [];
-          const html = nodeMetrics.map((m, index) => {
-            const metric =
-              topologyStore.nodeMetricValue[m].values.find(
-                (val: { id: string; value: unknown }) => val.id === data.id,
-              ) || {};
-            const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
-            const v = aggregation(metric.value, opt);
-            return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
-          });
-          return [` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`, ...html].join(" ");
-        },
-      },
-      tip.value,
-      settings.value.legend,
-    ).merge(node.value);
-    // line element
-    link.value = link.value.data(topologyStore.calls, (d: Call) => d.id);
-    link.value.exit().remove();
-    link.value = linkElement(link.value.enter()).merge(link.value);
-    // anchorElement
-    anchor.value = anchor.value.data(topologyStore.calls, (d: Call) => d.id);
-    anchor.value.exit().remove();
-    anchor.value = anchorElement(
-      anchor.value.enter(),
-      {
-        handleLinkClick: handleLinkClick,
-        tipHtml: (data: Call) => {
-          const linkClientMetrics: string[] = settings.value.linkClientMetrics || [];
-          const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
-          const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
-          const linkServerMetrics: string[] = settings.value.linkServerMetrics || [];
-          const htmlServer = linkServerMetrics.map((m, index) => {
-            const metric = topologyStore.linkServerMetrics[m].values.find(
-              (val: { id: string; value: unknown }) => val.id === data.id,
-            );
-            if (metric) {
-              const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
-              const v = aggregation(metric.value, opt);
-              return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
-            }
-          });
-          const htmlClient = linkClientMetrics.map((m: string, index: number) => {
-            const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
-            const metric = topologyStore.linkClientMetrics[m].values.find(
-              (val: { id: string; value: unknown }) => val.id === data.id,
-            );
-            if (metric) {
-              const v = aggregation(metric.value, opt);
-              return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
-            }
-          });
-          const html = [
-            ...htmlServer,
-            ...htmlClient,
-            `<div><span class="grey">${t("detectPoint")}:</span>${data.detectPoints.join(" | ")}</div>`,
-          ].join(" ");
-
-          return html;
-        },
-      },
-      tip.value,
-    ).merge(anchor.value);
-    // arrow marker
-    arrow.value = arrow.value.data(topologyStore.calls, (d: Call) => d.id);
-    arrow.value.exit().remove();
-    arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
-    // force element
-    simulation.value.nodes(topologyStore.nodes);
-    simulation.value
-      .force("link")
-      .links(topologyStore.calls)
-      .id((d: Call) => d.id);
-    simulationSkip(d3, simulation.value, ticked);
-    const loopMap: any = {};
-    for (let i = 0; i < topologyStore.calls.length; i++) {
-      const link: any = topologyStore.calls[i];
-      link.loopFactor = 1;
-      for (let j = 0; j < topologyStore.calls.length; j++) {
-        if (i === j || loopMap[i]) {
-          continue;
-        }
-        const otherLink = topologyStore.calls[j];
-        if (link.source.id === otherLink.target.id && link.target.id === otherLink.source.id) {
-          link.loopFactor = -1;
-          loopMap[j] = 1;
-          break;
-        }
-      }
-    }
-  }
   async function handleInspect() {
-    svg.value.selectAll(".topo-svg-graph").remove();
     const id = topologyStore.node.id;
-    topologyStore.setNode(null);
-    topologyStore.setLink(null);
     loading.value = true;
     const resp = await topologyStore.getDepthServiceTopology([id], Number(depth.value));
     loading.value = false;
     if (resp && resp.errors) {
       ElMessage.error(resp.errors);
     }
-    await init();
-    update();
+    await update();
+    topologyStore.setNode(null);
+    topologyStore.setLink(null);
   }
   function handleGoEndpoint(name: string) {
     const path = `/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${topologyStore.node.id}/${name}`;
@@ -408,16 +522,8 @@ limitations under the License. -->
     window.open(routeUrl.href, "_blank");
   }
   async function backToTopology() {
-    svg.value.selectAll(".topo-svg-graph").remove();
     loading.value = true;
-    const resp = await getTopology();
-    loading.value = false;
-
-    if (resp && resp.errors) {
-      ElMessage.error(resp.errors);
-    }
-    await init();
-    update();
+    await freshNodes();
     topologyStore.setNode(null);
     topologyStore.setLink(null);
   }
@@ -438,7 +544,6 @@ limitations under the License. -->
     };
     height.value = dom.height - 40;
     width.value = dom.width;
-    svg.value.attr("height", height.value).attr("width", width.value);
   }
   function updateSettings(config: any) {
     settings.value = config;
@@ -479,18 +584,14 @@ limitations under the License. -->
       }
     }
   }
-  async function freshNodes() {
-    if (!svg.value) {
-      return;
-    }
-    svg.value.selectAll(".topo-svg-graph").remove();
-    await init();
-    update();
+  function svgEvent() {
+    topologyStore.setNode(null);
+    topologyStore.setLink(null);
+    dashboardStore.selectWidget(props.config);
   }
 
   async function changeDepth(opt: Option[] | any) {
     depth.value = opt[0].value;
-    await getTopology();
     freshNodes();
   }
   onBeforeUnmount(() => {
@@ -498,7 +599,13 @@ limitations under the License. -->
   });
   watch(
     () => [selectorStore.currentService, selectorStore.currentDestService],
-    () => {
+    (newVal, oldVal) => {
+      if (oldVal[0].id === newVal[0].id && !oldVal[1]) {
+        return;
+      }
+      if (oldVal[0].id === newVal[0].id && oldVal[1].id === newVal[1].id) {
+        return;
+      }
       freshNodes();
     },
   );
@@ -512,23 +619,20 @@ limitations under the License. -->
   );
 </script>
 <style lang="scss">
-  .topo-svg {
-    width: 100%;
-    height: calc(100% - 5px);
-    cursor: move;
-  }
-
   .micro-topo-chart {
     position: relative;
-    height: calc(100% - 30px);
     overflow: auto;
     margin-top: 30px;
 
+    .svg-topology {
+      cursor: move;
+    }
+
     .legend {
       position: absolute;
       top: 10px;
-      left: 15px;
-      color: #ccc;
+      left: 25px;
+      color: #666;
 
       div {
         margin-bottom: 8px;
@@ -553,16 +657,22 @@ limitations under the License. -->
       right: 10px;
       width: 400px;
       height: 600px;
-      background-color: #2b3037;
       overflow: auto;
       padding: 0 15px;
       border-radius: 3px;
       color: #ccc;
+      border: 1px solid #ccc;
+      background-color: #fff;
+      box-shadow: #eee 1px 2px 10px;
       transition: all 0.5ms linear;
+
+      &.dark {
+        background-color: #2b3037;
+      }
     }
 
     .label {
-      color: #ccc;
+      color: #666;
       display: inline-block;
       margin-right: 5px;
     }
@@ -572,8 +682,10 @@ limitations under the License. -->
       color: #333;
       cursor: pointer;
       background-color: #fff;
-      border-radius: 3px;
+      border-radius: 5px;
       padding: 10px 0;
+      border: 1px solid #999;
+      box-shadow: #ddd 1px 2px 10px;
 
       span {
         display: block;
@@ -598,22 +710,23 @@ limitations under the License. -->
     .switch-icon {
       cursor: pointer;
       transition: all 0.5ms linear;
-      background-color: #252a2f99;
-      color: #ddd;
+      background: rgba(0, 0, 0, 0.3);
+      color: #fff;
       display: inline-block;
-      padding: 5px 8px 8px;
+      padding: 2px 4px;
       border-radius: 3px;
     }
 
     .topo-line {
       stroke-linecap: round;
-      stroke-width: 3px;
-      stroke-dasharray: 13 7;
+      stroke-width: 1px;
+      stroke-dasharray: 10 10;
       fill: none;
-      animation: topo-dash 0.5s linear infinite;
+      animation: topo-dash 0.3s linear infinite;
     }
 
-    .topo-line-anchor {
+    .topo-line-anchor,
+    .topo-node {
       cursor: pointer;
     }
 
@@ -624,37 +737,9 @@ limitations under the License. -->
       opacity: 0.8;
     }
   }
-
-  .d3-tip {
-    line-height: 1;
-    padding: 8px;
-    color: #eee;
-    border-radius: 4px;
-    font-size: 12px;
-    z-index: 9999;
-    background: #252a2f;
-  }
-
-  .d3-tip:after {
-    box-sizing: border-box;
-    display: block;
-    font-size: 10px;
-    width: 100%;
-    line-height: 0.8;
-    color: #252a2f;
-    content: "\25BC";
-    position: absolute;
-    text-align: center;
-  }
-
-  .d3-tip.n:after {
-    margin: -2px 0 0 0;
-    top: 100%;
-    left: 0;
-  }
   @keyframes topo-dash {
     from {
-      stroke-dashoffset: 20;
+      stroke-dashoffset: 10;
     }
 
     to {
@@ -665,4 +750,13 @@ limitations under the License. -->
   .el-loading-spinner {
     top: 30%;
   }
+
+  #tooltip {
+    position: absolute;
+    visibility: hidden;
+    padding: 5px;
+    border: 1px solid #000;
+    border-radius: 3px;
+    background-color: #fff;
+  }
 </style>
diff --git a/src/views/dashboard/related/topology/components/Settings.vue b/src/views/dashboard/related/topology/components/Settings.vue
index ce9d915..e0c9d4c 100644
--- a/src/views/dashboard/related/topology/components/Settings.vue
+++ b/src/views/dashboard/related/topology/components/Settings.vue
@@ -27,7 +27,7 @@ limitations under the License. -->
     />
     <div class="label">
       <span>{{ t("linkServerMetrics") }}</span>
-      <el-popover placement="left" :width="400" trigger="click" effect="dark" v-if="states.linkServerMetrics.length">
+      <el-popover placement="left" :width="400" trigger="click" v-if="states.linkServerMetrics.length">
         <template #reference>
           <span @click="setConfigType('linkServerMetricConfig')">
             <Icon class="cp ml-5" iconName="mode_edit" size="middle" />
@@ -48,7 +48,7 @@ limitations under the License. -->
     <span v-show="dashboardStore.entity !== EntityType[2].value">
       <div class="label">
         <span>{{ t("linkClientMetrics") }}</span>
-        <el-popover placement="left" :width="400" trigger="click" effect="dark" v-if="states.linkClientMetrics.length">
+        <el-popover placement="left" :width="400" trigger="click" v-if="states.linkClientMetrics.length">
           <template #reference>
             <span @click="setConfigType('linkClientMetricConfig')">
               <Icon class="cp ml-5" iconName="mode_edit" size="middle" />
@@ -110,7 +110,7 @@ limitations under the License. -->
     </div>
     <div class="label">
       <span>{{ t("nodeMetrics") }}</span>
-      <el-popover placement="left" :width="400" trigger="click" effect="dark" v-if="states.nodeMetrics.length">
+      <el-popover placement="left" :width="400" trigger="click" v-if="states.nodeMetrics.length">
         <template #reference>
           <span @click="setConfigType('nodeMetricConfig')">
             <Icon class="cp ml-5" iconName="mode_edit" size="middle" />
@@ -454,6 +454,7 @@ limitations under the License. -->
 
   .title {
     margin-bottom: 0;
+    color: #666;
   }
 
   .label {
diff --git a/src/views/dashboard/related/topology/components/utils/layout.ts b/src/views/dashboard/related/topology/components/utils/layout.ts
new file mode 100644
index 0000000..0ac2e56
--- /dev/null
+++ b/src/views/dashboard/related/topology/components/utils/layout.ts
@@ -0,0 +1,122 @@
+/**
+ * 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.
+ */
+import * as d3 from "d3";
+import type { Node, Call } from "@/types/topology";
+
+export function layout(levels: Node[][], calls: Call[], radius: number) {
+  // precompute level depth
+  levels.forEach((l: Node[], i: number) => l.forEach((n: any) => (n.level = i)));
+
+  const nodes: Node[] = levels.reduce((a, x) => a.concat(x), []);
+  // layout
+  const padding = 30;
+  const node_height = 120;
+  const node_width = 100;
+  const bundle_width = 14;
+  const metro_d = 4;
+  for (const n of nodes) {
+    n.height = 5 * metro_d;
+  }
+
+  let x_offset = padding;
+  let y_offset = 0;
+  for (const level of levels) {
+    y_offset = 0;
+    x_offset += 5 * bundle_width;
+    for (const l of level) {
+      const n: any = l;
+      for (const call of calls) {
+        if (call.source === n.id) {
+          call.sourceObj = n;
+        }
+        if (call.target === n.id) {
+          call.targetObj = n;
+        }
+      }
+      n.x = n.level * node_width + x_offset;
+      n.y = node_height + y_offset + n.height / 2;
+      y_offset += node_height + n.height;
+    }
+  }
+  const layout = {
+    width: d3.max(nodes as any, (n: { x: number }) => n.x) || 0 + node_width + 2 * padding,
+    height: d3.max(nodes as any, (n: { y: number }) => n.y) || 0 + node_height / 2 + 2 * padding,
+  };
+
+  return { nodes, layout, calls: computeCallPos(calls, radius) };
+}
+
+export function computeCallPos(calls: Call[], radius: number) {
+  for (const [index, call] of calls.entries()) {
+    const centrePoints = [call.sourceObj.x, call.sourceObj.y, call.targetObj.x, call.targetObj.y];
+    for (const [idx, link] of calls.entries()) {
+      if (
+        index < idx &&
+        call.id !== link.id &&
+        call.sourceObj.x === link.targetObj.x &&
+        call.sourceObj.y === link.targetObj.y &&
+        call.targetObj.x === link.sourceObj.x &&
+        call.targetObj.y === link.sourceObj.y
+      ) {
+        if (call.targetObj.y === call.sourceObj.y) {
+          centrePoints[1] = centrePoints[1] - 8;
+          centrePoints[3] = centrePoints[3] - 8;
+        } else if (call.targetObj.x === call.sourceObj.x) {
+          centrePoints[0] = centrePoints[0] - 8;
+          centrePoints[2] = centrePoints[2] - 8;
+        } else {
+          centrePoints[1] = centrePoints[1] + 6;
+          centrePoints[3] = centrePoints[3] + 6;
+          centrePoints[0] = centrePoints[0] - 6;
+          centrePoints[2] = centrePoints[2] - 6;
+        }
+      }
+    }
+    const pos: { x: number; y: number }[] = circleIntersection(
+      centrePoints[0],
+      centrePoints[1],
+      radius,
+      centrePoints[2],
+      centrePoints[3],
+      radius,
+    );
+    call.sourceX = pos[0].x;
+    call.sourceY = pos[0].y;
+    call.targetX = pos[1].x;
+    call.targetY = pos[1].y;
+  }
+  return calls;
+}
+
+export function circleIntersection(ax: number, ay: number, ar: number, bx: number, by: number, br: number) {
+  const dab = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
+
+  const dfx = (ar * Math.abs(ax - bx)) / dab;
+  const dfy = (ar * Math.abs(ay - by)) / dab;
+  const fx = bx > ax ? ax + dfx : ax - dfx;
+  const fy = ay > by ? ay - dfy : ay + dfy;
+
+  const dgx = (br * Math.abs(ax - bx)) / dab;
+  const dgy = (br * Math.abs(ay - by)) / dab;
+  const gx = bx > ax ? bx - dgx : bx + dgx;
+  const gy = ay > by ? by + dgy : by - dgy;
+
+  return [
+    { x: fx, y: fy },
+    { x: gx, y: gy },
+  ];
+}
diff --git a/src/views/dashboard/related/topology/components/utils/linkElement.ts b/src/views/dashboard/related/topology/components/utils/linkElement.ts
deleted file mode 100644
index efc3c38..0000000
--- a/src/views/dashboard/related/topology/components/utils/linkElement.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * 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.
- */
-
-export const linkElement = (graph: any) => {
-  const linkEnter = graph
-    .append("path")
-    .attr("class", "topo-line")
-    .attr("marker-end", "url(#arrow)")
-    .attr("stroke", "#217EF25f");
-  return linkEnter;
-};
-export const anchorElement = (graph: any, funcs: any, tip: any) => {
-  const linkEnter = graph
-    .append("circle")
-    .attr("class", "topo-line-anchor")
-    .attr("r", 5)
-    .attr("fill", "#217EF25f")
-    .on("mouseover", function (event: unknown, d: unknown) {
-      tip.html(funcs.tipHtml).show(d, this);
-    })
-    .on("mouseout", function () {
-      tip.hide(this);
-    })
-    .on("click", (event: unknown, d: unknown) => {
-      funcs.handleLinkClick(event, d);
-    });
-  return linkEnter;
-};
-export const arrowMarker = (graph: any) => {
-  const defs = graph.append("defs");
-  const arrow = defs
-    .append("marker")
-    .attr("id", "arrow")
-    .attr("class", "topo-line-arrow")
-    .attr("markerUnits", "strokeWidth")
-    .attr("markerWidth", "6")
-    .attr("markerHeight", "6")
-    .attr("viewBox", "0 0 12 12")
-    .attr("refX", "5")
-    .attr("refY", "6")
-    .attr("orient", "auto");
-  const arrowPath = "M2,2 L10,6 L2,10 L6,6 L2,2";
-
-  arrow.append("path").attr("d", arrowPath).attr("fill", "#217EF25f");
-  return arrow;
-};
diff --git a/src/views/dashboard/related/topology/components/utils/nodeElement.ts b/src/views/dashboard/related/topology/components/utils/nodeElement.ts
deleted file mode 100644
index 6aefc1e..0000000
--- a/src/views/dashboard/related/topology/components/utils/nodeElement.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * 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.
- */
-import icons from "@/assets/img/icons";
-import type { Node } from "@/types/topology";
-
-icons["KAFKA-CONSUMER"] = icons.KAFKA;
-export default (d3: any, graph: any, funcs: any, tip: any, legend?: any) => {
-  const nodeEnter = graph
-    .append("g")
-    .call(d3.drag().on("start", funcs.dragstart).on("drag", funcs.dragged).on("end", funcs.dragended))
-    .on("mouseover", function (event: PointerEvent, d: Node) {
-      tip.html(funcs.tipHtml).show(d, this);
-    })
-    .on("mouseout", function () {
-      tip.hide(this);
-    })
-    .on("click", (event: PointerEvent, d: Node | any) => {
-      event.stopPropagation();
-      event.preventDefault();
-      funcs.handleNodeClick(event, d);
-    });
-  nodeEnter
-    .append("image")
-    .attr("width", 49)
-    .attr("height", 49)
-    .attr("x", 2)
-    .attr("y", 10)
-    .attr("style", "cursor: move;")
-    .attr("xlink:href", (d: { [key: string]: number }) => {
-      if (!legend) {
-        return icons.CUBE;
-      }
-      if (!legend.length) {
-        return icons.CUBE;
-      }
-      let c = true;
-      for (const l of legend) {
-        if (l.condition === "<") {
-          c = c && d[l.name] < Number(l.value);
-        } else {
-          c = c && d[l.name] > Number(l.value);
-        }
-      }
-      return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
-    });
-  nodeEnter
-    .append("image")
-    .attr("width", 32)
-    .attr("height", 32)
-    .attr("x", 6)
-    .attr("y", -10)
-    .attr("style", "opacity: 0.5;")
-    .attr("xlink:href", icons.LOCAL);
-  nodeEnter
-    .append("image")
-    .attr("width", 18)
-    .attr("height", 18)
-    .attr("x", 13)
-    .attr("y", -7)
-    .attr("xlink:href", (d: { type: string }) =>
-      !d.type || d.type === "N/A" ? icons.UNDEFINED : icons[d.type.toUpperCase().replace("-", "")],
-    );
-  nodeEnter
-    .append("text")
-    .attr("class", "topo-text")
-    .attr("text-anchor", "middle")
-    .attr("x", 22)
-    .attr("y", 70)
-    .text((d: { name: string }) => (d.name.length > 20 ? `${d.name.substring(0, 20)}...` : d.name));
-  return nodeEnter;
-};
diff --git a/src/views/dashboard/related/topology/components/utils/simulation.ts b/src/views/dashboard/related/topology/components/utils/simulation.ts
deleted file mode 100644
index 926a93c..0000000
--- a/src/views/dashboard/related/topology/components/utils/simulation.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * 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.
- */
-export const simulationInit = (d3: any, nodes: any, links: any, ticked: any) => {
-  const simulation = d3
-    .forceSimulation(nodes)
-    .force(
-      "collide",
-      d3.forceCollide().radius(() => 60),
-    )
-    .force("yPos", d3.forceY().strength(1))
-    .force("xPos", d3.forceX().strength(1))
-    .force("charge", d3.forceManyBody().strength(-520))
-    .force(
-      "link",
-      d3.forceLink(links).id((d: { id: string }) => d.id),
-    )
-    .force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2 - 20))
-    .on("tick", ticked)
-    .stop();
-  simulationSkip(d3, simulation, ticked);
-  return simulation;
-};
-
-export const simulationSkip = (d3: any, simulation: any, ticked: any) => {
-  d3.timeout(() => {
-    const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
-    for (let i = 0; i < n; i += 1) {
-      simulation.tick();
-      ticked();
-    }
-  });
-};