feat: implement the Service and Instance hierarchy topology. (#360)
18
src/assets/icons/hierarchy_topology.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<!-- 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. -->
|
||||
|
||||
<svg t="1704964118567" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5167">
|
||||
<path d="M900.032 646.016h-56.064V502.976a16 16 0 0 0-16-16H544v-96h62.976c22.144 0 40-17.92 40-40V161.024a40 40 0 0 0-40-40H417.024a40 40 0 0 0-40 40v189.952c0 22.144 17.92 40 40 40H480v96H195.968a16 16 0 0 0-16 16v143.04h-55.936a38.016 38.016 0 0 0-38.016 38.016v176c0 20.928 17.024 37.952 37.952 37.952h176a38.016 38.016 0 0 0 38.016-38.016v-176a38.016 38.016 0 0 0-37.952-37.952h-56V550.976H480v95.04h-56a38.016 38.016 0 0 0-38.016 38.016v176c0 20.928 17.024 37.952 38.016 37.952h176a38.016 38.016 0 0 0 38.016-38.016v-176a38.016 38.016 0 0 0-38.016-37.952H544V550.976h236.032v95.04h-56.064a38.016 38.016 0 0 0-37.952 38.016v176c0 20.928 17.024 37.952 38.016 37.952h176a38.016 38.016 0 0 0 37.952-38.016v-176a38.016 38.016 0 0 0-38.016-37.952zM440.96 184.96h141.952v141.952H441.024V185.024zM278.016 838.016H145.92V705.92h132.032v132.032z m299.968 0H446.08V705.92H577.92v132.032z m300.032 0h-132.032V705.92h132.032v132.032z" p-id="5168"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/img/tools/BROWSER.png
Normal file
After Width: | Height: | Size: 314 B |
BIN
src/assets/img/tools/DATABASE.png
Normal file
After Width: | Height: | Size: 150 B |
BIN
src/assets/img/tools/ELASTICSEARCH.png
Normal file
After Width: | Height: | Size: 150 B |
BIN
src/assets/img/tools/GENERAL.png
Normal file
After Width: | Height: | Size: 211 B |
BIN
src/assets/img/tools/K8S.png
Normal file
After Width: | Height: | Size: 471 B |
BIN
src/assets/img/tools/K8S_SERVICE.png
Normal file
After Width: | Height: | Size: 471 B |
BIN
src/assets/img/tools/MESH.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
src/assets/img/tools/MESH_CP.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
src/assets/img/tools/MESH_DP.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
src/assets/img/tools/MONGODB.png
Normal file
After Width: | Height: | Size: 150 B |
BIN
src/assets/img/tools/MQ.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/img/tools/NGINX.png
Normal file
After Width: | Height: | Size: 263 B |
BIN
src/assets/img/tools/OS_LINUX.png
Normal file
After Width: | Height: | Size: 241 B |
BIN
src/assets/img/tools/POSTGRESQL.png
Normal file
After Width: | Height: | Size: 150 B |
BIN
src/assets/img/tools/RABBITMQ.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/img/tools/VIRTUAL_CACHE.png
Normal file
After Width: | Height: | Size: 211 B |
BIN
src/assets/img/tools/VIRTUAL_DATABASE.png
Normal file
After Width: | Height: | Size: 211 B |
BIN
src/assets/img/tools/VIRTUAL_GATEWAY.png
Normal file
After Width: | Height: | Size: 211 B |
@ -85,7 +85,7 @@ limitations under the License. -->
|
||||
.bar-select {
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--el-border-color);
|
||||
background: $theme-background;
|
||||
border-radius: 3px;
|
||||
color: $font-color;
|
||||
@ -97,8 +97,8 @@ limitations under the License. -->
|
||||
border-radius: 3px;
|
||||
margin: 3px;
|
||||
color: $active-color;
|
||||
background-color: #fafafa;
|
||||
border: 1px solid #e8e8e8;
|
||||
background-color: var(--theme-background);
|
||||
border: 1px solid var(--el-color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@ -139,7 +139,7 @@ limitations under the License. -->
|
||||
left: 0;
|
||||
background-color: $theme-background;
|
||||
box-shadow: 0 1px 6px rgb(99 99 99 / 20%);
|
||||
border: 1px solid #ddd;
|
||||
border: 1px solid var(--el-border-color);
|
||||
width: 100%;
|
||||
border-radius: 0 0 3px 3px;
|
||||
border-right-width: 1px !important;
|
||||
@ -169,7 +169,7 @@ limitations under the License. -->
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
background-color: var(--layout-background);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -79,7 +79,7 @@ limitations under the License. -->
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.input-name {
|
||||
width: 300px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.vertical {
|
||||
|
@ -99,3 +99,56 @@ export const ProcessTopology = {
|
||||
}
|
||||
`,
|
||||
};
|
||||
export const HierarchyServiceTopology = {
|
||||
variable: "$serviceId: ID!, $layer: String!",
|
||||
query: `
|
||||
hierarchyServiceTopology: getServiceHierarchy(serviceId: $serviceId, layer: $layer) {
|
||||
relations {
|
||||
upperService {
|
||||
id
|
||||
name
|
||||
layer
|
||||
normal
|
||||
}
|
||||
lowerService {
|
||||
id
|
||||
name
|
||||
layer
|
||||
normal
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
export const HierarchyInstanceTopology = {
|
||||
variable: "$instanceId: ID!, $layer: String!",
|
||||
query: `
|
||||
hierarchyInstanceTopology: getInstanceHierarchy(instanceId: $instanceId, layer: $layer) {
|
||||
relations {
|
||||
upperInstance {
|
||||
id
|
||||
name
|
||||
layer
|
||||
normal
|
||||
serviceName
|
||||
serviceId
|
||||
}
|
||||
lowerInstance {
|
||||
id
|
||||
name
|
||||
layer
|
||||
normal
|
||||
serviceName
|
||||
serviceId
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const ListLayerLevels = {
|
||||
query: `
|
||||
levels: listLayerLevels {
|
||||
layer
|
||||
level
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
@ -14,9 +14,20 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { InstanceTopology, EndpointTopology, ServicesTopology, ProcessTopology } from "../fragments/topology";
|
||||
import {
|
||||
InstanceTopology,
|
||||
EndpointTopology,
|
||||
ServicesTopology,
|
||||
ProcessTopology,
|
||||
HierarchyServiceTopology,
|
||||
HierarchyInstanceTopology,
|
||||
ListLayerLevels,
|
||||
} from "../fragments/topology";
|
||||
|
||||
export const getInstanceTopology = `query queryData(${InstanceTopology.variable}) {${InstanceTopology.query}}`;
|
||||
export const getEndpointTopology = `query queryData(${EndpointTopology.variable}) {${EndpointTopology.query}}`;
|
||||
export const getServicesTopology = `query queryData(${ServicesTopology.variable}) {${ServicesTopology.query}}`;
|
||||
export const getProcessTopology = `query queryData(${ProcessTopology.variable}) {${ProcessTopology.query}}`;
|
||||
export const getHierarchyInstanceTopology = `query queryData(${HierarchyInstanceTopology.variable}) {${HierarchyInstanceTopology.query}}`;
|
||||
export const getHierarchyServiceTopology = `query queryData(${HierarchyServiceTopology.variable}) {${HierarchyServiceTopology.query}}`;
|
||||
export const queryListLayerLevels = `query queryLayerLevels {${ListLayerLevels.query}}`;
|
||||
|
@ -17,15 +17,25 @@
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
import { ConfigFieldTypes } from "@/views/dashboard/data";
|
||||
|
||||
export default function getDashboard(param?: { name: string; layer: string; entity: string }) {
|
||||
export default function getDashboard(param?: { name?: string; layer: string; entity: string }, t?: string) {
|
||||
const type = t || ConfigFieldTypes.NAME; // "NAME" or "ISDEFAULT"
|
||||
const dashboardStore = useDashboardStore();
|
||||
const opt = param || dashboardStore.currentDashboard;
|
||||
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
|
||||
const dashboard = list.find(
|
||||
(d: { name: string; layer: string; entity: string }) =>
|
||||
d.name === opt.name && d.entity === opt.entity && d.layer === opt.layer,
|
||||
);
|
||||
let dashboard;
|
||||
if (type === ConfigFieldTypes.NAME) {
|
||||
dashboard = list.find(
|
||||
(d: { name: string; layer: string; entity: string }) =>
|
||||
d.name === opt.name && d.entity === opt.entity && d.layer === opt.layer,
|
||||
);
|
||||
} else {
|
||||
dashboard = list.find(
|
||||
(d: { name: string; layer: string; entity: string; isDefault: boolean }) =>
|
||||
d.isDefault && d.entity === opt.entity && d.layer === opt.layer,
|
||||
);
|
||||
}
|
||||
const all = dashboardStore.layout;
|
||||
const widgets: LayoutConfig[] = [];
|
||||
for (const item of all) {
|
||||
|
@ -332,10 +332,14 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
|
||||
let serviceInstanceName;
|
||||
let destServiceInstanceName;
|
||||
let destEndpointName;
|
||||
let normal = false;
|
||||
let destNormal;
|
||||
if (d.sourceObj && d.targetObj) {
|
||||
// instances = Calls
|
||||
serviceName = d.sourceObj.serviceName || d.sourceObj.name;
|
||||
destServiceName = d.targetObj.serviceName || d.targetObj.name;
|
||||
normal = d.sourceObj.normal || d.sourceObj.isReal || false;
|
||||
destNormal = d.targetObj.normal || d.targetObj.isReal || false;
|
||||
if (EntityType[4].value === dashboardStore.entity) {
|
||||
serviceInstanceName = d.sourceObj.name;
|
||||
destServiceInstanceName = d.targetObj.name;
|
||||
@ -347,6 +351,10 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
|
||||
} else {
|
||||
// instances = Nodes
|
||||
serviceName = d.serviceName || d.name;
|
||||
normal = d.normal || d.isReal || false;
|
||||
if (EntityType[3].value === dashboardStore.entity) {
|
||||
serviceInstanceName = d.name;
|
||||
}
|
||||
if (EntityType[4].value === dashboardStore.entity) {
|
||||
serviceInstanceName = d.name;
|
||||
}
|
||||
@ -356,11 +364,11 @@ export function useQueryTopologyExpressionsProcessor(metrics: string[], instance
|
||||
}
|
||||
const entity = {
|
||||
serviceName,
|
||||
normal: true,
|
||||
normal,
|
||||
serviceInstanceName,
|
||||
endpointName,
|
||||
destServiceName,
|
||||
destNormal: destServiceName ? true : undefined,
|
||||
destNormal: destServiceName ? destNormal : undefined,
|
||||
destServiceInstanceName,
|
||||
destEndpointName,
|
||||
};
|
||||
|
@ -385,6 +385,7 @@ const msg = {
|
||||
traceDesc:
|
||||
"The trace segment serves as a representation of a trace portion executed within one single OS process, such as a JVM. It comprises a collection of spans, typically associated with and collected from a single request or execution context.",
|
||||
tabExpressions: "Tab Expressions",
|
||||
hierarchyServicesSettings: "Service Hierarchy Topology Settings",
|
||||
hierarchyNodeMetrics: "Metrics related with hierarchy topology nodes",
|
||||
};
|
||||
export default msg;
|
||||
|
@ -385,6 +385,7 @@ const msg = {
|
||||
traceDesc:
|
||||
"The trace segment serves as a representation of a trace portion executed within one single OS process, such as a JVM. It comprises a collection of spans, typically associated with and collected from a single request or execution context.",
|
||||
tabExpressions: "Tab Expressions",
|
||||
hierarchyServicesSettings: "Service Hierarchy Topology Settings",
|
||||
hierarchyNodeMetrics: "Metrics related with hierarchy topology nodes",
|
||||
};
|
||||
export default msg;
|
||||
|
@ -383,6 +383,7 @@ const msg = {
|
||||
traceDesc:
|
||||
"Trace Segment代表在单一操作系统进程(例如JVM)中执行的追踪部分。它包含了一组跨度(spans),这些跨度通常与单一请求或执行上下文关联。",
|
||||
tabExpressions: "Tab表达式",
|
||||
hierarchyServicesSettings: "层次结构服务拓扑设置",
|
||||
hierarchyNodeMetrics: "层级拓扑节点关联的指标",
|
||||
};
|
||||
export default msg;
|
||||
|
@ -17,9 +17,10 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { store } from "@/store";
|
||||
import type { Service } from "@/types/selector";
|
||||
import type { Node, Call } from "@/types/topology";
|
||||
import type { Node, Call, HierarchyNode, ServiceHierarchy, InstanceHierarchy } from "@/types/topology";
|
||||
import graphql from "@/graphql";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import query from "@/graphql/fetch";
|
||||
@ -35,9 +36,15 @@ interface TopologyState {
|
||||
call: Nullable<Call>;
|
||||
calls: Call[];
|
||||
nodes: Node[];
|
||||
hierarchyServiceCalls: Call[];
|
||||
hierarchyServiceNodes: HierarchyNode[];
|
||||
hierarchyInstanceCalls: Call[];
|
||||
hierarchyInstanceNodes: HierarchyNode[];
|
||||
nodeMetricValue: MetricVal;
|
||||
linkServerMetrics: MetricVal;
|
||||
linkClientMetrics: MetricVal;
|
||||
hierarchyNodeMetrics: { [key: string]: MetricVal };
|
||||
hierarchyInstanceNodeMetrics: { [key: string]: MetricVal };
|
||||
}
|
||||
|
||||
export const topologyStore = defineStore({
|
||||
@ -45,11 +52,17 @@ export const topologyStore = defineStore({
|
||||
state: (): TopologyState => ({
|
||||
calls: [],
|
||||
nodes: [],
|
||||
hierarchyServiceCalls: [],
|
||||
hierarchyServiceNodes: [],
|
||||
hierarchyInstanceCalls: [],
|
||||
hierarchyInstanceNodes: [],
|
||||
node: null,
|
||||
call: null,
|
||||
nodeMetricValue: {},
|
||||
linkServerMetrics: {},
|
||||
linkClientMetrics: {},
|
||||
hierarchyNodeMetrics: {},
|
||||
hierarchyInstanceNodeMetrics: {},
|
||||
}),
|
||||
actions: {
|
||||
setNode(node: Node) {
|
||||
@ -106,8 +119,107 @@ export const topologyStore = defineStore({
|
||||
this.calls = calls;
|
||||
this.nodes = nodes;
|
||||
},
|
||||
setNodeMetricValue(m: MetricVal) {
|
||||
this.nodeMetricValue = m;
|
||||
setHierarchyInstanceTopology(data: InstanceHierarchy, levels: { layer: string; level: number }[]) {
|
||||
const relations = data.relations || [];
|
||||
const nodesMap = new Map();
|
||||
const callList = [];
|
||||
|
||||
for (const relation of relations) {
|
||||
const upperId = relation.upperInstance.id;
|
||||
const lowerId = relation.lowerInstance.id;
|
||||
const lowerKey = `${lowerId}-${relation.lowerInstance.layer}`;
|
||||
const upperKey = `${upperId}-${relation.upperInstance.layer}`;
|
||||
const lowerLevel = levels.find(
|
||||
(l: { layer: string; level: number }) => l.layer === relation.lowerInstance.layer,
|
||||
) || { level: undefined };
|
||||
const upperLevel = levels.find(
|
||||
(l: { layer: string; level: number }) => l.layer === relation.upperInstance.layer,
|
||||
) || { level: undefined };
|
||||
const lowerObj = {
|
||||
...relation.lowerInstance,
|
||||
key: lowerId,
|
||||
id: lowerKey,
|
||||
l: lowerLevel.level,
|
||||
};
|
||||
const upperObj = {
|
||||
...relation.upperInstance,
|
||||
key: upperId,
|
||||
id: upperKey,
|
||||
l: upperLevel.level,
|
||||
};
|
||||
if (!nodesMap.get(upperKey)) {
|
||||
nodesMap.set(upperKey, upperObj);
|
||||
}
|
||||
if (!nodesMap.get(lowerKey)) {
|
||||
nodesMap.set(lowerKey, lowerObj);
|
||||
}
|
||||
callList.push({
|
||||
target: lowerKey,
|
||||
source: upperKey,
|
||||
id: `${lowerKey}->${upperKey}`,
|
||||
sourceObj: upperObj,
|
||||
targetObj: lowerObj,
|
||||
});
|
||||
}
|
||||
this.hierarchyInstanceCalls = callList;
|
||||
this.hierarchyInstanceNodes = [];
|
||||
for (const d of nodesMap.values()) {
|
||||
this.hierarchyInstanceNodes.push(d);
|
||||
}
|
||||
},
|
||||
setHierarchyServiceTopology(data: ServiceHierarchy, levels: { layer: string; level: number }[]) {
|
||||
const relations = data.relations || [];
|
||||
const nodesMap = new Map();
|
||||
const callList = [];
|
||||
|
||||
for (const relation of relations) {
|
||||
const upperId = relation.upperService.id;
|
||||
const lowerId = relation.lowerService.id;
|
||||
const lowerKey = `${lowerId}-${relation.lowerService.layer}`;
|
||||
const upperKey = `${upperId}-${relation.upperService.layer}`;
|
||||
const lowerLevel = levels.find(
|
||||
(l: { layer: string; level: number }) => l.layer === relation.lowerService.layer,
|
||||
) || { level: undefined };
|
||||
const upperLevel = levels.find(
|
||||
(l: { layer: string; level: number }) => l.layer === relation.upperService.layer,
|
||||
) || { level: undefined };
|
||||
const lowerObj = {
|
||||
...relation.lowerService,
|
||||
key: lowerId,
|
||||
id: lowerKey,
|
||||
l: lowerLevel.level,
|
||||
};
|
||||
const upperObj = {
|
||||
...relation.upperService,
|
||||
key: upperId,
|
||||
id: upperKey,
|
||||
l: upperLevel.level,
|
||||
};
|
||||
if (!nodesMap.get(upperKey)) {
|
||||
nodesMap.set(upperKey, upperObj);
|
||||
}
|
||||
if (!nodesMap.get(lowerKey)) {
|
||||
nodesMap.set(lowerKey, lowerObj);
|
||||
}
|
||||
callList.push({
|
||||
target: lowerKey,
|
||||
source: upperKey,
|
||||
id: `${lowerKey}->${upperKey}`,
|
||||
sourceObj: upperObj,
|
||||
targetObj: lowerObj,
|
||||
});
|
||||
}
|
||||
this.hierarchyServiceCalls = callList;
|
||||
this.hierarchyServiceNodes = [];
|
||||
for (const d of nodesMap.values()) {
|
||||
this.hierarchyServiceNodes.push(d);
|
||||
}
|
||||
},
|
||||
setHierarchyNodeMetricValue(m: MetricVal, layer: string) {
|
||||
this.hierarchyNodeMetrics[layer] = m;
|
||||
},
|
||||
setHierarchyInstanceNodeMetricValue(m: MetricVal, layer: string) {
|
||||
this.hierarchyInstanceNodeMetrics[layer] = m;
|
||||
},
|
||||
setLinkServerMetrics(m: MetricVal) {
|
||||
this.linkServerMetrics = m;
|
||||
@ -115,6 +227,12 @@ export const topologyStore = defineStore({
|
||||
setLinkClientMetrics(m: MetricVal) {
|
||||
this.linkClientMetrics = m;
|
||||
},
|
||||
setNodeMetricValue(m: MetricVal) {
|
||||
this.nodeMetricValue = m;
|
||||
},
|
||||
setNodeValue(m: MetricVal) {
|
||||
this.nodeMetricValue = m;
|
||||
},
|
||||
setLegendValues(expressions: string, data: { [key: string]: any }) {
|
||||
for (let idx = 0; idx < this.nodes.length; idx++) {
|
||||
for (let index = 0; index < expressions.length; index++) {
|
||||
@ -481,6 +599,92 @@ export const topologyStore = defineStore({
|
||||
this.setLinkClientMetrics(res.data.data);
|
||||
return res.data;
|
||||
},
|
||||
async getHierarchyServiceTopology() {
|
||||
const dashboardStore = useDashboardStore();
|
||||
const { currentService } = useSelectorStore();
|
||||
const id = this.node ? this.node.id : (currentService || {}).id;
|
||||
const layer = this.node ? this.node.layer : dashboardStore.layerId;
|
||||
if (!(id && layer)) {
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
const res: AxiosResponse = await graphql
|
||||
.query("getHierarchyServiceTopology")
|
||||
.params({ serviceId: id, layer: layer });
|
||||
if (res.data.errors) {
|
||||
return res.data;
|
||||
}
|
||||
const resp = await this.getListLayerLevels();
|
||||
if (resp.errors) {
|
||||
return resp;
|
||||
}
|
||||
const levels = resp.data.levels || [];
|
||||
this.setHierarchyServiceTopology(res.data.data.hierarchyServiceTopology || {}, levels);
|
||||
return res.data;
|
||||
},
|
||||
async getListLayerLevels() {
|
||||
const res: AxiosResponse = await graphql.query("queryListLayerLevels").params({});
|
||||
|
||||
return res.data;
|
||||
},
|
||||
async getHierarchyInstanceTopology() {
|
||||
const { currentPod } = useSelectorStore();
|
||||
const dashboardStore = useDashboardStore();
|
||||
|
||||
if (!(currentPod && dashboardStore.layerId)) {
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
const res: AxiosResponse = await graphql
|
||||
.query("getHierarchyInstanceTopology")
|
||||
.params({ instanceId: currentPod.id, layer: dashboardStore.layerId });
|
||||
if (res.data.errors) {
|
||||
return res.data;
|
||||
}
|
||||
const resp = await this.getListLayerLevels();
|
||||
if (resp.errors) {
|
||||
return resp;
|
||||
}
|
||||
const levels = resp.data.levels || [];
|
||||
this.setHierarchyInstanceTopology(res.data.data.hierarchyInstanceTopology || {}, levels);
|
||||
return res.data;
|
||||
},
|
||||
async queryHierarchyExpressions(expressions: string[], nodes: Node[]) {
|
||||
const { getExpressionQuery, handleExpressionValues } = useQueryTopologyExpressionsProcessor(expressions, nodes);
|
||||
const param = getExpressionQuery();
|
||||
const res = await this.getNodeExpressionValue(param);
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
return;
|
||||
}
|
||||
const metrics = handleExpressionValues(res.data);
|
||||
return metrics;
|
||||
},
|
||||
async queryHierarchyNodeExpressions(expressions: string[], layer: string) {
|
||||
const nodes = this.hierarchyServiceNodes.filter((n: Node) => n.layer === layer);
|
||||
if (!nodes.length) {
|
||||
this.setHierarchyNodeMetricValue({}, layer);
|
||||
return;
|
||||
}
|
||||
if (!expressions.length) {
|
||||
this.setHierarchyNodeMetricValue({}, layer);
|
||||
return;
|
||||
}
|
||||
const metrics = await this.queryHierarchyExpressions(expressions, nodes);
|
||||
this.setHierarchyNodeMetricValue(metrics, layer);
|
||||
},
|
||||
async queryHierarchyInstanceNodeExpressions(expressions: string[], layer: string) {
|
||||
const nodes = this.hierarchyInstanceNodes.filter((n: Node) => n.layer === layer);
|
||||
|
||||
if (!expressions.length) {
|
||||
this.setHierarchyInstanceNodeMetricValue({}, layer);
|
||||
return;
|
||||
}
|
||||
if (!nodes.length) {
|
||||
this.setHierarchyInstanceNodeMetricValue({}, layer);
|
||||
return;
|
||||
}
|
||||
const metrics = await this.queryHierarchyExpressions(expressions, nodes);
|
||||
this.setHierarchyInstanceNodeMetricValue(metrics, layer);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -15,9 +15,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
|
||||
@keyframes topo-dash {
|
||||
from {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--sw-green: #70c877;
|
||||
--sw-orange: #e6a23c;
|
||||
--sw-topo-animation: topo-dash 0.3s linear infinite;
|
||||
}
|
||||
|
||||
html {
|
||||
@ -58,7 +68,7 @@ html {
|
||||
--sw-time-axis-text: #4d4d4d;
|
||||
--sw-drawer-header: #72767b;
|
||||
--sw-marketplace-border: #dedfe0;
|
||||
--sw-grid-item-active: #79bbff;
|
||||
--sw-grid-item-active: #d4d7de;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
@ -82,7 +92,7 @@ html.dark {
|
||||
--sw-config-header: #303133;
|
||||
--sw-topology-color: #ccc;
|
||||
--vis-tooltip-bg: #414243;
|
||||
--sw-topology-switch-icon: #aaa;
|
||||
--sw-topology-switch-icon: #999;
|
||||
--sw-topology-box-shadow: 0 0 2px 0 #444;
|
||||
--sw-topology-setting-bg: #333;
|
||||
--sw-topology-border: 1px solid #666;
|
||||
|
1
src/types/dashboard.d.ts
vendored
@ -23,6 +23,7 @@ export type DashboardItem = {
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
expressions?: string[];
|
||||
expressionsConfig?: MetricConfigOpt[];
|
||||
};
|
||||
export interface LayoutConfig {
|
||||
x: number;
|
||||
|
33
src/types/topology.d.ts
vendored
@ -14,6 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Call {
|
||||
source: string | any;
|
||||
target: string | any;
|
||||
@ -31,6 +32,12 @@ export interface Call {
|
||||
targetY?: number;
|
||||
targetX?: number;
|
||||
}
|
||||
export interface HierarchyNode {
|
||||
id: string;
|
||||
name: string;
|
||||
layer: string;
|
||||
level?: number;
|
||||
}
|
||||
export interface Node {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -39,7 +46,33 @@ export interface Node {
|
||||
layer?: string;
|
||||
serviceName?: string;
|
||||
height?: number;
|
||||
width?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
level?: number;
|
||||
l?: number;
|
||||
}
|
||||
|
||||
export interface ServiceHierarchy {
|
||||
relations: HierarchyServiceRelation[];
|
||||
}
|
||||
|
||||
export interface HierarchyServiceRelation {
|
||||
upperService: HierarchyRelated;
|
||||
lowerService: HierarchyRelated;
|
||||
}
|
||||
|
||||
type HierarchyRelated = {
|
||||
id: string;
|
||||
name: string;
|
||||
layer: string;
|
||||
};
|
||||
|
||||
export interface InstanceHierarchy {
|
||||
relations: HierarchyInstanceRelation[];
|
||||
}
|
||||
|
||||
export interface HierarchyInstanceRelation {
|
||||
upperInstance: HierarchyRelated;
|
||||
lowerInstance: HierarchyRelated;
|
||||
}
|
||||
|
@ -98,12 +98,13 @@ limitations under the License. -->
|
||||
{{ t("rename") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
:disabled="![EntityType[0].value, EntityType[3].value].includes(scope.row.entity)"
|
||||
v-if="[EntityType[0].value, EntityType[3].value].includes(scope.row.entity)"
|
||||
size="small"
|
||||
@click="handleEditMQE(scope.row)"
|
||||
>
|
||||
MQE
|
||||
</el-button>
|
||||
<span v-else class="placeholder"></span>
|
||||
<el-popconfirm :title="t('deleteTitle')" @confirm="handleDelete(scope.row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">
|
||||
@ -147,18 +148,34 @@ limitations under the License. -->
|
||||
/>
|
||||
</div>
|
||||
<el-dialog v-model="MQEVisible" title="Edit MQE" width="400px">
|
||||
<div>{{ t("hierarchyNodeMetrics") }}</div>
|
||||
<div>
|
||||
<span>{{ t("hierarchyNodeMetrics") }}</span>
|
||||
<el-popover placement="left" :width="400" trigger="click">
|
||||
<template #reference>
|
||||
<span>
|
||||
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
|
||||
</span>
|
||||
</template>
|
||||
<Metrics
|
||||
:type="'hierarchyServicesConfig'"
|
||||
:expressions="currentRow.expressions || []"
|
||||
:layer="currentRow.layer"
|
||||
:entity="currentRow.entity"
|
||||
@update="updateSettings"
|
||||
/>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="mt-10 expressions">
|
||||
<Tags
|
||||
:tags="currentRow.expressions || []"
|
||||
:vertical="true"
|
||||
:text="t('addExpressions')"
|
||||
@change="(param) => changeExpressions(param)"
|
||||
@change="(param: string[]) => changeExpressions(param)"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="MQEVisible = false">Cancel</el-button>
|
||||
<el-button @click="MQEVisible = false"> Cancel </el-button>
|
||||
<el-button type="primary" @click="saveMQE"> Confirm </el-button>
|
||||
</span>
|
||||
</template>
|
||||
@ -178,8 +195,9 @@ limitations under the License. -->
|
||||
import { EntityType } from "./data";
|
||||
import { isEmptyObject } from "@/utils/is";
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
import Metrics from "@/views/dashboard/related/topology/config/Metrics.vue";
|
||||
|
||||
/*global Nullable*/
|
||||
/*global Nullable, Recordable*/
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const pageSize = 20;
|
||||
@ -192,7 +210,7 @@ limitations under the License. -->
|
||||
const multipleSelection = ref<DashboardItem[]>([]);
|
||||
const dashboardFile = ref<Nullable<HTMLDivElement>>(null);
|
||||
const MQEVisible = ref<boolean>(false);
|
||||
const currentRow = ref<any>({});
|
||||
const currentRow = ref<DashboardItem | Recordable>({});
|
||||
|
||||
const handleSelectionChange = (val: DashboardItem[]) => {
|
||||
multipleSelection.value = val;
|
||||
@ -207,6 +225,13 @@ limitations under the License. -->
|
||||
currentRow.value.expressions = params;
|
||||
}
|
||||
|
||||
function updateSettings(config: Record<string, never>) {
|
||||
currentRow.value = {
|
||||
...currentRow.value,
|
||||
expressionsConfig: config.hierarchyServicesConfig,
|
||||
};
|
||||
}
|
||||
|
||||
function handleEditMQE(row: DashboardItem) {
|
||||
MQEVisible.value = !MQEVisible.value;
|
||||
currentRow.value = row;
|
||||
@ -218,6 +243,7 @@ limitations under the License. -->
|
||||
for (const d of dashboardStore.dashboards) {
|
||||
if (d.id === currentRow.value.id) {
|
||||
d.expressions = currentRow.value.expressions;
|
||||
d.expressionsConfig = currentRow.value.expressionsConfig;
|
||||
const key = [d.layer, d.entity, d.name].join("_");
|
||||
const layout = sessionStorage.getItem(key) || "{}";
|
||||
const c = {
|
||||
@ -247,8 +273,9 @@ limitations under the License. -->
|
||||
items.push(d);
|
||||
}
|
||||
dashboardStore.resetDashboards(items);
|
||||
searchDashboards(1);
|
||||
searchDashboards(currentPage.value);
|
||||
loading.value = false;
|
||||
MQEVisible.value = false;
|
||||
}
|
||||
|
||||
async function importTemplates(event: any) {
|
||||
@ -467,7 +494,7 @@ limitations under the License. -->
|
||||
items.push(d);
|
||||
}
|
||||
dashboardStore.resetDashboards(items);
|
||||
searchDashboards(1);
|
||||
searchDashboards(currentPage.value);
|
||||
loading.value = false;
|
||||
}
|
||||
async function handleTopLevel(row: DashboardItem) {
|
||||
@ -532,7 +559,7 @@ limitations under the License. -->
|
||||
items.push(d);
|
||||
}
|
||||
dashboardStore.resetDashboards(items);
|
||||
searchDashboards(1);
|
||||
searchDashboards(currentPage.value);
|
||||
loading.value = false;
|
||||
}
|
||||
function handleRename(row: DashboardItem) {
|
||||
@ -579,7 +606,7 @@ limitations under the License. -->
|
||||
...d,
|
||||
name: value,
|
||||
});
|
||||
dashboards.value = dashboardStore.dashboards.map((item: any) => {
|
||||
dashboards.value = dashboardStore.dashboards.map((item: DashboardItem) => {
|
||||
if (dashboardStore.currentDashboard.id === item.id) {
|
||||
item = dashboardStore.currentDashboard;
|
||||
}
|
||||
@ -611,7 +638,7 @@ limitations under the License. -->
|
||||
sessionStorage.setItem("dashboards", JSON.stringify(dashboards.value));
|
||||
sessionStorage.removeItem(`${row.layer}_${row.entity}_${row.name}`);
|
||||
}
|
||||
function searchDashboards(pageIndex?: any) {
|
||||
function searchDashboards(pageIndex: number) {
|
||||
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
|
||||
const arr = list.filter((d: { name: string }) => d.name.includes(searchText.value));
|
||||
|
||||
@ -695,6 +722,11 @@ limitations under the License. -->
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: inline-block;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.expressions {
|
||||
height: 300px;
|
||||
}
|
||||
|
@ -366,3 +366,7 @@ export enum CallTypes {
|
||||
Server = "SERVER",
|
||||
Client = "CLIENT",
|
||||
}
|
||||
export enum ConfigFieldTypes {
|
||||
ISDEFAULT = "ISDEFAULT",
|
||||
NAME = "NAME",
|
||||
}
|
||||
|
@ -26,6 +26,9 @@ limitations under the License. -->
|
||||
@change="changeService"
|
||||
class="selectors"
|
||||
/>
|
||||
<span class="ml-5 cp hierarchy-btn" v-if="dashboardStore.entity === 'Service'" @click="viewTopology">
|
||||
<Icon size="small" iconName="hierarchy_topology" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="selectors-item" v-if="key === 3 || key === 4 || key === 5 || key === 6">
|
||||
<span class="label">
|
||||
@ -41,6 +44,13 @@ limitations under the License. -->
|
||||
class="selectorPod"
|
||||
:isRemote="['EndpointRelation', 'Endpoint'].includes(dashboardStore.entity)"
|
||||
/>
|
||||
<span
|
||||
class="ml-5 cp hierarchy-btn"
|
||||
v-if="dashboardStore.entity === 'ServiceInstance'"
|
||||
@click="showHierarchy = true"
|
||||
>
|
||||
<Icon size="small" iconName="hierarchy_topology" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="selectors-item" v-if="key === 5 || key === 6">
|
||||
<span class="label"> $Process </span>
|
||||
@ -126,10 +136,17 @@ limitations under the License. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="showHierarchy" :destroy-on-close="true" @closed="showHierarchy = false" width="640px">
|
||||
<div class="hierarchy-related">
|
||||
<instance-map v-if="dashboardStore.entity === 'ServiceInstance'" />
|
||||
<hierarchy-map v-else />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, computed, watch } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import {
|
||||
@ -146,17 +163,21 @@ limitations under the License. -->
|
||||
WidgetType,
|
||||
} from "@/views/dashboard/data";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { Option } from "@/types/app";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import InstanceMap from "@/views/dashboard/related/topology/pod/InstanceMap.vue";
|
||||
import HierarchyMap from "@/views/dashboard/related/topology/service/HierarchyMap.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const selectorStore = useSelectorStore();
|
||||
const topologyStore = useTopologyStore();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const params = useRoute().params;
|
||||
const toolIcons = ref<{ name: string; content: string; id: WidgetType }[]>(AllTools);
|
||||
const loading = ref<boolean>(false);
|
||||
const showHierarchy = ref<boolean>(false);
|
||||
const states = reactive<{
|
||||
destService: string;
|
||||
destPod: string;
|
||||
@ -637,6 +658,10 @@ limitations under the License. -->
|
||||
};
|
||||
fetchPods(EntityType[6].value, selectorStore.currentDestService.id, false, param);
|
||||
}
|
||||
function viewTopology() {
|
||||
showHierarchy.value = true;
|
||||
topologyStore.setNode(null);
|
||||
}
|
||||
watch(
|
||||
() => dashboardStore.entity,
|
||||
(newVal, oldVal) => {
|
||||
@ -705,4 +730,19 @@ limitations under the License. -->
|
||||
.relation {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.hierarchy-btn {
|
||||
display: inline-block;
|
||||
padding: 0 2px 2px;
|
||||
border: 1px solid #666;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.hierarchy-related {
|
||||
height: 600px;
|
||||
width: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
@ -13,14 +13,14 @@ 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>
|
||||
<Graph :config="config" v-if="isService" />
|
||||
<PodTopology :config="config" v-else />
|
||||
<service-map :config="config" v-if="isService" />
|
||||
<pod-map :config="config" v-else />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import Graph from "./components/Graph.vue";
|
||||
import PodTopology from "./components/PodTopology.vue";
|
||||
import { EntityType } from "../../data";
|
||||
import ServiceMap from "./service/ServiceMap.vue";
|
||||
import PodMap from "./pod/PodMap.vue";
|
||||
import { EntityType } from "@/views/dashboard/data";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
|
||||
/*global defineProps */
|
||||
|
@ -13,335 +13,140 @@ 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"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(0, 0, 0, 0)"
|
||||
:style="`height: ${height}px`"
|
||||
>
|
||||
<svg class="svg-topology" :width="width - 100" :height="height" @click="svgEvent">
|
||||
<g class="svg-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)"
|
||||
<svg class="hierarchy-services-svg" :width="width" :height="height" @click="svgEvent" v-if="nodes.length">
|
||||
<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.layer || n.layer === `N/A` ? icons.MESH : icons[n.layer.toUpperCase().replace('-', '')]"
|
||||
/>
|
||||
<text
|
||||
class="node-text"
|
||||
:x="n.x - (Math.min(n.name.length, 30) * 6) / 2 + 6"
|
||||
:y="n.y + n.width + 8"
|
||||
style="pointer-events: none"
|
||||
>
|
||||
<image width="36" height="36" :x="n.x - 15" :y="n.y - 18" :href="getNodeStatus(n)" />
|
||||
<!-- <circle :cx="n.x" :cy="n.y" r="12" fill="none" stroke="red"/> -->
|
||||
<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)"
|
||||
/>
|
||||
<circle
|
||||
class="topo-line-anchor"
|
||||
:cx="(l.sourceX + l.targetX) / 2"
|
||||
:cy="(l.sourceY + l.targetY) / 2"
|
||||
r="4"
|
||||
fill="#97B0F8"
|
||||
@click="handleLinkClick($event, l)"
|
||||
@mouseover="showLinkTip($event, l)"
|
||||
@mouseout="hideTip"
|
||||
/>
|
||||
</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>
|
||||
{{ n.name.length > 30 ? `${n.name.substring(0, 30)}...` : n.name }}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
<div id="tooltip"></div>
|
||||
<div class="legend">
|
||||
<div>
|
||||
<img :src="icons.CUBE" />
|
||||
<span>
|
||||
{{ settings.description ? settings.description.healthy || "" : "" }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<img :src="icons.CUBEERROR" />
|
||||
<span>
|
||||
{{ settings.description ? settings.description.unhealthy || "" : "" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting" v-if="showSetting && dashboardStore.editMode">
|
||||
<Settings @update="updateSettings" @updateNodes="freshNodes" />
|
||||
</div>
|
||||
<div class="tool">
|
||||
<span v-show="graphConfig.showDepth">
|
||||
<span class="label">{{ t("currentDepth") }}</span>
|
||||
<Selector class="inputs" :value="depth" :options="DepthList" @change="changeDepth" />
|
||||
</span>
|
||||
<span class="switch-icon ml-5" title="Settings" @click="setConfig" v-if="dashboardStore.editMode">
|
||||
<Icon size="middle" iconName="settings" />
|
||||
</span>
|
||||
<span class="switch-icon ml-5" title="Back to overview topology" @click="backToTopology">
|
||||
<Icon size="middle" iconName="keyboard_backspace" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="operations-list"
|
||||
v-if="topologyStore.node"
|
||||
:style="{
|
||||
top: operationsPos.y + 5 + 'px',
|
||||
left: operationsPos.x + 5 + 'px',
|
||||
}"
|
||||
>
|
||||
<span v-for="(item, index) of items" :key="index" @click="item.func(item.dashboard)">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div v-else class="hierarchy-services"> No Data</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { ref, onMounted, onBeforeUnmount, reactive, watch, computed, nextTick } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import type { Node, Call } from "@/types/topology";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { EntityType, DepthList, MetricModes, CallTypes } from "../../../data";
|
||||
import router from "@/router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import Settings from "./Settings.vue";
|
||||
import type { Option } from "@/types/app";
|
||||
import type { Service } from "@/types/selector";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import type { MetricConfigOpt } from "@/types/dashboard";
|
||||
import { aggregation } from "@/hooks/useMetricsProcessor";
|
||||
import icons from "@/assets/img/icons";
|
||||
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
|
||||
import { layout, circleIntersection, computeCallPos } from "./utils/layout";
|
||||
import zoom from "../../components/utils/zoom";
|
||||
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
|
||||
import { changeNode, computeHierarchyLevels, hierarchy } from "./utils/layout";
|
||||
import zoom from "@/views/dashboard/related/components/utils/zoom";
|
||||
|
||||
/*global Nullable, defineProps */
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({ graph: {} }),
|
||||
default: () => ({}),
|
||||
},
|
||||
entity: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
nodes: {
|
||||
type: Array as PropType<Node[]>,
|
||||
default: () => [],
|
||||
},
|
||||
calls: {
|
||||
type: Array as PropType<Call[]>,
|
||||
default: () => [],
|
||||
},
|
||||
layerLevels: {
|
||||
type: Array as PropType<{ layer: string; level: number }[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const selectorStore = useSelectorStore();
|
||||
const topologyStore = useTopologyStore();
|
||||
const emits = defineEmits(["showNodeTip", "handleNodeClick", "hideTip", "getNodeMetrics"]);
|
||||
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 chart = ref<Nullable<HTMLDivElement>>(null);
|
||||
const showSetting = ref<boolean>(false);
|
||||
const settings = ref<any>(props.config);
|
||||
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
|
||||
const items = ref<{ id: string; title: string; func: any; dashboard?: string }[]>([]);
|
||||
const graphConfig = computed(() => props.config.graph || {});
|
||||
const depth = ref<number>(graphConfig.value.depth || 2);
|
||||
const topologyLayout = ref<any>({});
|
||||
const tooltip = ref<Nullable<any>>(null);
|
||||
const graphWidth = ref<number>(100);
|
||||
const currentNode = ref<Nullable<Node>>();
|
||||
const diff = computed(() => [(width.value - graphWidth.value - 130) / 2, 100]);
|
||||
const radius = 8;
|
||||
const graphHeight = ref<number>(100);
|
||||
const currentNode = ref<Nullable<Node>>(null);
|
||||
const diff = computed(() => [
|
||||
(width.value - graphWidth.value - 160) / 2,
|
||||
(height.value - graphHeight.value - 60) / 2,
|
||||
]);
|
||||
const radius = 10;
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 10);
|
||||
});
|
||||
async function init() {
|
||||
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
|
||||
height: 40,
|
||||
const dom = document.querySelector(".hierarchy-related")?.getBoundingClientRect() || {
|
||||
height: 80,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 40;
|
||||
height.value = dom.height - 80;
|
||||
width.value = dom.width;
|
||||
svg.value = d3.select(".svg-topology");
|
||||
graph.value = d3.select(".svg-graph");
|
||||
loading.value = true;
|
||||
const json = await selectorStore.fetchServices(dashboardStore.layerId);
|
||||
if (json.errors) {
|
||||
ElMessage.error(json.errors);
|
||||
return;
|
||||
}
|
||||
await freshNodes();
|
||||
svg.value.call(zoom(d3, graph.value, diff.value));
|
||||
}
|
||||
async function freshNodes() {
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
const resp = await getTopology();
|
||||
loading.value = false;
|
||||
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
await update();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
if (settings.value.metricMode === MetricModes.Expression) {
|
||||
topologyStore.queryNodeExpressions(settings.value.nodeExpressions || []);
|
||||
topologyStore.getLinkExpressions(settings.value.linkClientExpressions || [], CallTypes.Client);
|
||||
topologyStore.getLinkExpressions(settings.value.linkServerExpressions || [], CallTypes.Server);
|
||||
} else {
|
||||
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
|
||||
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
|
||||
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
|
||||
}
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
await initLegendMetrics();
|
||||
svg.value = d3.select(".hierarchy-services-svg");
|
||||
graph.value = d3.select(".hierarchy-services-graph");
|
||||
emits("getNodeMetrics");
|
||||
draw();
|
||||
tooltip.value = d3.select("#tooltip");
|
||||
setNodeTools(settings.value.nodeDashboard);
|
||||
}
|
||||
|
||||
function computeLevels(calls: Call[], nodeList: Node[], levels: any[]) {
|
||||
const node = findMostFrequent(calls);
|
||||
const nodes = JSON.parse(JSON.stringify(nodeList)).sort((a: Node, b: Node) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const index = nodes.findIndex((n: Node) => n.type === "USER");
|
||||
let key = index;
|
||||
if (index < 0) {
|
||||
key = nodes.findIndex((n: Node) => n.id === node.id);
|
||||
}
|
||||
levels.push([nodes[key]]);
|
||||
nodes.splice(key, 1);
|
||||
for (const level of levels) {
|
||||
const a = [];
|
||||
for (const l of level) {
|
||||
for (const n of calls) {
|
||||
if (n.target === l.id) {
|
||||
const i = nodes.findIndex((d: Node) => d.id === n.source);
|
||||
if (i > -1) {
|
||||
a.push(nodes[i]);
|
||||
nodes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (n.source === l.id) {
|
||||
const i = nodes.findIndex((d: Node) => d.id === n.target);
|
||||
if (i > -1) {
|
||||
a.push(nodes[i]);
|
||||
nodes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.length) {
|
||||
levels.push(a);
|
||||
}
|
||||
}
|
||||
if (nodes.length) {
|
||||
const ids = nodes.map((d: Node) => d.id);
|
||||
const links = calls.filter((item: Call) => ids.includes(item.source) || ids.includes(item.target));
|
||||
const list = computeLevels(links, nodes, []);
|
||||
levels = list.map((subArrayA, index) => subArrayA.concat(levels[index]));
|
||||
}
|
||||
return levels;
|
||||
svg.value.call(zoom(d3, graph.value, diff.value));
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const levels = computeLevels(topologyStore.calls, topologyStore.nodes, []);
|
||||
|
||||
topologyLayout.value = layout(levels, topologyStore.calls, radius);
|
||||
const levels = computeHierarchyLevels(props.nodes);
|
||||
topologyLayout.value = hierarchy(levels, props.calls, radius);
|
||||
graphWidth.value = topologyLayout.value.layout.width;
|
||||
graphHeight.value = topologyLayout.value.layout.height;
|
||||
const drag: any = d3.drag().on("drag", (d: { x: number; y: number }) => {
|
||||
moveNode(d);
|
||||
topologyLayout.value.calls = changeNode(d, currentNode.value, topologyLayout.value, radius);
|
||||
});
|
||||
setTimeout(() => {
|
||||
d3.selectAll(".topo-node").call(drag);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function moveNode(d: { x: number; y: number }) {
|
||||
if (!currentNode.value) {
|
||||
return;
|
||||
}
|
||||
for (const node of topologyLayout.value.nodes) {
|
||||
if (node.id === currentNode.value.id) {
|
||||
node.x = d.x;
|
||||
node.y = d.y;
|
||||
}
|
||||
}
|
||||
for (const call of topologyLayout.value.calls) {
|
||||
if (call.sourceObj.id === currentNode.value.id) {
|
||||
call.sourceObj.x = d.x;
|
||||
call.sourceObj.y = d.y;
|
||||
}
|
||||
if (call.targetObj.id === currentNode.value.id) {
|
||||
call.targetObj.x = d.x;
|
||||
call.targetObj.y = d.y;
|
||||
}
|
||||
if (call.targetObj.id === currentNode.value.id || call.sourceObj.id === currentNode.value.id) {
|
||||
const pos: any = circleIntersection(
|
||||
call.sourceObj.x,
|
||||
call.sourceObj.y,
|
||||
radius,
|
||||
call.targetObj.x,
|
||||
call.targetObj.y,
|
||||
radius,
|
||||
);
|
||||
call.sourceX = pos[0].x;
|
||||
call.sourceY = pos[0].y;
|
||||
call.targetX = pos[1].x;
|
||||
call.targetY = pos[1].y;
|
||||
}
|
||||
}
|
||||
topologyLayout.value.calls = computeCallPos(topologyLayout.value.calls, radius);
|
||||
}
|
||||
|
||||
function startMoveNode(event: MouseEvent, d: Node) {
|
||||
event.stopPropagation();
|
||||
currentNode.value = d;
|
||||
@ -349,396 +154,53 @@ limitations under the License. -->
|
||||
function stopMoveNode(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
currentNode.value = null;
|
||||
hideTip();
|
||||
}
|
||||
|
||||
function findMostFrequent(arr: Call[]) {
|
||||
let count: any = {};
|
||||
let maxCount = 0;
|
||||
let maxItem = null;
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let item = arr[i];
|
||||
count[item.sourceObj.id] = (count[item.sourceObj.id] || 0) + 1;
|
||||
if (count[item.sourceObj.id] > maxCount) {
|
||||
maxCount = count[item.sourceObj.id];
|
||||
maxItem = item.sourceObj;
|
||||
}
|
||||
count[item.targetObj.id] = (count[item.targetObj.id] || 0) + 1;
|
||||
if (count[item.targetObj.id] > maxCount) {
|
||||
maxCount = count[item.targetObj.id];
|
||||
maxItem = item.targetObj;
|
||||
}
|
||||
}
|
||||
|
||||
return maxItem;
|
||||
}
|
||||
|
||||
async function initLegendMetrics() {
|
||||
if (!topologyStore.nodes.length) {
|
||||
return;
|
||||
}
|
||||
if (settings.value.metricMode === MetricModes.Expression) {
|
||||
const expression = props.config.legendMQE && props.config.legendMQE.expression;
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
const { getExpressionQuery } = useQueryTopologyExpressionsProcessor([expression], topologyStore.nodes);
|
||||
const param = getExpressionQuery();
|
||||
const res = await topologyStore.getNodeExpressionValue(param);
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
} else {
|
||||
topologyStore.setLegendValues([expression], res.data);
|
||||
}
|
||||
} else {
|
||||
const names = props.config.legend.map((d: any) => d.name);
|
||||
if (!names.length) {
|
||||
return;
|
||||
}
|
||||
const ids = topologyStore.nodes.map((d: Node) => d.id);
|
||||
if (ids.length) {
|
||||
const param = await useQueryTopologyMetrics(names, ids);
|
||||
const res = await topologyStore.getLegendMetrics(param);
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeStatus(d: any) {
|
||||
const { legend, legendMQE } = settings.value;
|
||||
if (settings.value.metricMode === MetricModes.Expression) {
|
||||
if (!legendMQE) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
if (!legendMQE.expression) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
return Number(d[legendMQE.expression]) && d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||
}
|
||||
if (!legend) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
if (!legend.length) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
let c = true;
|
||||
for (const l of legend) {
|
||||
if (l.condition === "<") {
|
||||
c = c && d[l.name] < Number(l.value);
|
||||
} else {
|
||||
c = c && d[l.name] > Number(l.value);
|
||||
}
|
||||
}
|
||||
return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||
}
|
||||
function showNodeTip(event: MouseEvent, data: Node) {
|
||||
const nodeMetrics: string[] =
|
||||
(settings.value.metricMode === MetricModes.Expression
|
||||
? settings.value.nodeExpressions
|
||||
: settings.value.nodeMetrics) || [];
|
||||
const nodeMetricConfig = settings.value.nodeMetricConfig || [];
|
||||
const html = nodeMetrics.map((m, index) => {
|
||||
const metric =
|
||||
topologyStore.nodeMetricValue[m].values.find((val: { id: string; value: unknown }) => val.id === data.id) || {};
|
||||
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || "unknown"}</div>`;
|
||||
});
|
||||
const tipHtml = [
|
||||
`<div class="mb-5"><span class="grey">name: </span>${
|
||||
data.name
|
||||
}</div><div class="mb-5"><span class="grey">type: </span>${data.type || "UNKNOWN"}</div>`,
|
||||
...html,
|
||||
].join(" ");
|
||||
|
||||
tooltip.value
|
||||
.style("top", event.offsetY + 10 + "px")
|
||||
.style("left", event.offsetX + 10 + "px")
|
||||
.style("visibility", "visible")
|
||||
.html(tipHtml);
|
||||
}
|
||||
function showLinkTip(event: MouseEvent, data: Call) {
|
||||
const linkClientMetrics: string[] =
|
||||
settings.value.metricMode === MetricModes.Expression
|
||||
? settings.value.linkClientExpressions
|
||||
: settings.value.linkClientMetrics || [];
|
||||
const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
|
||||
const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
|
||||
const linkServerMetrics: string[] =
|
||||
settings.value.metricMode === MetricModes.Expression
|
||||
? settings.value.linkServerExpressions
|
||||
: settings.value.linkServerMetrics || [];
|
||||
const htmlServer = linkServerMetrics.map((m, index) => {
|
||||
const metric = topologyStore.linkServerMetrics[m].values.find(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||
);
|
||||
if (metric) {
|
||||
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||
}
|
||||
});
|
||||
const htmlClient = linkClientMetrics.map((m: string, index: number) => {
|
||||
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
|
||||
const metric = topologyStore.linkClientMetrics[m].values.find(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||
);
|
||||
if (metric) {
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||
}
|
||||
});
|
||||
const html = [
|
||||
...htmlServer,
|
||||
...htmlClient,
|
||||
`<div><span class="grey">${t("detectPoint")}:</span>${data.detectPoints.join(" | ")}</div>`,
|
||||
].join(" ");
|
||||
|
||||
tooltip.value
|
||||
.style("top", event.offsetY + "px")
|
||||
.style("left", event.offsetX + "px")
|
||||
.style("visibility", "visible")
|
||||
.html(html);
|
||||
emits("showNodeTip", event, data);
|
||||
}
|
||||
|
||||
function hideTip() {
|
||||
tooltip.value.style("visibility", "hidden");
|
||||
emits("hideTip");
|
||||
}
|
||||
|
||||
function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
|
||||
event.stopPropagation();
|
||||
hideTip();
|
||||
topologyStore.setNode(d);
|
||||
topologyStore.setLink(null);
|
||||
operationsPos.x = event.offsetX;
|
||||
operationsPos.y = event.offsetY;
|
||||
if (d.layer === String(dashboardStore.layerId)) {
|
||||
setNodeTools(settings.value.nodeDashboard);
|
||||
return;
|
||||
}
|
||||
items.value = [
|
||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||
{ id: "alerting", title: "Alerting", func: handleGoAlerting },
|
||||
];
|
||||
emits("handleNodeClick", event, d);
|
||||
}
|
||||
function handleLinkClick(event: MouseEvent, d: Call) {
|
||||
event.stopPropagation();
|
||||
if (d.sourceObj.layer !== dashboardStore.layerId || d.targetObj.layer !== dashboardStore.layerId) {
|
||||
return;
|
||||
}
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(d);
|
||||
if (!settings.value.linkDashboard) {
|
||||
return;
|
||||
}
|
||||
const origin = dashboardStore.entity;
|
||||
const e = dashboardStore.entity === EntityType[1].value ? EntityType[0].value : dashboardStore.entity;
|
||||
const { dashboard } = getDashboard({
|
||||
name: settings.value.linkDashboard,
|
||||
layer: dashboardStore.layerId,
|
||||
entity: `${e}Relation`,
|
||||
});
|
||||
if (!dashboard) {
|
||||
ElMessage.error(`The dashboard named ${settings.value.linkDashboard} doesn't exist`);
|
||||
return;
|
||||
}
|
||||
dashboardStore.setEntity(dashboard.entity);
|
||||
const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.sourceObj.id}/${d.targetObj.id}/${dashboard.name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
async function handleInspect() {
|
||||
const id = topologyStore.node.id;
|
||||
loading.value = true;
|
||||
const resp = await topologyStore.getDepthServiceTopology([id], Number(depth.value));
|
||||
loading.value = false;
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
await update();
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
}
|
||||
function handleGoEndpoint(name: string) {
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
function handleGoInstance(name: string) {
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[3].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
function handleGoDashboard(name: string) {
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
function handleGoAlerting() {
|
||||
const path = `/alerting`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
async function backToTopology() {
|
||||
loading.value = true;
|
||||
await freshNodes();
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
}
|
||||
async function getTopology() {
|
||||
const ids = selectorStore.services.map((d: Service) => d.id);
|
||||
const serviceIds = dashboardStore.entity === EntityType[0].value ? [selectorStore.currentService.id] : ids;
|
||||
const resp = await topologyStore.getDepthServiceTopology(serviceIds, Number(depth.value));
|
||||
return resp;
|
||||
}
|
||||
function setConfig() {
|
||||
showSetting.value = !showSetting.value;
|
||||
dashboardStore.selectWidget(props.config);
|
||||
}
|
||||
function resize() {
|
||||
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
|
||||
height: 40,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 40;
|
||||
width.value = dom.width;
|
||||
}
|
||||
function updateSettings(config: any) {
|
||||
settings.value = config;
|
||||
setNodeTools(config.nodeDashboard);
|
||||
}
|
||||
function setNodeTools(nodeDashboard: any) {
|
||||
items.value = [
|
||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||
{ id: "alerting", title: "Alerting", func: handleGoAlerting },
|
||||
];
|
||||
if (!(nodeDashboard && nodeDashboard.length)) {
|
||||
return;
|
||||
}
|
||||
for (const item of nodeDashboard) {
|
||||
if (item.scope === EntityType[0].value) {
|
||||
items.value.push({
|
||||
id: "dashboard",
|
||||
title: "Service Dashboard",
|
||||
func: handleGoDashboard,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
if (item.scope === EntityType[2].value) {
|
||||
items.value.push({
|
||||
id: "endpoint",
|
||||
title: "Endpoint Dashboard",
|
||||
func: handleGoEndpoint,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
if (item.scope === EntityType[3].value) {
|
||||
items.value.push({
|
||||
id: "instance",
|
||||
title: "Service Instance Dashboard",
|
||||
func: handleGoInstance,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function svgEvent() {
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
if (!props.config) {
|
||||
return;
|
||||
}
|
||||
dashboardStore.selectWidget(props.config);
|
||||
}
|
||||
|
||||
async function changeDepth(opt: Option[] | any) {
|
||||
depth.value = opt[0].value;
|
||||
freshNodes();
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resize);
|
||||
});
|
||||
watch(
|
||||
() => [selectorStore.currentService, selectorStore.currentDestService],
|
||||
(newVal, oldVal) => {
|
||||
if (oldVal[0].id === newVal[0].id && !oldVal[1]) {
|
||||
return;
|
||||
}
|
||||
if (oldVal[0].id === newVal[0].id && oldVal[1].id === newVal[1].id) {
|
||||
return;
|
||||
}
|
||||
freshNodes();
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => appStore.durationTime,
|
||||
() => [...props.calls, ...props.nodes],
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
freshNodes();
|
||||
if (!props.nodes.length) {
|
||||
return;
|
||||
}
|
||||
if (!props.calls.length) {
|
||||
return;
|
||||
}
|
||||
init();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.micro-topo-chart {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
margin-top: 30px;
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hierarchy-services-topo {
|
||||
.node-text {
|
||||
fill: var(--sw-topology-color);
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.svg-topology {
|
||||
.hierarchy-services-svg {
|
||||
cursor: move;
|
||||
background-color: $theme-background;
|
||||
}
|
||||
|
||||
.legend {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 25px;
|
||||
color: var(--sw-topology-color);
|
||||
|
||||
div {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right: 10px;
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
overflow: auto;
|
||||
padding: 0 15px;
|
||||
border-radius: 3px;
|
||||
color: $disabled-color;
|
||||
border: 1px solid $border-color-primary;
|
||||
background-color: var(--sw-topology-setting-bg);
|
||||
box-shadow: var(--sw-topology-box-shadow);
|
||||
transition: all 0.5ms linear;
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.label {
|
||||
@ -747,51 +209,12 @@ limitations under the License. -->
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.operations-list {
|
||||
position: absolute;
|
||||
color: $font-color;
|
||||
cursor: pointer;
|
||||
border: var(--sw-topology-border);
|
||||
border-radius: 3px;
|
||||
background-color: $theme-background;
|
||||
padding: 10px 0;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: left;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
color: $active-color;
|
||||
background-color: $popper-hover-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.tool {
|
||||
position: absolute;
|
||||
top: 35px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.switch-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.5ms linear;
|
||||
background: var(--sw-topology-switch-icon);
|
||||
color: $text-color;
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.topo-line {
|
||||
stroke-linecap: round;
|
||||
stroke-width: 1px;
|
||||
stroke-dasharray: 10 10;
|
||||
fill: none;
|
||||
animation: topo-dash 0.3s linear infinite;
|
||||
animation: var(--sw-topo-animation);
|
||||
}
|
||||
|
||||
.topo-line-anchor,
|
||||
@ -799,26 +222,11 @@ limitations under the License. -->
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@keyframes topo-dash {
|
||||
from {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-loading-spinner {
|
||||
top: 30%;
|
||||
}
|
||||
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
padding: 5px;
|
||||
border: var(--sw-topology-border);
|
||||
border-radius: 3px;
|
||||
background-color: $theme-background;
|
||||
.hierarchy-services {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@ -120,3 +120,165 @@ export function circleIntersection(ax: number, ay: number, ar: number, bx: numbe
|
||||
{ x: gx, y: gy },
|
||||
];
|
||||
}
|
||||
function findMostFrequent(arr: Call[]) {
|
||||
const count: any = {};
|
||||
let maxCount = 0;
|
||||
let maxItem = null;
|
||||
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const item = arr[i];
|
||||
count[item.sourceObj.id] = (count[item.sourceObj.id] || 0) + 1;
|
||||
if (count[item.sourceObj.id] > maxCount) {
|
||||
maxCount = count[item.sourceObj.id];
|
||||
maxItem = item.sourceObj;
|
||||
}
|
||||
count[item.targetObj.id] = (count[item.targetObj.id] || 0) + 1;
|
||||
if (count[item.targetObj.id] > maxCount) {
|
||||
maxCount = count[item.targetObj.id];
|
||||
maxItem = item.targetObj;
|
||||
}
|
||||
}
|
||||
|
||||
return maxItem;
|
||||
}
|
||||
export function computeLevels(calls: Call[], nodeList: Node[], levels: any[]) {
|
||||
const node = findMostFrequent(calls);
|
||||
const nodes = JSON.parse(JSON.stringify(nodeList)).sort((a: Node, b: Node) => {
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const index = nodes.findIndex((n: Node) => n.type === "USER");
|
||||
let key = index;
|
||||
if (index < 0) {
|
||||
key = nodes.findIndex((n: Node) => n.id === node.id);
|
||||
}
|
||||
levels.push([nodes[key]]);
|
||||
nodes.splice(key, 1);
|
||||
for (const level of levels) {
|
||||
const a = [];
|
||||
for (const l of level) {
|
||||
for (const n of calls) {
|
||||
if (n.target === l.id) {
|
||||
const i = nodes.findIndex((d: Node) => d.id === n.source);
|
||||
if (i > -1) {
|
||||
a.push(nodes[i]);
|
||||
nodes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (n.source === l.id) {
|
||||
const i = nodes.findIndex((d: Node) => d.id === n.target);
|
||||
if (i > -1) {
|
||||
a.push(nodes[i]);
|
||||
nodes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.length) {
|
||||
levels.push(a);
|
||||
}
|
||||
}
|
||||
if (nodes.length) {
|
||||
const ids = nodes.map((d: Node) => d.id);
|
||||
const links = calls.filter((item: Call) => ids.includes(item.source) || ids.includes(item.target));
|
||||
const list = computeLevels(links, nodes, []);
|
||||
levels = list.map((subArrayA, index) => subArrayA.concat(levels[index]));
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
export function changeNode(d: { x: number; y: number }, currentNode: Nullable<Node>, layout: any, radius: number) {
|
||||
if (!currentNode) {
|
||||
return;
|
||||
}
|
||||
for (const node of layout.nodes) {
|
||||
if (node.id === currentNode.id) {
|
||||
node.x = d.x;
|
||||
node.y = d.y;
|
||||
}
|
||||
}
|
||||
for (const call of layout.calls) {
|
||||
if (call.sourceObj.id === currentNode.id) {
|
||||
call.sourceObj.x = d.x;
|
||||
call.sourceObj.y = d.y;
|
||||
}
|
||||
if (call.targetObj.id === currentNode.id) {
|
||||
call.targetObj.x = d.x;
|
||||
call.targetObj.y = d.y;
|
||||
}
|
||||
if (call.targetObj.id === currentNode.id || call.sourceObj.id === currentNode.id) {
|
||||
const pos: any = circleIntersection(
|
||||
call.sourceObj.x,
|
||||
call.sourceObj.y,
|
||||
radius,
|
||||
call.targetObj.x,
|
||||
call.targetObj.y,
|
||||
radius,
|
||||
);
|
||||
call.sourceX = pos[0].x;
|
||||
call.sourceY = pos[0].y;
|
||||
call.targetX = pos[1].x;
|
||||
call.targetY = pos[1].y;
|
||||
}
|
||||
}
|
||||
return computeCallPos(layout.calls, radius);
|
||||
}
|
||||
export function hierarchy(levels: Node[][], calls: Call[], radius: number) {
|
||||
// precompute level depth
|
||||
levels.forEach((l: Node[], i: number) => l.forEach((n: any) => n && (n.level = i)));
|
||||
|
||||
const nodes: Node[] = levels.reduce((a, x) => a.concat(x), []);
|
||||
// layout
|
||||
const padding = 30;
|
||||
const node_height = 100;
|
||||
const node_width = 180;
|
||||
const bundle_width = 10;
|
||||
const metro_d = 4;
|
||||
for (const n of nodes) {
|
||||
n.width = 5 * metro_d;
|
||||
}
|
||||
|
||||
let y_offset = padding;
|
||||
let x_offset = 0;
|
||||
for (const level of levels) {
|
||||
x_offset = 0;
|
||||
y_offset += 3 * bundle_width;
|
||||
for (const l of level) {
|
||||
const n: any = l;
|
||||
for (const call of calls) {
|
||||
if (call.source === n.id) {
|
||||
call.sourceObj = n;
|
||||
}
|
||||
if (call.target === n.id) {
|
||||
call.targetObj = n;
|
||||
}
|
||||
}
|
||||
n.x = node_width + x_offset + n.width / 2;
|
||||
n.y = n.level * node_height + y_offset;
|
||||
x_offset += node_width + n.width;
|
||||
}
|
||||
}
|
||||
const layout = {
|
||||
width: d3.max(nodes as any, (n: { x: number }) => n.x) || 0 + node_width + 2 * padding,
|
||||
height: d3.max(nodes as any, (n: { y: number }) => n.y) || 0 + node_height / 2 + 2 * padding,
|
||||
};
|
||||
|
||||
return { nodes, layout, calls: computeCallPos(calls, radius) };
|
||||
}
|
||||
export function computeHierarchyLevels(nodes: Node[]) {
|
||||
const levelsNum: number[] = nodes.map((d: Node) => d.l || 0);
|
||||
const list = [...new Set(levelsNum)];
|
||||
const sortedArr = list.sort((a, b) => b - a);
|
||||
const nodesList = [];
|
||||
|
||||
for (const min of sortedArr) {
|
||||
const arr = nodes.filter((d) => d.l === min);
|
||||
nodesList.push(arr);
|
||||
}
|
||||
|
||||
return nodesList;
|
||||
}
|
||||
|
@ -15,9 +15,12 @@ limitations under the License. -->
|
||||
<template>
|
||||
<div class="config-panel">
|
||||
<div class="item mb-10">
|
||||
<span class="label">{{
|
||||
t(dashboardStore.selectedGrid.metricMode === MetricModes.General ? "metrics" : "expressions")
|
||||
}}</span>
|
||||
<span class="label" v-if="type === 'hierarchyServicesConfig'">
|
||||
{{ t("expressions") }}
|
||||
</span>
|
||||
<span class="label" v-else>
|
||||
{{ t(dashboardStore.selectedGrid.metricMode === MetricModes.General ? "metrics" : "expressions") }}
|
||||
</span>
|
||||
<SelectSingle :value="currentMetric" :options="metricList" @change="changeMetric" class="selectors" />
|
||||
</div>
|
||||
<div class="item mb-10">
|
||||
@ -40,7 +43,10 @@ limitations under the License. -->
|
||||
@change="changeConfigs({ label: currentConfig.label })"
|
||||
/>
|
||||
</div>
|
||||
<div class="item mb-10" v-if="dashboardStore.selectedGrid.metricMode === MetricModes.General">
|
||||
<div
|
||||
class="item mb-10"
|
||||
v-if="type !== 'hierarchyServicesConfig' && dashboardStore.selectedGrid.metricMode === MetricModes.General"
|
||||
>
|
||||
<span class="label">{{ t("aggregation") }}</span>
|
||||
<SelectSingle
|
||||
:value="currentConfig.calculation"
|
||||
@ -55,20 +61,27 @@ limitations under the License. -->
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { CalculationOpts, MetricModes } from "../../../data";
|
||||
import type { Option } from "@/types/app";
|
||||
import { CalculationOpts, MetricModes, EntityType, ConfigFieldTypes } from "@/views/dashboard/data";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import type { Option } from "element-plus/es/components/select-v2/src/select.types";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
|
||||
/*global defineEmits, defineProps */
|
||||
const props = defineProps({
|
||||
type: { type: String, default: "" },
|
||||
isExpression: { type: Boolean, default: true },
|
||||
layer: { type: String, default: "" },
|
||||
expressions: { type: Array<string>, default: () => [] },
|
||||
entity: { type: String, default: EntityType[0].value },
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const emit = defineEmits(["update"]);
|
||||
const dashboardStore = useDashboardStore();
|
||||
const getMetrics = computed(() => {
|
||||
let metrics = [];
|
||||
if (props.type === "hierarchyServicesConfig") {
|
||||
return props.expressions || [];
|
||||
}
|
||||
let metrics: string[] = [];
|
||||
const {
|
||||
linkServerExpressions,
|
||||
linkServerMetrics,
|
||||
@ -104,6 +117,16 @@ limitations under the License. -->
|
||||
});
|
||||
const currentIndex = ref<number>(0);
|
||||
const getMetricConfig = computed(() => {
|
||||
if (props.type === "hierarchyServicesConfig") {
|
||||
const { dashboard } = getDashboard(
|
||||
{
|
||||
layer: props.layer || "",
|
||||
entity: props.entity,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
);
|
||||
return (dashboard && dashboard.expressionsConfig) || [];
|
||||
}
|
||||
let config = [];
|
||||
|
||||
switch (props.type) {
|
||||
@ -143,7 +166,7 @@ limitations under the License. -->
|
||||
};
|
||||
}
|
||||
watch(
|
||||
() => props.type,
|
||||
() => [props.type, ...props.expressions],
|
||||
() => {
|
||||
currentMetric.value = metricList.value[0].value;
|
||||
const config = getMetricConfig.value || [];
|
@ -24,7 +24,7 @@ limitations under the License. -->
|
||||
@change="changeMetricMode"
|
||||
/>
|
||||
</div>
|
||||
<div class="link-settings">
|
||||
<div class="mb-20">
|
||||
<h5 class="title">{{ t("callSettings") }}</h5>
|
||||
<div class="label">{{ t("linkDashboard") }}</div>
|
||||
<Selector
|
||||
@ -57,7 +57,7 @@ limitations under the License. -->
|
||||
:tags="states.linkServerExpressions"
|
||||
:vertical="true"
|
||||
:text="t('addExpressions')"
|
||||
@change="(param) => changeLinkServerExpressions(param)"
|
||||
@change="(param: string[]) => changeLinkServerExpressions(param)"
|
||||
/>
|
||||
</div>
|
||||
<Selector
|
||||
@ -92,7 +92,7 @@ limitations under the License. -->
|
||||
:tags="states.linkClientExpressions"
|
||||
:vertical="true"
|
||||
:text="t('addExpressions')"
|
||||
@change="(param) => changeLinkClientExpressions(param)"
|
||||
@change="(param: string[]) => changeLinkClientExpressions(param)"
|
||||
/>
|
||||
</div>
|
||||
<Selector
|
||||
@ -107,7 +107,7 @@ limitations under the License. -->
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-settings">
|
||||
<div>
|
||||
<h5 class="title">{{ t("nodeSettings") }}</h5>
|
||||
<div class="label">{{ t("nodeDashboard") }}</div>
|
||||
<Selector
|
||||
@ -168,7 +168,7 @@ limitations under the License. -->
|
||||
:tags="states.nodeExpressions"
|
||||
:vertical="true"
|
||||
:text="t('addExpressions')"
|
||||
@change="(param) => changeNodeExpressions(param)"
|
||||
@change="(param: string[]) => changeNodeExpressions(param)"
|
||||
/>
|
||||
</div>
|
||||
<Selector
|
||||
@ -182,7 +182,7 @@ limitations under the License. -->
|
||||
@change="updateNodeMetrics"
|
||||
/>
|
||||
</div>
|
||||
<div class="legend-settings" v-show="isService">
|
||||
<div v-show="isService">
|
||||
<h5 class="title">{{ t("legendSettings") }}</h5>
|
||||
<span v-if="isExpression">
|
||||
<div class="label">Healthy Description</div>
|
||||
@ -246,7 +246,7 @@ limitations under the License. -->
|
||||
</span>
|
||||
<div class="label">Unhealthy Description</div>
|
||||
<el-input v-model="description.unhealthy" placeholder="Please input description" size="small" class="mt-5" />
|
||||
<el-button @click="setLegend" class="legend-btn" size="small" type="primary">
|
||||
<el-button @click="setLegend" class="mt-20" size="small" type="primary">
|
||||
{{ t("setLegend") }}
|
||||
</el-button>
|
||||
</div>
|
||||
@ -266,7 +266,7 @@ limitations under the License. -->
|
||||
MetricsType,
|
||||
MetricModes,
|
||||
CallTypes,
|
||||
} from "../../../data";
|
||||
} from "@/views/dashboard/data";
|
||||
import type { Option } from "@/types/app";
|
||||
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
|
||||
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
|
||||
@ -604,10 +604,6 @@ limitations under the License. -->
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.link-settings {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
margin-top: 8px;
|
||||
width: 355px;
|
||||
@ -640,7 +636,6 @@ limitations under the License. -->
|
||||
}
|
||||
|
||||
.legend-btn {
|
||||
margin: 20px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
188
src/views/dashboard/related/topology/pod/InstanceMap.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<!-- 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
|
||||
class="hierarchy-services-topo"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(0, 0, 0, 0)"
|
||||
:style="`height: ${height}px`"
|
||||
>
|
||||
<Graph
|
||||
:nodes="topologyStore.hierarchyInstanceNodes"
|
||||
:calls="topologyStore.hierarchyInstanceCalls"
|
||||
:entity="EntityType[3].value"
|
||||
@getNodeMetrics="getNodeMetrics"
|
||||
@showNodeTip="showNodeTip"
|
||||
@handleNodeClick="handleNodeClick"
|
||||
@hideTip="hideTip"
|
||||
/>
|
||||
<div id="popover"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, nextTick } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import type { Node } from "@/types/topology";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { EntityType, ConfigFieldTypes } from "@/views/dashboard/data";
|
||||
import router from "@/router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { MetricConfigOpt } from "@/types/dashboard";
|
||||
import { aggregation } from "@/hooks/useMetricsProcessor";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import Graph from "../components/Graph.vue";
|
||||
|
||||
/*global Nullable*/
|
||||
const topologyStore = useTopologyStore();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const height = ref<number>(100);
|
||||
const loading = ref<boolean>(false);
|
||||
const popover = ref<Nullable<any>>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
init();
|
||||
});
|
||||
|
||||
getTopology();
|
||||
|
||||
async function init() {
|
||||
const dom = document.querySelector(".hierarchy-related")?.getBoundingClientRect() || {
|
||||
height: 80,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 80;
|
||||
popover.value = d3.select("#popover");
|
||||
}
|
||||
|
||||
async function getTopology() {
|
||||
loading.value = true;
|
||||
const resp = await topologyStore.getHierarchyInstanceTopology();
|
||||
loading.value = false;
|
||||
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNodeMetrics() {
|
||||
const layerList = [];
|
||||
const layerMap = new Map();
|
||||
for (const n of topologyStore.hierarchyInstanceNodes) {
|
||||
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[3].value,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
);
|
||||
const exp = (dashboard && dashboard.expressions) || [];
|
||||
await topologyStore.queryHierarchyInstanceNodeExpressions(exp, list[0].layer);
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeTip(event: MouseEvent, data: Node) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (!popover.value) {
|
||||
return;
|
||||
}
|
||||
const dashboard =
|
||||
getDashboard(
|
||||
{
|
||||
layer: data.layer || "",
|
||||
entity: EntityType[3].value,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
).dashboard || {};
|
||||
const exprssions = dashboard.expressions || [];
|
||||
const nodeMetricConfig = dashboard.expressionsConfig || [];
|
||||
const html = exprssions.map((m: string, index: number) => {
|
||||
const metric =
|
||||
topologyStore.hierarchyInstanceNodeMetrics[data.layer || ""][m].values.find(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||
) || {};
|
||||
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||
});
|
||||
const tipHtml = [
|
||||
`<div class="mb-5"><span class="grey">name: </span>${data.name}</div><div class="mb-5"><span class="grey">layer: </span>${data.layer}</div>`,
|
||||
...html,
|
||||
].join(" ");
|
||||
|
||||
popover.value
|
||||
.style("top", event.offsetY + 10 + "px")
|
||||
.style("left", event.offsetX + 10 + "px")
|
||||
.style("visibility", "visible")
|
||||
.html(tipHtml);
|
||||
}
|
||||
|
||||
function hideTip() {
|
||||
popover.value.style("visibility", "hidden");
|
||||
}
|
||||
|
||||
function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
|
||||
const origin = dashboardStore.entity;
|
||||
event.stopPropagation();
|
||||
hideTip();
|
||||
const dashboard =
|
||||
getDashboard(
|
||||
{
|
||||
layer: d.layer || "",
|
||||
entity: EntityType[3].value,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
).dashboard || {};
|
||||
const name = dashboard.name;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.hierarchy-services-topo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.el-loading-spinner {
|
||||
top: 30%;
|
||||
}
|
||||
|
||||
#popover {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
padding: 5px;
|
||||
border: var(--sw-topology-border);
|
||||
border-radius: 3px;
|
||||
background-color: $theme-background;
|
||||
}
|
||||
</style>
|
@ -69,10 +69,10 @@ limitations under the License. -->
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { EntityType, DepthList, MetricModes, CallTypes } from "../../../data";
|
||||
import { EntityType, DepthList, MetricModes, CallTypes } from "@/views/dashboard/data";
|
||||
import { ElMessage } from "element-plus";
|
||||
import Sankey from "./Sankey.vue";
|
||||
import Settings from "./Settings.vue";
|
||||
import Settings from "../config/Settings.vue";
|
||||
import router from "@/router";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
|
@ -23,7 +23,7 @@ limitations under the License. -->
|
||||
import type { Node, Call } from "@/types/topology";
|
||||
import type { MetricConfigOpt } from "@/types/dashboard";
|
||||
import { aggregation } from "@/hooks/useMetricsProcessor";
|
||||
import { MetricModes } from "../../../data";
|
||||
import { MetricModes } from "@/views/dashboard/data";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { Themes } from "@/constants/data";
|
||||
|
196
src/views/dashboard/related/topology/service/HierarchyMap.vue
Normal file
@ -0,0 +1,196 @@
|
||||
<!-- 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
|
||||
class="hierarchy-services-topo"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(0, 0, 0, 0)"
|
||||
:style="`height: ${height}px`"
|
||||
>
|
||||
<Graph
|
||||
:config="config"
|
||||
:nodes="topologyStore.hierarchyServiceNodes"
|
||||
:calls="topologyStore.hierarchyServiceCalls"
|
||||
:entity="EntityType[0].value"
|
||||
@getNodeMetrics="getNodeMetrics"
|
||||
@showNodeTip="showNodeTip"
|
||||
@handleNodeClick="handleNodeClick"
|
||||
@hideTip="hideTip"
|
||||
/>
|
||||
<div id="popover"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { ref, onMounted, nextTick } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import type { Node } from "@/types/topology";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { EntityType, ConfigFieldTypes } from "@/views/dashboard/data";
|
||||
import router from "@/router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { MetricConfigOpt } from "@/types/dashboard";
|
||||
import { aggregation } from "@/hooks/useMetricsProcessor";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import Graph from "../components/Graph.vue";
|
||||
|
||||
/*global Nullable, defineProps */
|
||||
defineProps({
|
||||
config: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const topologyStore = useTopologyStore();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const height = ref<number>(100);
|
||||
const loading = ref<boolean>(false);
|
||||
const popover = ref<Nullable<any>>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
init();
|
||||
});
|
||||
|
||||
getTopology();
|
||||
|
||||
async function init() {
|
||||
const dom = document.querySelector(".hierarchy-related")?.getBoundingClientRect() || {
|
||||
height: 80,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 80;
|
||||
popover.value = d3.select("#popover");
|
||||
}
|
||||
|
||||
async function getTopology() {
|
||||
loading.value = true;
|
||||
const resp = await topologyStore.getHierarchyServiceTopology();
|
||||
loading.value = false;
|
||||
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNodeMetrics() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function showNodeTip(event: MouseEvent, data: Node) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (!popover.value) {
|
||||
return;
|
||||
}
|
||||
const dashboard =
|
||||
getDashboard(
|
||||
{
|
||||
layer: data.layer || "",
|
||||
entity: EntityType[0].value,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
).dashboard || {};
|
||||
const exprssions = dashboard.expressions || [];
|
||||
const nodeMetricConfig = dashboard.expressionsConfig || [];
|
||||
const html = exprssions.map((m: string, index: number) => {
|
||||
const metric =
|
||||
topologyStore.hierarchyNodeMetrics[data.layer || ""][m].values.find(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||
) || {};
|
||||
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||
});
|
||||
const tipHtml = [
|
||||
`<div class="mb-5"><span class="grey">name: </span>${data.name}</div><div class="mb-5"><span class="grey">layer: </span>${data.layer}</div>`,
|
||||
...html,
|
||||
].join(" ");
|
||||
|
||||
popover.value
|
||||
.style("top", event.offsetY + 10 + "px")
|
||||
.style("left", event.offsetX + 10 + "px")
|
||||
.style("visibility", "visible")
|
||||
.html(tipHtml);
|
||||
}
|
||||
|
||||
function hideTip() {
|
||||
popover.value.style("visibility", "hidden");
|
||||
}
|
||||
|
||||
function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
|
||||
const origin = dashboardStore.entity;
|
||||
event.stopPropagation();
|
||||
hideTip();
|
||||
const dashboard =
|
||||
getDashboard(
|
||||
{
|
||||
layer: d.layer || "",
|
||||
entity: EntityType[0].value,
|
||||
},
|
||||
ConfigFieldTypes.ISDEFAULT,
|
||||
).dashboard || {};
|
||||
const name = dashboard.name;
|
||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[0].value}/${topologyStore.node.id}/${name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.hierarchy-services-topo {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.el-loading-spinner {
|
||||
top: 30%;
|
||||
}
|
||||
|
||||
#popover {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
padding: 5px;
|
||||
border: var(--sw-topology-border);
|
||||
border-radius: 3px;
|
||||
background-color: $theme-background;
|
||||
}
|
||||
</style>
|
731
src/views/dashboard/related/topology/service/ServiceMap.vue
Normal file
@ -0,0 +1,731 @@
|
||||
<!-- 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"
|
||||
v-loading="loading"
|
||||
element-loading-background="rgba(0, 0, 0, 0)"
|
||||
:style="`height: ${height}px`"
|
||||
>
|
||||
<svg class="svg-topology" :width="width - 100" :height="height" @click="svgEvent">
|
||||
<g class="svg-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)"
|
||||
/>
|
||||
<circle
|
||||
class="topo-line-anchor"
|
||||
:cx="(l.sourceX + l.targetX) / 2"
|
||||
:cy="(l.sourceY + l.targetY) / 2"
|
||||
r="4"
|
||||
fill="#97B0F8"
|
||||
@click="handleLinkClick($event, l)"
|
||||
@mouseover="showLinkTip($event, l)"
|
||||
@mouseout="hideTip"
|
||||
/>
|
||||
</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>
|
||||
<div id="tooltip"></div>
|
||||
<div class="legend">
|
||||
<div>
|
||||
<img :src="icons.CUBE" />
|
||||
<span>
|
||||
{{ settings.description ? settings.description.healthy || "" : "" }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<img :src="icons.CUBEERROR" />
|
||||
<span>
|
||||
{{ settings.description ? settings.description.unhealthy || "" : "" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting" v-if="showSetting && dashboardStore.editMode">
|
||||
<Settings @update="updateSettings" @updateNodes="freshNodes" />
|
||||
</div>
|
||||
<div class="tool">
|
||||
<span v-show="graphConfig.showDepth">
|
||||
<span class="label">{{ t("currentDepth") }}</span>
|
||||
<Selector class="inputs" :value="depth" :options="DepthList" @change="changeDepth" />
|
||||
</span>
|
||||
<span class="switch-icon ml-5" title="Settings" @click="setConfig" v-if="dashboardStore.editMode">
|
||||
<Icon size="middle" iconName="settings" />
|
||||
</span>
|
||||
<span class="switch-icon ml-5" title="Back to overview topology" @click="backToTopology">
|
||||
<Icon size="middle" iconName="keyboard_backspace" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="operations-list"
|
||||
v-if="topologyStore.node"
|
||||
:style="{
|
||||
top: operationsPos.y + 5 + 'px',
|
||||
left: operationsPos.x + 5 + 'px',
|
||||
}"
|
||||
>
|
||||
<span v-for="(item, index) of items" :key="index" @click="item.func(item.dashboard)">
|
||||
{{ item.title }}
|
||||
</span>
|
||||
</div>
|
||||
<el-dialog v-model="hierarchyRelated" :destroy-on-close="true" @closed="hierarchyRelated = false" width="640px">
|
||||
<div class="hierarchy-related">
|
||||
<hierarchy-map :config="config" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { ref, onMounted, onBeforeUnmount, reactive, watch, computed, nextTick } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as d3 from "d3";
|
||||
import type { Node, Call } from "@/types/topology";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { useTopologyStore } from "@/store/modules/topology";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import { EntityType, DepthList, MetricModes, CallTypes } from "@/views/dashboard/data";
|
||||
import router from "@/router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import Settings from "../config/Settings.vue";
|
||||
import HierarchyMap from "./HierarchyMap.vue";
|
||||
import type { Option } from "@/types/app";
|
||||
import type { Service } from "@/types/selector";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import getDashboard from "@/hooks/useDashboardsSession";
|
||||
import type { MetricConfigOpt } from "@/types/dashboard";
|
||||
import { aggregation } from "@/hooks/useMetricsProcessor";
|
||||
import icons from "@/assets/img/icons";
|
||||
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
|
||||
import { layout, computeLevels, changeNode } from "../components/utils/layout";
|
||||
import zoom from "@/views/dashboard/related/components/utils/zoom";
|
||||
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
|
||||
/*global Nullable, defineProps */
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({ graph: {} }),
|
||||
},
|
||||
});
|
||||
const { t } = useI18n();
|
||||
const selectorStore = useSelectorStore();
|
||||
const topologyStore = useTopologyStore();
|
||||
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 chart = ref<Nullable<HTMLDivElement>>(null);
|
||||
const showSetting = ref<boolean>(false);
|
||||
const settings = ref<any>(props.config);
|
||||
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
|
||||
const items = ref<{ id: string; title: string; func: any; dashboard?: string }[]>([]);
|
||||
const graphConfig = computed(() => props.config.graph || {});
|
||||
const depth = ref<number>(graphConfig.value.depth || 2);
|
||||
const topologyLayout = ref<any>({});
|
||||
const tooltip = ref<Nullable<any>>(null);
|
||||
const graphWidth = ref<number>(100);
|
||||
const currentNode = ref<Nullable<Node>>(null);
|
||||
const diff = computed(() => [(width.value - graphWidth.value - 130) / 2, 100]);
|
||||
const hierarchyRelated = ref<boolean>(false);
|
||||
const radius = 8;
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
init();
|
||||
}, 10);
|
||||
});
|
||||
async function init() {
|
||||
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
|
||||
height: 40,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 40;
|
||||
width.value = dom.width;
|
||||
svg.value = d3.select(".svg-topology");
|
||||
graph.value = d3.select(".svg-graph");
|
||||
loading.value = true;
|
||||
const json = await selectorStore.fetchServices(dashboardStore.layerId);
|
||||
if (json.errors) {
|
||||
ElMessage.error(json.errors);
|
||||
return;
|
||||
}
|
||||
await freshNodes();
|
||||
svg.value.call(zoom(d3, graph.value, diff.value));
|
||||
}
|
||||
async function freshNodes() {
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
const resp = await getTopology();
|
||||
loading.value = false;
|
||||
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
await update();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
if (settings.value.metricMode === MetricModes.Expression) {
|
||||
topologyStore.queryNodeExpressions(settings.value.nodeExpressions || []);
|
||||
topologyStore.getLinkExpressions(settings.value.linkClientExpressions || [], CallTypes.Client);
|
||||
topologyStore.getLinkExpressions(settings.value.linkServerExpressions || [], CallTypes.Server);
|
||||
} else {
|
||||
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
|
||||
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
|
||||
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
|
||||
}
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
await initLegendMetrics();
|
||||
draw();
|
||||
tooltip.value = d3.select("#tooltip");
|
||||
setNodeTools(settings.value.nodeDashboard);
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const levels = computeLevels(topologyStore.calls, topologyStore.nodes, []);
|
||||
|
||||
topologyLayout.value = layout(levels, topologyStore.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;
|
||||
}
|
||||
|
||||
async function initLegendMetrics() {
|
||||
if (!topologyStore.nodes.length) {
|
||||
return;
|
||||
}
|
||||
if (settings.value.metricMode === MetricModes.Expression) {
|
||||
const expression = props.config.legendMQE && props.config.legendMQE.expression;
|
||||
if (!expression) {
|
||||
return;
|
||||
}
|
||||
const { getExpressionQuery } = useQueryTopologyExpressionsProcessor([expression], topologyStore.nodes);
|
||||
const param = getExpressionQuery();
|
||||
const res = await topologyStore.getNodeExpressionValue(param);
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
} else {
|
||||
topologyStore.setLegendValues([expression], res.data);
|
||||
}
|
||||
} else {
|
||||
const names = props.config.legend.map((d: any) => d.name);
|
||||
if (!names.length) {
|
||||
return;
|
||||
}
|
||||
const ids = topologyStore.nodes.map((d: Node) => d.id);
|
||||
if (ids.length) {
|
||||
const param = await useQueryTopologyMetrics(names, ids);
|
||||
const res = await topologyStore.getLegendMetrics(param);
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeStatus(d: any) {
|
||||
const { legend, legendMQE } = settings.value;
|
||||
if (settings.value.metricMode === MetricModes.Expression) {
|
||||
if (!legendMQE) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
if (!legendMQE.expression) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
return Number(d[legendMQE.expression]) && d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||
}
|
||||
if (!legend) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
if (!legend.length) {
|
||||
return icons.CUBE;
|
||||
}
|
||||
let c = true;
|
||||
for (const l of legend) {
|
||||
if (l.condition === "<") {
|
||||
c = c && d[l.name] < Number(l.value);
|
||||
} else {
|
||||
c = c && d[l.name] > Number(l.value);
|
||||
}
|
||||
}
|
||||
return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||
}
|
||||
function showNodeTip(event: MouseEvent, data: Node) {
|
||||
const nodeMetrics: string[] =
|
||||
(settings.value.metricMode === MetricModes.Expression
|
||||
? settings.value.nodeExpressions
|
||||
: settings.value.nodeMetrics) || [];
|
||||
const nodeMetricConfig = settings.value.nodeMetricConfig || [];
|
||||
const html = nodeMetrics.map((m, index) => {
|
||||
const metric =
|
||||
(topologyStore.nodeMetricValue[m] &&
|
||||
topologyStore.nodeMetricValue[m].values.find((val: { id: string; value: unknown }) => val.id === data.id)) ||
|
||||
{};
|
||||
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || "unknown"}</div>`;
|
||||
});
|
||||
const tipHtml = [
|
||||
`<div class="mb-5"><span class="grey">name: </span>${
|
||||
data.name
|
||||
}</div><div class="mb-5"><span class="grey">type: </span>${data.type || "UNKNOWN"}</div>`,
|
||||
...html,
|
||||
].join(" ");
|
||||
|
||||
tooltip.value
|
||||
.style("top", event.offsetY + 10 + "px")
|
||||
.style("left", event.offsetX + 10 + "px")
|
||||
.style("visibility", "visible")
|
||||
.html(tipHtml);
|
||||
}
|
||||
function showLinkTip(event: MouseEvent, data: Call) {
|
||||
const linkClientMetrics: string[] =
|
||||
settings.value.metricMode === MetricModes.Expression
|
||||
? settings.value.linkClientExpressions
|
||||
: settings.value.linkClientMetrics || [];
|
||||
const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
|
||||
const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
|
||||
const linkServerMetrics: string[] =
|
||||
settings.value.metricMode === MetricModes.Expression
|
||||
? settings.value.linkServerExpressions
|
||||
: settings.value.linkServerMetrics || [];
|
||||
const htmlServer = linkServerMetrics.map((m, index) => {
|
||||
const metric = topologyStore.linkServerMetrics[m].values.find(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||
);
|
||||
if (metric) {
|
||||
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||
}
|
||||
});
|
||||
const htmlClient = linkClientMetrics.map((m: string, index: number) => {
|
||||
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
|
||||
const metric = topologyStore.linkClientMetrics[m].values.find(
|
||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||
);
|
||||
if (metric) {
|
||||
const v = aggregation(metric.value, opt);
|
||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||
}
|
||||
});
|
||||
const html = [
|
||||
...htmlServer,
|
||||
...htmlClient,
|
||||
`<div><span class="grey">${t("detectPoint")}:</span>${data.detectPoints.join(" | ")}</div>`,
|
||||
].join(" ");
|
||||
|
||||
tooltip.value
|
||||
.style("top", event.offsetY + "px")
|
||||
.style("left", event.offsetX + "px")
|
||||
.style("visibility", "visible")
|
||||
.html(html);
|
||||
}
|
||||
|
||||
function hideTip() {
|
||||
tooltip.value.style("visibility", "hidden");
|
||||
}
|
||||
function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
|
||||
event.stopPropagation();
|
||||
hideTip();
|
||||
topologyStore.setNode(d);
|
||||
topologyStore.setLink(null);
|
||||
operationsPos.x = event.offsetX;
|
||||
operationsPos.y = event.offsetY;
|
||||
if (d.layer === String(dashboardStore.layerId)) {
|
||||
setNodeTools(settings.value.nodeDashboard);
|
||||
return;
|
||||
}
|
||||
items.value = [
|
||||
{ id: "hierarchyServices", title: "Hierarchy Services", func: handleHierarchyRelatedServices },
|
||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||
{ id: "alerting", title: "Alerting", func: handleGoAlerting },
|
||||
];
|
||||
}
|
||||
function handleLinkClick(event: MouseEvent, d: Call) {
|
||||
event.stopPropagation();
|
||||
if (d.sourceObj.layer !== dashboardStore.layerId || d.targetObj.layer !== dashboardStore.layerId) {
|
||||
return;
|
||||
}
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(d);
|
||||
if (!settings.value.linkDashboard) {
|
||||
return;
|
||||
}
|
||||
const origin = dashboardStore.entity;
|
||||
const e = dashboardStore.entity === EntityType[1].value ? EntityType[0].value : dashboardStore.entity;
|
||||
const { dashboard } = getDashboard({
|
||||
name: settings.value.linkDashboard,
|
||||
layer: dashboardStore.layerId,
|
||||
entity: `${e}Relation`,
|
||||
});
|
||||
if (!dashboard) {
|
||||
ElMessage.error(`The dashboard named ${settings.value.linkDashboard} doesn't exist`);
|
||||
return;
|
||||
}
|
||||
dashboardStore.setEntity(dashboard.entity);
|
||||
const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.sourceObj.id}/${d.targetObj.id}/${dashboard.name}`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
async function handleHierarchyRelatedServices() {
|
||||
hierarchyRelated.value = true;
|
||||
}
|
||||
async function handleInspect() {
|
||||
const id = topologyStore.node.id;
|
||||
loading.value = true;
|
||||
const resp = await topologyStore.getDepthServiceTopology([id], Number(depth.value));
|
||||
loading.value = false;
|
||||
if (resp && resp.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
await update();
|
||||
topologyStore.setNode(null);
|
||||
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 });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
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 });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
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 });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
dashboardStore.setEntity(origin);
|
||||
}
|
||||
function handleGoAlerting() {
|
||||
const path = `/alerting`;
|
||||
const routeUrl = router.resolve({ path });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
async function backToTopology() {
|
||||
loading.value = true;
|
||||
await freshNodes();
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
}
|
||||
async function getTopology() {
|
||||
const ids = selectorStore.services.map((d: Service) => d.id);
|
||||
const serviceIds = dashboardStore.entity === EntityType[0].value ? [selectorStore.currentService.id] : ids;
|
||||
const resp = await topologyStore.getDepthServiceTopology(serviceIds, Number(depth.value));
|
||||
return resp;
|
||||
}
|
||||
function setConfig() {
|
||||
showSetting.value = !showSetting.value;
|
||||
dashboardStore.selectWidget(props.config);
|
||||
}
|
||||
function resize() {
|
||||
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
|
||||
height: 40,
|
||||
width: 0,
|
||||
};
|
||||
height.value = dom.height - 40;
|
||||
width.value = dom.width;
|
||||
}
|
||||
function updateSettings(config: any) {
|
||||
settings.value = config;
|
||||
setNodeTools(config.nodeDashboard);
|
||||
}
|
||||
function setNodeTools(nodeDashboard: any) {
|
||||
items.value = [
|
||||
{ id: "hierarchyServices", title: "Hierarchy Services", func: handleHierarchyRelatedServices },
|
||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||
{ id: "alerting", title: "Alerting", func: handleGoAlerting },
|
||||
];
|
||||
if (!(nodeDashboard && nodeDashboard.length)) {
|
||||
return;
|
||||
}
|
||||
for (const item of nodeDashboard) {
|
||||
if (item.scope === EntityType[0].value) {
|
||||
items.value.push({
|
||||
id: "dashboard",
|
||||
title: "Service Dashboard",
|
||||
func: handleGoDashboard,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
if (item.scope === EntityType[2].value) {
|
||||
items.value.push({
|
||||
id: "endpoint",
|
||||
title: "Endpoint Dashboard",
|
||||
func: handleGoEndpoint,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
if (item.scope === EntityType[3].value) {
|
||||
items.value.push({
|
||||
id: "instance",
|
||||
title: "Service Instance Dashboard",
|
||||
func: handleGoInstance,
|
||||
...item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function svgEvent() {
|
||||
topologyStore.setNode(null);
|
||||
topologyStore.setLink(null);
|
||||
dashboardStore.selectWidget(props.config);
|
||||
}
|
||||
|
||||
async function changeDepth(opt: Option[] | any) {
|
||||
depth.value = opt[0].value;
|
||||
freshNodes();
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", resize);
|
||||
});
|
||||
watch(
|
||||
() => [selectorStore.currentService, selectorStore.currentDestService],
|
||||
(newVal, oldVal) => {
|
||||
if (!(oldVal[0] && newVal[0])) {
|
||||
return;
|
||||
}
|
||||
if (oldVal[0].id === newVal[0].id && !oldVal[1]) {
|
||||
return;
|
||||
}
|
||||
if (oldVal[0].id === newVal[0].id && oldVal[1].id === newVal[1].id) {
|
||||
return;
|
||||
}
|
||||
freshNodes();
|
||||
hierarchyRelated.value = false;
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => appStore.durationTime,
|
||||
() => {
|
||||
if (dashboardStore.entity === EntityType[1].value) {
|
||||
freshNodes();
|
||||
hierarchyRelated.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.hierarchy-related {
|
||||
height: 600px;
|
||||
width: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.micro-topo-chart {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
margin-top: 30px;
|
||||
|
||||
.node-text {
|
||||
fill: var(--sw-topology-color);
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.svg-topology {
|
||||
cursor: move;
|
||||
background-color: $theme-background;
|
||||
}
|
||||
|
||||
.legend {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 25px;
|
||||
color: var(--sw-topology-color);
|
||||
|
||||
div {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.setting {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right: 10px;
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
overflow: auto;
|
||||
padding: 0 15px;
|
||||
border-radius: 3px;
|
||||
color: $disabled-color;
|
||||
border: 1px solid $border-color-primary;
|
||||
background-color: var(--sw-topology-setting-bg);
|
||||
box-shadow: var(--sw-topology-box-shadow);
|
||||
transition: all 0.5ms linear;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--sw-topology-color);
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.operations-list {
|
||||
position: absolute;
|
||||
color: $font-color;
|
||||
cursor: pointer;
|
||||
border: var(--sw-topology-border);
|
||||
border-radius: 3px;
|
||||
background-color: $theme-background;
|
||||
padding: 10px 0;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: left;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
color: $active-color;
|
||||
background-color: $popper-hover-bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.tool {
|
||||
position: absolute;
|
||||
top: 35px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.switch-icon {
|
||||
cursor: pointer;
|
||||
transition: all 0.5ms linear;
|
||||
background: var(--sw-topology-switch-icon);
|
||||
color: $text-color;
|
||||
display: inline-block;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
#tooltip {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
padding: 5px;
|
||||
border: var(--sw-topology-border);
|
||||
border-radius: 3px;
|
||||
background-color: $theme-background;
|
||||
}
|
||||
</style>
|