You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2022/03/26 14:52:46 UTC

[skywalking-booster-ui] branch main updated: feat: Implement custom configurations for metrics on dashboards and topology (#39)

This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-booster-ui.git


The following commit(s) were added to refs/heads/main by this push:
     new c00d5d2  feat: Implement custom configurations for metrics on dashboards and topology (#39)
c00d5d2 is described below

commit c00d5d2a051ca9860124641543220128d434930c
Author: Fine0830 <fi...@outlook.com>
AuthorDate: Sat Mar 26 22:52:43 2022 +0800

    feat: Implement custom configurations for metrics on dashboards and topology (#39)
---
 .../icons/{clearclose.svg => arrow_drop_down.svg}  |   3 +-
 src/assets/icons/cancel.svg                        |   1 -
 src/assets/icons/clearclose.svg                    |   1 -
 .../{createmode_editedit.svg => mode_edit.svg}     |   1 -
 src/components/Select.vue                          | 177 ++++++++++++++++++++
 src/components/index.ts                            |   2 +
 src/hooks/data.ts                                  |   9 +
 src/hooks/useProcessor.ts                          |  89 +++++-----
 src/locales/lang/en.ts                             |   5 +-
 src/locales/lang/zh.ts                             |   5 +-
 src/store/data.ts                                  |   1 +
 src/types/dashboard.ts                             |  11 +-
 src/views/dashboard/configuration/Text.vue         |  20 +++
 src/views/dashboard/configuration/Widget.vue       |  11 +-
 .../configuration/widget/StandardOptions.vue       | 186 ---------------------
 .../configuration/widget/graph-styles/Card.vue     |  14 +-
 .../widget/{MetricOptions.vue => metric/Index.vue} |  59 ++++++-
 .../configuration/widget/metric/Standard.vue       | 129 ++++++++++++++
 src/views/dashboard/controls/Text.vue              |  41 ++---
 src/views/dashboard/controls/Widget.vue            |   4 +-
 src/views/dashboard/data.ts                        |  14 +-
 src/views/dashboard/graphs/Card.vue                |  20 ++-
 src/views/dashboard/graphs/EndpointList.vue        |  27 ++-
 src/views/dashboard/graphs/InstanceList.vue        |  23 ++-
 src/views/dashboard/graphs/ServiceList.vue         |  22 ++-
 .../related/topology/components/Graph.vue          |  46 +++--
 .../related/topology/components/Metrics.vue        | 170 +++++++++++++++++++
 .../related/topology/components/PodTopology.vue    |   2 +-
 .../related/topology/components/Sankey.vue         |  65 ++++---
 .../related/topology/components/Settings.vue       | 157 +++++++++++++----
 30 files changed, 949 insertions(+), 366 deletions(-)

diff --git a/src/assets/icons/clearclose.svg b/src/assets/icons/arrow_drop_down.svg
similarity index 81%
copy from src/assets/icons/clearclose.svg
copy to src/assets/icons/arrow_drop_down.svg
index af9e3a0..63151de 100644
--- a/src/assets/icons/clearclose.svg
+++ b/src/assets/icons/arrow_drop_down.svg
@@ -13,6 +13,5 @@ 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. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-<title>clearclose</title>
-<path d="M18.984 6.422l-5.578 5.578 5.578 5.578-1.406 1.406-5.578-5.578-5.578 5.578-1.406-1.406 5.578-5.578-5.578-5.578 1.406-1.406 5.578 5.578 5.578-5.578z"></path>
+<path d="M6.984 9.984h10.031l-5.016 5.016z"></path>
 </svg>
diff --git a/src/assets/icons/cancel.svg b/src/assets/icons/cancel.svg
index 00ca795..f52fb3d 100644
--- a/src/assets/icons/cancel.svg
+++ b/src/assets/icons/cancel.svg
@@ -13,6 +13,5 @@ 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. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-<title>cancel</title>
 <path d="M17.016 15.609l-3.609-3.609 3.609-3.609-1.406-1.406-3.609 3.609-3.609-3.609-1.406 1.406 3.609 3.609-3.609 3.609 1.406 1.406 3.609-3.609 3.609 3.609zM12 2.016q4.125 0 7.055 2.93t2.93 7.055-2.93 7.055-7.055 2.93-7.055-2.93-2.93-7.055 2.93-7.055 7.055-2.93z"></path>
 </svg>
diff --git a/src/assets/icons/clearclose.svg b/src/assets/icons/clearclose.svg
index af9e3a0..ddc3519 100644
--- a/src/assets/icons/clearclose.svg
+++ b/src/assets/icons/clearclose.svg
@@ -13,6 +13,5 @@ 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. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-<title>clearclose</title>
 <path d="M18.984 6.422l-5.578 5.578 5.578 5.578-1.406 1.406-5.578-5.578-5.578 5.578-1.406-1.406 5.578-5.578-5.578-5.578 1.406-1.406 5.578 5.578 5.578-5.578z"></path>
 </svg>
diff --git a/src/assets/icons/createmode_editedit.svg b/src/assets/icons/mode_edit.svg
similarity index 96%
rename from src/assets/icons/createmode_editedit.svg
rename to src/assets/icons/mode_edit.svg
index 1a1f2d8..51f6cef 100644
--- a/src/assets/icons/createmode_editedit.svg
+++ b/src/assets/icons/mode_edit.svg
@@ -13,6 +13,5 @@ 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. -->
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-<title>createmode_editedit</title>
 <path d="M20.719 7.031l-1.828 1.828-3.75-3.75 1.828-1.828q0.281-0.281 0.703-0.281t0.703 0.281l2.344 2.344q0.281 0.281 0.281 0.703t-0.281 0.703zM3 17.25l11.063-11.063 3.75 3.75-11.063 11.063h-3.75v-3.75z"></path>
 </svg>
diff --git a/src/components/Select.vue b/src/components/Select.vue
new file mode 100644
index 0000000..0c101d5
--- /dev/null
+++ b/src/components/Select.vue
@@ -0,0 +1,177 @@
+<!-- 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>
+  <div class="bar-select cp flex-h" :class="{ active: visible }">
+    <div class="bar-i" @click="setPopper">
+      <span v-if="selected.value">
+        {{ selected.label }}
+      </span>
+      <span class="no-data" v-else>Please select a option</span>
+      <span class="remove-icon" @click="removeSelected" v-if="clearable">
+        ×
+      </span>
+    </div>
+    <div class="opt-wrapper" v-show="visible">
+      <div
+        class="opt ell"
+        @click="handleSelect(i)"
+        :class="{ 'select-disabled': selected.value === i.value }"
+        v-for="i in options"
+        :key="i.value"
+      >
+        {{ i.label }}
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref, watch } from "vue";
+import type { PropType } from "vue";
+import { Option } from "@/types/app";
+
+/*global  defineProps, defineEmits*/
+const emit = defineEmits(["change"]);
+const props = defineProps({
+  options: {
+    type: Array as PropType<(Option & { disabled: boolean })[]>,
+    default: () => [],
+  },
+  value: {
+    type: String as PropType<string>,
+    default: () => "",
+  },
+  clearable: { type: Boolean, default: false },
+});
+const visible = ref<boolean>(false);
+const opt = props.options.find((d: Option) => props.value === d.value);
+const selected = ref<Option>(opt || { label: "", value: "" });
+
+function handleSelect(i: Option) {
+  selected.value = i;
+  emit("change", i.value);
+}
+function removeSelected() {
+  selected.value = { label: "", value: "" };
+  emit("change", "");
+}
+watch(
+  () => props.value,
+  (data) => {
+    const opt = props.options.find((d: Option) => data === d.value);
+    selected.value = opt || { label: "", value: "" };
+  }
+);
+document.body.addEventListener("click", handleClick, false);
+
+function handleClick() {
+  visible.value = false;
+}
+function setPopper(event: any) {
+  event.stopPropagation();
+  visible.value = !visible.value;
+}
+</script>
+<style lang="scss" scoped>
+.bar-select {
+  position: relative;
+  justify-content: space-between;
+  border: 1px solid #ddd;
+  background: #fff;
+  border-radius: 3px;
+  color: #000;
+  font-size: 12px;
+  height: 24px;
+
+  .selected {
+    padding: 0 3px;
+    border-radius: 3px;
+    margin: 3px;
+    color: #409eff;
+    background-color: #fafafa;
+    border: 1px solid #e8e8e8;
+    text-align: center;
+  }
+}
+
+.no-data {
+  color: #c0c4cc;
+}
+
+.bar-i {
+  height: 100%;
+  width: 100%;
+  padding: 2px 10px;
+  overflow: auto;
+  color: #606266;
+  position: relative;
+
+  &:hover {
+    .remove-icon {
+      display: block;
+    }
+  }
+}
+
+.remove-icon {
+  position: absolute;
+  right: 5px;
+  top: 0;
+  font-size: 14px;
+  display: none;
+  color: #aaa;
+  cursor: pointer;
+}
+
+.opt-wrapper {
+  color: #606266;
+  position: absolute;
+  top: 26px;
+  left: 0;
+  background: #fff;
+  box-shadow: 0 1px 6px rgba(99, 99, 99, 0.2);
+  border: 1px solid #ddd;
+  width: 100%;
+  border-radius: 0 0 3px 3px;
+  border-right-width: 1px !important;
+  z-index: 10;
+  overflow: auto;
+  max-height: 200px;
+  padding-bottom: 2px;
+
+  .close {
+    position: absolute;
+    right: 10px;
+    top: 12px;
+    opacity: 0.6;
+
+    &:hover {
+      opacity: 1;
+    }
+  }
+}
+
+.opt {
+  padding: 7px 15px;
+
+  &.select-disabled {
+    color: #409eff;
+    cursor: not-allowed;
+  }
+
+  &:hover {
+    background-color: #f5f5f5;
+  }
+}
+</style>
diff --git a/src/components/index.ts b/src/components/index.ts
index 9a17614..0ce63ae 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -19,6 +19,7 @@ import TimePicker from "./TimePicker.vue";
 import Selector from "./Selector.vue";
 import Graph from "./Graph.vue";
 import Radio from "./Radio.vue";
+import SelectSingle from "./Select.vue";
 import type { App } from "vue";
 import VueGridLayout from "vue-grid-layout";
 
@@ -29,6 +30,7 @@ const components: { [key: string]: any } = {
   Selector,
   Graph,
   Radio,
+  SelectSingle,
 };
 const componentsName: string[] = Object.keys(components);
 
diff --git a/src/hooks/data.ts b/src/hooks/data.ts
index 8914aff..5f1fdd8 100644
--- a/src/hooks/data.ts
+++ b/src/hooks/data.ts
@@ -22,6 +22,15 @@ export enum MetricQueryTypes {
   READHEATMAP = "readHeatMap",
   ReadSampledRecords = "readSampledRecords",
 }
+
+export enum Calculations {
+  Percentage = "percentage",
+  ByteToKB = "byteToKB",
+  Apdex = "apdex",
+  Precision = "precision",
+  ConvertSeconds = "convertSeconds",
+  ConvertMilliseconds = "convertMilliseconds",
+}
 export enum sizeEnum {
   XS = "XS",
   SM = "SM",
diff --git a/src/hooks/useProcessor.ts b/src/hooks/useProcessor.ts
index 98f1f76..f91f451 100644
--- a/src/hooks/useProcessor.ts
+++ b/src/hooks/useProcessor.ts
@@ -15,13 +15,13 @@
  * limitations under the License.
  */
 import dayjs from "dayjs";
-import { RespFields, MetricQueryTypes } from "./data";
+import { RespFields, MetricQueryTypes, Calculations } from "./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 { Instance, Endpoint, Service } from "@/types/selector";
-import { StandardConfig } from "@/types/dashboard";
+import { MetricConfigOpt } from "@/types/dashboard";
 
 export function useQueryProcessor(config: any) {
   if (!(config.metrics && config.metrics[0])) {
@@ -48,6 +48,7 @@ export function useQueryProcessor(config: any) {
   }
   const fragment = config.metrics.map((name: string, index: number) => {
     const metricType = config.metricTypes[index] || "";
+    const c = (config.metricConfig && config.metricConfig[index]) || {};
     if (
       [
         MetricQueryTypes.ReadSampledRecords,
@@ -63,11 +64,11 @@ export function useQueryProcessor(config: any) {
         normal: selectorStore.currentService.normal,
         scope: dashboardStore.entity,
         topN: 10,
-        order: config.standard.sortOrder || "DES",
+        order: c.sortOrder || "DES",
       };
     } else {
       if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
-        const labels = (config.labelsIndex || "")
+        const labels = (c.labelsIndex || "")
           .split(",")
           .map((item: string) => item.replace(/^\s*|\s*$/g, ""));
         variables.push(`$labels${index}: [String!]!`);
@@ -128,7 +129,7 @@ export function useSourceProcessor(
   config: {
     metrics: string[];
     metricTypes: string[];
-    standard: StandardConfig;
+    metricConfig: MetricConfigOpt[];
   }
 ) {
   if (resp.errors) {
@@ -140,23 +141,24 @@ export function useSourceProcessor(
 
   config.metricTypes.forEach((type: string, index) => {
     const m = config.metrics[index];
+    const c = (config.metricConfig && config.metricConfig[index]) || {};
 
     if (type === MetricQueryTypes.ReadMetricsValues) {
       source[m] = resp.data[keys[index]].values.values.map(
-        (d: { value: number }) => aggregation(d.value, config.standard)
+        (d: { value: number }) => aggregation(d.value, c)
       );
     }
     if (type === MetricQueryTypes.ReadLabeledMetricsValues) {
       const resVal = Object.values(resp.data)[0] || [];
-      const labels = (config.standard.metricLabels || "")
+      const labels = (c.label || "")
         .split(",")
         .map((item: string) => item.replace(/^\s*|\s*$/g, ""));
-      const labelsIdx = (config.standard.labelsIndex || "")
+      const labelsIdx = (c.labelsIndex || "")
         .split(",")
         .map((item: string) => item.replace(/^\s*|\s*$/g, ""));
       for (const item of resVal) {
         const values = item.values.values.map((d: { value: number }) =>
-          aggregation(Number(d.value), config.standard)
+          aggregation(Number(d.value), c)
         );
 
         const indexNum = labelsIdx.findIndex((d: string) => d === item.label);
@@ -168,10 +170,7 @@ export function useSourceProcessor(
       }
     }
     if (type === MetricQueryTypes.ReadMetricsValue) {
-      source[m] = aggregation(
-        Number(Object.values(resp.data)[0]),
-        config.standard
-      );
+      source[m] = aggregation(Number(Object.values(resp.data)[0]), c);
     }
     if (
       type === MetricQueryTypes.SortMetrics ||
@@ -179,7 +178,7 @@ export function useSourceProcessor(
     ) {
       source[m] = (Object.values(resp.data)[0] || []).map(
         (d: { value: unknown; name: string }) => {
-          d.value = aggregation(Number(d.value), config.standard);
+          d.value = aggregation(Number(d.value), c);
 
           return d;
         }
@@ -258,7 +257,11 @@ export function useQueryPodsMetrics(
 export function usePodsSource(
   pods: Array<Instance | Endpoint>,
   resp: { errors: string; data: { [key: string]: any } },
-  config: { metrics: string[]; metricTypes: string[] }
+  config: {
+    metrics: string[];
+    metricTypes: string[];
+    metricConfig: MetricConfigOpt[];
+  }
 ): any {
   if (resp.errors) {
     ElMessage.error(resp.errors);
@@ -266,13 +269,14 @@ export function usePodsSource(
   }
   const data = pods.map((d: Instance | any, idx: number) => {
     config.metrics.map((name: string, index: number) => {
+      const c = (config.metricConfig && config.metricConfig[index]) || {};
       const key = name + idx + index;
       if (config.metricTypes[index] === MetricQueryTypes.ReadMetricsValue) {
-        d[name] = resp.data[key];
+        d[name] = aggregation(resp.data[key], c);
       }
       if (config.metricTypes[index] === MetricQueryTypes.ReadMetricsValues) {
-        d[name] = resp.data[key].values.values.map(
-          (d: { value: number }) => d.value
+        d[name] = resp.data[key].values.values.map((d: { value: number }) =>
+          aggregation(d.value, c)
         );
       }
     });
@@ -307,32 +311,31 @@ export function useQueryTopologyMetrics(metrics: string[], ids: string[]) {
   return { queryStr, conditions };
 }
 
-function aggregation(val: number, standard: any): number | string {
-  let data: number | string = val;
+export function aggregation(val: number, config: any): number | string {
+  let data: number | string = Number(val);
 
-  if (!isNaN(standard.plus)) {
-    data = val + Number(standard.plus);
-    return data;
-  }
-  if (!isNaN(standard.minus)) {
-    data = val - Number(standard.plus);
-    return data;
-  }
-  if (!isNaN(standard.multiply) && standard.divide !== 0) {
-    data = val * Number(standard.multiply);
-    return data;
-  }
-  if (!isNaN(standard.divide) && standard.divide !== 0) {
-    data = val / Number(standard.divide);
-    return data;
-  }
-  if (standard.milliseconds) {
-    data = dayjs(val).format("YYYY-MM-DD HH:mm:ss");
-    return data;
-  }
-  if (standard.milliseconds) {
-    data = dayjs.unix(val).format("YYYY-MM-DD HH:mm:ss");
-    return data;
+  switch (config.calculation) {
+    case Calculations.Percentage:
+      data = val / 100;
+      break;
+    case Calculations.ByteToKB:
+      data = val / 1024;
+      break;
+    case Calculations.Apdex:
+      data = val / 10000;
+      break;
+    case Calculations.ConvertSeconds:
+      data = dayjs(val).format("YYYY-MM-DD HH:mm:ss");
+      break;
+    case Calculations.ConvertMilliseconds:
+      data = dayjs.unix(val).format("YYYY-MM-DD HH:mm:ss");
+      break;
+    case Calculations.Precision:
+      data = data.toFixed(2);
+      break;
+    default:
+      data;
+      break;
   }
 
   return data;
diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts
index c11b198..4bc2758 100644
--- a/src/locales/lang/en.ts
+++ b/src/locales/lang/en.ts
@@ -126,6 +126,9 @@ const msg = {
   kubernetesCluster: "Cluster",
   kubernetes: "Kubernetes",
   textUrl: "Text Hyperlink",
+  textAlign: "Text Align",
+  metricLabel: "Metric Label",
+  showUnit: "Show Unit",
   hourTip: "Select Hour",
   minuteTip: "Select Minute",
   secondTip: "Select Second",
@@ -258,7 +261,7 @@ const msg = {
   independentSelector: "Selectors",
   unknownMetrics: "Unknown Metrics",
   labels: "Labels",
-  aggregation: "Data Calculation",
+  aggregation: "Calculation",
   unit: "Unit",
   labelsIndex: "Label Subscript",
   parentService: "Parent Service",
diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts
index 6c1d334..43f09eb 100644
--- a/src/locales/lang/zh.ts
+++ b/src/locales/lang/zh.ts
@@ -126,6 +126,9 @@ const msg = {
   kubernetesCluster: "集群",
   kubernetes: "Kubernetes",
   textUrl: "文本超链接",
+  textAlign: "文本对齐",
+  metricLabel: "指标标签",
+  showUnit: "显示单位",
   hourTip: "选择小时",
   minuteTip: "选择分钟",
   secondTip: "选择秒数",
@@ -260,7 +263,7 @@ const msg = {
   independentSelector: "独立选择器",
   unknownMetrics: "未知指标",
   labels: "标签",
-  aggregation: "数据计算",
+  aggregation: "计算",
   unit: "单位",
   labelsIndex: "标签下标",
   parentService: "父级服务",
diff --git a/src/store/data.ts b/src/store/data.ts
index 6fdfdd9..eba3c3b 100644
--- a/src/store/data.ts
+++ b/src/store/data.ts
@@ -28,6 +28,7 @@ export const NewControl = {
   standard: {},
   metrics: [""],
   metricTypes: [""],
+  metricConfig: [],
 };
 export const TextConfig = {
   fontColor: "white",
diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts
index 62d12b9..3a1fe7e 100644
--- a/src/types/dashboard.ts
+++ b/src/types/dashboard.ts
@@ -36,8 +36,17 @@ export interface LayoutConfig {
   metricTypes: string[];
   children?: any;
   activedTabIndex?: number;
+  metricConfig?: MetricConfigOpt[];
 }
 
+export type MetricConfigOpt = {
+  unit: string;
+  label: string;
+  calculation: string;
+  labelsIndex: string;
+  sortOrder: string;
+};
+
 export interface WidgetConfig {
   title?: string;
   tips?: string;
@@ -87,7 +96,7 @@ export interface AreaConfig {
 export interface CardConfig {
   type?: string;
   fontSize?: number;
-  showUint?: boolean;
+  showUnit?: boolean;
   textAlign?: "center" | "right" | "left";
 }
 
diff --git a/src/views/dashboard/configuration/Text.vue b/src/views/dashboard/configuration/Text.vue
index 268be91..2e29815 100644
--- a/src/views/dashboard/configuration/Text.vue
+++ b/src/views/dashboard/configuration/Text.vue
@@ -30,6 +30,17 @@ limitations under the License. -->
     />
   </div>
   <div class="item">
+    <span class="label">{{ t("textAlign") }}</span>
+    <Selector
+      :value="textAlign"
+      :options="AlignStyle"
+      size="small"
+      placeholder="Select a color"
+      class="input"
+      @change="changeConfig({ textAlign: $event[0].value })"
+    />
+  </div>
+  <div class="item">
     <span class="label">{{ t("backgroundColors") }}</span>
     <Selector
       :value="backgroundColor"
@@ -85,6 +96,7 @@ const backgroundColor = ref(originConfig.graph.backgroundColor || "green");
 const fontColor = ref(originConfig.graph.fontColor || "white");
 const content = ref<string>(originConfig.graph.content || "");
 const fontSize = ref<number>(originConfig.graph.fontSize || 12);
+const textAlign = ref(originConfig.graph.textAlign || "left");
 const Colors = [
   {
     label: "Green",
@@ -97,6 +109,14 @@ const Colors = [
   { label: "Black", value: "black" },
   { label: "Orange", value: "orange" },
 ];
+const AlignStyle = [
+  {
+    label: "Left",
+    value: "left",
+  },
+  { label: "Center", value: "center" },
+  { label: "Right", value: "right" },
+];
 function changeConfig(param: { [key: string]: unknown }) {
   const { selectedGrid } = dashboardStore;
   const graph = {
diff --git a/src/views/dashboard/configuration/Widget.vue b/src/views/dashboard/configuration/Widget.vue
index 442e859..67f6949 100644
--- a/src/views/dashboard/configuration/Widget.vue
+++ b/src/views/dashboard/configuration/Widget.vue
@@ -36,7 +36,7 @@ limitations under the License. -->
             i: dashboardStore.selectedGrid.i,
             metrics: dashboardStore.selectedGrid.metrics,
             metricTypes: dashboardStore.selectedGrid.metricTypes,
-            standard: dashboardStore.selectedGrid.standard,
+            metricConfig: dashboardStore.selectedGrid.metricConfig,
           }"
           :isEdit="isEdit"
           @changeOpt="setStatus"
@@ -64,9 +64,6 @@ limitations under the License. -->
         <el-collapse-item :title="t('widgetOptions')" name="3">
           <WidgetOptions />
         </el-collapse-item>
-        <el-collapse-item :title="t('standardOptions')" name="4">
-          <StandardOptions @update="getSource" @loading="setLoading" />
-        </el-collapse-item>
       </el-collapse>
     </div>
     <div class="footer">
@@ -88,16 +85,14 @@ import { Option } from "@/types/app";
 import graphs from "../graphs";
 import configs from "./widget/graph-styles";
 import WidgetOptions from "./widget/WidgetOptions.vue";
-import StandardOptions from "./widget/StandardOptions.vue";
-import MetricOptions from "./widget/MetricOptions.vue";
+import MetricOptions from "./widget/metric/Index.vue";
 
 export default defineComponent({
-  name: "ConfigEdit",
+  name: "WidgetEdit",
   components: {
     ...graphs,
     ...configs,
     WidgetOptions,
-    StandardOptions,
     MetricOptions,
   },
   setup() {
diff --git a/src/views/dashboard/configuration/widget/StandardOptions.vue b/src/views/dashboard/configuration/widget/StandardOptions.vue
deleted file mode 100644
index 5b94f05..0000000
--- a/src/views/dashboard/configuration/widget/StandardOptions.vue
+++ /dev/null
@@ -1,186 +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. -->
-<template>
-  <div class="item">
-    <span class="label">{{ t("unit") }}</span>
-    <el-input
-      class="input"
-      v-model="selectedGrid.standard.unit"
-      size="small"
-      placeholder="Please input Unit"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("sortOrder") }}</span>
-    <Selector
-      :value="sortOrder"
-      :options="SortOrder"
-      size="small"
-      placeholder="Select a sort order"
-      class="selector"
-      @change="changeStandardOpt({ sortOrder })"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("labels") }}</span>
-    <el-input
-      class="input"
-      v-model="selectedGrid.standard.metricLabels"
-      size="small"
-      placeholder="auto"
-      @change="changeStandardOpt"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("labelsIndex") }}</span>
-    <el-input
-      class="input"
-      v-model="selectedGrid.standard.labelsIndex"
-      size="small"
-      placeholder="auto"
-      @change="changeStandardOpt"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("plus") }}</span>
-    <el-input-number
-      class="input"
-      v-model="selectedGrid.standard.plus"
-      :min="0"
-      size="small"
-      placeholder="Please input"
-      @change="changeStandardOpt"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("minus") }}</span>
-    <el-input-number
-      class="input"
-      v-model="selectedGrid.standard.minus"
-      :min="0"
-      size="small"
-      placeholder="Please input"
-      @change="changeStandardOpt"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("multiply") }}</span>
-    <el-input-number
-      class="input"
-      v-model="selectedGrid.standard.multiply"
-      :min="1"
-      size="small"
-      placeholder="Please input"
-      @change="changeStandardOpt"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("divide") }}</span>
-    <el-input-number
-      class="input"
-      v-model="selectedGrid.standard.divide"
-      size="small"
-      placeholder="Please input"
-      :min="1"
-      @change="changeStandardOpt"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("convertToMilliseconds") }}</span>
-    <el-input-number
-      class="input"
-      :min="0"
-      v-model="selectedGrid.standard.milliseconds"
-      size="small"
-      placeholder="Please input"
-      @change="changeStandardOpt"
-    />
-  </div>
-  <div class="item">
-    <span class="label">{{ t("convertToSeconds") }}</span>
-    <el-input-number
-      class="input"
-      :min="0"
-      v-model="selectedGrid.standard.seconds"
-      size="small"
-      placeholder="Please input"
-      @change="changeStandardOpt"
-    />
-  </div>
-</template>
-<script lang="ts" setup>
-import { ref } from "vue";
-import { useI18n } from "vue-i18n";
-import { SortOrder } from "../../data";
-import { useDashboardStore } from "@/store/modules/dashboard";
-import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
-import { ElMessage } from "element-plus";
-
-/*global defineEmits */
-const { t } = useI18n();
-const emit = defineEmits(["update", "loading"]);
-const dashboardStore = useDashboardStore();
-const { selectedGrid } = dashboardStore;
-const sortOrder = ref<string>(selectedGrid.standard.sortOrder || "DES");
-
-function changeStandardOpt(param?: any) {
-  let standard = dashboardStore.selectedGrid.standard;
-  if (param) {
-    standard = {
-      ...dashboardStore.selectedGrid.standard,
-      ...param,
-    };
-    dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, standard });
-  }
-  queryMetrics();
-}
-async function queryMetrics() {
-  const params = useQueryProcessor(dashboardStore.selectedGrid);
-  if (!params) {
-    emit("update", {});
-    return;
-  }
-
-  emit("loading", true);
-  const json = await dashboardStore.fetchMetricValue(params);
-  emit("loading", false);
-  if (json.errors) {
-    ElMessage.error(json.errors);
-    return;
-  }
-  const source = useSourceProcessor(json, dashboardStore.selectedGrid);
-  emit("update", source);
-}
-</script>
-<style lang="scss" scoped>
-.label {
-  font-size: 13px;
-  font-weight: 500;
-  display: block;
-  margin-bottom: 5px;
-}
-
-.input {
-  width: 500px;
-}
-
-.item {
-  margin-bottom: 10px;
-}
-
-.selector {
-  width: 500px;
-}
-</style>
diff --git a/src/views/dashboard/configuration/widget/graph-styles/Card.vue b/src/views/dashboard/configuration/widget/graph-styles/Card.vue
index 1a5bcb7..6259b14 100644
--- a/src/views/dashboard/configuration/widget/graph-styles/Card.vue
+++ b/src/views/dashboard/configuration/widget/graph-styles/Card.vue
@@ -26,6 +26,15 @@ limitations under the License. -->
       @change="updateConfig({ fontSize })"
     />
   </div>
+  <div class="item">
+    <span class="label">{{ t("showUnit") }}</span>
+    <el-switch
+      v-model="showUnit"
+      active-text="Yes"
+      inactive-text="No"
+      @change="updateConfig({ showUnit })"
+    />
+  </div>
 </template>
 <script lang="ts" setup>
 import { ref } from "vue";
@@ -36,13 +45,14 @@ const { t } = useI18n();
 const dashboardStore = useDashboardStore();
 const { selectedGrid } = dashboardStore;
 const fontSize = ref(selectedGrid.graph.fontSize);
+const showUnit = ref<boolean>(selectedGrid.graph.showUnit);
 
 function updateConfig(param: { [key: string]: unknown }) {
   const graph = {
-    ...selectedGrid.graph,
+    ...dashboardStore.selectedGrid.graph,
     ...param,
   };
-  dashboardStore.selectWidget({ ...selectedGrid, graph });
+  dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, graph });
 }
 </script>
 <style lang="scss" scoped>
diff --git a/src/views/dashboard/configuration/widget/MetricOptions.vue b/src/views/dashboard/configuration/widget/metric/Index.vue
similarity index 87%
rename from src/views/dashboard/configuration/widget/MetricOptions.vue
rename to src/views/dashboard/configuration/widget/metric/Index.vue
index 0e399f3..e118270 100644
--- a/src/views/dashboard/configuration/widget/MetricOptions.vue
+++ b/src/views/dashboard/configuration/widget/metric/Index.vue
@@ -49,6 +49,18 @@ limitations under the License. -->
       @change="changeMetricType(index, $event)"
       class="selectors"
     />
+    <el-popover placement="top" :width="400" trigger="click">
+      <template #reference>
+        <span @click="setMetricConfig(index)">
+          <Icon class="cp mr-5" iconName="mode_edit" size="middle" />
+        </span>
+      </template>
+      <Standard
+        @update="queryMetrics"
+        :currentMetricConfig="currentMetricConfig"
+        :index="index"
+      />
+    </el-popover>
     <span
       v-show="states.isList || states.metricTypes[0] === 'readMetricsValues'"
     >
@@ -96,12 +108,13 @@ import {
   ChartTypes,
   PodsChartTypes,
   MetricsType,
-} from "../../data";
+} from "../../../data";
 import { ElMessage } from "element-plus";
 import Icon from "@/components/Icon.vue";
 import { useQueryProcessor, useSourceProcessor } from "@/hooks/useProcessor";
 import { useI18n } from "vue-i18n";
-import { DashboardItem } from "@/types/dashboard";
+import { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
+import Standard from "./Standard.vue";
 
 /*global defineEmits */
 const { t } = useI18n();
@@ -127,6 +140,13 @@ const states = reactive<{
   dashboardName: graph.dashboardName,
   dashboardList: [{ label: "", value: "" }],
 });
+const currentMetricConfig = ref<MetricConfigOpt>({
+  unit: "",
+  label: "",
+  labelsIndex: "",
+  calculation: "",
+  sortOrder: "DES",
+});
 
 states.isList = ListChartTypes.includes(graph.type);
 const defaultLen = ref<number>(states.isList ? 5 : 20);
@@ -323,8 +343,8 @@ async function queryMetrics() {
   if (states.isList) {
     return;
   }
-  const { standard } = dashboardStore.selectedGrid;
-  const params = useQueryProcessor({ ...states, standard });
+  const { metricConfig } = dashboardStore.selectedGrid;
+  const params = useQueryProcessor({ ...states, metricConfig });
   if (!params) {
     emit("update", {});
     return;
@@ -337,7 +357,7 @@ async function queryMetrics() {
     ElMessage.error(json.errors);
     return;
   }
-  const source = useSourceProcessor(json, { ...states, standard });
+  const source = useSourceProcessor(json, { ...states, metricConfig });
   emit("update", source);
 }
 
@@ -372,11 +392,20 @@ function deleteMetric(index: number) {
     dashboardStore.selectWidget({
       ...dashboardStore.selectedGrid,
       ...{ metricTypes: states.metricTypes, metrics: states.metrics },
+      metricConfig: [],
     });
     return;
   }
   states.metrics.splice(index, 1);
   states.metricTypes.splice(index, 1);
+  const config = dashboardStore.selectedGrid.metricConfig || [];
+  const metricConfig = config[index] ? config.splice(index, 1) : config;
+  dashboardStore.selectWidget({
+    ...dashboardStore.selectedGrid,
+    ...{ metricTypes: states.metricTypes, metrics: states.metrics },
+    metricConfig,
+  });
+  console.log(dashboardStore.selectedGrid);
 }
 function setMetricTypeList(type: string) {
   if (type !== MetricsType.REGULAR_VALUE) {
@@ -393,6 +422,26 @@ function setMetricTypeList(type: string) {
   }
   return MetricTypes[type];
 }
+function setMetricConfig(index: number) {
+  const n = {
+    unit: "",
+    label: "",
+    calculation: "",
+    labelsIndex: "",
+    sortOrder: "DES",
+  };
+  if (
+    !dashboardStore.selectedGrid.metricConfig ||
+    !dashboardStore.selectedGrid.metricConfig[index]
+  ) {
+    currentMetricConfig.value = n;
+    return;
+  }
+  currentMetricConfig.value = {
+    ...n,
+    ...dashboardStore.selectedGrid.metricConfig[index],
+  };
+}
 </script>
 <style lang="scss" scoped>
 .ds-name {
diff --git a/src/views/dashboard/configuration/widget/metric/Standard.vue b/src/views/dashboard/configuration/widget/metric/Standard.vue
new file mode 100644
index 0000000..d0a2ff5
--- /dev/null
+++ b/src/views/dashboard/configuration/widget/metric/Standard.vue
@@ -0,0 +1,129 @@
+<!-- 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>
+  <div class="config-panel">
+    <div class="item mb-10">
+      <span class="label">{{ t("unit") }}</span>
+      <el-input
+        class="input"
+        v-model="currentMetric.unit"
+        size="small"
+        placeholder="Please input unit"
+        @change="changeConfigs(index, { unit: currentMetric.unit })"
+      />
+    </div>
+    <div class="item mb-10" v-show="metricType === 'readLabeledMetricsValues'">
+      <span class="label">{{ t("labels") }}</span>
+      <el-input
+        class="input"
+        v-model="currentMetric.label"
+        size="small"
+        placeholder="Please input a name"
+        @change="changeConfigs(index, { label: currentMetric.label })"
+      />
+    </div>
+    <div class="item mb-10" v-show="metricType === 'readLabeledMetricsValues'">
+      <span class="label">{{ t("labelsIndex") }}</span>
+      <el-input
+        class="input"
+        v-model="currentMetric.labelsIndex"
+        size="small"
+        placeholder="auto"
+        @change="
+          changeConfigs(index, { labelsIndex: currentMetric.labelsIndex })
+        "
+      />
+    </div>
+    <div class="item mb-10">
+      <span class="label">{{ t("aggregation") }}</span>
+      <SelectSingle
+        :value="currentMetric.calculation"
+        :options="CalculationOpts"
+        @change="changeConfigs(index, { calculation: $event })"
+        class="selectors"
+        :clearable="true"
+      />
+    </div>
+    <div
+      class="item"
+      v-show="['sortMetrics', 'readSampledRecords'].includes(metricType)"
+    >
+      <span class="label">{{ t("sortOrder") }}</span>
+      <SelectSingle
+        :value="currentMetric.sortOrder || 'DES'"
+        :options="SortOrder"
+        class="selectors"
+        @change="changeConfigs(index, { sortOrder: $event })"
+      />
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref } from "vue";
+import type { PropType } from "vue";
+import { useI18n } from "vue-i18n";
+import { SortOrder, CalculationOpts } from "../../../data";
+import { useDashboardStore } from "@/store/modules/dashboard";
+import { MetricConfigOpt } from "@/types/dashboard";
+
+/*global defineEmits, defineProps */
+const props = defineProps({
+  currentMetricConfig: {
+    type: Object as PropType<MetricConfigOpt>,
+    default: () => ({ unit: "" }),
+  },
+  index: { type: Number, default: 0 },
+});
+const { t } = useI18n();
+const emit = defineEmits(["update"]);
+const dashboardStore = useDashboardStore();
+const currentMetric = ref<MetricConfigOpt>(props.currentMetricConfig);
+const metricType = ref<string>(
+  dashboardStore.selectedGrid.metricTypes[props.index]
+);
+
+function changeConfigs(index: number, param: { [key: string]: string }) {
+  const metricConfig = dashboardStore.selectedGrid.metricConfig || [];
+  metricConfig[index] = { ...metricConfig[index], ...param };
+
+  dashboardStore.selectWidget({
+    ...dashboardStore.selectedGrid,
+    metricConfig,
+  });
+  emit("update");
+}
+</script>
+<style lang="scss" scoped>
+.config-panel {
+  padding: 10px 5px;
+  position: relative;
+}
+
+.label {
+  width: 150px;
+  display: inline-block;
+  font-size: 12px;
+}
+
+.close {
+  position: absolute;
+  top: -8px;
+  right: -15px;
+}
+
+.selectors {
+  width: 365px;
+}
+</style>
diff --git a/src/views/dashboard/controls/Text.vue b/src/views/dashboard/controls/Text.vue
index 60f77ed..72c65b8 100644
--- a/src/views/dashboard/controls/Text.vue
+++ b/src/views/dashboard/controls/Text.vue
@@ -36,18 +36,17 @@ limitations under the License. -->
     </div>
     <div
       class="body"
-      :style="{ backgroundColor: TextColors[data.graph.backgroundColor] }"
+      :class="data.graph.textAlign === 'center' ? 'center' : ''"
+      :style="{
+        backgroundColor: TextColors[data.graph.backgroundColor],
+        color: TextColors[data.graph.fontColor],
+        fontSize: data.graph.fontSize + 'px',
+        textAlign: data.graph.textAlign,
+      }"
     >
-      <div
-        :style="{
-          color: TextColors[data.graph.fontColor],
-          fontSize: data.graph.fontSize + 'px',
-        }"
-      >
-        <a :href="data.graph.url" target="_blank">
-          {{ data.graph.content }}
-        </a>
-      </div>
+      <a :href="data.graph.url" target="_blank">
+        {{ data.graph.content }}
+      </a>
     </div>
   </div>
 </template>
@@ -75,14 +74,6 @@ function editConfig() {
   dashboardStore.setConfigPanel(true);
   dashboardStore.selectWidget(props.data);
 }
-function viewText() {
-  const path = props.data.graph.url;
-  console.log(path);
-  if (!path) {
-    return;
-  }
-  window.open(path, "_blank");
-}
 </script>
 <style lang="scss" scoped>
 .topology {
@@ -97,21 +88,21 @@ function viewText() {
 
 .header {
   position: absolute;
-  top: 10px;
-  right: 10px;
+  top: 5px;
+  right: 5px;
 }
 
 .body {
   text-align: center;
   width: 100%;
   height: 100%;
+  line-height: 100%;
   cursor: pointer;
-  box-sizing: border-box;
-  color: #333;
-  display: -webkit-box;
   -webkit-box-orient: horizontal;
   -webkit-box-pack: center;
-  -webkit-box-align: center;
+  box-sizing: border-box;
+  display: -webkit-box;
+  // -webkit-box-align: left;
 }
 
 .tools {
diff --git a/src/views/dashboard/controls/Widget.vue b/src/views/dashboard/controls/Widget.vue
index c7915f1..2d71bf2 100644
--- a/src/views/dashboard/controls/Widget.vue
+++ b/src/views/dashboard/controls/Widget.vue
@@ -64,8 +64,8 @@ limitations under the License. -->
           metrics: data.metrics,
           metricTypes: data.metricTypes,
           i: data.i,
+          metricConfig: data.metricConfig,
         }"
-        :standard="data.standard"
         :needQuery="needQuery"
       />
     </div>
@@ -128,7 +128,7 @@ export default defineComponent({
       const d = {
         metrics: props.data.metrics,
         metricTypes: props.data.metricTypes,
-        standard: props.data.standard,
+        metricConfig: props.data.metricConfig || [],
       };
       state.source = useSourceProcessor(json, d);
     }
diff --git a/src/views/dashboard/data.ts b/src/views/dashboard/data.ts
index 9668d76..b938d5a 100644
--- a/src/views/dashboard/data.ts
+++ b/src/views/dashboard/data.ts
@@ -244,7 +244,7 @@ export enum LegendOpt {
 }
 export const DepthList = [1, 2, 3, 4, 5].map((item: number) => ({
   value: item,
-  label: item,
+  label: String(item),
 }));
 export const Status = [
   { label: "All", value: "ALL" },
@@ -264,3 +264,15 @@ export const TextColors: { [key: string]: string } = {
   black: "#000",
   orange: "#E6A23C",
 };
+
+export const CalculationOpts = [
+  { label: "Percentage", value: "percentage" },
+  { label: "Byte to KB", value: "byteToKB" },
+  { label: "Apdex", value: "apdex" },
+  {
+    label: "Convert milliseconds to YYYY-MM-DD HH:mm:ss",
+    value: "convertMilliseconds",
+  },
+  { label: "Convert seconds to YYYY-MM-DD HH:mm:ss", value: "convertSeconds" },
+  { label: "Precision is 2", value: "precision" },
+];
diff --git a/src/views/dashboard/graphs/Card.vue b/src/views/dashboard/graphs/Card.vue
index 1b0422e..b715881 100644
--- a/src/views/dashboard/graphs/Card.vue
+++ b/src/views/dashboard/graphs/Card.vue
@@ -26,12 +26,14 @@ limitations under the License. -->
         ? null
         : singleVal.toFixed(2)
     }}
-    <span v-show="config.showUint">{{ standard.unit }}</span>
+    <span v-show="config.showUnit">
+      {{ metricConfig[0]?.unit }}
+    </span>
   </div>
 </template>
 <script lang="ts" setup>
 import { computed, PropType } from "vue";
-import { CardConfig, StandardConfig } from "@/types/dashboard";
+import { CardConfig, MetricConfigOpt } from "@/types/dashboard";
 
 /*global defineProps */
 const props = defineProps({
@@ -40,14 +42,16 @@ const props = defineProps({
     default: () => ({}),
   },
   config: {
-    type: Object as PropType<CardConfig>,
-    default: () => ({ fontSize: 12, showUint: true, textAlign: "center" }),
-  },
-  standard: {
-    type: Object as PropType<StandardConfig>,
-    default: () => ({ unit: "" }),
+    type: Object as PropType<CardConfig & { metricConfig?: MetricConfigOpt[] }>,
+    default: () => ({
+      fontSize: 12,
+      showUnit: true,
+      textAlign: "center",
+      metricConfig: [],
+    }),
   },
 });
+const metricConfig = computed(() => props.config.metricConfig || []);
 const key = computed(() => Object.keys(props.data)[0]);
 const singleVal = computed(() => props.data[key.value]);
 </script>
diff --git a/src/views/dashboard/graphs/EndpointList.vue b/src/views/dashboard/graphs/EndpointList.vue
index a8d4c35..1191915 100644
--- a/src/views/dashboard/graphs/EndpointList.vue
+++ b/src/views/dashboard/graphs/EndpointList.vue
@@ -44,7 +44,7 @@ limitations under the License. -->
         </el-table-column>
         <el-table-column
           v-for="(metric, index) in config.metrics"
-          :label="metric"
+          :label="`${metric} ${getUnit(index)}`"
           :key="metric + index"
         >
           <template #default="scope">
@@ -92,6 +92,7 @@ import Card from "./Card.vue";
 import { EntityType } from "../data";
 import router from "@/router";
 import getDashboard from "@/hooks/useDashboardsSession";
+import { MetricConfigOpt } from "@/types/dashboard";
 
 /*global defineProps */
 const props = defineProps({
@@ -104,7 +105,7 @@ const props = defineProps({
         i: string;
         metrics: string[];
         metricTypes: string[];
-      }
+      } & { metricConfig: MetricConfigOpt[] }
     >,
     default: () => ({ dashboardName: "", fontSize: 12, i: "" }),
   },
@@ -155,7 +156,12 @@ async function queryEndpointMetrics(currentPods: Endpoint[]) {
       ElMessage.error(json.errors);
       return;
     }
-    endpoints.value = usePodsSource(currentPods, json, props.config);
+    const metricConfig = props.config.metricConfig || [];
+
+    endpoints.value = usePodsSource(currentPods, json, {
+      ...props.config,
+      metricConfig: metricConfig,
+    });
     return;
   }
   endpoints.value = currentPods;
@@ -184,12 +190,21 @@ async function searchList() {
   const limit = searchText.value ? undefined : total;
   await queryEndpoints(limit);
 }
+function getUnit(index: number) {
+  const u =
+    (props.config.metricConfig &&
+      props.config.metricConfig[index] &&
+      props.config.metricConfig[index].unit) ||
+    "";
+  if (u) {
+    return `(${u})`;
+  }
+  return u;
+}
 watch(
   () => [props.config.metricTypes, props.config.metrics],
   async () => {
-    if (props.isEdit) {
-      queryEndpointMetrics(endpoints.value);
-    }
+    queryEndpointMetrics(endpoints.value);
     // emit("changeOpt", false);
   }
 );
diff --git a/src/views/dashboard/graphs/InstanceList.vue b/src/views/dashboard/graphs/InstanceList.vue
index b2c7aa7..f918ae2 100644
--- a/src/views/dashboard/graphs/InstanceList.vue
+++ b/src/views/dashboard/graphs/InstanceList.vue
@@ -44,7 +44,7 @@ limitations under the License. -->
         </el-table-column>
         <el-table-column
           v-for="(metric, index) in config.metrics"
-          :label="metric"
+          :label="`${metric} ${getUnit(index)}`"
           :key="metric + index"
         >
           <template #default="scope">
@@ -112,6 +112,7 @@ import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useProcessor";
 import { EntityType } from "../data";
 import router from "@/router";
 import getDashboard from "@/hooks/useDashboardsSession";
+import { MetricConfigOpt } from "@/types/dashboard";
 
 /*global defineProps */
 const props = defineProps({
@@ -122,7 +123,7 @@ const props = defineProps({
         metrics: string[];
         metricTypes: string[];
         isEdit: boolean;
-      }
+      } & { metricConfig: MetricConfigOpt[] }
     >,
     default: () => ({
       dashboardName: "",
@@ -180,7 +181,11 @@ async function queryInstanceMetrics(currentInstances: Instance[]) {
       ElMessage.error(json.errors);
       return;
     }
-    instances.value = usePodsSource(currentInstances, json, props.config);
+    const metricConfig = props.config.metricConfig || [];
+    instances.value = usePodsSource(currentInstances, json, {
+      ...props.config,
+      metricConfig,
+    });
     return;
   }
   instances.value = currentInstances;
@@ -214,6 +219,18 @@ function searchList() {
   instances.value = searchInstances.value.splice(0, pageSize);
 }
 
+function getUnit(index: number) {
+  const u =
+    (props.config.metricConfig &&
+      props.config.metricConfig[index] &&
+      props.config.metricConfig[index].unit) ||
+    "";
+  if (u) {
+    return `(${u})`;
+  }
+  return u;
+}
+
 watch(
   () => [props.config.metricTypes, props.config.metrics],
   () => {
diff --git a/src/views/dashboard/graphs/ServiceList.vue b/src/views/dashboard/graphs/ServiceList.vue
index 8eae295..4423601 100644
--- a/src/views/dashboard/graphs/ServiceList.vue
+++ b/src/views/dashboard/graphs/ServiceList.vue
@@ -56,7 +56,7 @@ limitations under the License. -->
         </el-table-column>
         <el-table-column
           v-for="(metric, index) in config.metrics"
-          :label="metric"
+          :label="`${metric} ${getUnit(index)}`"
           :key="metric + index"
         >
           <template #default="scope">
@@ -104,6 +104,7 @@ import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useProcessor";
 import { EntityType } from "../data";
 import router from "@/router";
 import getDashboard from "@/hooks/useDashboardsSession";
+import { MetricConfigOpt } from "@/types/dashboard";
 
 /*global defineProps */
 const props = defineProps({
@@ -117,7 +118,7 @@ const props = defineProps({
         metrics: string[];
         metricTypes: string[];
         isEdit: boolean;
-      }
+      } & { metricConfig: MetricConfigOpt[] }
     >,
     default: () => ({ dashboardName: "", fontSize: 12 }),
   },
@@ -214,7 +215,11 @@ async function queryServiceMetrics(currentServices: Service[]) {
       ElMessage.error(json.errors);
       return;
     }
-    services.value = usePodsSource(currentServices, json, props.config);
+    const metricConfig = props.config.metricConfig || [];
+    services.value = usePodsSource(currentServices, json, {
+      ...props.config,
+      metricConfig: metricConfig || [],
+    });
     return;
   }
   services.value = currentServices;
@@ -250,6 +255,17 @@ function searchList() {
   );
   services.value = searchServices.splice(0, pageSize);
 }
+function getUnit(index: number) {
+  const u =
+    (props.config.metricConfig &&
+      props.config.metricConfig[index] &&
+      props.config.metricConfig[index].unit) ||
+    "";
+  if (u) {
+    return `(${u})`;
+  }
+  return u;
+}
 watch(
   () => [props.config.metricTypes, props.config.metrics],
   () => {
diff --git a/src/views/dashboard/related/topology/components/Graph.vue b/src/views/dashboard/related/topology/components/Graph.vue
index f48705a..f1f84b3 100644
--- a/src/views/dashboard/related/topology/components/Graph.vue
+++ b/src/views/dashboard/related/topology/components/Graph.vue
@@ -90,6 +90,8 @@ import { Option } from "@/types/app";
 import { Service } from "@/types/selector";
 import { useAppStoreWithOut } from "@/store/modules/app";
 import getDashboard from "@/hooks/useDashboardsSession";
+import { MetricConfigOpt } from "@/types/dashboard";
+import { aggregation } from "@/hooks/useProcessor";
 
 /*global Nullable, defineProps */
 const props = defineProps({
@@ -275,13 +277,17 @@ function update() {
       handleNodeClick: handleNodeClick,
       tipHtml: (data: Node) => {
         const nodeMetrics: string[] = settings.value.nodeMetrics || [];
-        const html = nodeMetrics.map((m) => {
+        const nodeMetricConfig = settings.value.nodeMetricConfig || [];
+        const html = nodeMetrics.map((m, index) => {
           const metric =
-            topologyStore.nodeMetricValue[m].values.filter(
+            topologyStore.nodeMetricValue[m].values.find(
               (val: { id: string; value: unknown }) => val.id === data.id
-            )[0] || {};
-          const val = m.includes("_sla") ? metric.value / 100 : metric.value;
-          return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
+            ) || {};
+          const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
+          const v = aggregation(metric.value, opt);
+          return ` <div class="mb-5"><span class="grey">${
+            opt.label || m
+          }: </span>${v} ${opt.unit || ""}</div>`;
         });
         return [
           ` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`,
@@ -306,24 +312,34 @@ function update() {
       tipHtml: (data: Call) => {
         const linkClientMetrics: string[] =
           settings.value.linkClientMetrics || [];
+        const linkServerMetricConfig: MetricConfigOpt[] =
+          settings.value.linkServerMetricConfig || [];
+        const linkClientMetricConfig: MetricConfigOpt[] =
+          settings.value.linkClientMetricConfig || [];
         const linkServerMetrics: string[] =
           settings.value.linkServerMetrics || [];
-        const htmlServer = linkServerMetrics.map((m) => {
-          const metric = topologyStore.linkServerMetrics[m].values.filter(
+        const htmlServer = linkServerMetrics.map((m, index) => {
+          const metric = topologyStore.linkServerMetrics[m].values.find(
             (val: { id: string; value: unknown }) => val.id === data.id
-          )[0];
+          );
           if (metric) {
-            const val = m.includes("_sla") ? metric.value / 100 : metric.value;
-            return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
+            const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
+            const v = aggregation(metric.value, opt);
+            return ` <div class="mb-5"><span class="grey">${
+              opt.label || m
+            }: </span>${v} ${opt.unit || ""}</div>`;
           }
         });
-        const htmlClient = linkClientMetrics.map((m) => {
-          const metric = topologyStore.linkClientMetrics[m].values.filter(
+        const htmlClient = linkClientMetrics.map((m: string, index: number) => {
+          const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
+          const metric = topologyStore.linkClientMetrics[m].values.find(
             (val: { id: string; value: unknown }) => val.id === data.id
-          )[0];
+          );
           if (metric) {
-            const val = m.includes("_sla") ? metric.value / 100 : metric.value;
-            return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
+            const v = aggregation(metric.value, opt);
+            return ` <div class="mb-5"><span class="grey">${
+              opt.label || m
+            }: </span>${v} ${opt.unit || ""}</div>`;
           }
         });
         const html = [
diff --git a/src/views/dashboard/related/topology/components/Metrics.vue b/src/views/dashboard/related/topology/components/Metrics.vue
new file mode 100644
index 0000000..43a4298
--- /dev/null
+++ b/src/views/dashboard/related/topology/components/Metrics.vue
@@ -0,0 +1,170 @@
+<!-- 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>
+  <div class="config-panel">
+    <div class="item mb-10">
+      <span class="label">{{ t("metrics") }}</span>
+      <SelectSingle
+        :value="currentMetric"
+        :options="metrics"
+        @change="changeMetric"
+        class="selectors"
+      />
+    </div>
+    <div class="item mb-10">
+      <span class="label">{{ t("unit") }}</span>
+      <el-input
+        class="input"
+        v-model="currentConfig.unit"
+        size="small"
+        placeholder="Please input unit"
+        @change="changeConfigs({ unit: currentConfig.unit })"
+      />
+    </div>
+    <div class="item mb-10">
+      <span class="label">{{ t("labels") }}</span>
+      <el-input
+        class="input"
+        v-model="currentConfig.label"
+        size="small"
+        placeholder="Please input a label"
+        @change="changeConfigs({ label: currentConfig.label })"
+      />
+    </div>
+    <div class="item mb-10">
+      <span class="label">{{ t("aggregation") }}</span>
+      <SelectSingle
+        :value="currentConfig.calculation"
+        :options="CalculationOpts"
+        @change="changeConfigs({ calculation: $event })"
+        class="selectors"
+        :clearable="true"
+      />
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref, computed, watch } from "vue";
+import type { PropType } from "vue";
+import { useI18n } from "vue-i18n";
+import { CalculationOpts } from "../../../data";
+import { useDashboardStore } from "@/store/modules/dashboard";
+import { MetricConfigOpt } from "@/types/dashboard";
+import { 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: () => [] },
+});
+const { t } = useI18n();
+const emit = defineEmits(["update"]);
+const dashboardStore = useDashboardStore();
+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 currentConfig = ref<{ unit: string; calculation: string; label: string }>(
+  {
+    unit: "",
+    calculation: "",
+    label: "",
+  }
+);
+const currentIndex = ref<number>(0);
+const getMetricConfig = computed(() => {
+  let config = [];
+  switch (props.type) {
+    case "linkServerMetricConfig":
+      config = dashboardStore.selectedGrid.linkServerMetricConfig;
+      break;
+    case "linkClientMetricConfig":
+      config = dashboardStore.selectedGrid.linkClientMetricConfig;
+      break;
+    case "nodeMetricConfig":
+      config = dashboardStore.selectedGrid.nodeMetricConfig;
+      break;
+  }
+  return config || [];
+});
+
+function changeConfigs(param: { [key: string]: string }) {
+  const metricConfig = getMetricConfig.value;
+  metricConfig[currentIndex.value] = {
+    ...metricConfig[currentIndex.value],
+    ...param,
+  };
+  emit("update", { [props.type]: metricConfig });
+}
+function changeMetric(val: string) {
+  currentMetric.value = val;
+  const index = metrics.value.findIndex((d: Option) => d.value === val);
+  currentIndex.value = index || 0;
+  const config = getMetricConfig.value || [];
+
+  currentConfig.value = {
+    unit: "",
+    label: "",
+    calculation: "",
+    ...config[index],
+  };
+}
+watch(
+  () => props.type,
+  () => {
+    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;
+    const config = getMetricConfig.value || [];
+    currentIndex.value = 0;
+    currentConfig.value = {
+      unit: "",
+      label: "",
+      calculation: "",
+      ...config[0],
+    };
+  }
+);
+</script>
+<style lang="scss" scoped>
+.config-panel {
+  padding: 10px 5px;
+  position: relative;
+  min-height: 300px;
+}
+
+.label {
+  width: 150px;
+  display: inline-block;
+  font-size: 12px;
+}
+
+.close {
+  position: absolute;
+  top: -8px;
+  right: -15px;
+}
+
+.selectors {
+  width: 365px;
+}
+</style>
diff --git a/src/views/dashboard/related/topology/components/PodTopology.vue b/src/views/dashboard/related/topology/components/PodTopology.vue
index 8fc4df7..131221b 100644
--- a/src/views/dashboard/related/topology/components/PodTopology.vue
+++ b/src/views/dashboard/related/topology/components/PodTopology.vue
@@ -54,7 +54,7 @@ limitations under the License. -->
     element-loading-background="rgba(0, 0, 0, 0)"
     @click="handleClick"
   >
-    <Sankey @click="selectNodeLink" />
+    <Sankey @click="selectNodeLink" :settings="settings" />
   </div>
   <div
     class="operations-list"
diff --git a/src/views/dashboard/related/topology/components/Sankey.vue b/src/views/dashboard/related/topology/components/Sankey.vue
index 47b3cca..e92257c 100644
--- a/src/views/dashboard/related/topology/components/Sankey.vue
+++ b/src/views/dashboard/related/topology/components/Sankey.vue
@@ -17,11 +17,19 @@ limitations under the License. -->
   <Graph :option="option" @select="clickChart" />
 </template>
 <script lang="ts" setup>
-import { computed } from "vue";
+import { computed, PropType } from "vue";
 import { useTopologyStore } from "@/store/modules/topology";
 import { Node, Call } from "@/types/topology";
+import { MetricConfigOpt } from "@/types/dashboard";
+import { aggregation } from "@/hooks/useProcessor";
 
-/*global defineEmits */
+/*global defineEmits, defineProps */
+const props = defineProps({
+  settings: {
+    type: Object as PropType<any>,
+    default: () => ({}),
+  },
+});
 const emit = defineEmits(["click"]);
 const topologyStore = useTopologyStore();
 const option = computed(() => getOption());
@@ -77,23 +85,34 @@ function getOption() {
 function linkTooltip(data: Call) {
   const clientMetrics: string[] = Object.keys(topologyStore.linkClientMetrics);
   const serverMetrics: string[] = Object.keys(topologyStore.linkServerMetrics);
-  const htmlServer = serverMetrics.map((m) => {
-    const metric = topologyStore.linkServerMetrics[m].values.filter(
-      (val: { id: string; value: unknown }) => val.id === data.id
-    )[0];
+  const linkServerMetricConfig: MetricConfigOpt[] =
+    props.settings.linkServerMetricConfig || [];
+  const linkClientMetricConfig: MetricConfigOpt[] =
+    props.settings.linkClientMetricConfig || [];
+
+  const htmlServer = serverMetrics.map((m, index) => {
+    const metric =
+      topologyStore.linkServerMetrics[m].values.find(
+        (val: { id: string; value: unknown }) => val.id === data.id
+      ) || {};
     if (metric) {
-      const val = m.includes("_sla") ? metric.value / 100 : metric.value;
-      return ` <div><span>${m}: </span>${val}</div>`;
+      const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
+      const v = aggregation(metric.value, opt);
+      return ` <div class="mb-5"><span class="grey">${
+        opt.label || m
+      }: </span>${v} ${opt.unit || ""}</div>`;
     }
   });
-  const htmlClient = clientMetrics.map((m) => {
-    const metric = topologyStore.linkClientMetrics[m].values.filter(
-      (val: { id: string; value: unknown }) => val.id === data.id
-    )[0];
-    if (metric) {
-      const val = m.includes("_sla") ? metric.value / 100 : metric.value;
-      return ` <div><span>${m}: </span>${val}</div>`;
-    }
+  const htmlClient = clientMetrics.map((m, index) => {
+    const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
+    const metric =
+      topologyStore.linkClientMetrics[m].values.find(
+        (val: { id: string; value: unknown }) => val.id === data.id
+      ) || {};
+    const v = aggregation(metric.value, opt);
+    return ` <div class="mb-5"><span class="grey">${
+      opt.label || m
+    }: </span>${v} ${opt.unit || ""}</div>`;
   });
   const html = [
     `<div>${data.sourceObj.serviceName} -> ${data.targetObj.serviceName}</div>`,
@@ -106,13 +125,17 @@ function linkTooltip(data: Call) {
 
 function nodeTooltip(data: Node) {
   const nodeMetrics: string[] = Object.keys(topologyStore.nodeMetricValue);
-  const html = nodeMetrics.map((m) => {
+  const nodeMetricConfig = props.settings.nodeMetricConfig || [];
+  const html = nodeMetrics.map((m, index) => {
     const metric =
-      topologyStore.nodeMetricValue[m].values.filter(
+      topologyStore.nodeMetricValue[m].values.find(
         (val: { id: string; value: unknown }) => val.id === data.id
-      )[0] || {};
-    const val = m.includes("_sla") ? metric.value / 100 : metric.value;
-    return ` <div><span>${m}: </span>${val}</div>`;
+      ) || {};
+    const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
+    const v = aggregation(metric.value, opt);
+    return ` <div class="mb-5"><span class="grey">${
+      opt.label || m
+    }: </span>${v} ${opt.unit || ""}</div>`;
   });
   return [` <div><span>name: </span>${data.serviceName}</div>`, ...html].join(
     " "
diff --git a/src/views/dashboard/related/topology/components/Settings.vue b/src/views/dashboard/related/topology/components/Settings.vue
index 21e15c8..bfbc3fb 100644
--- a/src/views/dashboard/related/topology/components/Settings.vue
+++ b/src/views/dashboard/related/topology/components/Settings.vue
@@ -25,7 +25,27 @@ limitations under the License. -->
       class="inputs"
       :clearable="true"
     />
-    <div class="label">{{ t("linkServerMetrics") }}</div>
+    <div class="label">
+      <span>{{ t("linkServerMetrics") }}</span>
+      <el-popover
+        placement="left"
+        :width="400"
+        trigger="click"
+        effect="dark"
+        v-if="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"
+        />
+      </el-popover>
+    </div>
     <Selector
       class="inputs"
       :multiple="true"
@@ -33,11 +53,29 @@ limitations under the License. -->
       :options="states.linkMetricList"
       size="small"
       placeholder="Select metrics"
-      @change="changeLinkServerMetrics"
+      @change="updateLinkServerMetrics"
     />
     <span v-show="dashboardStore.entity !== EntityType[2].value">
       <div class="label">
-        {{ t("linkClientMetrics") }}
+        <span>{{ t("linkClientMetrics") }}</span>
+        <el-popover
+          placement="left"
+          :width="400"
+          trigger="click"
+          effect="dark"
+          v-if="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"
+          />
+        </el-popover>
       </div>
       <Selector
         class="inputs"
@@ -46,7 +84,7 @@ limitations under the License. -->
         :options="states.linkMetricList"
         size="small"
         placeholder="Select metrics"
-        @change="changeLinkClientMetrics"
+        @change="updateLinkClientMetrics"
       />
     </span>
   </div>
@@ -100,7 +138,27 @@ limitations under the License. -->
         />
       </span>
     </div>
-    <div class="label">{{ t("nodeMetrics") }}</div>
+    <div class="label">
+      <span>{{ t("nodeMetrics") }}</span>
+      <el-popover
+        placement="left"
+        :width="400"
+        trigger="click"
+        effect="dark"
+        v-if="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"
+        />
+      </el-popover>
+    </div>
     <Selector
       class="inputs"
       :multiple="true"
@@ -108,7 +166,7 @@ limitations under the License. -->
       :options="states.nodeMetricList"
       size="small"
       placeholder="Select metrics"
-      @change="changeNodeMetrics"
+      @change="updateNodeMetrics"
     />
   </div>
   <div class="legend-settings" v-show="isService">
@@ -157,15 +215,6 @@ limitations under the License. -->
       </span>
       <div v-show="index !== legend.metric.length - 1">&&</div>
     </div>
-    <!-- <div class="label">{{ t("conditions") }}</div>
-    <Selector
-      class="inputs"
-      :value="legend.condition"
-      :options="LegendConditions"
-      size="small"
-      placeholder="Select a condition"
-      @change="changeCondition"
-    /> -->
     <el-button
       @click="setLegend"
       class="legend-btn"
@@ -177,7 +226,7 @@ limitations under the License. -->
   </div>
 </template>
 <script lang="ts" setup>
-import { reactive } from "vue";
+import { reactive, ref } from "vue";
 import { useI18n } from "vue-i18n";
 import { useDashboardStore } from "@/store/modules/dashboard";
 import { useTopologyStore } from "@/store/modules/topology";
@@ -186,8 +235,9 @@ import { MetricCatalog, ScopeType, MetricConditions } from "../../../data";
 import { Option } from "@/types/app";
 import { useQueryTopologyMetrics } from "@/hooks/useProcessor";
 import { Node } from "@/types/topology";
-import { DashboardItem } from "@/types/dashboard";
+import { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
 import { EntityType, LegendOpt, MetricsType } from "../../../data";
+import Metrics from "./Metrics.vue";
 
 /*global defineEmits */
 const emit = defineEmits(["update", "updateNodes"]);
@@ -238,6 +288,7 @@ const legend = reactive<{
 }>({
   metric: l ? selectedGrid.legend : [{ name: "", condition: "", value: "" }],
 });
+const configType = ref<string>("");
 
 getMetricList();
 async function getMetricList() {
@@ -347,11 +398,12 @@ function deleteItem(index: number) {
   items.splice(index, 1);
   updateSettings();
 }
-function updateSettings() {
+function updateSettings(metricConfig?: { [key: string]: MetricConfigOpt[] }) {
   const metrics = legend.metric.filter(
     (d: any) => d.name && d.value && d.condition
   );
   const param = {
+    ...dashboardStore.selectedGrid,
     linkDashboard: states.linkDashboard,
     nodeDashboard: isService
       ? items.filter((d: { scope: string; dashboard: string }) => d.dashboard)
@@ -360,32 +412,76 @@ function updateSettings() {
     linkClientMetrics: states.linkClientMetrics,
     nodeMetrics: states.nodeMetrics,
     legend: metrics,
+    ...metricConfig,
   };
-  dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, ...param });
-  dashboardStore.setConfigs({ ...dashboardStore.selectedGrid, ...param });
+  dashboardStore.selectWidget(param);
+  dashboardStore.setConfigs(param);
   emit("update", param);
 }
-async function changeLinkServerMetrics(options: Option[] | any) {
-  states.linkServerMetrics = options.map((d: Option) => d.value);
-  updateSettings();
+function updateLinkServerMetrics(options: Option[] | any) {
+  const opt = options.map((d: Option) => d.value);
+  const index = states.linkServerMetrics.findIndex(
+    (d: any) => !opt.includes(d)
+  );
+  states.linkServerMetrics = opt;
+  if (index < 0) {
+    changeLinkServerMetrics();
+    return;
+  }
+  const origin = dashboardStore.selectedGrid.linkServerMetricConfig || [];
+  const config = origin.length === 1 ? [] : origin.splice(index, 1);
+  changeLinkServerMetrics({ linkServerMetricConfig: config });
+}
+async function changeLinkServerMetrics(config?: {
+  [key: string]: MetricConfigOpt[];
+}) {
+  updateSettings(config);
   if (!states.linkServerMetrics.length) {
     topologyStore.setLinkServerMetrics({});
     return;
   }
   topologyStore.getLinkServerMetrics(states.linkServerMetrics);
 }
-async function changeLinkClientMetrics(options: Option[] | any) {
-  states.linkClientMetrics = options.map((d: Option) => d.value);
-  updateSettings();
+function updateLinkClientMetrics(options: Option[] | any) {
+  const opt = options.map((d: Option) => d.value);
+  const index = states.linkClientMetrics.findIndex(
+    (d: any) => !opt.includes(d)
+  );
+  states.linkClientMetrics = opt;
+  if (index < 0) {
+    changeLinkClientMetrics();
+    return;
+  }
+  const origin = dashboardStore.selectedGrid.linkClientMetricConfig || [];
+  const config = origin.length === 1 ? [] : origin.splice(index, 1);
+  changeLinkClientMetrics({ linkClientMetricConfig: config });
+}
+async function changeLinkClientMetrics(config?: {
+  [key: string]: MetricConfigOpt[];
+}) {
+  updateSettings(config);
   if (!states.linkClientMetrics.length) {
     topologyStore.setLinkClientMetrics({});
     return;
   }
   topologyStore.getLinkClientMetrics(states.linkClientMetrics);
 }
-async function changeNodeMetrics(options: Option[] | any) {
-  states.nodeMetrics = options.map((d: Option) => d.value);
-  updateSettings();
+function updateNodeMetrics(options: Option[] | any) {
+  const opt = options.map((d: Option) => d.value);
+  const index = states.nodeMetrics.findIndex((d: any) => !opt.includes(d));
+  states.nodeMetrics = opt;
+  if (index < 0) {
+    changeNodeMetrics();
+    return;
+  }
+  const origin = dashboardStore.selectedGrid.nodeMetricConfig || [];
+  const config = origin.length === 1 ? [] : origin.splice(index, 1);
+  changeNodeMetrics({ nodeMetricConfig: config });
+}
+async function changeNodeMetrics(config?: {
+  [key: string]: MetricConfigOpt[];
+}) {
+  updateSettings(config);
   if (!states.nodeMetrics.length) {
     topologyStore.setNodeMetricValue({});
     return;
@@ -402,6 +498,9 @@ function deleteMetric(index: number) {
 function addMetric() {
   legend.metric.push({ name: "", condition: "", value: "" });
 }
+function setConfigType(type: string) {
+  configType.value = type;
+}
 </script>
 <style lang="scss" scoped>
 .link-settings {