feat: enhance the process topology graph to support dragging nodes (#158)

This commit is contained in:
Fine0830 2022-09-15 17:18:39 +08:00 committed by GitHub
parent 9ed0121fd0
commit 26817e9f92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 311 additions and 291 deletions

View File

@ -141,7 +141,6 @@ const filterMenus = (menus: any[]) => {
.side-bar { .side-bar {
background: #252a2f; background: #252a2f;
height: 100%; height: 100%;
position: relative;
margin-bottom: 100px; margin-bottom: 100px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@ -174,7 +173,7 @@ span.collapse {
.menu-control { .menu-control {
position: absolute; position: absolute;
top: 7px; top: 7px;
left: 200px; left: 220px;
cursor: pointer; cursor: pointer;
transition: all 0.2s linear; transition: all 0.2s linear;
z-index: 99; z-index: 99;

View File

@ -92,15 +92,21 @@ export const networkProfilingStore = defineStore({
} }
return prev; return prev;
}, []); }, []);
calls = calls.map((d: any) => { const param = {} as any;
d.sourceId = d.source; calls = data.calls.reduce((prev: (Call | any)[], next: Call | any) => {
d.targetId = d.target; if (param[next.targetId + next.sourceId]) {
d.source = d.sourceObj; next.lowerArc = true;
d.target = d.targetObj; }
delete d.sourceObj; param[next.sourceId + next.targetId] = true;
delete d.targetObj; next.sourceId = next.source;
return d; 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.calls = calls;
this.nodes = data.nodes; this.nodes = data.nodes;
}, },

2
src/types/ebpf.d.ts vendored
View File

@ -74,4 +74,6 @@ export type ProcessNode = {
serviceInstanceName: string; serviceInstanceName: string;
name: string; name: string;
isReal: boolean; isReal: boolean;
x?: number;
y?: number;
}; };

View File

@ -228,6 +228,11 @@ export default defineComponent({
watch( watch(
() => [selectorStore.currentProcess, selectorStore.currentDestProcess], () => [selectorStore.currentProcess, selectorStore.currentDestProcess],
() => { () => {
if (
!(selectorStore.currentDestProcess && selectorStore.currentProcess)
) {
return;
}
if (dashboardStore.entity === EntityType[7].value) { if (dashboardStore.entity === EntityType[7].value) {
queryMetrics(); queryMetrics();
} }

View File

@ -19,11 +19,11 @@ export default (d3: any, graph: any, diff: number[]) =>
d3 d3
.zoom() .zoom()
.scaleExtent([0.3, 10]) .scaleExtent([0.3, 10])
.on("zoom", (event: any) => { .on("zoom", (d: any) => {
graph.attr( graph.attr(
"transform", "transform",
`translate(${event.transform.x + diff[0]},${ `translate(${d.transform.x + diff[0]},${
event.transform.y + diff[1] d.transform.y + diff[1]
})scale(${event.transform.k})` })scale(${d.transform.k})`
); );
}); });

View File

@ -53,7 +53,7 @@ const { t } = useI18n();
height: 100%; height: 100%;
flex-grow: 2; flex-grow: 2;
min-width: 700px; min-width: 700px;
overflow: auto; overflow: hidden;
position: relative; position: relative;
width: calc(100% - 330px); width: calc(100% - 330px);
} }

View File

@ -17,96 +17,6 @@
import icons from "@/assets/img/icons"; import icons from "@/assets/img/icons";
import { Call } from "@/types/topology"; 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 // Control Point coordinates of quadratic Bezier curve
function computeControlPoint(ps: number[], pe: number[], arc = 0.5) { function computeControlPoint(ps: number[], pe: number[], arc = 0.5) {
const deltaX = pe[0] - ps[0]; 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; const y = (1 - t) * (1 - t) * ps.y + 2 * t * (1 - t) * pc.y + t * t * pe.y;
return [x, 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( const controlPos = computeControlPoint(
[d.source.x, d.source.y], [d.source.x, d.source.y],
[d.target.x, d.target.y], [d.target.x, d.target.y],
0.5 0.5
); );
if (d.lowerArc) { if (d.lowerArc) {
controlPos[1] = controlPos[1] = -controlPos[1];
Math.abs(controlPos[1]) < 50 ? -controlPos[1] + 100 : -controlPos[1] - 10;
} }
const p = quadraticBezier( const p = quadraticBezier(
0.5, 0.5,
@ -155,3 +70,43 @@ function getMidpoint(d: Call) {
); );
return p; 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;
}
}

View File

@ -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;
};

View File

@ -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 See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div ref="chart" class="process-topo"></div> <div ref="chart" class="process-topo">
<el-popover <svg
placement="bottom" class="process-svg"
:width="295" :width="width"
trigger="click" :height="height"
v-if="dashboardStore.editMode" @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> <template #reference>
<div class="switch-icon-edit ml-5" title="Settings" @click="setConfig"> <div class="switch-icon-edit ml-5" title="Settings" @click="setConfig">
<Icon size="middle" iconName="setting_empty" /> <Icon size="middle" iconName="setting_empty" />
@ -39,9 +129,7 @@ import router from "@/router";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling"; import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import d3tip from "d3-tip"; import { linkPath, getAnchor, getMidpoint } from "./Graph/linkProcess";
import { linkElement, anchorElement, arrowMarker } from "./Graph/linkProcess";
import nodeElement from "./Graph/nodeProcess";
import { Call } from "@/types/topology"; import { Call } from "@/types/topology";
import zoom from "../../components/utils/zoom"; import zoom from "../../components/utils/zoom";
import { ProcessNode } from "@/types/ebpf"; import { ProcessNode } from "@/types/ebpf";
@ -52,6 +140,7 @@ import getDashboard from "@/hooks/useDashboardsSession";
import { Layout } from "./Graph/layout"; import { Layout } from "./Graph/layout";
import TimeLine from "./TimeLine.vue"; import TimeLine from "./TimeLine.vue";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import icons from "@/assets/img/icons";
/*global Nullable, defineProps */ /*global Nullable, defineProps */
const props = defineProps({ const props = defineProps({
@ -67,19 +156,18 @@ const selectorStore = useSelectorStore();
const networkProfilingStore = useNetworkProfilingStore(); const networkProfilingStore = useNetworkProfilingStore();
const height = ref<number>(100); const height = ref<number>(100);
const width = ref<number>(100); const width = ref<number>(100);
const svg = ref<Nullable<any>>(null);
const chart = ref<Nullable<HTMLDivElement>>(null); const chart = ref<Nullable<HTMLDivElement>>(null);
const tip = ref<Nullable<HTMLDivElement>>(null); const tooltip = ref<Nullable<any>>(null);
const graph = ref<any>(null); const svg = ref<Nullable<any>>(null);
const node = ref<any>(null); const graph = ref<Nullable<any>>(null);
const link = ref<any>(null);
const anchor = ref<any>(null);
const arrow = ref<any>(null);
const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 }); const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
const config = ref<any>(props.config || {}); const config = ref<any>(props.config || {});
const diff = ref<number[]>([220, 200]); const diff = ref<number[]>([220, 200]);
const radius = 210; const radius = 210;
const dates = ref<Nullable<{ start: number; end: number }>>(null); const dates = ref<Nullable<{ start: number; end: number }>>(null);
const nodeList = ref<ProcessNode[]>([]);
const currentNode = ref<Nullable<ProcessNode>>(null);
const origin = [0, 0];
onMounted(() => { onMounted(() => {
init(); init();
@ -90,12 +178,14 @@ onMounted(() => {
}); });
async function init() { async function init() {
svg.value = d3.select(chart.value).append("svg").attr("class", "process-svg");
if (!networkProfilingStore.nodes.length) { if (!networkProfilingStore.nodes.length) {
return; return;
} }
drawGraph(); svg.value = d3.select(".process-svg");
createLayout(); graph.value = d3.select(".svg-graph");
tooltip.value = d3.select("#tooltip");
freshNodes();
useThrottleFn(resize, 500)();
} }
function drawGraph() { function drawGraph() {
@ -105,27 +195,16 @@ function drawGraph() {
}; };
height.value = (dom.height || 40) - 20; height.value = (dom.height || 40) - 20;
width.value = dom.width; 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; 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.call(zoom(d3, graph.value, diff.value));
svg.value.on("click", (event: any) => { }
event.stopPropagation();
event.preventDefault(); function clickTopology(event: MouseEvent) {
networkProfilingStore.setNode(null); event.stopPropagation();
networkProfilingStore.setLink(null); event.preventDefault();
dashboardStore.selectWidget(props.config); networkProfilingStore.setNode(null);
}); networkProfilingStore.setLink(null);
useThrottleFn(resize, 500)(); dashboardStore.selectWidget(props.config);
} }
function hexGrid(n = 1, radius = 1, origin = [0, 0]) { 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) { function getCirclePoint(radius: number, p = 1) {
const data = []; const data = [];
const origin = [0, 0];
for (let index = 0; index < 360; index = index + p) { for (let index = 0; index < 360; index = index + p) {
if (index < 230 || index > 310) { if (index < 230 || index > 310) {
let x = radius * Math.cos((Math.PI * 2 * index) / 360); let x = radius * Math.cos((Math.PI * 2 * index) / 360);
@ -167,10 +245,22 @@ function getCirclePoint(radius: number, p = 1) {
} }
return data; return data;
} }
function createLayout() {
if (!node.value || !link.value) { function getHexPolygonVertices() {
return; 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()) || { const dom: any = (chart.value && chart.value.getBoundingClientRect()) || {
width: 0, width: 0,
height: 0, height: 0,
@ -182,28 +272,6 @@ function createLayout() {
count: 1, count: 1,
radius, // layout hexagons radius 300 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( const nodeArr = networkProfilingStore.nodes.filter(
(d: ProcessNode) => d.isReal || d.name === "UNKNOWN_LOCAL" (d: ProcessNode) => d.isReal || d.name === "UNKNOWN_LOCAL"
); );
@ -278,67 +346,11 @@ function createLayout() {
outNodes[v].x = pointArr[v][0]; outNodes[v].x = pointArr[v][0];
outNodes[v].y = pointArr[v][1]; outNodes[v].y = pointArr[v][1];
} }
drawTopology([...nodeArr, ...outNodes]); nodeList.value = [...nodeArr, ...outNodes];
} const drag: any = d3.drag().on("drag", (d: ProcessNode) => {
moveNode(d);
function drawTopology(nodeArr: any[]) { });
node.value = node.value.data(nodeArr, (d: ProcessNode) => d.id); d3.selectAll(".node").call(drag);
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);
} }
function shuffleArray(array: number[][]) { function shuffleArray(array: number[][]) {
@ -347,7 +359,6 @@ function shuffleArray(array: number[][]) {
[array[i], array[j]] = [array[j], array[i]]; [array[i], array[j]] = [array[j], array[i]];
} }
} }
function handleLinkClick(event: any, d: Call) { function handleLinkClick(event: any, d: Call) {
event.stopPropagation(); event.stopPropagation();
networkProfilingStore.setNode(null); networkProfilingStore.setNode(null);
@ -423,13 +434,100 @@ function resize() {
} }
async function freshNodes() { async function freshNodes() {
svg.value.selectAll(".svg-graph").remove();
if (!networkProfilingStore.nodes.length) { if (!networkProfilingStore.nodes.length) {
return; return;
} }
drawGraph(); drawGraph();
createLayout(); 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( watch(
() => networkProfilingStore.nodes, () => networkProfilingStore.nodes,
@ -500,4 +598,13 @@ watch(
.query { .query {
margin-left: 510px; margin-left: 510px;
} }
#tooltip {
position: absolute;
visibility: hidden;
padding: 5px;
border: 1px solid #000;
border-radius: 3px;
background-color: #fff;
}
</style> </style>