feat: implement MQE on topology widget (#312)

This commit is contained in:
Fine0830 2023-08-22 23:34:16 +08:00 committed by GitHub
parent 8c1ddb109c
commit 60a4232759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 583 additions and 76 deletions

View File

@ -44,6 +44,7 @@ module.exports = {
"workflow", "workflow",
"types", "types",
"release", "release",
"merge",
], ],
], ],
}, },

87
src/components/Tags.vue Normal file
View File

@ -0,0 +1,87 @@
<!-- 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>
<span :class="vertical ? 'vertical' : 'horizontal'" v-for="tag in dynamicTags" :key="tag">
<el-tag closable :disable-transitions="false" @close="handleClose(tag)">
{{ tag }}
</el-tag>
</span>
<el-input
v-if="inputVisible"
ref="InputRef"
v-model="inputValue"
class="ml-5 input-name"
size="small"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
/>
<el-button v-else size="small" @click="showInput"> + {{ text }} </el-button>
</template>
<script lang="ts" setup>
import { nextTick, ref } from "vue";
import type { PropType } from "vue";
import { ElInput } from "element-plus";
/*global defineProps, defineEmits*/
const emits = defineEmits(["change"]);
const props = defineProps({
tags: {
type: Array as PropType<string[]>,
default: () => [],
},
text: { type: String, default: "" },
vertical: { type: Boolean, default: false },
});
const inputValue = ref("");
const dynamicTags = ref<string[]>(props.tags || []);
const inputVisible = ref(false);
const InputRef = ref<InstanceType<typeof ElInput>>();
const handleClose = (tag: string) => {
dynamicTags.value.splice(dynamicTags.value.indexOf(tag), 1);
};
const showInput = () => {
inputVisible.value = true;
nextTick(() => {
InputRef.value!.input!.focus();
});
};
const handleInputConfirm = () => {
if (inputValue.value) {
dynamicTags.value.push(inputValue.value);
}
inputVisible.value = false;
inputValue.value = "";
emits("change", dynamicTags.value);
};
</script>
<style lang="scss" scoped>
.input-name {
width: 300px;
}
.vertical {
display: block;
margin-bottom: 5px;
}
.horizontal {
display: inline-block;
margin-right: 5px;
}
</style>

View File

