diff --git a/commitlint.config.js b/commitlint.config.js index d5861f6b..6da72baa 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -44,6 +44,7 @@ module.exports = { "workflow", "types", "release", + "merge", ], ], }, diff --git a/src/components/Tags.vue b/src/components/Tags.vue new file mode 100644 index 00000000..90fb670c --- /dev/null +++ b/src/components/Tags.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/components/index.ts b/src/components/index.ts index 607c7ce7..2992ee49 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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); diff --git a/src/hooks/useExpressionsProcessor.ts b/src/hooks/useExpressionsProcessor.ts index 6855696f..3ddf8662 100644 --- a/src/hooks/useExpressionsProcessor.ts +++ b/src/hooks/useExpressionsProcessor.ts @@ -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 }; +} diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index db2eb4ab..1d983f65 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -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; diff --git a/src/locales/lang/es.ts b/src/locales/lang/es.ts index aa7953e6..b92ba40b 100644 --- a/src/locales/lang/es.ts +++ b/src/locales/lang/es.ts @@ -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; diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts index 430755e5..edc9a665 100644 --- a/src/locales/lang/zh.ts +++ b/src/locales/lang/zh.ts @@ -376,5 +376,9 @@ const msg = { menusManagement: "菜单", saveReload: "保存并重新加载页面", document: "文档", + metricMode: "指标模式", + addExpressions: "添加表达式", + expressions: "表达式", + unhealthyExpression: "非健康表达式", }; export default msg; diff --git a/src/store/modules/event.ts b/src/store/modules/event.ts index ac89f648..bb6ec794 100644 --- a/src/store/modules/event.ts +++ b/src/store/modules/event.ts @@ -57,7 +57,7 @@ export const eventStore = defineStore({ this.instances = [{ value: "", label: "All" }, ...res.data.data.pods] || [{ value: "", label: "All" }]; return res.data; }, - async getEndpoints(keyword?: string) { + async getEndpoints(keyword: string) { const serviceId = useSelectorStore().currentService ? useSelectorStore().currentService.id : ""; if (!serviceId) { return; diff --git a/src/store/modules/topology.ts b/src/store/modules/topology.ts index 50bae018..74bc7e15 100644 --- a/src/store/modules/topology.ts +++ b/src/store/modules/topology.ts @@ -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); diff --git a/src/types/components.d.ts b/src/types/components.d.ts index cae7912a..7e641d1c 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -36,6 +36,7 @@ declare module '@vue/runtime-core' { ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTag: typeof import('element-plus/es')['ElTag'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] Graph: typeof import('./../components/Graph.vue')['default'] Icon: typeof import('./../components/Icon.vue')['default'] @@ -45,6 +46,7 @@ declare module '@vue/runtime-core' { RouterView: typeof import('vue-router')['RouterView'] Selector: typeof import('./../components/Selector.vue')['default'] SelectSingle: typeof import('./../components/SelectSingle.vue')['default'] + Tags: typeof import('./../components/Tags.vue')['default'] TimePicker: typeof import('./../components/TimePicker.vue')['default'] } } diff --git a/src/views/dashboard/configuration/widget/metric/Index.vue b/src/views/dashboard/configuration/widget/metric/Index.vue index 5882f67e..ac316047 100644 --- a/src/views/dashboard/configuration/widget/metric/Index.vue +++ b/src/views/dashboard/configuration/widget/metric/Index.vue @@ -160,7 +160,7 @@ limitations under the License. --> }, }); const dashboardStore = useDashboardStore(); - const isExpression = ref(dashboardStore.selectedGrid.metricMode === MetricModes.Expression ? true : false); + const isExpression = ref(dashboardStore.selectedGrid.metricMode === MetricModes.Expression); const metrics = computed( () => (isExpression.value ? dashboardStore.selectedGrid.expressions : dashboardStore.selectedGrid.metrics) || [], ); diff --git a/src/views/dashboard/related/topology/components/Graph.vue b/src/views/dashboard/related/topology/components/Graph.vue index 22fc4078..14df6465 100644 --- a/src/views/dashboard/related/topology/components/Graph.vue +++ b/src/views/dashboard/related/topology/components/Graph.vue @@ -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; diff --git a/src/views/dashboard/related/topology/components/Metrics.vue b/src/views/dashboard/related/topology/components/Metrics.vue index 3296e13e..6ae9e1bb 100644 --- a/src/views/dashboard/related/topology/components/Metrics.vue +++ b/src/views/dashboard/related/topology/components/Metrics.vue @@ -15,7 +15,9 @@ limitations under the License. -->