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",
"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

@ -14,7 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<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">
<span class="red" v-show="timeRange">{{ t("timeTips") }}</span>
<TimePicker
@ -45,20 +69,47 @@ limitations under the License. -->
import { useI18n } from "vue-i18n";
import timeFormat from "@/utils/timeFormat";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useDashboardStore } from "@/store/modules/dashboard";
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 */
const { t } = useI18n();
const { t, te } = useI18n();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const route = useRoute();
const pageName = ref<string>("");
const pathNames = ref<{ path?: string; name: string; selected: boolean }[][]>([]);
const timeRange = ref<number>(0);
const pageTitle = ref<string>("");
resetDuration();
getVersion();
const setConfig = (value: string) => {
pageName.value = value || "";
};
getNavPaths();
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() {
const gap = appStore.duration.end.getTime() - appStore.duration.start.getTime();
@ -73,19 +124,138 @@ limitations under the License. -->
}
appStore.setDuration(timeFormat(val));
}
setConfig(String(route.meta.title));
watch(
() => route.meta.title,
(title: unknown) => {
setConfig(String(title));
},
);
function getNavPaths() {
pathNames.value = [];
pageTitle.value = "";
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() {
const res = await appStore.fetchVersion();
if (res.errors) {
ElMessage.error(res.errors);
}
}
function resetDuration() {
const { duration }: Indexable = route.params;
if (duration) {
@ -99,10 +269,22 @@ limitations under the License. -->
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>
<style lang="scss" scoped>
.nav-bar {
padding: 5px 10px;
padding: 5px;
text-align: left;
justify-content: space-between;
background-color: #fafbfc;
@ -120,11 +302,17 @@ limitations under the License. -->
.title {
font-size: $font-size-normal;
font-weight: 500;
height: 28px;
line-height: 28px;
}
.nav-tabs {
padding: 10px;
}
.name {
display: inline-block;
max-width: 250px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
</style>

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

@ -32,7 +32,6 @@ interface AppState {
eventStack: (() => unknown)[];
timer: Nullable<TimeoutHandle>;
autoRefresh: boolean;
pageTitle: string;
version: string;
isMobile: boolean;
reloadTimer: Nullable<IntervalHandle>;
@ -53,7 +52,6 @@ export const appStore = defineStore({
eventStack: [],
timer: null,
autoRefresh: false,
pageTitle: "",
version: "",
isMobile: false,
reloadTimer: null,
@ -146,9 +144,6 @@ export const appStore = defineStore({
setAutoRefresh(auto: boolean) {
this.autoRefresh = auto;
},
setPageTitle(title: string) {
this.pageTitle = title;
},
runEventStack() {
if (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" }];
return res.data;
},
async getEndpoints() {
async getEndpoints(keyword: string) {
const serviceId = useSelectorStore().currentService ? useSelectorStore().currentService.id : "";
if (!serviceId) {
return;
@ -65,7 +65,7 @@ export const eventStore = defineStore({
const res: AxiosResponse = await graphql.query("queryEndpoints").params({
serviceId,
duration: useAppStoreWithOut().durationTime,
keyword: "",
keyword: keyword || "",
});
if (res.data.errors) {
return res.data;

View File

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

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

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

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

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

View File

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

@ -55,8 +55,8 @@ export interface SegmentSpan {
component: string;
isError: boolean;
layer: string;
tags: any[];
logs: any[];
tags: Recordable[];
logs: Recordable[];
}
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>
</template>
<script lang="ts" setup>
import { useAppStoreWithOut } from "@/store/modules/app";
import Header from "./alarm/Header.vue";
import Content from "./alarm/Content.vue";
const appStore = useAppStoreWithOut();
appStore.setPageTitle("Alerting");
</script>
<style lang="scss" scoped>
.alarm {

View File

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

View File

@ -24,11 +24,9 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard";
import Dashboard from "./dashboard/Edit.vue";
import { useI18n } from "vue-i18n";
import { useAppStoreWithOut } from "@/store/modules/app";
const route = useRoute();
const { t } = useI18n();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
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,
);
if (!item) {
appStore.setPageTitle(dashboardStore.layer);
dashboardStore.setCurrentDashboard(null);
dashboardStore.setEntity(EntityType[1].value);
return;

View File

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

View File

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

View File

@ -142,7 +142,79 @@ limitations under the License. -->
}
</script>
<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 {
width: 100%;
@ -150,4 +222,29 @@ limitations under the License. -->
text-align: center;
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>

View File

@ -71,7 +71,7 @@ limitations under the License. -->
import { ElMessage } from "element-plus";
import { useAppStoreWithOut } from "@/store/modules/app";
/*global defineEmits, defineProps */
/*global defineEmits, defineProps, Recordable */
const emit = defineEmits(["update"]);
const props = defineProps({
type: { type: String, default: "TRACE" },
@ -118,7 +118,7 @@ limitations under the License. -->
emit("update", { tagsMap, tagsList: tagsList.value });
}
async function fetchTagKeys() {
let resp: any = {};
let resp: Recordable = {};
if (props.type === "TRACE") {
resp = await traceStore.getTagKeys();
} else {
@ -137,7 +137,7 @@ limitations under the License. -->
async function fetchTagValues() {
const param = tags.value.split("=")[0];
let resp: any = {};
let resp: Recordable = {};
if (props.type === "TRACE") {
resp = await traceStore.getTagValues(param);
} 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>
</template>
<script lang="ts">
import { ref, defineComponent } from "vue";
import { ref, defineComponent, onUnmounted } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import GridLayout from "./panel/Layout.vue";
import Tool from "./panel/Tool.vue";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import Configuration from "./configuration";
import type { LayoutConfig } from "@/types/dashboard";
import WidgetLink from "./components/WidgetLink.vue";
@ -57,7 +56,6 @@ limitations under the License. -->
components: { ...Configuration, GridLayout, Tool, WidgetLink },
setup() {
const dashboardStore = useDashboardStore();
const appStore = useAppStoreWithOut();
const { t } = useI18n();
const p = useRoute().params;
const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`);
@ -77,7 +75,6 @@ limitations under the License. -->
const layout: any = c.configuration || {};
dashboardStore.setLayout(setWidgetsID(layout.children || []));
appStore.setPageTitle(layout.name);
if (p.entity) {
dashboardStore.setCurrentDashboard({
layer: p.layerId,
@ -114,6 +111,10 @@ limitations under the License. -->
}
}
onUnmounted(() => {
dashboardStore.setCurrentDashboard({});
});
return {
t,
handleClick,

View File

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

View File

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

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

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

View File

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

View File

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

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() {
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
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,18 +361,49 @@ limitations under the License. -->
}
async function initLegendMetrics() {
const ids = topologyStore.nodes.map((d: Node) => d.id);
const names = props.config.legend.map((d: any) => d.name);
if (names.length && ids.length) {
const param = await useQueryTopologyMetrics(names, ids);
const res = await topologyStore.getLegendMetrics(param);
if (!topologyStore.nodes.length) {
return;
}
if (settings.value.metricMode === MetricModes.Expression) {
const expression = props.config.legendMQE && props.config.legendMQE.expression;
if (!expression) {
return;
}
const { getExpressionQuery } = useQueryTopologyExpressionsProcessor([expression], topologyStore.nodes);
const param = getExpressionQuery();
const res = await topologyStore.getNodeExpressionValue(param);
if (res.errors) {
ElMessage.error(res.errors);
} else {
topologyStore.setLegendValues([expression], res.data);
}
} else {
const names = props.config.legend.map((d: any) => d.name);
if (!names.length) {
return;
}
const ids = topologyStore.nodes.map((d: Node) => d.id);
if (ids.length) {
const param = await useQueryTopologyMetrics(names, ids);
const res = await topologyStore.getLegendMetrics(param);
if (res.errors) {
ElMessage.error(res.errors);
}
}
}
}
function getNodeStatus(d: any) {
const legend = 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,8 +15,10 @@ limitations under the License. -->
<template>
<div class="config-panel">
<div class="item mb-10">
<span class="label">{{ t("metrics") }}</span>
<SelectSingle :value="currentMetric" :options="metrics" @change="changeMetric" class="selectors" />
<span class="label">{{
t(dashboardStore.selectedGrid.metricMode === MetricModes.General ? "metrics" : "expressions")
}}</span>
<SelectSingle :value="currentMetric" :options="metricList" @change="changeMetric" class="selectors" />
</div>
<div class="item mb-10">
<span class="label">{{ t("unit") }}</span>
@ -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: () => [] },
});
@ -74,8 +71,8 @@ limitations under the License. -->
const m = props.metrics.map((d: string) => {
return { label: d, value: d };
});
const metrics = ref<Option[]>(m.length ? m : [{ label: "", value: "" }]);
const currentMetric = ref<string>(metrics.value[0].value);
const metricList = ref<Option[]>(m.length ? m : [{ label: "", value: "" }]);
const currentMetric = ref<string>(metricList.value[0].value);
const currentConfig = ref<{ unit: string; calculation: string; label: string }>({
unit: "",
calculation: "",
@ -109,7 +106,7 @@ limitations under the License. -->
}
function changeMetric(val: string) {
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;
const config = getMetricConfig.value || [];
@ -126,8 +123,8 @@ limitations under the License. -->
const m = props.metrics.map((d: string) => {
return { label: d, value: d };
});
metrics.value = m.length ? m : [{ label: "", value: "" }];
currentMetric.value = metrics.value[0].value;
metricList.value = m.length ? m : [{ label: "", value: "" }];
currentMetric.value = metricList.value[0].value;
const config = getMetricConfig.value || [];
currentIndex.value = 0;
currentConfig.value = {

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,9 +119,15 @@ limitations under the License. -->
};
height.value = dom.height - 70;
width.value = dom.width - 5;
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
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() {
@ -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>
<div class="label">Healthy Description</div>
<el-input v-model="description.healthy" placeholder="Please input description" size="small" class="mt-5" />
<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 (!names.length) {
emit("updateNodes");
return;
}
const param = await useQueryTopologyMetrics(names, ids);
const res = await topologyStore.getLegendMetrics(param);
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);
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 {