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

wusheng 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 26817e9  feat: enhance the process topology graph to support dragging 
nodes (#158)
26817e9 is described below

commit 26817e9f927dd07fb439f415227bf371f45a9645
Author: Fine0830 <[email protected]>
AuthorDate: Thu Sep 15 17:18:39 2022 +0800

    feat: enhance the process topology graph to support dragging nodes (#158)
---
 src/layout/components/SideBar.vue                  |   3 +-
 src/store/modules/network-profiling.ts             |  24 +-
 src/types/ebpf.d.ts                                |   2 +
 src/views/dashboard/controls/Widget.vue            |   5 +
 .../dashboard/related/components/utils/zoom.ts     |   8 +-
 .../related/network-profiling/Content.vue          |   2 +-
 .../components/Graph/linkProcess.ts                | 141 +++-----
 .../components/Graph/nodeProcess.ts                |  54 ---
 .../components/ProcessTopology.vue                 | 363 +++++++++++++--------
 9 files changed, 311 insertions(+), 291 deletions(-)

diff --git a/src/layout/components/SideBar.vue 
b/src/layout/components/SideBar.vue
index e05e9af..a343cd5 100644
--- a/src/layout/components/SideBar.vue
+++ b/src/layout/components/SideBar.vue
@@ -141,7 +141,6 @@ const filterMenus = (menus: any[]) => {
 .side-bar {
   background: #252a2f;
   height: 100%;
-  position: relative;
   margin-bottom: 100px;
   overflow-y: auto;
   overflow-x: hidden;
@@ -174,7 +173,7 @@ span.collapse {
 .menu-control {
   position: absolute;
   top: 7px;
-  left: 200px;
+  left: 220px;
   cursor: pointer;
   transition: all 0.2s linear;
   z-index: 99;
diff --git a/src/store/modules/network-profiling.ts 
b/src/store/modules/network-profiling.ts
index 88cd54d..2c45208 100644
--- a/src/store/modules/network-profiling.ts
+++ b/src/store/modules/network-profiling.ts
@@ -92,15 +92,21 @@ export const networkProfilingStore = defineStore({
         }
         return prev;
       }, []);
-      calls = calls.map((d: any) => {
-        d.sourceId = d.source;
-        d.targetId = d.target;
-        d.source = d.sourceObj;
-        d.target = d.targetObj;
-        delete d.sourceObj;
-        delete d.targetObj;
-        return d;
-      });
+      const param = {} as any;
+      calls = data.calls.reduce((prev: (Call | any)[], next: Call | any) => {
+        if (param[next.targetId + next.sourceId]) {
+          next.lowerArc = true;
+        }
+        param[next.sourceId + next.targetId] = true;
+        next.sourceId = next.source;
+        next.targetId = next.target;
+        next.source = next.sourceObj;
+        next.target = next.targetObj;
+        delete next.sourceObj;
+        delete next.targetObj;
+        prev.push(next);
+        return prev;
+      }, []);
       this.calls = calls;
       this.nodes = data.nodes;
     },
diff --git a/src/types/ebpf.d.ts b/src/types/ebpf.d.ts
index 5dfca0e..e9986ef 100644
--- a/src/types/ebpf.d.ts
+++ b/src/types/ebpf.d.ts
@@ -74,4 +74,6 @@ export type ProcessNode = {
   serviceInstanceName: string;
   name: string;
   isReal: boolean;
+  x?: number;
+  y?: number;
 };
diff --git a/src/views/dashboard/controls/Widget.vue 
b/src/views/dashboard/controls/Widget.vue
index ec58266..6d986f2 100644
--- a/src/views/dashboard/controls/Widget.vue
+++ b/src/views/dashboard/controls/Widget.vue
@@ -228,6 +228,11 @@ export default defineComponent({
     watch(
       () => [selectorStore.currentProcess, selectorStore.currentDestProcess],
       () => {
+        if (
+          !(selectorStore.currentDestProcess && selectorStore.currentProcess)
+        ) {
+          return;
+        }
         if (dashboardStore.entity === EntityType[7].value) {
           queryMetrics();
         }
diff --git a/src/views/dashboard/related/components/utils/zoom.ts 
b/src/views/dashboard/related/components/utils/zoom.ts
index 9227089..6ba31c0 100644
--- a/src/views/dashboard/related/components/utils/zoom.ts
+++ b/src/views/dashboard/related/components/utils/zoom.ts
@@ -19,11 +19,11 @@ export default (d3: any, graph: any, diff: number[]) =>
   d3
     .zoom()
     .scaleExtent([0.3, 10])
-    .on("zoom", (event: any) => {
+    .on("zoom", (d: any) => {
       graph.attr(
         "transform",
-        `translate(${event.transform.x + diff[0]},${
-          event.transform.y + diff[1]
-        })scale(${event.transform.k})`
+        `translate(${d.transform.x + diff[0]},${
+          d.transform.y + diff[1]
+        })scale(${d.transform.k})`
       );
     });
diff --git a/src/views/dashboard/related/network-profiling/Content.vue 
b/src/views/dashboard/related/network-profiling/Content.vue
index f041b75..d9d615a 100644
--- a/src/views/dashboard/related/network-profiling/Content.vue
+++ b/src/views/dashboard/related/network-profiling/Content.vue
@@ -53,7 +53,7 @@ const { t } = useI18n();
   height: 100%;
   flex-grow: 2;
   min-width: 700px;
-  overflow: auto;
+  overflow: hidden;
   position: relative;
   width: calc(100% - 330px);
 }
diff --git 
a/src/views/dashboard/related/network-profiling/components/Graph/linkProcess.ts 
b/src/views/dashboard/related/network-profiling/components/Graph/linkProcess.ts
index 858e090..26c94e4 100644
--- 
a/src/views/dashboard/related/network-profiling/components/Graph/linkProcess.ts
+++ 
b/src/views/dashboard/related/network-profiling/components/Graph/linkProcess.ts
@@ -17,96 +17,6 @@
 import icons from "@/assets/img/icons";
 import { Call } from "@/types/topology";
 
-export const linkElement = (graph: any) => {
-  const linkEnter = graph
-    .append("path")
-    .attr("class", "topo-call")
-    .attr("marker-end", "url(#arrow)")
-    .attr("stroke", "#97B0F8")
-    .attr("d", (d: Call) => {
-      const controlPos = computeControlPoint(
-        [d.source.x, d.source.y - 5],
-        [d.target.x, d.target.y - 5],
-        0.5
-      );
-      if (d.lowerArc) {
-        controlPos[1] =
-          Math.abs(controlPos[1]) < 50
-            ? -controlPos[1] + 90
-            : -controlPos[1] - 10;
-      }
-      return (
-        "M" +
-        d.source.x +
-        " " +
-        (d.source.y - 5) +
-        " " +
-        "Q" +
-        controlPos[0] +
-        " " +
-        controlPos[1] +
-        " " +
-        d.target.x +
-        " " +
-        (d.target.y - 5)
-      );
-    });
-  return linkEnter;
-};
-export const anchorElement = (graph: any, funcs: any, tip: any) => {
-  const linkEnter = graph
-    .append("g")
-    .attr("class", "topo-line-anchor")
-    .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);
-    });
-  linkEnter
-    .append("image")
-    .attr("width", 15)
-    .attr("height", 15)
-    .attr("x", (d: Call) => {
-      const p = getMidpoint(d);
-      return p[0] - 8;
-    })
-    .attr("y", (d: Call) => {
-      const p = getMidpoint(d);
-      return p[1] - 13;
-    })
-    .attr("xlink:href", (d: Call) => {
-      const types = [...d.sourceComponents, ...d.targetComponents];
-      if (types.includes("tcp") || types.includes("http")) {
-        return icons.HTTPDARK;
-      }
-      if (types.includes("https") || types.includes("tls")) {
-        return icons.HTTPS;
-      }
-    });
-  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", "8")
-    .attr("markerHeight", "8")
-    .attr("viewBox", "0 0 12 12")
-    .attr("refX", "10")
-    .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", "#97B0F8");
-  return arrow;
-};
 // Control Point coordinates of quadratic Bezier curve
 function computeControlPoint(ps: number[], pe: number[], arc = 0.5) {
   const deltaX = pe[0] - ps[0];
@@ -137,15 +47,20 @@ function quadraticBezier(
   const y = (1 - t) * (1 - t) * ps.y + 2 * t * (1 - t) * pc.y + t * t * pe.y;
   return [x, y];
 }
-function getMidpoint(d: Call) {
+export function getMidpoint(d: Call) {
+  if (isNaN(d.source.x) || isNaN(d.source.y)) {
+    return [0, 0];
+  }
+  if (isNaN(d.target.x) || isNaN(d.target.y)) {
+    return [0, 0];
+  }
   const controlPos = computeControlPoint(
     [d.source.x, d.source.y],
     [d.target.x, d.target.y],
     0.5
   );
   if (d.lowerArc) {
-    controlPos[1] =
-      Math.abs(controlPos[1]) < 50 ? -controlPos[1] + 100 : -controlPos[1] - 
10;
+    controlPos[1] = -controlPos[1];
   }
   const p = quadraticBezier(
     0.5,
@@ -155,3 +70,43 @@ function getMidpoint(d: Call) {
   );
   return p;
 }
+export function linkPath(d: Call) {
+  if (isNaN(d.source.x) || isNaN(d.source.y)) {
+    return;
+  }
+  if (isNaN(d.target.x) || isNaN(d.target.y)) {
+    return;
+  }
+  const controlPos = computeControlPoint(
+    [d.source.x, d.source.y - 5],
+    [d.target.x, d.target.y - 5],
+    0.5
+  );
+  if (d.lowerArc) {
+    controlPos[1] = -controlPos[1] - 10;
+  }
+  return (
+    "M" +
+    d.source.x +
+    " " +
+    (d.source.y - 5) +
+    " " +
+    "Q" +
+    controlPos[0] +
+    " " +
+    controlPos[1] +
+    " " +
+    d.target.x +
+    " " +
+    (d.target.y - 5)
+  );
+}
+export function getAnchor(d: Call) {
+  const types = [...d.sourceComponents, ...d.targetComponents];
+  if (types.includes("tcp") || types.includes("http")) {
+    return icons.HTTPDARK;
+  }
+  if (types.includes("https") || types.includes("tls")) {
+    return icons.HTTPS;
+  }
+}
diff --git 
a/src/views/dashboard/related/network-profiling/components/Graph/nodeProcess.ts 
b/src/views/dashboard/related/network-profiling/components/Graph/nodeProcess.ts
deleted file mode 100644
index 23a033d..0000000
--- 
a/src/views/dashboard/related/network-profiling/components/Graph/nodeProcess.ts
+++ /dev/null
@@ -1,54 +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 { Node } from "@/types/topology";
-
-export default (d3: any, graph: any, funcs: any, tip: 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: unknown, d: Node) {
-      tip.html(funcs.tipHtml).show(d, this);
-    })
-    .on("mouseout", function () {
-      tip.hide(this);
-    });
-  nodeEnter
-    .append("image")
-    .attr("width", 35)
-    .attr("height", 35)
-    .attr("x", (d: any) => d.x - 15)
-    .attr("y", (d: any) => d.y - 15)
-    .attr("style", "cursor: move;")
-    .attr("xlink:href", icons.CUBE);
-  nodeEnter
-    .append("text")
-    .attr("fill", "#000")
-    .attr("text-anchor", "middle")
-    .attr("x", (d: any) => d.x)
-    .attr("y", (d: any) => d.y + 28)
-    .text((d: { name: string }) =>
-      d.name.length > 10 ? `${d.name.substring(0, 10)}...` : d.name
-    );
-  return nodeEnter;
-};
diff --git 
a/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue 
b/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue
index 23df629..a3316d9 100644
--- 
a/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue
+++ 
b/src/views/dashboard/related/network-profiling/components/ProcessTopology.vue
@@ -13,13 +13,103 @@ 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 ref="chart" class="process-topo"></div>
-  <el-popover
-    placement="bottom"
-    :width="295"
-    trigger="click"
-    v-if="dashboardStore.editMode"
-  >
+  <div ref="chart" class="process-topo">
+    <svg
+      class="process-svg"
+      :width="width"
+      :height="height"
+      @click="clickTopology"
+    >
+      <g class="svg-graph" :transform="`translate(${diff[0]}, ${diff[1]})`">
+        <g class="hex-polygon">
+          <path
+            :d="getHexPolygonVertices()"
+            stroke="#D5DDF6"
+            stroke-width="2"
+            fill="none"
+          />
+          <text :x="0" :y="radius - 15" fill="#000" text-anchor="middle">
+            {{ selectorStore.currentPod.label }}
+          </text>
+        </g>
+        <g class="nodes">
+          <g
+            v-for="(node, index) in nodeList"
+            :key="index"
+            class="node"
+            @mouseover="showNodeTip(node, $event)"
+            @mouseout="hideNodeTip"
+            @mousedown="startMoveNode($event, node)"
+            @mouseup="stopMoveNode($event)"
+          >
+            <image
+              :href="icons.CUBE"
+              style="cursor: 'move'"
+              width="35"
+              height="35"
+              :x="(node.x || 0) - 15"
+              :y="(node.y || 0) - 15"
+            />
+            <text
+              :x="node.x"
+              :y="(node.y || 0) + 28"
+              fill="#000"
+              text-anchor="middle"
+            >
+              {{
+                node.name.length > 10
+                  ? `${node.name.substring(0, 10)}...`
+                  : node.name
+              }}
+            </text>
+          </g>
+        </g>
+        <g class="calls">
+          <path
+            v-for="(call, index) in networkProfilingStore.calls"
+            :key="index"
+            class="topo-call"
+            marker-end="url(#arrow)"
+            stroke="#97B0F8"
+            :d="linkPath(call)"
+          />
+        </g>
+        <g class="anchors">
+          <image
+            v-for="(call, index) in networkProfilingStore.calls"
+            :key="index"
+            class="topo-line-anchor"
+            :href="getAnchor(call)"
+            width="15"
+            height="15"
+            :x="getMidpoint(call)[0] - 8"
+            :y="getMidpoint(call)[1] - 13"
+            @click="handleLinkClick($event, call)"
+            @mouseover="showLinkTip(call, $event)"
+            @mouseout="hideLinkTip"
+          />
+        </g>
+        <g class="arrows">
+          <defs v-for="(_, index) in networkProfilingStore.calls" :key="index">
+            <marker
+              id="arrow"
+              markerUnits="strokeWidth"
+              markerWidth="8"
+              markerHeight="8"
+              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>
+  <el-popover placement="bottom" :width="295" trigger="click">
     <template #reference>
       <div class="switch-icon-edit ml-5" title="Settings" @click="setConfig">
         <Icon size="middle" iconName="setting_empty" />
@@ -39,9 +129,7 @@ import router from "@/router";
 import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
 import { useDashboardStore } from "@/store/modules/dashboard";
 import { useSelectorStore } from "@/store/modules/selectors";
-import d3tip from "d3-tip";
-import { linkElement, anchorElement, arrowMarker } from "./Graph/linkProcess";
-import nodeElement from "./Graph/nodeProcess";
+import { linkPath, getAnchor, getMidpoint } from "./Graph/linkProcess";
 import { Call } from "@/types/topology";
 import zoom from "../../components/utils/zoom";
 import { ProcessNode } from "@/types/ebpf";
@@ -52,6 +140,7 @@ import getDashboard from "@/hooks/useDashboardsSession";
 import { Layout } from "./Graph/layout";
 import TimeLine from "./TimeLine.vue";
 import { useAppStoreWithOut } from "@/store/modules/app";
+import icons from "@/assets/img/icons";
 
 /*global Nullable, defineProps */
 const props = defineProps({
@@ -67,19 +156,18 @@ const selectorStore = useSelectorStore();
 const networkProfilingStore = useNetworkProfilingStore();
 const height = ref<number>(100);
 const width = ref<number>(100);
-const svg = 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 tooltip = ref<Nullable<any>>(null);
+const svg = ref<Nullable<any>>(null);
+const graph = ref<Nullable<any>>(null);
 const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
 const config = ref<any>(props.config || {});
 const diff = ref<number[]>([220, 200]);
 const radius = 210;
 const dates = ref<Nullable<{ start: number; end: number }>>(null);
+const nodeList = ref<ProcessNode[]>([]);
+const currentNode = ref<Nullable<ProcessNode>>(null);
+const origin = [0, 0];
 
 onMounted(() => {
   init();
@@ -90,12 +178,14 @@ onMounted(() => {
 });
 
 async function init() {
-  svg.value = d3.select(chart.value).append("svg").attr("class", 
"process-svg");
   if (!networkProfilingStore.nodes.length) {
     return;
   }
-  drawGraph();
-  createLayout();
+  svg.value = d3.select(".process-svg");
+  graph.value = d3.select(".svg-graph");
+  tooltip.value = d3.select("#tooltip");
+  freshNodes();
+  useThrottleFn(resize, 500)();
 }
 
 function drawGraph() {
@@ -105,27 +195,16 @@ function drawGraph() {
   };
   height.value = (dom.height || 40) - 20;
   width.value = dom.width;
-  svg.value.attr("height", height.value).attr("width", width.value);
-  tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
   diff.value[0] = (dom.width - radius * 2) / 2 + radius;
-  graph.value = svg.value
-    .append("g")
-    .attr("class", "svg-graph")
-    .attr("transform", `translate(${diff.value[0]}, ${diff.value[1]})`);
-  graph.value.call(tip.value);
-  node.value = graph.value.append("g").selectAll(".topo-node");
-  link.value = graph.value.append("g").selectAll(".topo-call");
-  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, diff.value));
-  svg.value.on("click", (event: any) => {
-    event.stopPropagation();
-    event.preventDefault();
-    networkProfilingStore.setNode(null);
-    networkProfilingStore.setLink(null);
-    dashboardStore.selectWidget(props.config);
-  });
-  useThrottleFn(resize, 500)();
+}
+
+function clickTopology(event: MouseEvent) {
+  event.stopPropagation();
+  event.preventDefault();
+  networkProfilingStore.setNode(null);
+  networkProfilingStore.setLink(null);
+  dashboardStore.selectWidget(props.config);
 }
 
 function hexGrid(n = 1, radius = 1, origin = [0, 0]) {
@@ -157,7 +236,6 @@ function createPolygon(radius: number, sides = 6, offset = 
0) {
 }
 function getCirclePoint(radius: number, p = 1) {
   const data = [];
-  const origin = [0, 0];
   for (let index = 0; index < 360; index = index + p) {
     if (index < 230 || index > 310) {
       let x = radius * Math.cos((Math.PI * 2 * index) / 360);
@@ -167,10 +245,22 @@ function getCirclePoint(radius: number, p = 1) {
   }
   return data;
 }
-function createLayout() {
-  if (!node.value || !link.value) {
-    return;
+
+function getHexPolygonVertices() {
+  const p = {
+    count: 1,
+    radius, // layout hexagons radius 300
+  };
+  const polygon = createPolygon(p.radius, 6, 0);
+  const vertices: any = []; // a hexagon vertices
+  for (let v = 0; v < polygon.length; v++) {
+    vertices.push([origin[0] + polygon[v][0], origin[1] + polygon[v][1]]);
   }
+  const linePath = d3.line();
+  linePath.curve(d3.curveLinearClosed);
+  return linePath(vertices) || "";
+}
+function createLayout() {
   const dom: any = (chart.value && chart.value.getBoundingClientRect()) || {
     width: 0,
     height: 0,
@@ -182,28 +272,6 @@ function createLayout() {
     count: 1,
     radius, // layout hexagons radius 300
   };
-  const polygon = createPolygon(p.radius, 6, 0);
-  const origin = [0, 0];
-  const vertices: any = []; // a hexagon vertices
-  for (let v = 0; v < polygon.length; v++) {
-    vertices.push([origin[0] + polygon[v][0], origin[1] + polygon[v][1]]);
-  }
-  const linePath = d3.line();
-  linePath.curve(d3.curveLinearClosed);
-  const hexPolygon = graph.value.append("g");
-  hexPolygon
-    .append("path")
-    .attr("d", linePath(vertices))
-    .attr("stroke", "#D5DDF6")
-    .attr("stroke-width", 2)
-    .style("fill", "none");
-  hexPolygon
-    .append("text")
-    .attr("fill", "#000")
-    .attr("text-anchor", "middle")
-    .attr("x", 0)
-    .attr("y", p.radius - 15)
-    .text(() => selectorStore.currentPod.label);
   const nodeArr = networkProfilingStore.nodes.filter(
     (d: ProcessNode) => d.isReal || d.name === "UNKNOWN_LOCAL"
   );
@@ -278,67 +346,11 @@ function createLayout() {
     outNodes[v].x = pointArr[v][0];
     outNodes[v].y = pointArr[v][1];
   }
-  drawTopology([...nodeArr, ...outNodes]);
-}
-
-function drawTopology(nodeArr: any[]) {
-  node.value = node.value.data(nodeArr, (d: ProcessNode) => d.id);
-  node.value.exit().remove();
-  node.value = nodeElement(
-    d3,
-    node.value.enter(),
-    {
-      tipHtml: (data: ProcessNode) => {
-        return ` <div class="mb-5"><span class="grey">name: 
</span>${data.name}</div>`;
-      },
-    },
-    tip.value
-  ).merge(node.value);
-  // line element
-  const obj = {} as any;
-  const calls = networkProfilingStore.calls.reduce((prev: any[], next: any) => 
{
-    if (obj[next.targetId + next.sourceId]) {
-      next.lowerArc = true;
-    }
-    obj[next.sourceId + next.targetId] = true;
-    prev.push(next);
-    return prev;
-  }, []);
-
-  link.value = link.value.data(calls, (d: Call) => d.id);
-  link.value.exit().remove();
-  link.value = linkElement(link.value.enter()).merge(link.value);
-  anchor.value = anchor.value.data(calls, (d: Call) => d.id);
-  anchor.value.exit().remove();
-  anchor.value = anchorElement(
-    anchor.value.enter(),
-    {
-      handleLinkClick: handleLinkClick,
-      tipHtml: (data: Call) => {
-        const types = [...data.sourceComponents, ...data.targetComponents];
-        let l = "TCP";
-        if (types.includes("https")) {
-          l = "HTTPS";
-        }
-        if (types.includes("http")) {
-          l = "HTTP";
-        }
-        if (types.includes("tls")) {
-          l = "TLS";
-        }
-        const html = `<div><span class="grey">${t(
-          "detectPoint"
-        )}: </span>${data.detectPoints.join(" | ")}</div>
-        <div><span class="grey">Type: </span>${l}</div>`;
-        return html;
-      },
-    },
-    tip.value
-  ).merge(anchor.value);
-  // arrow marker
-  arrow.value = arrow.value.data(calls, (d: Call) => d.id);
-  arrow.value.exit().remove();
-  arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
+  nodeList.value = [...nodeArr, ...outNodes];
+  const drag: any = d3.drag().on("drag", (d: ProcessNode) => {
+    moveNode(d);
+  });
+  d3.selectAll(".node").call(drag);
 }
 
 function shuffleArray(array: number[][]) {
@@ -347,7 +359,6 @@ function shuffleArray(array: number[][]) {
     [array[i], array[j]] = [array[j], array[i]];
   }
 }
-
 function handleLinkClick(event: any, d: Call) {
   event.stopPropagation();
   networkProfilingStore.setNode(null);
@@ -423,13 +434,100 @@ function resize() {
 }
 
 async function freshNodes() {
-  svg.value.selectAll(".svg-graph").remove();
   if (!networkProfilingStore.nodes.length) {
     return;
   }
   drawGraph();
   createLayout();
 }
+function startMoveNode(event: MouseEvent, d: ProcessNode) {
+  event.stopPropagation();
+  currentNode.value = d;
+}
+function stopMoveNode(event: MouseEvent) {
+  event.stopPropagation();
+  currentNode.value = null;
+}
+function moveNode(d: ProcessNode) {
+  if (!currentNode.value) {
+    return;
+  }
+  const inNode =
+    currentNode.value.isReal || currentNode.value.name === "UNKNOWN_LOCAL";
+  const diff = inNode ? -20 : 20;
+  const inside = posInHex(d.x || 0, d.y || 0, diff);
+  if (inNode) {
+    if (!inside) {
+      return;
+    }
+  } else {
+    if (inside) {
+      return;
+    }
+  }
+  nodeList.value = nodeList.value.map((node: ProcessNode) => {
+    if (currentNode.value && node.id === currentNode.value.id) {
+      node.x = d.x;
+      node.y = d.y;
+    }
+    return node;
+  });
+}
+function posInHex(posX: number, posY: number, diff: number) {
+  const halfSideLen = (radius + diff) / 2;
+  const mathSqrt3 = Math.sqrt(3);
+  const dx = Math.abs(origin[0] - posX);
+  const dy = Math.abs(origin[1] - posY);
+
+  if (dx < halfSideLen) {
+    return dy <= halfSideLen * mathSqrt3;
+  } else {
+    const maxY = -mathSqrt3 * (dx - halfSideLen) + halfSideLen * mathSqrt3;
+    return dy < maxY;
+  }
+}
+
+function showNodeTip(d: ProcessNode, event: MouseEvent) {
+  const tipHtml = ` <div class="mb-5"><span class="grey">name: 
</span>${d.name}</div>`;
+
+  tooltip.value
+    .style("top", event.offsetY + "px")
+    .style("left", event.offsetX + "px")
+    .style("visibility", "visible")
+    .html(tipHtml);
+}
+
+function hideNodeTip() {
+  tooltip.value.style("visibility", "hidden");
+}
+
+function showLinkTip(link: Call, event: MouseEvent) {
+  const types = [...link.sourceComponents, ...link.targetComponents];
+  let l = "TCP";
+  if (types.includes("https")) {
+    l = "HTTPS";
+  }
+  if (types.includes("http")) {
+    l = "HTTP";
+  }
+  if (types.includes("tls")) {
+    l = "TLS";
+  }
+  const tipHtml = `<div><span class="grey">${t(
+    "detectPoint"
+  )}: </span>${link.detectPoints.join(" | ")}</div>
+        <div><span class="grey">Type: </span>${l}</div>`;
+
+  tooltip.value
+    .style("top", event.offsetY + "px")
+    .style("left", event.offsetX + "px")
+    .style("visibility", "visible")
+    .html(tipHtml);
+}
+
+function hideLinkTip() {
+  tooltip.value.style("visibility", "hidden");
+}
 
 watch(
   () => networkProfilingStore.nodes,
@@ -500,4 +598,13 @@ watch(
 .query {
   margin-left: 510px;
 }
+
+#tooltip {
+  position: absolute;
+  visibility: hidden;
+  padding: 5px;
+  border: 1px solid #000;
+  border-radius: 3px;
+  background-color: #fff;
+}
 </style>

Reply via email to