mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-07-18 21:25:24 +00:00
draw process topology
This commit is contained in:
parent
341d796aef
commit
39cedf1ddb
@ -113,18 +113,12 @@ export const dashboardStore = defineStore({
|
||||
: 3,
|
||||
};
|
||||
}
|
||||
if (
|
||||
[
|
||||
"Trace",
|
||||
"Profile",
|
||||
"Log",
|
||||
"DemandLog",
|
||||
"Ebpf",
|
||||
"NetworkProfiling",
|
||||
].includes(type)
|
||||
) {
|
||||
if (["Trace", "Profile", "Log", "DemandLog", "Ebpf"].includes(type)) {
|
||||
newItem.h = 36;
|
||||
}
|
||||
if (["NetworkProfiling"].includes(type)) {
|
||||
newItem.h = 44;
|
||||
}
|
||||
if (type === "Text") {
|
||||
newItem.h = 6;
|
||||
newItem.graph = TextConfig;
|
||||
@ -184,18 +178,12 @@ export const dashboardStore = defineStore({
|
||||
showDepth: true,
|
||||
};
|
||||
}
|
||||
if (
|
||||
[
|
||||
"Trace",
|
||||
"Profile",
|
||||
"Log",
|
||||
"DemandLog",
|
||||
"Ebpf",
|
||||
"NetworkProfiling",
|
||||
].includes(type)
|
||||
) {
|
||||
if (["Trace", "Profile", "Log", "DemandLog", "Ebpf"].includes(type)) {
|
||||
newItem.h = 32;
|
||||
}
|
||||
if (["NetworkProfiling"].includes(type)) {
|
||||
newItem.h = 44;
|
||||
}
|
||||
if (type === "Text") {
|
||||
newItem.h = 6;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: calc(100% - 210px);
|
||||
background-color: #333840;
|
||||
// background-color: #333840;
|
||||
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 { 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 nodeElement from "../../components/D3Graph/nodeProcess";
|
||||
import { Call } from "@/types/topology";
|
||||
// import zoom from "../../components/D3Graph/zoom";
|
||||
import { ProcessNode } from "@/types/ebpf";
|
||||
@ -53,6 +49,7 @@ import { useThrottleFn } from "@vueuse/core";
|
||||
import Settings from "./Settings.vue";
|
||||
import { EntityType } from "@/views/dashboard/data";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import { Layout } from "./Graph/layout";
|
||||
|
||||
/*global Nullable, defineProps */
|
||||
const props = defineProps({
|
||||
@ -66,7 +63,6 @@ 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);
|
||||
@ -108,14 +104,8 @@ function drawGraph() {
|
||||
graph.value = svg.value
|
||||
.append("g")
|
||||
.attr("class", "svg-graph")
|
||||
.attr("transform", `translate(-250, -220)`);
|
||||
.attr("transform", `translate(200, 200)`);
|
||||
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");
|
||||
@ -130,15 +120,73 @@ function drawGraph() {
|
||||
});
|
||||
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() {
|
||||
// node element
|
||||
if (!node.value || !link.value) {
|
||||
return;
|
||||
}
|
||||
node.value = node.value.data(
|
||||
networkProfilingStore.nodes,
|
||||
(d: ProcessNode) => d.id
|
||||
);
|
||||
const obj: any = (chart.value && chart.value.getBoundingClientRect()) || {};
|
||||
const p = {
|
||||
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 = nodeElement(
|
||||
d3,
|
||||
@ -150,62 +198,6 @@ function update() {
|
||||
},
|
||||
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);
|
||||
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) {
|
||||
@ -306,6 +298,8 @@ watch(
|
||||
margin: 0 5px 5px 0;
|
||||
height: 100%;
|
||||
min-height: 150px;
|
||||
min-width: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.process-svg {
|
||||
|
@ -143,7 +143,6 @@ async function createTask() {
|
||||
(selectorStore.currentService && selectorStore.currentService.id) || "";
|
||||
const serviceInstanceId =
|
||||
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
|
||||
console.log(selectorStore.currentPod);
|
||||
if (!serviceId) {
|
||||
return;
|
||||
}
|
||||
@ -235,5 +234,6 @@ async function fetchTasks() {
|
||||
|
||||
.new-task {
|
||||
float: right;
|
||||
margin-right: 3px;
|
||||
}
|
||||
</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