@ -21,6 +21,7 @@ import Selector from "./Selector.vue";
import Graph from "./Graph.vue"; import Graph from "./Graph.vue";
import Radio from "./Radio.vue"; import Radio from "./Radio.vue";
import SelectSingle from "./SelectSingle.vue"; import SelectSingle from "./SelectSingle.vue";
import Tags from "./Tags.vue";
import VueGridLayout from "vue-grid-layout"; import VueGridLayout from "vue-grid-layout";
const components: Indexable = { const components: Indexable = {
@ -31,6 +32,7 @@ const components: Indexable = {
Graph, Graph,
Radio, Radio,
SelectSingle, SelectSingle,
Tags,
}; };
const componentsName: string[] = Object.keys(components); const componentsName: string[] = Object.keys(components);

View File

@ -15,13 +15,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { RespFields } from "./data"; import { RespFields } from "./data";
import { ExpressionResultType } from "@/views/dashboard/data"; import { EntityType, ExpressionResultType } from "@/views/dashboard/data";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import type { MetricConfigOpt } from "@/types/dashboard"; import type { MetricConfigOpt } from "@/types/dashboard";
import type { Instance, Endpoint, Service } from "@/types/selector"; import type { Instance, Endpoint, Service } from "@/types/selector";
import type { Node, Call } from "@/types/topology";
export async function useExpressionsQueryProcessor(config: Indexable) { export async function useExpressionsQueryProcessor(config: Indexable) {
function expressionsGraphqlPods() { function expressionsGraphqlPods() {
@ -312,3 +313,88 @@ export async function useExpressionsQueryPodsMetrics(
return expressionParams; return expressionParams;
} }
export function useQueryTopologyExpressionsProcessor(metrics: string[], instances: (Call | Node)[]) {
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
function getExpressionQuery() {
const conditions: { [key: string]: unknown } = {
duration: appStore.durationTime,
};
const variables: string[] = [`$duration: Duration!`];
const fragmentList = instances.map((d: any, index: number) => {
let serviceName;
let destServiceName;
let endpointName;
let serviceInstanceName;
let destServiceInstanceName;
let destEndpointName;
if (d.sourceObj && d.targetObj) {
// instances = Calls
serviceName = d.sourceObj.serviceName || d.sourceObj.name;
destServiceName = d.targetObj.serviceName || d.targetObj.name;
if (EntityType[4].value === dashboardStore.entity) {
serviceInstanceName = d.sourceObj.name;
destServiceInstanceName = d.targetObj.name;
}
if (EntityType[2].value === dashboardStore.entity) {
endpointName = d.sourceObj.name;
destEndpointName = d.targetObj.name;
}
} else {
// instances = Nodes
serviceName = d.serviceName || d.name;
if (EntityType[4].value === dashboardStore.entity) {
serviceInstanceName = d.name;
}
if (EntityType[2].value === dashboardStore.entity) {
endpointName = d.name;
}
}
const entity = {
serviceName,
normal: true,
serviceInstanceName,
endpointName,
destServiceName,
destNormal: destServiceName ? true : undefined,
destServiceInstanceName,
destEndpointName,
};
variables.push(`$entity${index}: Entity!`);
conditions[`entity${index}`] = entity;
const f = metrics.map((name: string, idx: number) => {
if (index === 0) {
variables.push(`$expression${idx}: String!`);
conditions[`expression${idx}`] = name;
}
return `expression${index}${idx}: execExpression(expression: $expression${idx}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`;
});
return f;
});
const fragment = fragmentList.flat(1).join(" ");
const queryStr = `query queryData(${variables}) {${fragment}}`;
return { queryStr, conditions };
}
function handleExpressionValues(resp: { [key: string]: any }) {
const obj: any = {};
for (let idx = 0; idx < instances.length; idx++) {
for (let index = 0; index < metrics.length; index++) {
const k = "expression" + idx + index;
if (metrics[index]) {
if (!obj[metrics[index]]) {
obj[metrics[index]] = {
values: [],
};
}
obj[metrics[index]].values.push({ value: resp[k].results[0].values[0].value, id: instances[idx].id });
}
}
}
return obj;
}
return { getExpressionQuery, handleExpressionValues };
}

View File

@ -378,5 +378,9 @@ const msg = {
menus: "Menus", menus: "Menus",
saveReload: "Save and reload the page", saveReload: "Save and reload the page",
document: "Documentation", document: "Documentation",
metricMode: "Metric Mode",
addExpressions: "Add Expressions",
expressions: "Expression",
unhealthyExpression: "Unhealthy Expression",
}; };
export default msg; export default msg;

View File

@ -378,5 +378,9 @@ const msg = {
menus: "Menus", menus: "Menus",
saveReload: "Save and reload the page", saveReload: "Save and reload the page",
document: "Documentation", document: "Documentation",
metricMode: "Metric Mode",
addExpressions: "Add Expressions",
expressions: "Expression",
unhealthyExpression: "Unhealthy Expression",
}; };
export default msg; export default msg;

View File

@ -376,5 +376,9 @@ const msg = {
menusManagement: "菜单", menusManagement: "菜单",
saveReload: "保存并重新加载页面", saveReload: "保存并重新加载页面",
document: "文档", document: "文档",
metricMode: "指标模式",
addExpressions: "添加表达式",
expressions: "表达式",
unhealthyExpression: "非健康表达式",
}; };
export default msg; export default msg;

View File

@ -57,7 +57,7 @@ export const eventStore = defineStore({
this.instances = [{ value: "", label: "All" }, ...res.data.data.pods] || [{ value: "", label: "All" }]; this.instances = [{ value: "", label: "All" }, ...res.data.data.pods] || [{ value: "", label: "All" }];
return res.data; return res.data;
}, },
async getEndpoints(keyword?: string) { async getEndpoints(keyword: string) {
const serviceId = useSelectorStore().currentService ? useSelectorStore().currentService.id : ""; const serviceId = useSelectorStore().currentService ? useSelectorStore().currentService.id : "";
if (!serviceId) { if (!serviceId) {
return; return;

View File

@ -24,6 +24,7 @@ import { useAppStoreWithOut } from "@/store/modules/app";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import query from "@/graphql/fetch"; import query from "@/graphql/fetch";
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor"; import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
interface MetricVal { interface MetricVal {
@ -114,6 +115,16 @@ export const topologyStore = defineStore({
setLinkClientMetrics(m: MetricVal) { setLinkClientMetrics(m: MetricVal) {
this.linkClientMetrics = m; this.linkClientMetrics = 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++) {
const k = "expression" + idx + index;
if (expressions[index]) {
this.nodes[idx][expressions[index]] = Number(data[k].results[0].values[0].value);
}
}
}
},
async getDepthServiceTopology(serviceIds: string[], depth: number) { async getDepthServiceTopology(serviceIds: string[], depth: number) {
const res = await this.getServicesTopology(serviceIds); const res = await this.getServicesTopology(serviceIds);
if (depth > 1) { if (depth > 1) {
@ -321,6 +332,15 @@ export const topologyStore = defineStore({
this.setNodeMetricValue(res.data.data); this.setNodeMetricValue(res.data.data);
return res.data; return res.data;
}, },
async getNodeExpressionValue(param: { queryStr: string; conditions: { [key: string]: unknown } }) {
const res: AxiosResponse = await query(param);
if (res.data.errors) {
return res.data;
}
return res.data;
},
async getLinkClientMetrics(linkClientMetrics: string[]) { async getLinkClientMetrics(linkClientMetrics: string[]) {
if (!linkClientMetrics.length) { if (!linkClientMetrics.length) {
this.setLinkClientMetrics({}); this.setLinkClientMetrics({});
@ -353,6 +373,29 @@ export const topologyStore = defineStore({
ElMessage.error(res.errors); ElMessage.error(res.errors);
} }
}, },
async getLinkExpressions(expressions: string[], type: string) {
if (!expressions.length) {
this.setLinkServerMetrics({});
return;
}
const calls = this.calls.filter((i: Call) => i.detectPoints.includes(type));
if (!calls.length) {
return;
}
const { getExpressionQuery, handleExpressionValues } = useQueryTopologyExpressionsProcessor(expressions, calls);
const param = getExpressionQuery();
const res = await this.getNodeExpressionValue(param);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const metrics = handleExpressionValues(res.data);
if (type === "SERVER") {
this.setLinkServerMetrics(metrics);
} else {
this.setLinkClientMetrics(metrics);
}
},
async queryNodeMetrics(nodeMetrics: string[]) { async queryNodeMetrics(nodeMetrics: string[]) {
if (!nodeMetrics.length) { if (!nodeMetrics.length) {
this.setNodeMetricValue({}); this.setNodeMetricValue({});
@ -369,6 +412,28 @@ export const topologyStore = defineStore({
ElMessage.error(res.errors); ElMessage.error(res.errors);
} }
}, },
async queryNodeExpressions(expressions: string[]) {
if (!expressions.length) {
this.setNodeMetricValue({});
return;
}
if (!this.nodes.length) {
this.setNodeMetricValue({});
return;
}
const { getExpressionQuery, handleExpressionValues } = useQueryTopologyExpressionsProcessor(
expressions,
this.nodes,
);
const param = getExpressionQuery();
const res = await this.getNodeExpressionValue(param);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const metrics = handleExpressionValues(res.data);
this.setNodeMetricValue(metrics);
},
async getLegendMetrics(param: { queryStr: string; conditions: { [key: string]: unknown } }) { async getLegendMetrics(param: { queryStr: string; conditions: { [key: string]: unknown } }) {
const res: AxiosResponse = await query(param); const res: AxiosResponse = await query(param);

View File

@ -36,6 +36,7 @@ declare module '@vue/runtime-core' {
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
Graph: typeof import('./../components/Graph.vue')['default'] Graph: typeof import('./../components/Graph.vue')['default']
Icon: typeof import('./../components/Icon.vue')['default'] Icon: typeof import('./../components/Icon.vue')['default']
@ -45,6 +46,7 @@ declare module '@vue/runtime-core' {
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Selector: typeof import('./../components/Selector.vue')['default'] Selector: typeof import('./../components/Selector.vue')['default']
SelectSingle: typeof import('./../components/SelectSingle.vue')['default'] SelectSingle: typeof import('./../components/SelectSingle.vue')['default']
Tags: typeof import('./../components/Tags.vue')['default']
TimePicker: typeof import('./../components/TimePicker.vue')['default'] TimePicker: typeof import('./../components/TimePicker.vue')['default']
} }
} }

View File

@ -160,7 +160,7 @@ limitations under the License. -->
}, },
}); });
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const isExpression = ref<boolean>(dashboardStore.selectedGrid.metricMode === MetricModes.Expression ? true : false); const isExpression = ref<boolean>(dashboardStore.selectedGrid.metricMode === MetricModes.Expression);
const metrics = computed( const metrics = computed(
() => (isExpression.value ? dashboardStore.selectedGrid.expressions : dashboardStore.selectedGrid.metrics) || [], () => (isExpression.value ? dashboardStore.selectedGrid.expressions : dashboardStore.selectedGrid.metrics) || [],
); );

View File

@ -139,7 +139,7 @@ limitations under the License. -->
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { useTopologyStore } from "@/store/modules/topology"; import { useTopologyStore } from "@/store/modules/topology";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { EntityType, DepthList } from "../../../data"; import { EntityType, DepthList, MetricModes } from "../../../data";
import router from "@/router"; import router from "@/router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import Settings from "./Settings.vue"; import Settings from "./Settings.vue";
@ -153,6 +153,7 @@ limitations under the License. -->
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor"; import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
import { layout, circleIntersection, computeCallPos } from "./utils/layout"; import { layout, circleIntersection, computeCallPos } from "./utils/layout";
import zoom from "../../components/utils/zoom"; import zoom from "../../components/utils/zoom";
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
/*global Nullable, defineProps */ /*global Nullable, defineProps */
const props = defineProps({ const props = defineProps({
@ -220,19 +221,27 @@ limitations under the License. -->
} }
async function update() { async function update() {
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []); if (settings.value.metricMode === MetricModes.Expression) {
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []); topologyStore.queryNodeExpressions(settings.value.nodeExpressions || []);
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []); topologyStore.getLinkExpressions(settings.value.linkClientExpressions || []);
topologyStore.getLinkExpressions(settings.value.linkServerExpressions || []);
} else {
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
}
window.addEventListener("resize", resize); window.addEventListener("resize", resize);
await initLegendMetrics(); await initLegendMetrics();
draw(); draw();
tooltip.value = d3.select("#tooltip"); tooltip.value = d3.select("#tooltip");
setNodeTools(settings.value.nodeDashboard); setNodeTools(settings.value.nodeDashboard);
} }
function draw() { function draw() {
const node = findMostFrequent(topologyStore.calls); const node = findMostFrequent(topologyStore.calls);
const levels = []; const levels = [];
const nodes = topologyStore.nodes.sort((a: Node, b: Node) => { const nodes = JSON.parse(JSON.stringify(topologyStore.nodes)).sort((a: Node, b: Node) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) { if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1; return -1;
} }
@ -352,18 +361,49 @@ limitations under the License. -->
} }
async function initLegendMetrics() { async function initLegendMetrics() {
const ids = topologyStore.nodes.map((d: Node) => d.id); if (!topologyStore.nodes.length) {
const names = props.config.legend.map((d: any) => d.name); return;
if (names.length && ids.length) { }
const param = await useQueryTopologyMetrics(names, ids); if (settings.value.metricMode === MetricModes.Expression) {
const res = await topologyStore.getLegendMetrics(param); 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) { if (res.errors) {
ElMessage.error(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) { function getNodeStatus(d: any) {
const legend = settings.value.legend; 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) { if (!legend) {
return icons.CUBE; return icons.CUBE;
} }
@ -381,7 +421,10 @@ limitations under the License. -->
return c && d.isReal ? icons.CUBEERROR : icons.CUBE; return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
} }
function showNodeTip(event: MouseEvent, data: Node) { function showNodeTip(event: MouseEvent, data: Node) {
const nodeMetrics: string[] = settings.value.nodeMetrics || []; const nodeMetrics: string[] =
(settings.value.metricMode === MetricModes.Expression
? settings.value.nodeExpressions
: settings.value.nodeMetrics) || [];
const nodeMetricConfig = settings.value.nodeMetricConfig || []; const nodeMetricConfig = settings.value.nodeMetricConfig || [];
const html = nodeMetrics.map((m, index) => { const html = nodeMetrics.map((m, index) => {
const metric = const metric =
@ -404,10 +447,16 @@ limitations under the License. -->
.html(tipHtml); .html(tipHtml);
} }
function showLinkTip(event: MouseEvent, data: Call) { function showLinkTip(event: MouseEvent, data: Call) {
const linkClientMetrics: string[] = settings.value.linkClientMetrics || []; const linkClientMetrics: string[] =
settings.value.metricMode === MetricModes.Expression
? settings.value.linkClientExpressions
: settings.value.linkClientMetrics || [];
const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || []; const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || []; const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
const linkServerMetrics: string[] = settings.value.linkServerMetrics || []; const linkServerMetrics: string[] =
settings.value.metricMode === MetricModes.Expression
? settings.value.linkServerExpressions
: settings.value.linkServerMetrics || [];
const htmlServer = linkServerMetrics.map((m, index) => { const htmlServer = linkServerMetrics.map((m, index) => {
const metric = topologyStore.linkServerMetrics[m].values.find( const metric = topologyStore.linkServerMetrics[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id, (val: { id: string; value: unknown }) => val.id === data.id,
@ -667,7 +716,7 @@ limitations under the License. -->
padding: 0 15px; padding: 0 15px;
border-radius: 3px; border-radius: 3px;
color: $disabled-color; color: $disabled-color;
border: 1px solid $disabled-color; border: 1px solid #eee;
background-color: $theme-background; background-color: $theme-background;
box-shadow: #eee 1px 2px 10px; box-shadow: #eee 1px 2px 10px;
transition: all 0.5ms linear; transition: all 0.5ms linear;

View File

@ -15,7 +15,9 @@ limitations under the License. -->
<template> <template>
<div class="config-panel"> <div class="config-panel">
<div class="item mb-10"> <div class="item mb-10">
<span class="label">{{ t("metrics") }}</span> <span class="label">{{
t(dashboardStore.selectedGrid.metricMode === MetricModes.General ? "metrics" : "expressions")
}}</span>
<SelectSingle :value="currentMetric" :options="metrics" @change="changeMetric" class="selectors" /> <SelectSingle :value="currentMetric" :options="metrics" @change="changeMetric" class="selectors" />
</div> </div>
<div class="item mb-10"> <div class="item mb-10">
@ -38,7 +40,7 @@ limitations under the License. -->
@change="changeConfigs({ label: currentConfig.label })" @change="changeConfigs({ label: currentConfig.label })"
/> />
</div> </div>
<div class="item mb-10"> <div class="item mb-10" v-if="dashboardStore.selectedGrid.metricMode === MetricModes.General">
<span class="label">{{ t("aggregation") }}</span> <span class="label">{{ t("aggregation") }}</span>
<SelectSingle <SelectSingle
:value="currentConfig.calculation" :value="currentConfig.calculation"
@ -54,17 +56,12 @@ limitations under the License. -->
import { ref, computed, watch } from "vue"; import { ref, computed, watch } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { CalculationOpts } from "../../../data"; import { CalculationOpts, MetricModes } from "../../../data";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import type { MetricConfigOpt } from "@/types/dashboard";
import type { Option } from "element-plus/es/components/select-v2/src/select.types"; import type { Option } from "element-plus/es/components/select-v2/src/select.types";
/*global defineEmits, defineProps */ /*global defineEmits, defineProps */
const props = defineProps({ const props = defineProps({
currentMetricConfig: {
type: Object as PropType<MetricConfigOpt>,
default: () => ({ unit: "" }),
},
type: { type: String, default: "" }, type: { type: String, default: "" },
metrics: { type: Array as PropType<string[]>, default: () => [] }, metrics: { type: Array as PropType<string[]>, default: () => [] },
}); });

View File

@ -22,6 +22,7 @@ limitations under the License. -->
:options="DepthList" :options="DepthList"
placeholder="Select a option" placeholder="Select a option"
@change="changeDepth" @change="changeDepth"
size="small"
/> />
</span> </span>
<span class="switch-icon ml-5" title="Settings" @click="setConfig" v-if="dashboardStore.editMode"> <span class="switch-icon ml-5" title="Settings" @click="setConfig" v-if="dashboardStore.editMode">
@ -68,7 +69,7 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType, DepthList } from "../../../data"; import { EntityType, DepthList, MetricModes } from "../../../data";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import Sankey from "./Sankey.vue"; import Sankey from "./Sankey.vue";
import Settings from "./Settings.vue"; import Settings from "./Settings.vue";
@ -118,9 +119,15 @@ limitations under the License. -->
}; };
height.value = dom.height - 70; height.value = dom.height - 70;
width.value = dom.width - 5; width.value = dom.width - 5;
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []); if (settings.value.metricMode === MetricModes.Expression) {
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []); topologyStore.queryNodeExpressions(settings.value.nodeExpressions || []);
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []); topologyStore.getLinkExpressions(settings.value.linkClientExpressions || []);
topologyStore.getLinkExpressions(settings.value.linkServerExpressions || []);
} else {
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
}
} }
function resize() { function resize() {
@ -265,7 +272,6 @@ limitations under the License. -->
<style lang="scss" scoped> <style lang="scss" scoped>
.sankey { .sankey {
margin-top: 10px; margin-top: 10px;
background-color: #333840 !important;
color: #ddd; color: #ddd;
} }
@ -275,7 +281,8 @@ limitations under the License. -->
right: 10px; right: 10px;
width: 400px; width: 400px;
height: 600px; height: 600px;
background-color: #2b3037; border: 1px solid #eee;
background-color: $theme-background;
overflow: auto; overflow: auto;
padding: 10px 15px; padding: 10px 15px;
border-radius: 3px; border-radius: 3px;
@ -283,6 +290,7 @@ limitations under the License. -->
transition: all 0.5ms linear; transition: all 0.5ms linear;
z-index: 99; z-index: 99;
text-align: left; text-align: left;
box-shadow: #eee 1px 2px 10px;
} }
.tool { .tool {
@ -299,8 +307,8 @@ limitations under the License. -->
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: all 0.5ms linear; transition: all 0.5ms linear;
background-color: #252a2f99; background: rgb(0 0 0 / 30%);
color: #ddd; color: $text-color;
display: inline-block; display: inline-block;
border-radius: 3px; border-radius: 3px;
} }

View File

@ -23,6 +23,7 @@ limitations under the License. -->
import type { Node, Call } from "@/types/topology"; import type { Node, Call } from "@/types/topology";
import type { MetricConfigOpt } from "@/types/dashboard"; import type { MetricConfigOpt } from "@/types/dashboard";
import { aggregation } from "@/hooks/useMetricsProcessor"; import { aggregation } from "@/hooks/useMetricsProcessor";
import { MetricModes } from "../../../data";
/*global defineEmits, defineProps */ /*global defineEmits, defineProps */
const props = defineProps({ const props = defineProps({
@ -51,16 +52,16 @@ limitations under the License. -->
data: topologyStore.nodes, data: topologyStore.nodes,
links: topologyStore.calls, links: topologyStore.calls,
label: { label: {
color: "#fff", color: "#666",
formatter: (param: any) => param.data.name, formatter: (param: any) => param.data.name,
}, },
color: ["#3fe1da", "#6be6c1", "#3fcfdc", "#626c91", "#3fbcde", "#a0a7e6", "#3fa9e1", "#96dee8", "#bf99f8"], color: ["#6be6c1", "#3fcfdc", "#626c91", "#3fbcde", "#a0a7e6", "#3fa9e1", "#96dee8", "#bf99f8"],
itemStyle: { itemStyle: {
borderWidth: 0, borderWidth: 0,
}, },
lineStyle: { lineStyle: {
color: "source", color: "source",
opacity: 0.12, opacity: 0.3,
}, },
tooltip: { tooltip: {
position: "bottom", position: "bottom",
@ -75,8 +76,14 @@ limitations under the License. -->
}; };
} }
function linkTooltip(data: Call) { function linkTooltip(data: Call) {
const clientMetrics: string[] = Object.keys(topologyStore.linkClientMetrics); const clientMetrics: string[] =
const serverMetrics: string[] = Object.keys(topologyStore.linkServerMetrics); props.settings.metricMode === MetricModes.Expression
? props.settings.linkClientExpressions
: props.settings.linkClientMetrics;
const serverMetrics: string[] =
props.settings.metricMode === MetricModes.Expression
? props.settings.linkServerExpressions
: props.settings.linkServerMetrics;
const linkServerMetricConfig: MetricConfigOpt[] = props.settings.linkServerMetricConfig || []; const linkServerMetricConfig: MetricConfigOpt[] = props.settings.linkServerMetricConfig || [];
const linkClientMetricConfig: MetricConfigOpt[] = props.settings.linkClientMetricConfig || []; const linkClientMetricConfig: MetricConfigOpt[] = props.settings.linkClientMetricConfig || [];
@ -108,7 +115,10 @@ limitations under the License. -->
} }
function nodeTooltip(data: Node) { function nodeTooltip(data: Node) {
const nodeMetrics: string[] = Object.keys(topologyStore.nodeMetricValue); const nodeMetrics: string[] =
props.settings.metricMode === MetricModes.Expression
? props.settings.nodeExpressions
: props.settings.nodeMetrics;
const nodeMetricConfig = props.settings.nodeMetricConfig || []; const nodeMetricConfig = props.settings.nodeMetricConfig || [];
const html = nodeMetrics.map((m, index) => { const html = nodeMetrics.map((m, index) => {
const metric = const metric =

View File

@ -13,6 +13,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="mt-20">
<h5 class="title">{{ t("metricMode") }}</h5>
<el-switch
v-model="isExpression"
class="mt-5"
active-text="Expressions"
inactive-text="General"
size="small"
@change="changeMetricMode"
/>
</div>
<div class="link-settings"> <div class="link-settings">
<h5 class="title">{{ t("callSettings") }}</h5> <h5 class="title">{{ t("callSettings") }}</h5>
<div class="label">{{ t("linkDashboard") }}</div> <div class="label">{{ t("linkDashboard") }}</div>
@ -27,16 +38,34 @@ limitations under the License. -->
/> />
<div class="label"> <div class="label">
<span>{{ t("linkServerMetrics") }}</span> <span>{{ t("linkServerMetrics") }}</span>
<el-popover placement="left" :width="400" trigger="click" v-if="states.linkServerMetrics.length"> <el-popover
placement="left"
:width="400"
trigger="click"
v-if="isExpression ? states.linkServerExpressions.length : states.linkServerMetrics.length"
>
<template #reference> <template #reference>
<span @click="setConfigType('linkServerMetricConfig')"> <span @click="setConfigType('linkServerMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" /> <Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span> </span>
</template> </template>
<Metrics :type="configType" :metrics="states.linkServerMetrics" @update="changeLinkServerMetrics" /> <Metrics
:type="configType"
:metrics="isExpression ? states.linkServerExpressions : states.linkServerMetrics"
@update="updateSettings"
/>
</el-popover> </el-popover>
</div> </div>
<div v-if="isExpression">
<Tags
:tags="states.linkServerExpressions"
:vertical="true"
:text="t('addExpressions')"
@change="(param) => changeLinkServerExpressions(param)"
/>
</div>
<Selector <Selector
v-else
class="inputs" class="inputs"
:multiple="true" :multiple="true"
:value="states.linkServerMetrics" :value="states.linkServerMetrics"
@ -48,16 +77,34 @@ limitations under the License. -->
<span v-show="dashboardStore.entity !== EntityType[2].value"> <span v-show="dashboardStore.entity !== EntityType[2].value">
<div class="label"> <div class="label">
<span>{{ t("linkClientMetrics") }}</span> <span>{{ t("linkClientMetrics") }}</span>
<el-popover placement="left" :width="400" trigger="click" v-if="states.linkClientMetrics.length"> <el-popover
placement="left"
:width="400"
trigger="click"
v-if="isExpression ? states.linkClientExpressions.length : states.linkClientMetrics.length"
>
<template #reference> <template #reference>
<span @click="setConfigType('linkClientMetricConfig')"> <span @click="setConfigType('linkClientMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" /> <Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span> </span>
</template> </template>
<Metrics :type="configType" :metrics="states.linkClientMetrics" @update="changeLinkClientMetrics" /> <Metrics
:type="configType"
:metrics="isExpression ? states.linkClientExpressions : states.linkClientMetrics"
@update="updateSettings"
/>
</el-popover> </el-popover>
</div> </div>
<div v-if="isExpression">
<Tags
:tags="states.linkClientExpressions"
:vertical="true"
:text="t('addExpressions')"
@change="(param) => changeLinkClientExpressions(param)"
/>
</div>
<Selector <Selector
v-else
class="inputs" class="inputs"
:multiple="true" :multiple="true"
:value="states.linkClientMetrics" :value="states.linkClientMetrics"
@ -110,16 +157,34 @@ limitations under the License. -->
</div> </div>
<div class="label"> <div class="label">
<span>{{ t("nodeMetrics") }}</span> <span>{{ t("nodeMetrics") }}</span>
<el-popover placement="left" :width="400" trigger="click" v-if="states.nodeMetrics.length"> <el-popover
placement="left"
:width="400"
trigger="click"
v-if="isExpression ? states.nodeExpressions.length : states.nodeMetrics.length"
>
<template #reference> <template #reference>
<span @click="setConfigType('nodeMetricConfig')"> <span @click="setConfigType('nodeMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" /> <Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span> </span>
</template> </template>
<Metrics :type="configType" :metrics="states.nodeMetrics" @update="changeNodeMetrics" /> <Metrics
:type="configType"
:metrics="isExpression ? states.nodeExpressions : states.nodeMetrics"
@update="updateSettings"
/>
</el-popover> </el-popover>
</div> </div>
<div v-if="isExpression">
<Tags
:tags="states.nodeExpressions"
:vertical="true"
:text="t('addExpressions')"
@change="(param) => changeNodeExpressions(param)"
/>
</div>
<Selector <Selector
v-else
class="inputs" class="inputs"
:multiple="true" :multiple="true"
:value="states.nodeMetrics" :value="states.nodeMetrics"
@ -131,8 +196,26 @@ limitations under the License. -->
</div> </div>
<div class="legend-settings" v-show="isService"> <div class="legend-settings" v-show="isService">
<h5 class="title">{{ t("legendSettings") }}</h5> <h5 class="title">{{ t("legendSettings") }}</h5>
<div class="label">{{ t("conditions") }}</div> <span v-if="isExpression">
<div v-for="(metric, index) of legend.metric" :key="metric.name + index"> <div class="label">Healthy Description</div>
<el-input v-model="description.healthy" placeholder="Please input description" size="small" class="mt-5" />
</span>
<div class="label">
<span>{{ t(isExpression ? "unhealthyExpression" : "conditions") }}</span>
<el-tooltip
class="cp"
v-if="isExpression"
content="The node would be red to indicate unhealthy status when the expression return greater than 0"
>
<span>
<Icon class="icon-help ml-5" iconName="help" size="small" />
</span>
</el-tooltip>
</div>
<div v-if="isExpression">
<el-input v-model="legendMQE.expression" placeholder="Please input a expression" size="small" class="inputs" />
</div>
<div v-for="(metric, index) of legend" :key="index" v-else>
<Selector <Selector
class="item" class="item"
:value="metric.name" :value="metric.name"
@ -163,14 +246,16 @@ limitations under the License. -->
class="cp" class="cp"
iconName="add_circle_outlinecontrol_point" iconName="add_circle_outlinecontrol_point"
size="middle" size="middle"
v-show="index === legend.metric.length - 1 && legend.metric.length < 5" v-show="index === legend.length - 1 && legend.length < 5"
@click="addMetric" @click="addMetric"
/> />
</span> </span>
<div v-show="index !== legend.metric.length - 1">&&</div> <div v-show="index !== legend.length - 1">&&</div>
</div> </div>
<div class="label">Healthy Description</div> <span v-if="!isExpression">
<el-input v-model="description.healthy" placeholder="Please input description" size="small" class="mt-5" /> <div class="label">Healthy Description</div>
<el-input v-model="description.healthy" placeholder="Please input description" size="small" class="mt-5" />
</span>
<div class="label">Unhealthy Description</div> <div class="label">Unhealthy Description</div>
<el-input v-model="description.unhealthy" placeholder="Please input description" size="small" class="mt-5" /> <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="legend-btn" size="small" type="primary">
@ -184,12 +269,20 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useTopologyStore } from "@/store/modules/topology"; import { useTopologyStore } from "@/store/modules/topology";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { MetricCatalog, ScopeType, MetricConditions } from "../../../data"; import {
MetricCatalog,
ScopeType,
MetricConditions,
EntityType,
LegendOpt,
MetricsType,
MetricModes,
} from "../../../data";
import type { Option } from "@/types/app"; import type { Option } from "@/types/app";
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor"; import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
import type { Node } from "@/types/topology"; import type { Node } from "@/types/topology";
import type { DashboardItem, MetricConfigOpt } from "@/types/dashboard"; import type { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
import { EntityType, LegendOpt, MetricsType } from "../../../data";
import Metrics from "./Metrics.vue"; import Metrics from "./Metrics.vue";
/*global defineEmits */ /*global defineEmits */
@ -198,6 +291,7 @@ limitations under the License. -->
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const topologyStore = useTopologyStore(); const topologyStore = useTopologyStore();
const { selectedGrid } = dashboardStore; const { selectedGrid } = dashboardStore;
const isExpression = ref<boolean>(dashboardStore.selectedGrid.metricMode === MetricModes.Expression);
const nodeDashboard = const nodeDashboard =
selectedGrid.nodeDashboard && selectedGrid.nodeDashboard.length ? selectedGrid.nodeDashboard : ""; selectedGrid.nodeDashboard && selectedGrid.nodeDashboard.length ? selectedGrid.nodeDashboard : "";
const isService = [EntityType[0].value, EntityType[1].value].includes(dashboardStore.entity); const isService = [EntityType[0].value, EntityType[1].value].includes(dashboardStore.entity);
@ -220,6 +314,9 @@ limitations under the License. -->
linkMetricList: Option[]; linkMetricList: Option[];
linkDashboards: (DashboardItem & { label: string; value: string })[]; linkDashboards: (DashboardItem & { label: string; value: string })[];
nodeDashboards: (DashboardItem & { label: string; value: string })[]; nodeDashboards: (DashboardItem & { label: string; value: string })[];
linkServerExpressions: string[];
linkClientExpressions: string[];
nodeExpressions: string[];
}>({ }>({
linkDashboard: selectedGrid.linkDashboard || "", linkDashboard: selectedGrid.linkDashboard || "",
nodeDashboard: selectedGrid.nodeDashboard || [], nodeDashboard: selectedGrid.nodeDashboard || [],
@ -230,13 +327,15 @@ limitations under the License. -->
linkMetricList: [], linkMetricList: [],
linkDashboards: [], linkDashboards: [],
nodeDashboards: [], nodeDashboards: [],
linkServerExpressions: selectedGrid.linkServerExpressions || [],
linkClientExpressions: selectedGrid.linkClientExpressions || [],
nodeExpressions: selectedGrid.nodeExpressions || [],
}); });
const l = selectedGrid.legend && selectedGrid.legend.length; const l = selectedGrid.legend && selectedGrid.legend.length;
const legend = reactive<{ const legend = ref<{ name: string; condition: string; value: string }[]>(
metric: { name: string; condition: string; value: string }[]; l ? selectedGrid.legend : [{ name: "", condition: "", value: "" }],
}>({ );
metric: l ? selectedGrid.legend : [{ name: "", condition: "", value: "" }], const legendMQE = ref<{ expression: string }>(selectedGrid.legendMQE || { expression: "" });
});
const configType = ref<string>(""); const configType = ref<string>("");
const description = reactive<any>(selectedGrid.description || {}); const description = reactive<any>(selectedGrid.description || {});
@ -285,18 +384,35 @@ limitations under the License. -->
} }
async function setLegend() { async function setLegend() {
updateSettings(); updateSettings();
const ids = topologyStore.nodes.map((d: Node) => d.id); if (isExpression.value) {
const names = dashboardStore.selectedGrid.legend.map((d: any) => d.name); const expression = dashboardStore.selectedGrid.legendMQE && dashboardStore.selectedGrid.legendMQE.expression;
if (!names.length) { if (!expression) {
emit("updateNodes"); emit("updateNodes");
return; return;
} }
const param = await useQueryTopologyMetrics(names, ids); const { getExpressionQuery } = useQueryTopologyExpressionsProcessor([expression], topologyStore.nodes);
const res = await topologyStore.getLegendMetrics(param); 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 = dashboardStore.selectedGrid.legend.map((d: any) => d.name && d.condition && d.value);
if (!names.length) {
emit("updateNodes");
return;
}
const ids = topologyStore.nodes.map((d: Node) => d.id);
const param = await useQueryTopologyMetrics(names, ids);
const res = await topologyStore.getLegendMetrics(param);
if (res.errors) { if (res.errors) {
ElMessage.error(res.errors); ElMessage.error(res.errors);
}
} }
emit("updateNodes"); emit("updateNodes");
} }
function changeNodeDashboard(opt: any) { function changeNodeDashboard(opt: any) {
@ -308,7 +424,7 @@ limitations under the License. -->
updateSettings(); updateSettings();
} }
function changeLegend(type: string, opt: any, index: number) { function changeLegend(type: string, opt: any, index: number) {
(legend.metric[index] as any)[type] = opt[0].value || opt; (legend.value[index] as any)[type] = opt[0].value || opt;
} }
function changeScope(index: number, opt: Option[] | any) { function changeScope(index: number, opt: Option[] | any) {
items[index].scope = opt[0].value; items[index].scope = opt[0].value;
@ -340,7 +456,13 @@ limitations under the License. -->
updateSettings(); updateSettings();
} }
function updateSettings(metricConfig?: { [key: string]: MetricConfigOpt[] }) { function updateSettings(metricConfig?: { [key: string]: MetricConfigOpt[] }) {
const metrics = legend.metric.filter((d: any) => d.name && d.value && d.condition); let metrics = [];
if (isExpression.value) {
metrics = legend.value.filter((d: any) => d.name);
} else {
metrics = legend.value.filter((d: any) => d.name && d.value && d.condition);
}
const param = { const param = {
...dashboardStore.selectedGrid, ...dashboardStore.selectedGrid,
linkDashboard: states.linkDashboard, linkDashboard: states.linkDashboard,
@ -350,7 +472,12 @@ limitations under the License. -->
linkServerMetrics: states.linkServerMetrics, linkServerMetrics: states.linkServerMetrics,
linkClientMetrics: states.linkClientMetrics, linkClientMetrics: states.linkClientMetrics,
nodeMetrics: states.nodeMetrics, nodeMetrics: states.nodeMetrics,
linkServerExpressions: states.linkServerExpressions,
linkClientExpressions: states.linkClientExpressions,
nodeExpressions: states.nodeExpressions,
metricMode: isExpression.value ? MetricModes.Expression : MetricModes.General,
legend: metrics, legend: metrics,
legendMQE: legendMQE.value,
...metricConfig, ...metricConfig,
description, description,
}; };
@ -378,6 +505,30 @@ limitations under the License. -->
} }
topologyStore.getLinkServerMetrics(states.linkServerMetrics); topologyStore.getLinkServerMetrics(states.linkServerMetrics);
} }
function changeLinkServerExpressions(param: string[]) {
if (!isExpression.value) {
return;
}
states.linkServerExpressions = param;
updateSettings();
if (!states.linkServerExpressions.length) {
topologyStore.setLinkServerMetrics({});
return;
}
topologyStore.getLinkExpressions(states.linkServerExpressions, "SERVER");
}
function changeLinkClientExpressions(param: string[]) {
if (!isExpression.value) {
return;
}
states.linkClientExpressions = param;
updateSettings();
if (!states.linkClientExpressions.length) {
topologyStore.changeLinkClientMetrics({});
return;
}
topologyStore.getLinkExpressions(states.linkClientExpressions, "CLIENT");
}
function updateLinkClientMetrics(options: Option[] | any) { function updateLinkClientMetrics(options: Option[] | any) {
const opt = options.map((d: Option) => d.value); const opt = options.map((d: Option) => d.value);
const index = states.linkClientMetrics.findIndex((d: any) => !opt.includes(d)); const index = states.linkClientMetrics.findIndex((d: any) => !opt.includes(d));
@ -419,18 +570,49 @@ limitations under the License. -->
topologyStore.queryNodeMetrics(states.nodeMetrics); topologyStore.queryNodeMetrics(states.nodeMetrics);
} }
function deleteMetric(index: number) { function deleteMetric(index: number) {
if (legend.metric.length === 1) { if (legend.value.length === 1) {
legend.metric = [{ name: "", condition: "", value: "" }]; legend.value = [{ name: "", condition: "", value: "" }];
return; return;
} }
legend.metric.splice(index, 1); legend.value.splice(index, 1);
} }
function addMetric() { function addMetric() {
legend.metric.push({ name: "", condition: "", value: "" }); legend.value.push({ name: "", condition: "", value: "" });
} }
function setConfigType(type: string) { function setConfigType(type: string) {
configType.value = type; configType.value = type;
} }
function changeNodeExpressions(param: string[]) {
if (!isExpression.value) {
return;
}
states.nodeExpressions = param;
updateSettings();
if (!states.nodeExpressions.length) {
topologyStore.setNodeMetricValue({});
return;
}
topologyStore.queryNodeExpressions(states.nodeExpressions);
}
function changeMetricMode() {
legend.value = [{ name: "", condition: "", value: "" }];
const config = {
linkServerMetricConfig: [],
linkClientMetricConfig: [],
nodeMetricConfig: [],
};
if (isExpression.value) {
states.linkServerMetrics = [];
states.linkClientMetrics = [];
states.nodeMetrics = [];
} else {
states.linkServerExpressions = [];
states.linkClientExpressions = [];
states.nodeExpressions = [];
}
updateSettings(config);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.link-settings { .link-settings {
@ -442,6 +624,11 @@ limitations under the License. -->
width: 355px; width: 355px;
} }
.legend-inputs {
margin-top: 8px;
width: 310px;
}
.item { .item {
width: 130px; width: 130px;
margin-top: 5px; margin-top: 5px;
@ -453,13 +640,14 @@ limitations under the License. -->
} }
.title { .title {
margin-bottom: 0;
color: #666; color: #666;
margin-bottom: 0;
} }
.label { .label {
font-size: $font-size-smaller; font-size: $font-size-smaller;
margin-top: 10px; margin-top: 10px;
color: #666;
} }
.legend-btn { .legend-btn {