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>