feat: implement the Service and Instance hierarchy topology. (#360)

This commit is contained in:
Fine0830 2024-01-13 11:05:32 +08:00 committed by GitHub
parent 867af6924d
commit 306508856b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1901 additions and 771 deletions

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

BIN
src/assets/img/tools/MQ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

View File

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

View File

@ -79,7 +79,7 @@ limitations under the License. -->
</script>
<style lang="scss" scoped>
.input-name {
width: 300px;
width: 250px;
}
.vertical {

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

@ -383,6 +383,7 @@ const msg = {
traceDesc:
"Trace Segment代表在单一操作系统进程例如JVM中执行的追踪部分。它包含了一组跨度spans这些跨度通常与单一请求或执行上下文关联。",
tabExpressions: "Tab表达式",
hierarchyServicesSettings: "层次结构服务拓扑设置",
hierarchyNodeMetrics: "层级拓扑节点关联的指标",
};
export default msg;

View File

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

View File

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

View File

@ -23,6 +23,7 @@ export type DashboardItem = {
name: string;
isDefault: boolean;
expressions?: string[];
expressionsConfig?: MetricConfigOpt[];
};
export interface LayoutConfig {
x: number;

View File

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

View File

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

View File

@ -366,3 +366,7 @@ export enum CallTypes {
Server = "SERVER",
Client = "CLIENT",
}
export enum ConfigFieldTypes {
ISDEFAULT = "ISDEFAULT",
NAME = "NAME",
}

View File

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

View File

@ -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 */

View File

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

View File

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

View File

@ -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 || [];

View File

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

View 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>

View File

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

View File

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

View 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>

View 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>