Merge branch 'main' into feature-kafka-monitoring

This commit is contained in:
吴晟 Wu Sheng 2023-09-07 14:14:01 +08:00 committed by GitHub
commit fd6880ce9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 958 additions and 265 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

@ -14,7 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License. --> limitations under the License. -->
<template> <template>
<div class="nav-bar flex-h"> <div class="nav-bar flex-h">
<div class="title">{{ route.name === "ViewWidget" ? "" : appStore.pageTitle || pageName }}</div> <el-breadcrumb
:separator-icon="ArrowRight"
class="title flex-h"
v-if="pathNames.length"
:style="{ '--el-text-color-placeholder': '#666' }"
>
<el-breadcrumb-item
v-for="(path, index) in pathNames"
:key="index"
:replace="true"
:to="{ path: getName(path).path || '' }"
>
<el-dropdown size="small" placement="bottom" :persistent="false">
<span class="cp name">{{ getName(path).name }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="setName(p)" v-for="(p, index) in path" :key="index">
<span>{{ p.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-breadcrumb-item>
</el-breadcrumb>
<div class="title" v-else>{{ pageTitle }}</div>
<div class="app-config"> <div class="app-config">
<span class="red" v-show="timeRange">{{ t("timeTips") }}</span> <span class="red" v-show="timeRange">{{ t("timeTips") }}</span>
<TimePicker <TimePicker
@ -45,20 +69,47 @@ limitations under the License. -->
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import timeFormat from "@/utils/timeFormat"; import timeFormat from "@/utils/timeFormat";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { MetricCatalog } from "@/views/dashboard/data";
import type { DashboardItem } from "@/types/dashboard";
import router from "@/router";
import { ArrowRight } from "@element-plus/icons-vue";
/*global Indexable */ /*global Indexable */
const { t } = useI18n(); const { t, te } = useI18n();
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const route = useRoute(); const route = useRoute();
const pageName = ref<string>(""); const pathNames = ref<{ path?: string; name: string; selected: boolean }[][]>([]);
const timeRange = ref<number>(0); const timeRange = ref<number>(0);
const pageTitle = ref<string>("");
resetDuration(); resetDuration();
getVersion(); getVersion();
const setConfig = (value: string) => { getNavPaths();
pageName.value = value || "";
}; function getName(list: any[]) {
return list.find((d: any) => d.selected) || {};
}
function setName(item: any) {
pathNames.value = pathNames.value.map((list: { path?: string; name: string; selected: boolean }[]) => {
const p = list.find((i: any) => i.entity === item.entity && item.layer === i.layer && i.name === item.name);
if (p) {
list = list.map((d: any) => {
d.selected = false;
if (d.entity === item.entity && item.layer === d.layer && d.name === item.name) {
d.selected = true;
}
return d;
});
}
return list;
});
item.path && router.push(item.path);
}
function handleReload() { function handleReload() {
const gap = appStore.duration.end.getTime() - appStore.duration.start.getTime(); const gap = appStore.duration.end.getTime() - appStore.duration.start.getTime();
@ -73,19 +124,138 @@ limitations under the License. -->
} }
appStore.setDuration(timeFormat(val)); appStore.setDuration(timeFormat(val));
} }
setConfig(String(route.meta.title));
watch( function getNavPaths() {
() => route.meta.title, pathNames.value = [];
(title: unknown) => { pageTitle.value = "";
setConfig(String(title)); const dashboard = dashboardStore.currentDashboard;
},
); if (!(dashboard && dashboard.name)) {
updateNavTitle();
return;
}
const root =
dashboardStore.dashboards.filter((d: DashboardItem) => d.isRoot && dashboard.layer === d.layer)[0] || {};
for (const item of appStore.allMenus) {
if (item.subItems && item.subItems.length) {
for (const subItem of item.subItems) {
if (subItem.layer === root.layer) {
root.path = subItem.path;
}
}
} else {
if (item.layer === root.layer) {
root.path = item.path;
}
}
}
pathNames.value.push([{ ...root, selected: true }]);
if (dashboard.entity === MetricCatalog.ALL) {
return;
}
if (dashboard.entity === MetricCatalog.SERVICE) {
pathNames.value.push([
{
name: dashboard.name,
selected: true,
},
]);
return;
}
const serviceDashboards = dashboardStore.dashboards.filter(
(d: DashboardItem) => MetricCatalog.SERVICE === d.entity && dashboard.layer === d.layer,
);
if (!serviceDashboards.length) {
return;
}
const serviceId = route.params.serviceId;
const list = serviceDashboards.map((d: { path: string } & DashboardItem, index: number) => {
let path = `/dashboard/${d.layer}/${d.entity}/${d.name}`;
if (serviceId) {
path = `/dashboard/${d.layer}/${d.entity}/${serviceId}/${d.name}`;
}
const selected = index === 0;
return {
...d,
path,
selected,
};
});
pathNames.value.push(list);
const podId = route.params.podId;
if (dashboard.entity === MetricCatalog.ENDPOINT_RELATION) {
const endpointDashboards = dashboardStore.dashboards.filter(
(d: DashboardItem) => MetricCatalog.ENDPOINT === d.entity && dashboard.layer === d.layer,
);
const list = endpointDashboards.map((d: { path: string } & DashboardItem, index: number) => {
let path = `/dashboard/${d.layer}/${d.entity}/${d.name}`;
if (podId) {
path = `/dashboard/${d.layer}/${d.entity}/${serviceId}/${podId}/${d.name}`;
}
const selected = index === 0;
return {
...d,
path,
selected,
};
});
pathNames.value.push(list);
}
const destServiceId = route.params.destServiceId;
if (dashboard.entity === MetricCatalog.SERVICE_INSTANCE_RELATION) {
const serviceRelationDashboards = dashboardStore.dashboards.filter(
(d: DashboardItem) => MetricCatalog.SERVICE_RELATION === d.entity && dashboard.layer === d.layer,
);
const list = serviceRelationDashboards.map((d: { path: string } & DashboardItem, index: number) => {
let path = `/dashboard/${d.layer}/${d.entity}/${d.name}`;
if (destServiceId) {
path = `/dashboard/related/${d.layer}/${d.entity}/${serviceId}/${destServiceId}/${d.name}`;
}
const selected = index === 0;
return {
...d,
path,
selected,
};
});
pathNames.value.push(list);
}
if ([MetricCatalog.Process, MetricCatalog.PROCESS_RELATION].includes(dashboard.entity)) {
const InstanceDashboards = dashboardStore.dashboards.filter(
(d: DashboardItem) => MetricCatalog.SERVICE_INSTANCE === d.entity && dashboard.layer === d.layer,
);
const list = InstanceDashboards.map((d: { path: string } & DashboardItem, index: number) => {
let path = `/dashboard/${d.layer}/${d.entity}/${d.name}`;
if (podId) {
path = `/dashboard/${d.layer}/${d.entity}/${serviceId}/${podId}/${d.name}`;
}
const selected = index === 0;
return {
...d,
path,
selected,
};
});
pathNames.value.push(list);
}
pathNames.value.push([
{
name: dashboard.name,
selected: true,
},
]);
}
async function getVersion() { async function getVersion() {
const res = await appStore.fetchVersion(); const res = await appStore.fetchVersion();
if (res.errors) { if (res.errors) {
ElMessage.error(res.errors); ElMessage.error(res.errors);
} }
} }
function resetDuration() { function resetDuration() {
const { duration }: Indexable = route.params; const { duration }: Indexable = route.params;
if (duration) { if (duration) {
@ -99,10 +269,22 @@ limitations under the License. -->
appStore.updateUTC(d.utc); appStore.updateUTC(d.utc);
} }
} }
function updateNavTitle() {
const key = String(route.meta.i18nKey);
pageTitle.value = te(key) ? t(key) : String(route.meta.title);
}
watch(
() => [dashboardStore.currentDashboard, route.name],
() => {
getNavPaths();
},
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.nav-bar { .nav-bar {
padding: 5px 10px; padding: 5px;
text-align: left; text-align: left;
justify-content: space-between; justify-content: space-between;
background-color: #fafbfc; background-color: #fafbfc;
@ -120,11 +302,17 @@ limitations under the License. -->
.title { .title {
font-size: $font-size-normal; font-size: $font-size-normal;
font-weight: 500; font-weight: 500;
height: 28px;
line-height: 28px;
} }
.nav-tabs { .nav-tabs {
padding: 10px; padding: 10px;
} }
.name {
display: inline-block;
max-width: 250px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
</style> </style>

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

@ -32,7 +32,6 @@ interface AppState {
eventStack: (() => unknown)[]; eventStack: (() => unknown)[];
timer: Nullable<TimeoutHandle>; timer: Nullable<TimeoutHandle>;
autoRefresh: boolean; autoRefresh: boolean;
pageTitle: string;
version: string; version: string;
isMobile: boolean; isMobile: boolean;
reloadTimer: Nullable<IntervalHandle>; reloadTimer: Nullable<IntervalHandle>;
@ -53,7 +52,6 @@ export const appStore = defineStore({
eventStack: [], eventStack: [],
timer: null, timer: null,
autoRefresh: false, autoRefresh: false,
pageTitle: "",
version: "", version: "",
isMobile: false, isMobile: false,
reloadTimer: null, reloadTimer: null,
@ -146,9 +144,6 @@ export const appStore = defineStore({
setAutoRefresh(auto: boolean) { setAutoRefresh(auto: boolean) {
this.autoRefresh = auto; this.autoRefresh = auto;
}, },
setPageTitle(title: string) {
this.pageTitle = title;
},
runEventStack() { runEventStack() {
if (this.timer) { if (this.timer) {
clearTimeout(this.timer); clearTimeout(this.timer);

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() { async getEndpoints(keyword: string) {
const serviceId = useSelectorStore().currentService ? useSelectorStore().currentService.id : ""; const serviceId = useSelectorStore().currentService ? useSelectorStore().currentService.id : "";
if (!serviceId) { if (!serviceId) {
return; return;
@ -65,7 +65,7 @@ export const eventStore = defineStore({
const res: AxiosResponse = await graphql.query("queryEndpoints").params({ const res: AxiosResponse = await graphql.query("queryEndpoints").params({
serviceId, serviceId,
duration: useAppStoreWithOut().durationTime, duration: useAppStoreWithOut().durationTime,
keyword: "", keyword: keyword || "",
}); });
if (res.data.errors) { if (res.data.errors) {
return res.data; return res.data;

View File

@ -184,7 +184,7 @@ export const selectorStore = defineStore({
if (isRelation) { if (isRelation) {
this.currentDestPod = res.data.data.instance || null; this.currentDestPod = res.data.data.instance || null;
this.destPods = [res.data.data.instance]; this.destPods = [res.data.data.instance];
return; return res.data;
} }
this.currentPod = res.data.data.instance || null; this.currentPod = res.data.data.instance || null;
this.pods = [res.data.data.instance]; this.pods = [res.data.data.instance];
@ -199,16 +199,16 @@ export const selectorStore = defineStore({
const res: AxiosResponse = await graphql.query("queryEndpoint").params({ const res: AxiosResponse = await graphql.query("queryEndpoint").params({
endpointId, endpointId,
}); });
if (!res.data.errors) { if (res.data.errors) {
if (isRelation) { return res.data;
this.currentDestPod = res.data.data.endpoint || null;
this.destPods = [res.data.data.endpoint];
return;
}
this.currentPod = res.data.data.endpoint || null;
this.pods = [res.data.data.endpoint];
} }
if (isRelation) {
this.currentDestPod = res.data.data.endpoint || null;
this.destPods = [res.data.data.endpoint];
return res.data;
}
this.currentPod = res.data.data.endpoint || null;
this.pods = [res.data.data.endpoint];
return res.data; return res.data;
}, },
async getProcess(processId: string, isRelation?: boolean) { async getProcess(processId: string, isRelation?: boolean) {
@ -222,7 +222,7 @@ export const selectorStore = defineStore({
if (isRelation) { if (isRelation) {
this.currentDestProcess = res.data.data.process || null; this.currentDestProcess = res.data.data.process || null;
this.destProcesses = [res.data.data.process]; this.destProcesses = [res.data.data.process];
return; return res.data;
} }
this.currentProcess = res.data.data.process || null; this.currentProcess = res.data.data.process || null;
this.processes = [res.data.data.process]; this.processes = [res.data.data.process];

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

@ -212,6 +212,7 @@ div.vis-tooltip {
div:has(> a.menu-title) { div:has(> a.menu-title) {
display: none; display: none;
} }
.el-input-number .el-input__inner { .el-input-number .el-input__inner {
text-align: left !important; text-align: left !important;
} }

2
src/types/app.d.ts vendored
View File

@ -44,6 +44,8 @@ export type EventParams = {
dataType: string; dataType: string;
value: number | any[]; value: number | any[];
color: string; color: string;
event: Record<string, T>;
dataIndex: number;
event: any; event: any;
}; };

View File

@ -6,6 +6,8 @@ import '@vue/runtime-core'
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
DateCalendar: typeof import('./../components/DateCalendar.vue')['default'] DateCalendar: typeof import('./../components/DateCalendar.vue')['default']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCollapse: typeof import('element-plus/es')['ElCollapse'] ElCollapse: typeof import('element-plus/es')['ElCollapse']
@ -36,6 +38,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 +48,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

@ -55,8 +55,8 @@ export interface SegmentSpan {
component: string; component: string;
isError: boolean; isError: boolean;
layer: string; layer: string;
tags: any[]; tags: Recordable[];
logs: any[]; logs: Recordable[];
} }
export interface ProfileTaskCreationRequest { export interface ProfileTaskCreationRequest {

View File

@ -0,0 +1,31 @@
/**
* 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.
*/
export function deduplication(arr: any, labels: string[]) {
const map = new Map();
for (const i of arr) {
const key = labels
.map((d: string) => {
return i[d];
})
.join("");
if (!map.has(i[key])) {
map.set(i[key], i);
}
}
return [...map.values()];
}

View File

@ -19,12 +19,8 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAppStoreWithOut } from "@/store/modules/app";
import Header from "./alarm/Header.vue"; import Header from "./alarm/Header.vue";
import Content from "./alarm/Content.vue"; import Content from "./alarm/Content.vue";
const appStore = useAppStoreWithOut();
appStore.setPageTitle("Alerting");
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.alarm { .alarm {

View File

@ -19,13 +19,8 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useAppStoreWithOut } from "@/store/modules/app";
import Header from "./event/Header.vue"; import Header from "./event/Header.vue";
import Content from "./event/Content.vue"; import Content from "./event/Content.vue";
const appStore = useAppStoreWithOut();
appStore.setPageTitle("Events");
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.event { .event {

View File

@ -24,11 +24,9 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import Dashboard from "./dashboard/Edit.vue"; import Dashboard from "./dashboard/Edit.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useAppStoreWithOut } from "@/store/modules/app";
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const layer = ref<string>("GENERAL"); const layer = ref<string>("GENERAL");
@ -44,7 +42,6 @@ limitations under the License. -->
d.layer === dashboardStore.layerId && [EntityType[0].value, EntityType[1].value].includes(d.entity) && d.isRoot, d.layer === dashboardStore.layerId && [EntityType[0].value, EntityType[1].value].includes(d.entity) && d.isRoot,
); );
if (!item) { if (!item) {
appStore.setPageTitle(dashboardStore.layer);
dashboardStore.setCurrentDashboard(null); dashboardStore.setCurrentDashboard(null);
dashboardStore.setEntity(EntityType[1].value); dashboardStore.setEntity(EntityType[1].value);
return; return;

View File

@ -63,8 +63,6 @@ limitations under the License. -->
function handleItems(item: MenuOptions) { function handleItems(item: MenuOptions) {
currentItems.value = item; currentItems.value = item;
} }
appStore.setPageTitle("Marketplace");
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.menus { .menus {

View File

@ -66,7 +66,6 @@ limitations under the License. -->
const utcHour = ref<number>(appStore.utcHour); const utcHour = ref<number>(appStore.utcHour);
const utcMin = ref<number>(appStore.utcMin); const utcMin = ref<number>(appStore.utcMin);
appStore.setPageTitle("Setting");
const handleReload = () => { const handleReload = () => {
const gap = appStore.duration.end.getTime() - appStore.duration.start.getTime(); const gap = appStore.duration.end.getTime() - appStore.duration.start.getTime();
const dates: Date[] = [ const dates: Date[] = [

View File

@ -142,7 +142,79 @@ limitations under the License. -->
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url("../components/style.scss"); .timeline-table {
padding: 30px 20px 20px 40px;
flex-grow: 1;
overflow: auto;
height: 100%;
}
.time-line {
padding: 14px 30px;
min-height: 63px;
max-width: 132px;
}
.timeline-table-i {
padding: 10px 15px;
border-left: 4px solid #eee;
position: relative;
&::after {
content: "";
display: inline-block;
position: absolute;
width: 7px;
height: 7px;
left: -23px;
top: 25px;
border-radius: 4px;
background-color: #448dfe;
}
&::before {
content: "";
display: inline-block;
position: absolute;
width: 1px;
height: calc(100% + 11px);
top: 0;
left: -20px;
border-radius: 5px;
background-color: #448dfe99;
}
}
.timeline-table-i-scope {
display: inline-block;
padding: 0 8px;
border: 1px solid;
margin-top: -1px;
border-radius: 4px;
}
.timeline-item {
cursor: pointer;
margin-bottom: 9px;
}
.keys {
font-weight: bold;
display: inline-block;
width: 120px;
}
.source > span {
display: inline-block;
}
.source > div {
padding-left: 120px;
}
.uuid {
width: 280px;
}
.tips { .tips {
width: 100%; width: 100%;
@ -150,4 +222,29 @@ limitations under the License. -->
text-align: center; text-align: center;
font-size: $font-size-normal; font-size: $font-size-normal;
} }
.alarm-detail {
max-height: 600px;
overflow: auto;
ul {
min-height: 100px;
overflow: auto;
margin-bottom: 20px;
}
li {
cursor: pointer;
> span {
width: 160px;
height: 20px;
line-height: 20px;
text-align: center;
display: inline-block;
border-bottom: 1px solid $disabled-color;
overflow: hidden;
}
}
}
</style> </style>

View File

@ -71,7 +71,7 @@ limitations under the License. -->
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
/*global defineEmits, defineProps */ /*global defineEmits, defineProps, Recordable */
const emit = defineEmits(["update"]); const emit = defineEmits(["update"]);
const props = defineProps({ const props = defineProps({
type: { type: String, default: "TRACE" }, type: { type: String, default: "TRACE" },
@ -118,7 +118,7 @@ limitations under the License. -->
emit("update", { tagsMap, tagsList: tagsList.value }); emit("update", { tagsMap, tagsList: tagsList.value });
} }
async function fetchTagKeys() { async function fetchTagKeys() {
let resp: any = {}; let resp: Recordable = {};
if (props.type === "TRACE") { if (props.type === "TRACE") {
resp = await traceStore.getTagKeys(); resp = await traceStore.getTagKeys();
} else { } else {
@ -137,7 +137,7 @@ limitations under the License. -->
async function fetchTagValues() { async function fetchTagValues() {
const param = tags.value.split("=")[0]; const param = tags.value.split("=")[0];
let resp: any = {}; let resp: Recordable = {};
if (props.type === "TRACE") { if (props.type === "TRACE") {
resp = await traceStore.getTagValues(param); resp = await traceStore.getTagValues(param);
} else { } else {

View File

@ -1,115 +0,0 @@
/**
* 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.
*/
.timeline-table {
padding: 30px 20px 20px 40px;
flex-grow: 1;
overflow: auto;
height: 100%;
}
.time-line {
padding: 14px 30px;
min-height: 63px;
max-width: 132px;
}
.timeline-table-i {
padding: 10px 15px;
border-left: 4px solid #eee;
position: relative;
&::after {
content: "";
display: inline-block;
position: absolute;
width: 7px;
height: 7px;
left: -23px;
top: 25px;
border-radius: 4px;
background-color: #448dfe;
}
&::before {
content: "";
display: inline-block;
position: absolute;
width: 1px;
height: calc(100% + 11px);
top: 0;
left: -20px;
border-radius: 5px;
background-color: #448dfe99;
}
}
.timeline-table-i-scope {
display: inline-block;
padding: 0 8px;
border: 1px solid;
margin-top: -1px;
border-radius: 4px;
}
.timeline-item {
cursor: pointer;
margin-bottom: 9px;
}
.alarm-detail {
max-height: 600px;
overflow: auto;
ul {
min-height: 100px;
overflow: auto;
margin-bottom: 20px;
}
li {
cursor: pointer;
> span {
width: 160px;
height: 20px;
line-height: 20px;
text-align: center;
display: inline-block;
border-bottom: 1px solid $disabled-color;
overflow: hidden;
}
}
}
.keys {
font-weight: bold;
display: inline-block;
width: 120px;
}
.source > span {
display: inline-block;
}
.source > div {
padding-left: 120px;
}
.uuid {
width: 280px;
}

View File

@ -41,13 +41,12 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ref, defineComponent } from "vue"; import { ref, defineComponent, onUnmounted } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import GridLayout from "./panel/Layout.vue"; import GridLayout from "./panel/Layout.vue";
import Tool from "./panel/Tool.vue"; import Tool from "./panel/Tool.vue";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import Configuration from "./configuration"; import Configuration from "./configuration";
import type { LayoutConfig } from "@/types/dashboard"; import type { LayoutConfig } from "@/types/dashboard";
import WidgetLink from "./components/WidgetLink.vue"; import WidgetLink from "./components/WidgetLink.vue";
@ -57,7 +56,6 @@ limitations under the License. -->
components: { ...Configuration, GridLayout, Tool, WidgetLink }, components: { ...Configuration, GridLayout, Tool, WidgetLink },
setup() { setup() {
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const appStore = useAppStoreWithOut();
const { t } = useI18n(); const { t } = useI18n();
const p = useRoute().params; const p = useRoute().params;
const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`); const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`);
@ -77,7 +75,6 @@ limitations under the License. -->
const layout: any = c.configuration || {}; const layout: any = c.configuration || {};
dashboardStore.setLayout(setWidgetsID(layout.children || [])); dashboardStore.setLayout(setWidgetsID(layout.children || []));
appStore.setPageTitle(layout.name);
if (p.entity) { if (p.entity) {
dashboardStore.setCurrentDashboard({ dashboardStore.setCurrentDashboard({
layer: p.layerId, layer: p.layerId,
@ -114,6 +111,10 @@ limitations under the License. -->
} }
} }
onUnmounted(() => {
dashboardStore.setCurrentDashboard({});
});
return { return {
t, t,
handleClick, handleClick,

View File

@ -135,7 +135,6 @@ limitations under the License. -->
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { ElMessageBox, ElMessage } from "element-plus"; import { ElMessageBox, ElMessage } from "element-plus";
import { ElTable } from "element-plus"; import { ElTable } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import router from "@/router"; import router from "@/router";
import type { DashboardItem, LayoutConfig } from "@/types/dashboard"; import type { DashboardItem, LayoutConfig } from "@/types/dashboard";
@ -145,7 +144,6 @@ limitations under the License. -->
/*global Nullable*/ /*global Nullable*/
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const pageSize = 20; const pageSize = 20;
const dashboards = ref<DashboardItem[]>([]); const dashboards = ref<DashboardItem[]>([]);
@ -157,7 +155,6 @@ limitations under the License. -->
const multipleSelection = ref<DashboardItem[]>([]); const multipleSelection = ref<DashboardItem[]>([]);
const dashboardFile = ref<Nullable<HTMLDivElement>>(null); const dashboardFile = ref<Nullable<HTMLDivElement>>(null);
appStore.setPageTitle("Dashboard List");
const handleSelectionChange = (val: DashboardItem[]) => { const handleSelectionChange = (val: DashboardItem[]) => {
multipleSelection.value = val; multipleSelection.value = val;
}; };

View File

@ -53,12 +53,9 @@ limitations under the License. -->
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { EntityType } from "./data"; import { EntityType } from "./data";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
appStore.setPageTitle("Dashboard New");
const { t } = useI18n(); const { t } = useI18n();
const selectorStore = useSelectorStore(); const selectorStore = useSelectorStore();
const states = reactive({ const states = reactive({

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

@ -161,6 +161,7 @@ export enum MetricCatalog {
SERVICE_RELATION = "ServiceRelation", SERVICE_RELATION = "ServiceRelation",
SERVICE_INSTANCE_RELATION = "ServiceInstanceRelation", SERVICE_INSTANCE_RELATION = "ServiceInstanceRelation",
ENDPOINT_RELATION = "EndpointRelation", ENDPOINT_RELATION = "EndpointRelation",
Process = "Process",
PROCESS_RELATION = "ProcessRelation", PROCESS_RELATION = "ProcessRelation",
} }
export const EntityType = [ export const EntityType = [

View File

@ -294,4 +294,8 @@ limitations under the License. -->
max-height: 400px; max-height: 400px;
overflow: auto; overflow: auto;
} }
.link {
color: $active-color;
}
</style> </style>

View File

@ -216,7 +216,7 @@ limitations under the License. -->
width: 330px; width: 330px;
height: calc(100% - 10px); height: calc(100% - 10px);
overflow: auto; overflow: auto;
border-right: 1px solid rgba(0, 0, 0, 0.1); border-right: 1px solid rgb(0 0 0 / 10%);
} }
.item span { .item span {
@ -225,7 +225,7 @@ limitations under the License. -->
.profile-td { .profile-td {
padding: 10px 5px 10px 10px; padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07); border-bottom: 1px solid rgb(0 0 0 / 7%);
&.selected { &.selected {
background-color: #ededed; background-color: #ededed;
@ -253,13 +253,13 @@ limitations under the License. -->
.profile-tr { .profile-tr {
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.04); background-color: rgb(0 0 0 / 4%);
} }
} }
.profile-t-tool { .profile-t-tool {
padding: 10px 5px 10px 10px; padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07); border-bottom: 1px solid rgb(0 0 0 / 7%);
background: #f3f4f9; background: #f3f4f9;
width: 100%; width: 100%;
} }

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,8 +15,10 @@ 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">{{
<SelectSingle :value="currentMetric" :options="metrics" @change="changeMetric" class="selectors" /> t(dashboardStore.selectedGrid.metricMode === MetricModes.General ? "metrics" : "expressions")
}}</span>
<SelectSingle :value="currentMetric" :options="metricList" @change="changeMetric" class="selectors" />
</div> </div>
<div class="item mb-10"> <div class="item mb-10">
<span class="label">{{ t("unit") }}</span> <span class="label">{{ t("unit") }}</span>
@ -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: () => [] },
}); });
@ -74,8 +71,8 @@ limitations under the License. -->
const m = props.metrics.map((d: string) => { const m = props.metrics.map((d: string) => {
return { label: d, value: d }; return { label: d, value: d };
}); });
const metrics = ref<Option[]>(m.length ? m : [{ label: "", value: "" }]); const metricList = ref<Option[]>(m.length ? m : [{ label: "", value: "" }]);
const currentMetric = ref<string>(metrics.value[0].value); const currentMetric = ref<string>(metricList.value[0].value);
const currentConfig = ref<{ unit: string; calculation: string; label: string }>({ const currentConfig = ref<{ unit: string; calculation: string; label: string }>({
unit: "", unit: "",
calculation: "", calculation: "",
@ -109,7 +106,7 @@ limitations under the License. -->
} }
function changeMetric(val: string) { function changeMetric(val: string) {
currentMetric.value = val; currentMetric.value = val;
const index = metrics.value.findIndex((d: Option) => d.value === val); const index = metricList.value.findIndex((d: Option) => d.value === val);
currentIndex.value = index || 0; currentIndex.value = index || 0;
const config = getMetricConfig.value || []; const config = getMetricConfig.value || [];
@ -126,8 +123,8 @@ limitations under the License. -->
const m = props.metrics.map((d: string) => { const m = props.metrics.map((d: string) => {
return { label: d, value: d }; return { label: d, value: d };
}); });
metrics.value = m.length ? m : [{ label: "", value: "" }]; metricList.value = m.length ? m : [{ label: "", value: "" }];
currentMetric.value = metrics.value[0].value; currentMetric.value = metricList.value[0].value;
const config = getMetricConfig.value || []; const config = getMetricConfig.value || [];
currentIndex.value = 0; currentIndex.value = 0;
currentConfig.value = { currentConfig.value = {

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 {