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

View File

@ -15,13 +15,14 @@
* limitations under the License.
*/
import { RespFields } from "./data";
import { ExpressionResultType } from "@/views/dashboard/data";
import { EntityType, ExpressionResultType } from "@/views/dashboard/data";
import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import type { MetricConfigOpt } from "@/types/dashboard";
import type { Instance, Endpoint, Service } from "@/types/selector";
import type { Node, Call } from "@/types/topology";
export async function useExpressionsQueryProcessor(config: Indexable) {
function expressionsGraphqlPods() {
@ -312,3 +313,88 @@ export async function useExpressionsQueryPodsMetrics(
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",
saveReload: "Save and reload the page",
document: "Documentation",
metricMode: "Metric Mode",
addExpressions: "Add Expressions",
expressions: "Expression",
unhealthyExpression: "Unhealthy Expression",
};
export default msg;

View File

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

View File

@ -376,5 +376,9 @@ const msg = {
menusManagement: "菜单",
saveReload: "保存并重新加载页面",
document: "文档",
metricMode: "指标模式",
addExpressions: "添加表达式",
expressions: "表达式",
unhealthyExpression: "非健康表达式",
};
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" }];
return res.data;
},
async getEndpoints(keyword?: string) {
async getEndpoints(keyword: string) {
const serviceId = useSelectorStore().currentService ? useSelectorStore().currentService.id : "";
if (!serviceId) {
return;

View File

@ -24,6 +24,7 @@ import { useAppStoreWithOut } from "@/store/modules/app";
import type { AxiosResponse } from "axios";
import query from "@/graphql/fetch";
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
import { ElMessage } from "element-plus";
interface MetricVal {
@ -114,6 +115,16 @@ export const topologyStore = defineStore({
setLinkClientMetrics(m: MetricVal) {
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) {
const res = await this.getServicesTopology(serviceIds);
if (depth > 1) {
@ -321,6 +332,15 @@ export const topologyStore = defineStore({
this.setNodeMetricValue(res.data.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[]) {
if (!linkClientMetrics.length) {
this.setLinkClientMetrics({});
@ -353,6 +373,29 @@ export const topologyStore = defineStore({
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[]) {
if (!nodeMetrics.length) {
this.setNodeMetricValue({});
@ -369,6 +412,28 @@ export const topologyStore = defineStore({
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 } }) {
const res: AxiosResponse = await query(param);

View File

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

View File

@ -160,7 +160,7 @@ limitations under the License. -->
},
});
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(
() => (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 { useTopologyStore } from "@/store/modules/topology";
import { useDashboardStore } from "@/store/modules/dashboard";
import { EntityType, DepthList } from "../../../data";
import { EntityType, DepthList, MetricModes } from "../../../data";
import router from "@/router";
import { ElMessage } from "element-plus";
import Settings from "./Settings.vue";
@ -153,6 +153,7 @@ limitations under the License. -->
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
import { layout, circleIntersection, computeCallPos } from "./utils/layout";
import zoom from "../../components/utils/zoom";
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
/*global Nullable, defineProps */
const props = defineProps({
@ -220,19 +221,27 @@ limitations under the License. -->
}
async function update() {
if (settings.value.metricMode === MetricModes.Expression) {
topologyStore.queryNodeExpressions(settings.value.nodeExpressions || []);
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);
await initLegendMetrics();
draw();
tooltip.value = d3.select("#tooltip");
setNodeTools(settings.value.nodeDashboard);
}
function draw() {
const node = findMostFrequent(topologyStore.calls);
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()) {
return -1;
}
@ -352,9 +361,29 @@ limitations under the License. -->
}
async function initLegendMetrics() {
const ids = topologyStore.nodes.map((d: Node) => d.id);
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 && ids.length) {
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) {
@ -362,8 +391,19 @@ limitations under the License. -->
}
}
}
}
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) {
return icons.CUBE;
}
@ -381,7 +421,10 @@ limitations under the License. -->
return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
}
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 html = nodeMetrics.map((m, index) => {
const metric =
@ -404,10 +447,16 @@ limitations under the License. -->
.html(tipHtml);
}
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 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 metric = topologyStore.linkServerMetrics[m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id,
@ -667,7 +716,7 @@ limitations under the License. -->
padding: 0 15px;
border-radius: 3px;
color: $disabled-color;
border: 1px solid $disabled-color;
border: 1px solid #eee;
background-color: $theme-background;
box-shadow: #eee 1px 2px 10px;
transition: all 0.5ms linear;

View File

@ -15,7 +15,9 @@ limitations under the License. -->
<template>
<div class="config-panel">
<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" />
</div>
<div class="item mb-10">
@ -38,7 +40,7 @@ limitations under the License. -->
@change="changeConfigs({ label: currentConfig.label })"
/>
</div>
<div class="item mb-10">
<div class="item mb-10" v-if="dashboardStore.selectedGrid.metricMode === MetricModes.General">
<span class="label">{{ t("aggregation") }}</span>
<SelectSingle
:value="currentConfig.calculation"
@ -54,17 +56,12 @@ limitations under the License. -->
import { ref, computed, watch } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { CalculationOpts } from "../../../data";
import { CalculationOpts, MetricModes } from "../../../data";
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";
/*global defineEmits, defineProps */
const props = defineProps({
currentMetricConfig: {
type: Object as PropType<MetricConfigOpt>,
default: () => ({ unit: "" }),
},
type: { type: String, default: "" },
metrics: { type: Array as PropType<string[]>, default: () => [] },
});

View File

@ -22,6 +22,7 @@ limitations under the License. -->
:options="DepthList"
placeholder="Select a option"
@change="changeDepth"
size="small"
/>
</span>
<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 { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType, DepthList } from "../../../data";
import { EntityType, DepthList, MetricModes } from "../../../data";
import { ElMessage } from "element-plus";
import Sankey from "./Sankey.vue";
import Settings from "./Settings.vue";
@ -118,10 +119,16 @@ limitations under the License. -->
};
height.value = dom.height - 70;
width.value = dom.width - 5;
if (settings.value.metricMode === MetricModes.Expression) {
topologyStore.queryNodeExpressions(settings.value.nodeExpressions || []);
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() {
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
@ -265,7 +272,6 @@ limitations under the License. -->
<style lang="scss" scoped>
.sankey {
margin-top: 10px;
background-color: #333840 !important;
color: #ddd;
}
@ -275,7 +281,8 @@ limitations under the License. -->
right: 10px;
width: 400px;
height: 600px;
background-color: #2b3037;
border: 1px solid #eee;
background-color: $theme-background;
overflow: auto;
padding: 10px 15px;
border-radius: 3px;
@ -283,6 +290,7 @@ limitations under the License. -->
transition: all 0.5ms linear;
z-index: 99;
text-align: left;
box-shadow: #eee 1px 2px 10px;
}
.tool {
@ -299,8 +307,8 @@ limitations under the License. -->
text-align: center;
cursor: pointer;
transition: all 0.5ms linear;
background-color: #252a2f99;
color: #ddd;
background: rgb(0 0 0 / 30%);
color: $text-color;
display: inline-block;
border-radius: 3px;
}

View File

@ -23,6 +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";
/*global defineEmits, defineProps */
const props = defineProps({
@ -51,16 +52,16 @@ limitations under the License. -->
data: topologyStore.nodes,
links: topologyStore.calls,
label: {
color: "#fff",
color: "#666",
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: {
borderWidth: 0,
},
lineStyle: {
color: "source",
opacity: 0.12,
opacity: 0.3,
},
tooltip: {
position: "bottom",
@ -75,8 +76,14 @@ limitations under the License. -->
};
}
function linkTooltip(data: Call) {
const clientMetrics: string[] = Object.keys(topologyStore.linkClientMetrics);
const serverMetrics: string[] = Object.keys(topologyStore.linkServerMetrics);
const clientMetrics: string[] =
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 linkClientMetricConfig: MetricConfigOpt[] = props.settings.linkClientMetricConfig || [];
@ -108,7 +115,10 @@ limitations under the License. -->
}
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 html = nodeMetrics.map((m, index) => {
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
limitations under the License. -->
<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">
<h5 class="title">{{ t("callSettings") }}</h5>
<div class="label">{{ t("linkDashboard") }}</div>
@ -27,16 +38,34 @@ limitations under the License. -->
/>
<div class="label">
<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>
<span @click="setConfigType('linkServerMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span>
</template>
<Metrics :type="configType" :metrics="states.linkServerMetrics" @update="changeLinkServerMetrics" />
<Metrics
:type="configType"
:metrics="isExpression ? states.linkServerExpressions : states.linkServerMetrics"
@update="updateSettings"
/>
</el-popover>
</div>
<div v-if="isExpression">
<Tags
:tags="states.linkServerExpressions"
:vertical="true"
:text="t('addExpressions')"
@change="(param) => changeLinkServerExpressions(param)"
/>
</div>
<Selector
v-else
class="inputs"
:multiple="true"
:value="states.linkServerMetrics"
@ -48,16 +77,34 @@ limitations under the License. -->
<span v-show="dashboardStore.entity !== EntityType[2].value">
<div class="label">
<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>
<span @click="setConfigType('linkClientMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span>
</template>
<Metrics :type="configType" :metrics="states.linkClientMetrics" @update="changeLinkClientMetrics" />
<Metrics
:type="configType"
:metrics="isExpression ? states.linkClientExpressions : states.linkClientMetrics"
@update="updateSettings"
/>
</el-popover>
</div>
<div v-if="isExpression">
<Tags
:tags="states.linkClientExpressions"
:vertical="true"
:text="t('addExpressions')"
@change="(param) => changeLinkClientExpressions(param)"
/>
</div>
<Selector
v-else
class="inputs"
:multiple="true"
:value="states.linkClientMetrics"
@ -110,16 +157,34 @@ limitations under the License. -->
</div>
<div class="label">
<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>
<span @click="setConfigType('nodeMetricConfig')">
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
</span>
</template>
<Metrics :type="configType" :metrics="states.nodeMetrics" @update="changeNodeMetrics" />
<Metrics
:type="configType"
:metrics="isExpression ? states.nodeExpressions : states.nodeMetrics"
@update="updateSettings"
/>
</el-popover>
</div>
<div v-if="isExpression">
<Tags
:tags="states.nodeExpressions"
:vertical="true"
:text="t('addExpressions')"
@change="(param) => changeNodeExpressions(param)"
/>
</div>
<Selector
v-else
class="inputs"
:multiple="true"
:value="states.nodeMetrics"
@ -131,8 +196,26 @@ limitations under the License. -->
</div>
<div class="legend-settings" v-show="isService">
<h5 class="title">{{ t("legendSettings") }}</h5>
<div class="label">{{ t("conditions") }}</div>
<div v-for="(metric, index) of legend.metric" :key="metric.name + index">
<span v-if="isExpression">
<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
class="item"
:value="metric.name"
@ -163,14 +246,16 @@ limitations under the License. -->
class="cp"
iconName="add_circle_outlinecontrol_point"
size="middle"
v-show="index === legend.metric.length - 1 && legend.metric.length < 5"
v-show="index === legend.length - 1 && legend.length < 5"
@click="addMetric"
/>
</span>
<div v-show="index !== legend.metric.length - 1">&&</div>
<div v-show="index !== legend.length - 1">&&</div>
</div>
<span v-if="!isExpression">
<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>
<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">
@ -184,12 +269,20 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard";
import { useTopologyStore } from "@/store/modules/topology";
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 { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
import { useQueryTopologyExpressionsProcessor } from "@/hooks/useExpressionsProcessor";
import type { Node } from "@/types/topology";
import type { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
import { EntityType, LegendOpt, MetricsType } from "../../../data";
import Metrics from "./Metrics.vue";
/*global defineEmits */
@ -198,6 +291,7 @@ limitations under the License. -->
const dashboardStore = useDashboardStore();
const topologyStore = useTopologyStore();
const { selectedGrid } = dashboardStore;
const isExpression = ref<boolean>(dashboardStore.selectedGrid.metricMode === MetricModes.Expression);
const nodeDashboard =
selectedGrid.nodeDashboard && selectedGrid.nodeDashboard.length ? selectedGrid.nodeDashboard : "";
const isService = [EntityType[0].value, EntityType[1].value].includes(dashboardStore.entity);
@ -220,6 +314,9 @@ limitations under the License. -->
linkMetricList: Option[];
linkDashboards: (DashboardItem & { label: string; value: string })[];
nodeDashboards: (DashboardItem & { label: string; value: string })[];
linkServerExpressions: string[];
linkClientExpressions: string[];
nodeExpressions: string[];
}>({
linkDashboard: selectedGrid.linkDashboard || "",
nodeDashboard: selectedGrid.nodeDashboard || [],
@ -230,13 +327,15 @@ limitations under the License. -->
linkMetricList: [],
linkDashboards: [],
nodeDashboards: [],
linkServerExpressions: selectedGrid.linkServerExpressions || [],
linkClientExpressions: selectedGrid.linkClientExpressions || [],
nodeExpressions: selectedGrid.nodeExpressions || [],
});
const l = selectedGrid.legend && selectedGrid.legend.length;
const legend = reactive<{
metric: { name: string; condition: string; value: string }[];
}>({
metric: l ? selectedGrid.legend : [{ name: "", condition: "", value: "" }],
});
const legend = ref<{ name: string; condition: string; value: string }[]>(
l ? selectedGrid.legend : [{ name: "", condition: "", value: "" }],
);
const legendMQE = ref<{ expression: string }>(selectedGrid.legendMQE || { expression: "" });
const configType = ref<string>("");
const description = reactive<any>(selectedGrid.description || {});
@ -285,18 +384,35 @@ limitations under the License. -->
}
async function setLegend() {
updateSettings();
const ids = topologyStore.nodes.map((d: Node) => d.id);
const names = dashboardStore.selectedGrid.legend.map((d: any) => d.name);
if (isExpression.value) {
const expression = dashboardStore.selectedGrid.legendMQE && dashboardStore.selectedGrid.legendMQE.expression;
if (!expression) {
emit("updateNodes");
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 = 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) {
ElMessage.error(res.errors);
}
}
emit("updateNodes");
}
function changeNodeDashboard(opt: any) {
@ -308,7 +424,7 @@ limitations under the License. -->
updateSettings();
}
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) {
items[index].scope = opt[0].value;
@ -340,7 +456,13 @@ limitations under the License. -->
updateSettings();
}
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 = {
...dashboardStore.selectedGrid,
linkDashboard: states.linkDashboard,
@ -350,7 +472,12 @@ limitations under the License. -->
linkServerMetrics: states.linkServerMetrics,
linkClientMetrics: states.linkClientMetrics,
nodeMetrics: states.nodeMetrics,
linkServerExpressions: states.linkServerExpressions,
linkClientExpressions: states.linkClientExpressions,
nodeExpressions: states.nodeExpressions,
metricMode: isExpression.value ? MetricModes.Expression : MetricModes.General,
legend: metrics,
legendMQE: legendMQE.value,
...metricConfig,
description,
};
@ -378,6 +505,30 @@ limitations under the License. -->
}
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) {
const opt = options.map((d: Option) => d.value);
const index = states.linkClientMetrics.findIndex((d: any) => !opt.includes(d));
@ -419,18 +570,49 @@ limitations under the License. -->
topologyStore.queryNodeMetrics(states.nodeMetrics);
}
function deleteMetric(index: number) {
if (legend.metric.length === 1) {
legend.metric = [{ name: "", condition: "", value: "" }];
if (legend.value.length === 1) {
legend.value = [{ name: "", condition: "", value: "" }];
return;
}
legend.metric.splice(index, 1);
legend.value.splice(index, 1);
}
function addMetric() {
legend.metric.push({ name: "", condition: "", value: "" });
legend.value.push({ name: "", condition: "", value: "" });
}
function setConfigType(type: string) {
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>
<style lang="scss" scoped>
.link-settings {
@ -442,6 +624,11 @@ limitations under the License. -->
width: 355px;
}
.legend-inputs {
margin-top: 8px;
width: 310px;
}
.item {
width: 130px;
margin-top: 5px;
@ -453,13 +640,14 @@ limitations under the License. -->
}
.title {
margin-bottom: 0;
color: #666;
margin-bottom: 0;
}
.label {
font-size: $font-size-smaller;
margin-top: 10px;
color: #666;
}
.legend-btn {