mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-05-12 15:52:57 +00:00
refactor: topology
This commit is contained in:
parent
f32e09defa
commit
cf255b2a75
251
src/views/dashboard/related/topology/service/Graph.vue
Normal file
251
src/views/dashboard/related/topology/service/Graph.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<!-- 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>
|
||||
<svg class="hierarchy-services-svg" :width="width" :height="height" @click="svgEvent">
|
||||
<g class="hierarchy-services-graph" :transform="`translate(${diff[0]}, ${diff[1]})`">
|
||||
<g
|
||||
class="topo-node"
|
||||
v-for="(n, index) in topologyLayout.nodes"
|
||||
:key="index"
|
||||
@mouseout="hideTip"
|
||||
@mouseover="showNodeTip($event, n)"
|
||||
@click="handleNodeClick($event, n)"
|
||||
@mousedown="startMoveNode($event, n)"
|
||||
@mouseup="stopMoveNode($event)"
|
||||
>
|
||||
<image width="36" height="36" :x="n.x - 15" :y="n.y - 18" :href="icons.CUBE" />
|
||||
<image width="28" height="25" :x="n.x - 14" :y="n.y - 43" :href="icons.LOCAL" style="opacity: 0.8" />
|
||||
<image
|
||||
width="12"
|
||||
height="12"
|
||||
:x="n.x - 6"
|
||||
:y="n.y - 38"
|
||||
:href="!n.type || n.type === `N/A` ? icons.UNDEFINED : icons[n.type.toUpperCase().replace('-', '')]"
|
||||
/>
|
||||
<text
|
||||
class="node-text"
|
||||
:x="n.x - (Math.min(n.name.length, 20) * 6) / 2 + 6"
|
||||
:y="n.y + n.height + 8"
|
||||
style="pointer-events: none"
|
||||
>
|
||||
{{ n.name.length > 20 ? `${n.name.substring(0, 20)}...` : n.name }}
|
||||
</text>
|
||||
</g>
|
||||
<g v-for="(l, index) in topologyLayout.calls" :key="index">
|
||||
<path
|
||||
class="topo-line"
|
||||
:d="`M${l.sourceX} ${l.sourceY} L${l.targetX} ${l.targetY}`"
|
||||
stroke="#97B0F8"
|
||||
marker-end="url(#arrow)"
|
||||
/>
|
||||
</g>
|
||||
<g class="arrows">
|
||||
<defs v-for="(_, index) in topologyLayout.calls" :key="index">
|
||||
<marker
|
||||
id="arrow"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="16"
|
||||
markerHeight="16"
|
||||
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>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import type { Node, Call } from "@/types/topology";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { EntityType, ConfigFieldTypes } from "@/views/dashboard/data";
|
||||
import icons from "@/assets/img/icons";
|
||||
import { layout, changeNode, computeLevels } from "./utils/layout";
|
||||
import zoom from "@/views/dashboard/related/components/utils/zoom";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
|
||||
/*global Nullable, defineProps */
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({}),
|
||||
},
|
||||
entity: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
nodes: {
|
||||
type: Array as PropType<Node[]>,
|
||||
default: () => [],
|
||||
},
|
||||
calls: {
|
||||
type: Array as PropType<Call[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(["showNodeTip", "handleNodeClick", "hideTip"]);
|
||||
const topologyStore = useTopologyStore();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const height = ref<number>(100);
|
||||
const width = ref<number>(100);
|
||||
const svg = ref<Nullable<any>>(null);
|
||||
const graph = ref<Nullable<any>>(null);
|
||||
const topologyLayout = ref<any>({});
|
||||
const graphWidth = ref<number>(100);
|
||||
const currentNode = ref<Nullable<Node>>(null);
|
||||
const diff = computed(() => [(width.value - graphWidth.value - 120) / 2, 0]);
|
||||
const radius = 8;
|
||||
|
||||
async function init() {
|
||||
const dom = document.querySelector(".hierarchy-related")?.getBoundingClientRect() || {
|
||||
height: 80,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 80;
|
||||
width.value = dom.width;
|
||||
svg.value = d3.select(".hierarchy-services-svg");
|
||||
graph.value = d3.select(".hierarchy-services-graph");
|
||||
await update();
|
||||
svg.value.call(zoom(d3, graph.value, diff.value));
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const layerList = [];
|
||||
const layerMap = new Map();
|
||||
for (const n of props.nodes) {
|
||||
if (layerMap.get(n.layer)) {
|
||||
const arr = layerMap.get(n.layer);
|
||||
arr.push(n);
|
||||
layerMap.set(n.layer, arr);
|
||||
} else {
|
||||
layerMap.set(n.layer, [n]);
|
||||
}
|
||||
}
|
||||
for (const d of layerMap.values()) {
|
||||
layerList.push(d);
|
||||
}
|
||||
for (const list of layerList) {
|
||||
const { dashboard } = getDashboard(
|
||||
{
|
||||
layer: list[0].layer || "",
|
||||
entity: EntityType[0].value,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
);
|
||||
const exp = (dashboard && dashboard.expressions) || [];
|
||||
await topologyStore.queryHierarchyNodeExpressions(exp, list[0].layer);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const levels = computeLevels(props.calls, props.nodes, []);
|
||||
|
||||
topologyLayout.value = layout(levels, props.calls, radius);
|
||||
graphWidth.value = topologyLayout.value.layout.width;
|
||||
const drag: any = d3.drag().on("drag", (d: { x: number; y: number }) => {
|
||||
topologyLayout.value.calls = changeNode(d, currentNode.value, topologyLayout.value, radius);
|
||||
});
|
||||
setTimeout(() => {
|
||||
d3.selectAll(".topo-node").call(drag);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function startMoveNode(event: MouseEvent, d: Node) {
|
||||
event.stopPropagation();
|
||||
currentNode.value = d;
|
||||
}
|
||||
function stopMoveNode(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
currentNode.value = null;
|
||||
}
|
||||
|
||||
function showNodeTip(event: MouseEvent, data: Node) {
|
||||
emits("showNodeTip", event, data);
|
||||
}
|
||||
|
||||
function hideTip() {
|
||||
emits("showNodeTip");
|
||||
}
|
||||
|
||||
function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
|
||||
event.stopPropagation();
|
||||
emits("handleNodeClick", event, d);
|
||||
}
|
||||
|
||||
function svgEvent() {
|
||||
if (!props.config) {
|
||||
return;
|
||||
}
|
||||
dashboardStore.selectWidget(props.config);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [...props.calls, ...props.nodes],
|
||||
() => {
|
||||
if (!props.nodes.length) {
|
||||
return;
|
||||
}
|
||||
if (!props.calls.length) {
|
||||
return;
|
||||
}
|
||||
init();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.hierarchy-services-topo {
|
||||
.node-text {
|
||||
fill: var(--sw-topology-color);
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.hierarchy-services-svg {
|
||||
cursor: move;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--sw-topology-color);
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.topo-line {
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1px;
|
||||
stroke-dasharray: 10 10;
|
||||
fill: none;
|
||||
animation: var(--sw-topo-animation);
|
||||
}
|
||||
|
||||
.topo-line-anchor,
|
||||
.topo-node {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.el-loading-spinner {
|
||||
top: 30%;
|
||||
}
|
||||
</style>
|
@ -19,68 +19,21 @@ limitations under the License. -->
|
||||
element-loading-background="rgba(0, 0, 0, 0)"
|
||||
:style="`height: ${height}px`"
|
||||
>
|
||||
<svg class="hierarchy-services-svg" :width="width" :height="height" @click="svgEvent">
|
||||
<g class="hierarchy-services-graph" :transform="`translate(${diff[0]}, ${diff[1]})`">
|
||||
<g
|
||||
class="topo-node"
|
||||
v-for="(n, index) in topologyLayout.nodes"
|
||||
:key="index"
|
||||
@mouseout="hideTip"
|
||||
@mouseover="showNodeTip($event, n)"
|
||||
@click="handleNodeClick($event, n)"
|
||||
@mousedown="startMoveNode($event, n)"
|
||||
@mouseup="stopMoveNode($event)"
|
||||
>
|
||||
<image width="36" height="36" :x="n.x - 15" :y="n.y - 18" :href="getNodeStatus(n)" />
|
||||
<image width="28" height="25" :x="n.x - 14" :y="n.y - 43" :href="icons.LOCAL" style="opacity: 0.8" />
|
||||
<image
|
||||
width="12"
|
||||
height="12"
|
||||
:x="n.x - 6"
|
||||
:y="n.y - 38"
|
||||
:href="!n.type || n.type === `N/A` ? icons.UNDEFINED : icons[n.type.toUpperCase().replace('-', '')]"
|
||||
/>
|
||||
<text
|
||||
class="node-text"
|
||||
:x="n.x - (Math.min(n.name.length, 20) * 6) / 2 + 6"
|
||||
:y="n.y + n.height + 8"
|
||||
style="pointer-events: none"
|
||||
>
|
||||
{{ n.name.length > 20 ? `${n.name.substring(0, 20)}...` : n.name }}
|
||||
</text>
|
||||
</g>
|
||||
<g v-for="(l, index) in topologyLayout.calls" :key="index">
|
||||
<path
|
||||
class="topo-line"
|
||||
:d="`M${l.sourceX} ${l.sourceY} L${l.targetX} ${l.targetY}`"
|
||||
stroke="#97B0F8"
|
||||
marker-end="url(#arrow)"
|
||||
/>
|
||||
</g>
|
||||
<g class="arrows">
|
||||
<defs v-for="(_, index) in topologyLayout.calls" :key="index">
|
||||
<marker
|
||||
id="arrow"
|
||||
markerUnits="strokeWidth"
|
||||
markerWidth="16"
|
||||
markerHeight="16"
|
||||
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>
|
||||
<Graph
|
||||
:config="config"
|
||||
:nodes="topologyStore.hierarchyServiceNodes"
|
||||
:calls="topologyStore.hierarchyServiceCalls"
|
||||
:entity="EntityType[0].value"
|
||||
@showNodeTip="showNodeTip"
|
||||
@handleNodeClick="handleNodeClick"
|
||||
@hideTip="hideTip"
|
||||
/>
|
||||
<div id="popover"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { ref, onMounted, watch, computed, nextTick } from "vue";
|
||||
import { ref, onMounted, watch, nextTick } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import type { Node } from "@/types/topology";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
@ -91,13 +44,11 @@ limitations under the License. -->
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import type { MetricConfigOpt } from "@/types/dashboard";
|
||||
import { aggregation } from "@/hooks/useMetricsProcessor";
|
||||
import icons from "@/assets/img/icons";
|
||||
import { layout, changeNode, computeLevels } from "./utils/layout";
|
||||
import zoom from "@/views/dashboard/related/components/utils/zoom";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import Graph from "./Graph.vue";
|
||||
|
||||
/*global Nullable, defineProps */
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
config: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({}),
|
||||
@ -107,102 +58,42 @@ limitations under the License. -->
|
||||
const dashboardStore = useDashboardStore();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const height = ref<number>(100);
|
||||
const width = ref<number>(100);
|
||||
const loading = ref<boolean>(false);
|
||||
const svg = ref<Nullable<any>>(null);
|
||||
const graph = ref<Nullable<any>>(null);
|
||||
const topologyLayout = ref<any>({});
|
||||
const popover = ref<Nullable<any>>(null);
|
||||
const graphWidth = ref<number>(100);
|
||||
const currentNode = ref<Nullable<Node>>(null);
|
||||
const diff = computed(() => [(width.value - graphWidth.value - 120) / 2, 0]);
|
||||
const radius = 8;
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 10);
|
||||
init();
|
||||
});
|
||||
|
||||
getTopology();
|
||||
|
||||
async function init() {
|
||||
const dom = document.querySelector(".hierarchy-related")?.getBoundingClientRect() || {
|
||||
height: 80,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 80;
|
||||
width.value = dom.width;
|
||||
svg.value = d3.select(".hierarchy-services-svg");
|
||||
graph.value = d3.select(".hierarchy-services-graph");
|
||||
loading.value = true;
|
||||
await freshNodes();
|
||||
svg.value.call(zoom(d3, graph.value, diff.value));
|
||||
popover.value = d3.select("#popover");
|
||||
}
|
||||
async function freshNodes() {
|
||||
|
||||
async function getTopology() {
|
||||
loading.value = true;
|
||||
const resp = await topologyStore.getHierarchyServiceTopology();
|
||||
loading.value = false;
|
||||
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
await update();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
const layerList = [];
|
||||
const layerMap = new Map();
|
||||
for (const n of topologyStore.hierarchyServiceNodes) {
|
||||
if (layerMap.get(n.layer)) {
|
||||
const arr = layerMap.get(n.layer);
|
||||
arr.push(n);
|
||||
layerMap.set(n.layer, arr);
|
||||
} else {
|
||||
layerMap.set(n.layer, [n]);
|
||||
}
|
||||
}
|
||||
for (const d of layerMap.values()) {
|
||||
layerList.push(d);
|
||||
}
|
||||
for (const list of layerList) {
|
||||
const { dashboard } = getDashboard(
|
||||
{
|
||||
layer: list[0].layer || "",
|
||||
entity: EntityType[0].value,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
);
|
||||
const exp = (dashboard && dashboard.expressions) || [];
|
||||
await topologyStore.queryHierarchyNodeExpressions(exp, list[0].layer);
|
||||
}
|
||||
draw();
|
||||
popover.value = d3.select("#popover");
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const levels = computeLevels(topologyStore.hierarchyServiceCalls, topologyStore.hierarchyServiceNodes, []);
|
||||
|
||||
topologyLayout.value = layout(levels, topologyStore.hierarchyServiceCalls, radius);
|
||||
graphWidth.value = topologyLayout.value.layout.width;
|
||||
const drag: any = d3.drag().on("drag", (d: { x: number; y: number }) => {
|
||||
topologyLayout.value.calls = changeNode(d, currentNode.value, topologyLayout.value, radius);
|
||||
});
|
||||
setTimeout(() => {
|
||||
d3.selectAll(".topo-node").call(drag);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function startMoveNode(event: MouseEvent, d: Node) {
|
||||
event.stopPropagation();
|
||||
currentNode.value = d;
|
||||
}
|
||||
function stopMoveNode(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
currentNode.value = null;
|
||||
}
|
||||
|
||||
function getNodeStatus(d: any) {
|
||||
return d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||
}
|
||||
function showNodeTip(event: MouseEvent, data: Node) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (!popover.value) {
|
||||
return;
|
||||
}
|
||||
const dashboard =
|
||||
getDashboard(
|
||||
{
|
||||
@ -239,6 +130,7 @@ limitations under the License. -->
|
||||
}
|
||||
|
||||
function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
|
||||
const origin = dashboardStore.entity;
|
||||
event.stopPropagation();
|
||||
hideTip();
|
||||
const dashboard =
|
||||
@ -256,19 +148,6 @@ limitations under the License. -->
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
|
||||
function svgEvent() {
|
||||
dashboardStore.selectWidget(props.config);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => appStore.durationTime,
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
freshNodes();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.hierarchy-services-topo {
|
||||
|
@ -463,6 +463,7 @@ limitations under the License. -->
|
||||
topologyStore.setLink(null);
|
||||
}
|
||||
function handleGoEndpoint(name: string) {
|
||||
const origin = dashboardStore.entity;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
@ -470,6 +471,7 @@ limitations under the License. -->
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
function handleGoInstance(name: string) {
|
||||
const origin = dashboardStore.entity;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[3].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
@ -477,6 +479,7 @@ limitations under the License. -->
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
function handleGoDashboard(name: string) {
|
||||
const origin = dashboardStore.entity;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
@ -569,6 +572,9 @@ limitations under the License. -->
|
||||
watch(
|
||||
() => [selectorStore.currentService, selectorStore.currentDestService],
|
||||
(newVal, oldVal) => {
|
||||
if (!(oldVal[0] && newVal[0])) {
|
||||
return;
|
||||
}
|
||||
if (oldVal[0].id === newVal[0].id && !oldVal[1]) {
|
||||
return;
|
||||
}
|
||||
@ -576,6 +582,7 @@ limitations under the License. -->
|
||||
return;
|
||||
}
|
||||
freshNodes();
|
||||
hierarchyRelated.value = false;
|
||||
},
|
||||
);
|
||||
watch(
|
||||
@ -583,6 +590,7 @@ limitations under the License. -->
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
freshNodes();
|
||||
hierarchyRelated.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user