mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-07-18 23:45:23 +00:00
draw process topology
This commit is contained in:
parent
341d796aef
commit
39cedf1ddb
@ -113,18 +113,12 @@ export const dashboardStore = defineStore({
|
|||||||
: 3,
|
: 3,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (["Trace", "Profile", "Log", "DemandLog", "Ebpf"].includes(type)) {
|
||||||
[
|
|
||||||
"Trace",
|
|
||||||
"Profile",
|
|
||||||
"Log",
|
|
||||||
"DemandLog",
|
|
||||||
"Ebpf",
|
|
||||||
"NetworkProfiling",
|
|
||||||
].includes(type)
|
|
||||||
) {
|
|
||||||
newItem.h = 36;
|
newItem.h = 36;
|
||||||
}
|
}
|
||||||
|
if (["NetworkProfiling"].includes(type)) {
|
||||||
|
newItem.h = 44;
|
||||||
|
}
|
||||||
if (type === "Text") {
|
if (type === "Text") {
|
||||||
newItem.h = 6;
|
newItem.h = 6;
|
||||||
newItem.graph = TextConfig;
|
newItem.graph = TextConfig;
|
||||||
@ -184,18 +178,12 @@ export const dashboardStore = defineStore({
|
|||||||
showDepth: true,
|
showDepth: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (["Trace", "Profile", "Log", "DemandLog", "Ebpf"].includes(type)) {
|
||||||
[
|
|
||||||
"Trace",
|
|
||||||
"Profile",
|
|
||||||
"Log",
|
|
||||||
"DemandLog",
|
|
||||||
"Ebpf",
|
|
||||||
"NetworkProfiling",
|
|
||||||
].includes(type)
|
|
||||||
) {
|
|
||||||
newItem.h = 32;
|
newItem.h = 32;
|
||||||
}
|
}
|
||||||
|
if (["NetworkProfiling"].includes(type)) {
|
||||||
|
newItem.h = 44;
|
||||||
|
}
|
||||||
if (type === "Text") {
|
if (type === "Text") {
|
||||||
newItem.h = 6;
|
newItem.h = 6;
|
||||||
newItem.graph = TextConfig;
|
newItem.graph = TextConfig;
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* 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", 40)
|
||||||
|
.attr("height", 40)
|
||||||
|
.attr("x", (d: any) => d.x)
|
||||||
|
.attr("y", (d: any) => d.y)
|
||||||
|
.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 + 15)
|
||||||
|
.attr("y", (d: any) => d.y + 50)
|
||||||
|
.text((d: { name: string }) =>
|
||||||
|
d.name.length > 20 ? `${d.name.substring(0, 20)}...` : d.name
|
||||||
|
);
|
||||||
|
return nodeEnter;
|
||||||
|
};
|
@ -49,7 +49,7 @@ const networkProfilingStore = useNetworkProfilingStore();
|
|||||||
.item {
|
.item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 210px);
|
height: calc(100% - 210px);
|
||||||
background-color: #333840;
|
// background-color: #333840;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
class Orientation {
|
||||||
|
public f0 = 0;
|
||||||
|
public f1 = 0;
|
||||||
|
public f2 = 0;
|
||||||
|
public f3 = 0;
|
||||||
|
public b0? = 0;
|
||||||
|
public b1? = 0;
|
||||||
|
public b2? = 0;
|
||||||
|
public b3? = 0;
|
||||||
|
public start_angle? = 0;
|
||||||
|
constructor(
|
||||||
|
f0: number,
|
||||||
|
f1: number,
|
||||||
|
f2: number,
|
||||||
|
f3: number,
|
||||||
|
b0: number,
|
||||||
|
b1: number,
|
||||||
|
b2: number,
|
||||||
|
b3: number,
|
||||||
|
start_angle: number
|
||||||
|
) {
|
||||||
|
this.f0 = f0;
|
||||||
|
this.f1 = f1;
|
||||||
|
this.f2 = f2;
|
||||||
|
this.f3 = f3;
|
||||||
|
this.b0 = b0;
|
||||||
|
this.b1 = b1;
|
||||||
|
this.b2 = b2;
|
||||||
|
this.b3 = b3;
|
||||||
|
this.start_angle = start_angle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SQRT3 = Math.sqrt(3.0);
|
||||||
|
class Layout {
|
||||||
|
static Pointy = new Orientation(
|
||||||
|
SQRT3,
|
||||||
|
SQRT3 / 2.0,
|
||||||
|
0.0,
|
||||||
|
3.0 / 2.0,
|
||||||
|
SQRT3 / 3.0,
|
||||||
|
-1.0 / 3.0,
|
||||||
|
0.0,
|
||||||
|
2.0 / 3.0,
|
||||||
|
0.5
|
||||||
|
);
|
||||||
|
static Flat = new Orientation(
|
||||||
|
3.0 / 2.0,
|
||||||
|
0.0,
|
||||||
|
SQRT3 / 2.0,
|
||||||
|
SQRT3,
|
||||||
|
2.0 / 3.0,
|
||||||
|
0.0,
|
||||||
|
-1.0 / 3.0,
|
||||||
|
SQRT3 / 3.0,
|
||||||
|
0.0
|
||||||
|
);
|
||||||
|
|
||||||
|
static spacing(radius: number, isPointy = false): number[] {
|
||||||
|
return isPointy
|
||||||
|
? [SQRT3 * radius, 2 * radius * (3 / 4)]
|
||||||
|
: [2 * radius * (3 / 4), SQRT3 * radius];
|
||||||
|
}
|
||||||
|
|
||||||
|
private radius = 1;
|
||||||
|
private orientation: Orientation = { f0: 0, f1: 0, f2: 0, f3: 0 };
|
||||||
|
private origin = [0, 0];
|
||||||
|
|
||||||
|
constructor(radius: number, origin = [0, 0], orientation?: Orientation) {
|
||||||
|
this.radius = radius; //Layout.spacing( radius, ( orientation === Layout.Pointy ) );
|
||||||
|
this.orientation = orientation || Layout.Flat;
|
||||||
|
this.origin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as HexToPixel, Except it takes raw coords instead of hex object.
|
||||||
|
axialToPixel(ax: number, ay: number): number[] {
|
||||||
|
const M = this.orientation;
|
||||||
|
const x = (M.f0 * ax + M.f1 * ay) * this.radius;
|
||||||
|
const y = (M.f2 * ax + M.f3 * ay) * this.radius;
|
||||||
|
|
||||||
|
return [x + this.origin[0], y + this.origin[1]];
|
||||||
|
}
|
||||||
|
|
||||||
|
hexToPixel(h: { x: number; y: number }): number[] {
|
||||||
|
const M = this.orientation;
|
||||||
|
const x = (M.f0 * h.x + M.f1 * h.y) * this.radius;
|
||||||
|
const y = (M.f2 * h.x + M.f3 * h.y) * this.radius;
|
||||||
|
|
||||||
|
return [x + this.origin[0], y + this.origin[1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Hex extends Int16Array {
|
||||||
|
constructor(x: number, y: number, z = null) {
|
||||||
|
super(3);
|
||||||
|
this.xyz(x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
xyz(x: number, y: number, z: number | null = null): Hex {
|
||||||
|
if (z == null) z = -x - y;
|
||||||
|
if (x + y + z != 0) {
|
||||||
|
console.log("Bad Axial Coordinate : : q %d r %d s %d", x, y, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
this[0] = x;
|
||||||
|
this[1] = y;
|
||||||
|
this[2] = z;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get x(): number {
|
||||||
|
return this[0];
|
||||||
|
}
|
||||||
|
get y(): number {
|
||||||
|
return this[1];
|
||||||
|
}
|
||||||
|
get z(): number {
|
||||||
|
return this[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
get len(): number {
|
||||||
|
return Math.floor(
|
||||||
|
(Math.abs(this[0]) + Math.abs(this[1]) + Math.abs(this[2])) / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Hex, Orientation, Layout };
|
@ -36,16 +36,12 @@ 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 d3tip from "d3-tip";
|
import d3tip from "d3-tip";
|
||||||
import {
|
|
||||||
simulationInit,
|
|
||||||
simulationSkip,
|
|
||||||
} from "../../components/D3Graph/simulation";
|
|
||||||
import {
|
import {
|
||||||
linkElement,
|
linkElement,
|
||||||
anchorElement,
|
anchorElement,
|
||||||
arrowMarker,
|
arrowMarker,
|
||||||
} from "../../components/D3Graph/linkElement";
|
} from "../../components/D3Graph/linkElement";
|
||||||
import nodeElement from "../../components/D3Graph/nodeElement";
|
import nodeElement from "../../components/D3Graph/nodeProcess";
|
||||||
import { Call } from "@/types/topology";
|
import { Call } from "@/types/topology";
|
||||||
// import zoom from "../../components/D3Graph/zoom";
|
// import zoom from "../../components/D3Graph/zoom";
|
||||||
import { ProcessNode } from "@/types/ebpf";
|
import { ProcessNode } from "@/types/ebpf";
|
||||||
@ -53,6 +49,7 @@ import { useThrottleFn } from "@vueuse/core";
|
|||||||
import Settings from "./Settings.vue";
|
import Settings from "./Settings.vue";
|
||||||
import { EntityType } from "@/views/dashboard/data";
|
import { EntityType } from "@/views/dashboard/data";
|
||||||
import getDashboard from "@/hooks/useDashboardsSession";
|
import getDashboard from "@/hooks/useDashboardsSession";
|
||||||
|
import { Layout } from "./Graph/layout";
|
||||||
|
|
||||||
/*global Nullable, defineProps */
|
/*global Nullable, defineProps */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -66,7 +63,6 @@ const dashboardStore = useDashboardStore();
|
|||||||
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 simulation = ref<any>(null);
|
|
||||||
const svg = ref<Nullable<any>>(null);
|
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 tip = ref<Nullable<HTMLDivElement>>(null);
|
||||||
@ -108,14 +104,8 @@ function drawGraph() {
|
|||||||
graph.value = svg.value
|
graph.value = svg.value
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("class", "svg-graph")
|
.attr("class", "svg-graph")
|
||||||
.attr("transform", `translate(-250, -220)`);
|
.attr("transform", `translate(200, 200)`);
|
||||||
graph.value.call(tip.value);
|
graph.value.call(tip.value);
|
||||||
simulation.value = simulationInit(
|
|
||||||
d3,
|
|
||||||
networkProfilingStore.nodes,
|
|
||||||
networkProfilingStore.calls,
|
|
||||||
ticked
|
|
||||||
);
|
|
||||||
node.value = graph.value.append("g").selectAll(".topo-node");
|
node.value = graph.value.append("g").selectAll(".topo-node");
|
||||||
link.value = graph.value.append("g").selectAll(".topo-line");
|
link.value = graph.value.append("g").selectAll(".topo-line");
|
||||||
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
|
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
|
||||||
@ -130,15 +120,73 @@ function drawGraph() {
|
|||||||
});
|
});
|
||||||
useThrottleFn(resize, 500)();
|
useThrottleFn(resize, 500)();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hexGrid(n = 1, radius = 1, origin = [0, 0]) {
|
||||||
|
let x, y, yn, p;
|
||||||
|
const gLayout = new Layout(radius, origin);
|
||||||
|
const pos = [];
|
||||||
|
// x = -1; n = 1.5
|
||||||
|
for (x = -n; x <= n; x++) {
|
||||||
|
y = Math.max(-n, -x - n); // 0
|
||||||
|
yn = Math.min(n, -x + n); // 1
|
||||||
|
// y = 0 yn = 1
|
||||||
|
for (y; y <= yn; y++) {
|
||||||
|
p = gLayout.axialToPixel(x, y);
|
||||||
|
pos.push(...p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPolygon(radius: number, sides = 6, offset = 0) {
|
||||||
|
const poly: number[][] = [];
|
||||||
|
let i, rad;
|
||||||
|
for (i = 0; i < sides; i++) {
|
||||||
|
rad = Math.PI * 2 * (i / sides);
|
||||||
|
poly.push([
|
||||||
|
Math.cos(rad + offset) * radius,
|
||||||
|
Math.sin(rad + offset) * radius,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
return poly;
|
||||||
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
// node element
|
|
||||||
if (!node.value || !link.value) {
|
if (!node.value || !link.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
node.value = node.value.data(
|
const obj: any = (chart.value && chart.value.getBoundingClientRect()) || {};
|
||||||
networkProfilingStore.nodes,
|
const p = {
|
||||||
(d: ProcessNode) => d.id
|
hexagonParam: [27, 0.04, 5, 0.04, 0],
|
||||||
);
|
count: 1,
|
||||||
|
radius: obj.width / 5, // layout hexagons radius
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
graph.value
|
||||||
|
.append("path")
|
||||||
|
.attr("d", linePath(vertices))
|
||||||
|
.attr("stroke", "#ccc")
|
||||||
|
.attr("stroke-width", 2)
|
||||||
|
.style("fill", "none");
|
||||||
|
const centers = hexGrid(p.count, 50, origin); // cube centers
|
||||||
|
const pos = hexGrid(p.count, 50, [p.radius * 2 + 20.0]); // cube centers
|
||||||
|
// console.log(centers);
|
||||||
|
const nodeArr = networkProfilingStore.nodes;
|
||||||
|
for (let v = 0; v < 7; v++) {
|
||||||
|
const x = origin[0] + centers[v * 2];
|
||||||
|
const y = origin[1] + centers[v * 2 + 1];
|
||||||
|
nodeArr[v].x = x;
|
||||||
|
nodeArr[v].y = y;
|
||||||
|
}
|
||||||
|
node.value = node.value.data(nodeArr, (d: ProcessNode) => d.id);
|
||||||
|
|
||||||
node.value.exit().remove();
|
node.value.exit().remove();
|
||||||
node.value = nodeElement(
|
node.value = nodeElement(
|
||||||
d3,
|
d3,
|
||||||
@ -150,62 +198,6 @@ function update() {
|
|||||||
},
|
},
|
||||||
tip.value
|
tip.value
|
||||||
).merge(node.value);
|
).merge(node.value);
|
||||||
// line element
|
|
||||||
link.value = link.value.data(networkProfilingStore.calls, (d: Call) => d.id);
|
|
||||||
link.value.exit().remove();
|
|
||||||
link.value = linkElement(link.value.enter()).merge(link.value);
|
|
||||||
// anchorElement
|
|
||||||
anchor.value = anchor.value.data(
|
|
||||||
networkProfilingStore.calls,
|
|
||||||
(d: Call) => d.id
|
|
||||||
);
|
|
||||||
anchor.value.exit().remove();
|
|
||||||
anchor.value = anchorElement(
|
|
||||||
anchor.value.enter(),
|
|
||||||
{
|
|
||||||
handleLinkClick: handleLinkClick,
|
|
||||||
tipHtml: (data: Call) => {
|
|
||||||
const html = `<div><span class="grey">${t(
|
|
||||||
"detectPoint"
|
|
||||||
)}:</span>${data.detectPoints.join(" | ")}</div>`;
|
|
||||||
return html;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tip.value
|
|
||||||
).merge(anchor.value);
|
|
||||||
// arrow marker
|
|
||||||
arrow.value = arrow.value.data(
|
|
||||||
networkProfilingStore.calls,
|
|
||||||
(d: Call) => d.id
|
|
||||||
);
|
|
||||||
arrow.value.exit().remove();
|
|
||||||
arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
|
|
||||||
// force element
|
|
||||||
simulation.value.nodes(networkProfilingStore.nodes);
|
|
||||||
simulation.value
|
|
||||||
.force("link")
|
|
||||||
.links(networkProfilingStore.calls)
|
|
||||||
.id((d: Call) => d.id);
|
|
||||||
simulationSkip(d3, simulation.value, ticked);
|
|
||||||
const loopMap: any = {};
|
|
||||||
for (let i = 0; i < networkProfilingStore.calls.length; i++) {
|
|
||||||
const link: any = networkProfilingStore.calls[i];
|
|
||||||
link.loopFactor = 1;
|
|
||||||
for (let j = 0; j < networkProfilingStore.calls.length; j++) {
|
|
||||||
if (i === j || loopMap[i]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const otherLink = networkProfilingStore.calls[j];
|
|
||||||
if (
|
|
||||||
link.source.id === otherLink.target.id &&
|
|
||||||
link.target.id === otherLink.source.id
|
|
||||||
) {
|
|
||||||
link.loopFactor = -1;
|
|
||||||
loopMap[j] = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLinkClick(event: any, d: Call) {
|
function handleLinkClick(event: any, d: Call) {
|
||||||
@ -306,6 +298,8 @@ watch(
|
|||||||
margin: 0 5px 5px 0;
|
margin: 0 5px 5px 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
|
min-width: 300px;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-svg {
|
.process-svg {
|
||||||
|
@ -143,7 +143,6 @@ async function createTask() {
|
|||||||
(selectorStore.currentService && selectorStore.currentService.id) || "";
|
(selectorStore.currentService && selectorStore.currentService.id) || "";
|
||||||
const serviceInstanceId =
|
const serviceInstanceId =
|
||||||
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
||||||
console.log(selectorStore.currentPod);
|
|
||||||
if (!serviceId) {
|
if (!serviceId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -235,5 +234,6 @@ async function fetchTasks() {
|
|||||||
|
|
||||||
.new-task {
|
.new-task {
|
||||||
float: right;
|
float: right;
|
||||||
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -0,0 +1,343 @@
|
|||||||
|
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
contributor license agreements. See the NOTICE file distributed with
|
||||||
|
this work for additional information regarding copyright ownership.
|
||||||
|
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
(the "License"); you may not use this file except in compliance with
|
||||||
|
the License. You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License. -->
|
||||||
|
<template>
|
||||||
|
<div ref="chart" class="micro-topo-chart"></div>
|
||||||
|
<div
|
||||||
|
class="switch-icon ml-5"
|
||||||
|
title="Settings"
|
||||||
|
@click="setConfig"
|
||||||
|
v-if="dashboardStore.editMode"
|
||||||
|
>
|
||||||
|
<Icon size="middle" iconName="settings" />
|
||||||
|
</div>
|
||||||
|
<div class="setting" v-if="showSettings && dashboardStore.editMode">
|
||||||
|
<Settings @update="updateSettings" @updateNodes="freshNodes" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import { ref, onMounted, watch } from "vue";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import router from "@/router";
|
||||||
|
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
|
||||||
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
import d3tip from "d3-tip";
|
||||||
|
import {
|
||||||
|
simulationInit,
|
||||||
|
simulationSkip,
|
||||||
|
} from "../../components/D3Graph/simulation";
|
||||||
|
import {
|
||||||
|
linkElement,
|
||||||
|
anchorElement,
|
||||||
|
arrowMarker,
|
||||||
|
} from "../../components/D3Graph/linkElement";
|
||||||
|
import nodeElement from "../../components/D3Graph/nodeElement";
|
||||||
|
import { Call } from "@/types/topology";
|
||||||
|
// import zoom from "../../components/D3Graph/zoom";
|
||||||
|
import { ProcessNode } from "@/types/ebpf";
|
||||||
|
import { useThrottleFn } from "@vueuse/core";
|
||||||
|
import Settings from "./Settings.vue";
|
||||||
|
import { EntityType } from "@/views/dashboard/data";
|
||||||
|
import getDashboard from "@/hooks/useDashboardsSession";
|
||||||
|
|
||||||
|
/*global Nullable, defineProps */
|
||||||
|
const props = defineProps({
|
||||||
|
config: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: () => ({ graph: {} }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { t } = useI18n();
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
const networkProfilingStore = useNetworkProfilingStore();
|
||||||
|
const height = ref<number>(100);
|
||||||
|
const width = ref<number>(100);
|
||||||
|
const simulation = ref<any>(null);
|
||||||
|
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 oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||||
|
const showSettings = ref<boolean>(false);
|
||||||
|
const config = ref<any>({});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
oldVal.value = (chart.value && chart.value.getBoundingClientRect()) || {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
svg.value = d3.select(chart.value).append("svg").attr("class", "process-svg");
|
||||||
|
if (!networkProfilingStore.nodes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawGraph();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGraph() {
|
||||||
|
const dom = chart.value?.getBoundingClientRect() || {
|
||||||
|
height: 20,
|
||||||
|
width: 0,
|
||||||
|
};
|
||||||
|
height.value = dom.height - 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]);
|
||||||
|
graph.value = svg.value
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "svg-graph")
|
||||||
|
.attr("transform", `translate(-250, -220)`);
|
||||||
|
graph.value.call(tip.value);
|
||||||
|
simulation.value = simulationInit(
|
||||||
|
d3,
|
||||||
|
networkProfilingStore.nodes,
|
||||||
|
networkProfilingStore.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));
|
||||||
|
svg.value.on("click", (event: any) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
networkProfilingStore.setNode(null);
|
||||||
|
networkProfilingStore.setLink(null);
|
||||||
|
dashboardStore.selectWidget(props.config);
|
||||||
|
});
|
||||||
|
useThrottleFn(resize, 500)();
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
// node element
|
||||||
|
if (!node.value || !link.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.value = node.value.data(
|
||||||
|
networkProfilingStore.nodes,
|
||||||
|
(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
|
||||||
|
link.value = link.value.data(networkProfilingStore.calls, (d: Call) => d.id);
|
||||||
|
link.value.exit().remove();
|
||||||
|
link.value = linkElement(link.value.enter()).merge(link.value);
|
||||||
|
// anchorElement
|
||||||
|
anchor.value = anchor.value.data(
|
||||||
|
networkProfilingStore.calls,
|
||||||
|
(d: Call) => d.id
|
||||||
|
);
|
||||||
|
anchor.value.exit().remove();
|
||||||
|
anchor.value = anchorElement(
|
||||||
|
anchor.value.enter(),
|
||||||
|
{
|
||||||
|
handleLinkClick: handleLinkClick,
|
||||||
|
tipHtml: (data: Call) => {
|
||||||
|
const html = `<div><span class="grey">${t(
|
||||||
|
"detectPoint"
|
||||||
|
)}:</span>${data.detectPoints.join(" | ")}</div>`;
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tip.value
|
||||||
|
).merge(anchor.value);
|
||||||
|
// arrow marker
|
||||||
|
arrow.value = arrow.value.data(
|
||||||
|
networkProfilingStore.calls,
|
||||||
|
(d: Call) => d.id
|
||||||
|
);
|
||||||
|
arrow.value.exit().remove();
|
||||||
|
arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
|
||||||
|
// force element
|
||||||
|
simulation.value.nodes(networkProfilingStore.nodes);
|
||||||
|
simulation.value
|
||||||
|
.force("link")
|
||||||
|
.links(networkProfilingStore.calls)
|
||||||
|
.id((d: Call) => d.id);
|
||||||
|
const loopMap: any = {};
|
||||||
|
for (let i = 0; i < networkProfilingStore.calls.length; i++) {
|
||||||
|
const link: any = networkProfilingStore.calls[i];
|
||||||
|
link.loopFactor = 1;
|
||||||
|
for (let j = 0; j < networkProfilingStore.calls.length; j++) {
|
||||||
|
if (i === j || loopMap[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const otherLink = networkProfilingStore.calls[j];
|
||||||
|
if (
|
||||||
|
link.source.id === otherLink.target.id &&
|
||||||
|
link.target.id === otherLink.source.id
|
||||||
|
) {
|
||||||
|
link.loopFactor = -1;
|
||||||
|
loopMap[j] = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLinkClick(event: any, d: Call) {
|
||||||
|
if (
|
||||||
|
d.source.layer !== dashboardStore.layerId ||
|
||||||
|
d.target.layer !== dashboardStore.layerId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
networkProfilingStore.setNode(null);
|
||||||
|
networkProfilingStore.setLink(d);
|
||||||
|
if (!config.value.linkDashboard) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { dashboard } = getDashboard({
|
||||||
|
name: config.value.linkDashboard,
|
||||||
|
layer: dashboardStore.layerId,
|
||||||
|
entity: EntityType[7].value,
|
||||||
|
});
|
||||||
|
if (!dashboard) {
|
||||||
|
ElMessage.error(
|
||||||
|
`The dashboard named ${config.value.linkDashboard} doesn't exist`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const path = `/dashboard/related/${dashboard.layer}/${EntityType[7].value}/${d.source.id}/${d.target.id}/${dashboard.name}`;
|
||||||
|
const routeUrl = router.resolve({ path });
|
||||||
|
window.open(routeUrl.href, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 updateSettings(config: any) {
|
||||||
|
config.value = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfig() {
|
||||||
|
showSettings.value = !showSettings.value;
|
||||||
|
dashboardStore.selectWidget(props.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
const entry = entries[0];
|
||||||
|
const cr = entry.contentRect;
|
||||||
|
if (
|
||||||
|
Math.abs(cr.width - oldVal.value.width) < 5 &&
|
||||||
|
Math.abs(cr.height - oldVal.value.height) < 5
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
freshNodes();
|
||||||
|
oldVal.value = { width: cr.width, height: cr.height };
|
||||||
|
});
|
||||||
|
if (chart.value) {
|
||||||
|
observer.observe(chart.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function freshNodes() {
|
||||||
|
svg.value.selectAll(".svg-graph").remove();
|
||||||
|
if (!networkProfilingStore.nodes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drawGraph();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => networkProfilingStore.nodes,
|
||||||
|
() => {
|
||||||
|
freshNodes();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.micro-topo-chart {
|
||||||
|
width: calc(100% - 10px);
|
||||||
|
margin: 0 5px 5px 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.5ms linear;
|
||||||
|
background-color: #252a2f99;
|
||||||
|
color: #ddd;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 8px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
position: absolute;
|
||||||
|
top: 65px;
|
||||||
|
right: 10px;
|
||||||
|
width: 300px;
|
||||||
|
height: 160px;
|
||||||
|
background-color: #2b3037;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #ccc;
|
||||||
|
transition: all 0.5ms linear;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user