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/25 07:51:09 UTC

[skywalking-booster-ui] branch main updated: feat: Implement Text control and update Topology (#37)

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 99e23c3  feat: Implement Text control and update Topology (#37)
99e23c3 is described below

commit 99e23c33dc55add382b02233b5fea725f33d74d6
Author: Fine0830 <fi...@outlook.com>
AuthorDate: Fri Mar 25 15:51:06 2022 +0800

    feat: Implement Text control and update Topology (#37)
---
 src/locales/lang/en.ts                             |   1 +
 src/locales/lang/zh.ts                             |   1 +
 src/store/data.ts                                  |   9 +-
 src/store/modules/dashboard.ts                     |  17 ++-
 src/types/dashboard.ts                             |   8 ++
 src/views/Layer.vue                                |   5 +-
 src/views/dashboard/Edit.vue                       |  96 +++++++------
 src/views/dashboard/configuration/Text.vue         | 149 +++++++++++++++++++++
 src/views/dashboard/configuration/Widget.vue       |   1 -
 .../dashboard/{controls => configuration}/index.ts |  14 +-
 src/views/dashboard/controls/Tab.vue               |   4 +-
 src/views/dashboard/controls/Text.vue              | 129 ++++++++++++++++++
 src/views/dashboard/controls/index.ts              |   3 +-
 src/views/dashboard/data.ts                        |  18 ++-
 src/views/dashboard/panel/Layout.vue               |   3 +
 src/views/dashboard/panel/Tool.vue                 |   6 +
 .../related/topology/components/Graph.vue          |  14 +-
 .../related/topology/components/PodTopology.vue    |   5 +-
 .../related/topology/components/Settings.vue       |  21 ++-
 .../dashboard/related/trace/utils/d3-trace-list.ts |  12 +-
 20 files changed, 436 insertions(+), 80 deletions(-)

diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts
index 08cf4bb..c11b198 100644
--- a/src/locales/lang/en.ts
+++ b/src/locales/lang/en.ts
@@ -125,6 +125,7 @@ const msg = {
   kubernetesService: "Service",
   kubernetesCluster: "Cluster",
   kubernetes: "Kubernetes",
+  textUrl: "Text Hyperlink",
   hourTip: "Select Hour",
   minuteTip: "Select Minute",
   secondTip: "Select Second",
diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts
index 3b01a71..6c1d334 100644
--- a/src/locales/lang/zh.ts
+++ b/src/locales/lang/zh.ts
@@ -125,6 +125,7 @@ const msg = {
   kubernetesService: "服务",
   kubernetesCluster: "集群",
   kubernetes: "Kubernetes",
+  textUrl: "文本超链接",
   hourTip: "选择小时",
   minuteTip: "选择分钟",
   secondTip: "选择秒数",
diff --git a/src/store/data.ts b/src/store/data.ts
index 50dbbd2..6fdfdd9 100644
--- a/src/store/data.ts
+++ b/src/store/data.ts
@@ -22,10 +22,17 @@ export const NewControl = {
   i: "0",
   type: "Widget",
   widget: {
-    title: "Title",
+    title: "",
   },
   graph: {},
   standard: {},
   metrics: [""],
   metricTypes: [""],
 };
+export const TextConfig = {
+  fontColor: "white",
+  backgroundColor: "green",
+  content: "Text",
+  fontSize: 14,
+  textAlign: "left",
+};
diff --git a/src/store/modules/dashboard.ts b/src/store/modules/dashboard.ts
index 2536a4c..410df5d 100644
--- a/src/store/modules/dashboard.ts
+++ b/src/store/modules/dashboard.ts
@@ -22,7 +22,7 @@ import query from "@/graphql/fetch";
 import { DashboardItem } from "@/types/dashboard";
 import { useAppStoreWithOut } from "@/store/modules/app";
 import { useSelectorStore } from "@/store/modules/selectors";
-import { NewControl } from "../data";
+import { NewControl, TextConfig } from "../data";
 import { Duration } from "@/types/app";
 import { AxiosResponse } from "axios";
 import { ElMessage } from "element-plus";
@@ -112,6 +112,10 @@ export const dashboardStore = defineStore({
       if (type === "Trace" || type === "Profile" || type === "Log") {
         newItem.h = 36;
       }
+      if (type === "Text") {
+        newItem.h = 6;
+        newItem.graph = TextConfig;
+      }
       this.activedGridItem = newItem.i;
       this.selectedGrid = newItem;
       this.layout = this.layout.map((d: LayoutConfig) => {
@@ -158,6 +162,10 @@ export const dashboardStore = defineStore({
       if (type === "Trace" || type === "Profile" || type === "Log") {
         newItem.h = 32;
       }
+      if (type === "Text") {
+        newItem.h = 6;
+        newItem.graph = TextConfig;
+      }
       if (this.layout[idx].children) {
         const items = children.map((d: LayoutConfig) => {
           d.y = d.y + newItem.h;
@@ -171,10 +179,9 @@ export const dashboardStore = defineStore({
     activeGridItem(index: string) {
       this.activedGridItem = index;
     },
-    setActiveTabIndex(index: number) {
-      const idx = this.layout.findIndex(
-        (d: LayoutConfig) => d.i === this.activedGridItem
-      );
+    setActiveTabIndex(index: number, target?: number) {
+      const m = target || this.activedGridItem;
+      const idx = this.layout.findIndex((d: LayoutConfig) => d.i === m);
       if (idx < 0) {
         return;
       }
diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts
index bae7588..62d12b9 100644
--- a/src/types/dashboard.ts
+++ b/src/types/dashboard.ts
@@ -91,6 +91,14 @@ export interface CardConfig {
   textAlign?: "center" | "right" | "left";
 }
 
+export interface TextConfig {
+  fontSize: number;
+  backgroundColor: string;
+  textAlign: string;
+  fontColor: string;
+  content: string;
+}
+
 export interface TableConfig {
   type?: string;
   showTableValues: boolean;
diff --git a/src/views/Layer.vue b/src/views/Layer.vue
index 71dbb62..d18a018 100644
--- a/src/views/Layer.vue
+++ b/src/views/Layer.vue
@@ -13,7 +13,7 @@ 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>
-  <Edit v-if="dashboardStore.currentDashboard" />
+  <Dashboard v-if="dashboardStore.currentDashboard" />
   <div v-else class="no-root">
     {{ t("noRoot") }} {{ dashboardStore.layerId }}
   </div>
@@ -24,7 +24,7 @@ import { ref } from "vue";
 import { useRoute } from "vue-router";
 import { EntityType } from "./dashboard/data";
 import { useDashboardStore } from "@/store/modules/dashboard";
-import Edit from "./dashboard/Edit.vue";
+import Dashboard from "./dashboard/Edit.vue";
 import { useI18n } from "vue-i18n";
 import { useAppStoreWithOut } from "@/store/modules/app";
 
@@ -52,7 +52,6 @@ getDashboard();
 
 async function getDashboard() {
   layer.value = routesMap[String(route.name)];
-  console.log(layer.value);
   dashboardStore.setLayer(layer.value);
   dashboardStore.setEntity(EntityType[1].value);
   dashboardStore.setMode(false);
diff --git a/src/views/dashboard/Edit.vue b/src/views/dashboard/Edit.vue
index 595a089..ad4b8c9 100644
--- a/src/views/dashboard/Edit.vue
+++ b/src/views/dashboard/Edit.vue
@@ -28,63 +28,71 @@ limitations under the License. -->
       :destroy-on-close="true"
       @closed="dashboardStore.setConfigPanel(false)"
     >
-      <TopologyConfig v-if="dashboardStore.selectedGrid.type === 'Topology'" />
-      <WidgetConfig v-else />
+      <component :is="dashboardStore.selectedGrid.type" />
     </el-dialog>
   </div>
 </template>
-<script lang="ts" setup>
-import { ref } from "vue";
+<script lang="ts">
+import { ref, defineComponent } from "vue";
 import { useI18n } from "vue-i18n";
 import { useRoute } from "vue-router";
 import GridLayout from "./panel/Layout.vue";
 import Tool from "./panel/Tool.vue";
-import TopologyConfig from "./configuration/Topology.vue";
-import WidgetConfig from "./configuration/Widget.vue";
 import { useDashboardStore } from "@/store/modules/dashboard";
 import { useAppStoreWithOut } from "@/store/modules/app";
+import Configuration from "./configuration";
 
-const dashboardStore = useDashboardStore();
-const appStore = useAppStoreWithOut();
-const { t } = useI18n();
-const p = useRoute().params;
-const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`);
+export default defineComponent({
+  name: "Dashboard",
+  components: { ...Configuration, GridLayout, Tool },
+  setup() {
+    const dashboardStore = useDashboardStore();
+    const appStore = useAppStoreWithOut();
+    const { t } = useI18n();
+    const p = useRoute().params;
+    const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`);
+    setTemplate();
+    async function setTemplate() {
+      await dashboardStore.setDashboards();
 
-setTemplate();
-async function setTemplate() {
-  await dashboardStore.setDashboards();
-
-  if (!p.entity) {
-    if (!dashboardStore.currentDashboard) {
-      return;
+      if (!p.entity) {
+        if (!dashboardStore.currentDashboard) {
+          return;
+        }
+        const { layer, entity, name } = dashboardStore.currentDashboard;
+        layoutKey.value = `${layer}_${entity}_${name}`;
+      }
+      const c: { configuration: string; id: string } = JSON.parse(
+        sessionStorage.getItem(layoutKey.value) || "{}"
+      );
+      const layout: any = c.configuration || {};
+      dashboardStore.setLayout(layout.children || []);
+      appStore.setPageTitle(layout.name);
+      if (p.entity) {
+        dashboardStore.setCurrentDashboard({
+          layer: p.layerId,
+          entity: p.entity,
+          name: p.name,
+          id: c.id,
+          isRoot: layout.isRoot,
+        });
+      }
+    }
+    function handleClick(e: any) {
+      e.stopPropagation();
+      if (e.target.className === "ds-main") {
+        dashboardStore.activeGridItem("");
+        dashboardStore.selectWidget(null);
+      }
     }
-    const { layer, entity, name } = dashboardStore.currentDashboard;
-    layoutKey.value = `${layer}_${entity}_${name}`;
-  }
-  const c: { configuration: string; id: string } = JSON.parse(
-    sessionStorage.getItem(layoutKey.value) || "{}"
-  );
-  const layout: any = c.configuration || {};
-  dashboardStore.setLayout(layout.children || []);
-  appStore.setPageTitle(layout.name);
-  if (p.entity) {
-    dashboardStore.setCurrentDashboard({
-      layer: p.layerId,
-      entity: p.entity,
-      name: p.name,
-      id: c.id,
-      isRoot: layout.isRoot,
-    });
-  }
-}
 
-function handleClick(e: any) {
-  e.stopPropagation();
-  if (e.target.className === "ds-main") {
-    dashboardStore.activeGridItem("");
-    dashboardStore.selectWidget(null);
-  }
-}
+    return {
+      t,
+      handleClick,
+      dashboardStore,
+    };
+  },
+});
 </script>
 <style lang="scss" scoped>
 .ds-main {
diff --git a/src/views/dashboard/configuration/Text.vue b/src/views/dashboard/configuration/Text.vue
new file mode 100644
index 0000000..268be91
--- /dev/null
+++ b/src/views/dashboard/configuration/Text.vue
@@ -0,0 +1,149 @@
+<!-- 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("textUrl") }}</span>
+    <el-input
+      class="input"
+      v-model="url"
+      size="small"
+      @change="changeConfig({ url })"
+    />
+  </div>
+  <div class="item">
+    <span class="label">{{ t("content") }}</span>
+    <el-input
+      class="input"
+      v-model="content"
+      size="small"
+      @change="changeConfig({ content })"
+    />
+  </div>
+  <div class="item">
+    <span class="label">{{ t("backgroundColors") }}</span>
+    <Selector
+      :value="backgroundColor"
+      :options="Colors"
+      size="small"
+      placeholder="Select a color"
+      class="input"
+      @change="changeConfig({ backgroundColor: $event[0].value })"
+    />
+  </div>
+  <div class="item">
+    <span class="label">{{ t("fontSize") }}</span>
+    <el-slider
+      class="slider"
+      v-model="fontSize"
+      show-input
+      input-size="small"
+      :min="12"
+      :max="30"
+      :step="1"
+      @change="changeConfig({ fontSize })"
+    />
+  </div>
+  <div class="item">
+    <span class="label">{{ t("fontColors") }}</span>
+    <Selector
+      :value="fontColor"
+      :options="Colors"
+      size="small"
+      placeholder="Select a color"
+      class="input"
+      @change="changeConfig({ fontColor: $event[0].value })"
+    />
+  </div>
+  <div class="footer">
+    <el-button size="small" @click="cancelConfig">
+      {{ t("cancel") }}
+    </el-button>
+    <el-button size="small" type="primary" @click="applyConfig">
+      {{ t("apply") }}
+    </el-button>
+  </div>
+</template>
+<script lang="ts" setup>
+import { useI18n } from "vue-i18n";
+import { ref } from "vue";
+import { useDashboardStore } from "@/store/modules/dashboard";
+const { t } = useI18n();
+const dashboardStore = useDashboardStore();
+const originConfig = dashboardStore.selectedGrid;
+const url = ref(originConfig.graph.url || "");
+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 Colors = [
+  {
+    label: "Green",
+    value: "green",
+  },
+  { label: "Blue", value: "blue" },
+  { label: "Red", value: "red" },
+  { label: "Grey", value: "grey" },
+  { label: "White", value: "white" },
+  { label: "Black", value: "black" },
+  { label: "Orange", value: "orange" },
+];
+function changeConfig(param: { [key: string]: unknown }) {
+  const { selectedGrid } = dashboardStore;
+  const graph = {
+    ...selectedGrid.graph,
+    ...param,
+  };
+  dashboardStore.selectWidget({ ...selectedGrid, graph });
+}
+function applyConfig() {
+  dashboardStore.setConfigPanel(false);
+  dashboardStore.setConfigs(dashboardStore.selectedGrid);
+}
+
+function cancelConfig() {
+  dashboardStore.selectWidget(originConfig);
+  dashboardStore.setConfigPanel(false);
+}
+</script>
+<style lang="scss" scoped>
+.slider {
+  width: 500px;
+  margin-top: -3px;
+}
+
+.label {
+  font-size: 13px;
+  font-weight: 500;
+  display: block;
+  margin-bottom: 5px;
+}
+
+.input {
+  width: 500px;
+}
+
+.item {
+  margin-bottom: 10px;
+}
+
+.footer {
+  position: fixed;
+  bottom: 0;
+  right: 0;
+  border-top: 1px solid #eee;
+  padding: 10px;
+  text-align: right;
+  width: 100%;
+  background-color: #fff;
+}
+</style>
diff --git a/src/views/dashboard/configuration/Widget.vue b/src/views/dashboard/configuration/Widget.vue
index 57c1184..442e859 100644
--- a/src/views/dashboard/configuration/Widget.vue
+++ b/src/views/dashboard/configuration/Widget.vue
@@ -90,7 +90,6 @@ 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 { ListChartTypes } from "../data";
 
 export default defineComponent({
   name: "ConfigEdit",
diff --git a/src/views/dashboard/controls/index.ts b/src/views/dashboard/configuration/index.ts
similarity index 82%
copy from src/views/dashboard/controls/index.ts
copy to src/views/dashboard/configuration/index.ts
index 7fdcd85..a1359ee 100644
--- a/src/views/dashboard/controls/index.ts
+++ b/src/views/dashboard/configuration/index.ts
@@ -14,11 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import Topology from "./Topology.vue";
-import Tab from "./Tab.vue";
+
+import Text from "./Text.vue";
 import Widget from "./Widget.vue";
-import Trace from "./Trace.vue";
-import Profile from "./Profile.vue";
-import Log from "./Log.vue";
+import Topology from "./Topology.vue";
 
-export default { Tab, Widget, Trace, Topology, Profile, Log };
+export default {
+  Text,
+  Widget,
+  Topology,
+};
diff --git a/src/views/dashboard/controls/Tab.vue b/src/views/dashboard/controls/Tab.vue
index 2582f3b..26d1163 100644
--- a/src/views/dashboard/controls/Tab.vue
+++ b/src/views/dashboard/controls/Tab.vue
@@ -121,6 +121,7 @@ import Widget from "./Widget.vue";
 import Trace from "./Trace.vue";
 import Profile from "./Profile.vue";
 import Log from "./Log.vue";
+import Text from "./Text.vue";
 
 const props = {
   data: {
@@ -131,7 +132,7 @@ const props = {
 };
 export default defineComponent({
   name: "Tab",
-  components: { Topology, Widget, Trace, Profile, Log },
+  components: { Topology, Widget, Trace, Profile, Log, Text },
   props,
   setup(props) {
     const { t } = useI18n();
@@ -149,6 +150,7 @@ export default defineComponent({
       dashboardStore.setCurrentTabItems(
         dashboardStore.layout[l].children[activeTabIndex.value].children
       );
+      dashboardStore.setActiveTabIndex(activeTabIndex.value, props.data.i);
     }
 
     function clickTabs(e: Event, idx: number) {
diff --git a/src/views/dashboard/controls/Text.vue b/src/views/dashboard/controls/Text.vue
new file mode 100644
index 0000000..60f77ed
--- /dev/null
+++ b/src/views/dashboard/controls/Text.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="topology">
+    <div class="header">
+      <el-popover
+        placement="bottom"
+        trigger="click"
+        :width="100"
+        v-if="dashboardStore.editMode"
+      >
+        <template #reference>
+          <span>
+            <Icon iconName="ellipsis_v" size="middle" class="operation" />
+          </span>
+        </template>
+        <div class="tools" @click="editConfig">
+          <span>{{ t("edit") }}</span>
+        </div>
+        <div class="tools" @click="removeTopo">
+          <span>{{ t("delete") }}</span>
+        </div>
+      </el-popover>
+    </div>
+    <div
+      class="body"
+      :style="{ backgroundColor: TextColors[data.graph.backgroundColor] }"
+    >
+      <div
+        :style="{
+          color: TextColors[data.graph.fontColor],
+          fontSize: data.graph.fontSize + 'px',
+        }"
+      >
+        <a :href="data.graph.url" target="_blank">
+          {{ data.graph.content }}
+        </a>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import type { PropType } from "vue";
+import { useI18n } from "vue-i18n";
+import { useDashboardStore } from "@/store/modules/dashboard";
+import { TextColors } from "@/views/dashboard/data";
+
+/*global defineProps */
+const props = defineProps({
+  data: {
+    type: Object as PropType<any>,
+    default: () => ({ graph: {} }),
+  },
+  activeIndex: { type: String, default: "" },
+});
+const { t } = useI18n();
+const dashboardStore = useDashboardStore();
+
+function removeTopo() {
+  dashboardStore.removeControls(props.data);
+}
+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 {
+  font-size: 12px;
+  height: 100%;
+  position: relative;
+}
+
+.operation {
+  cursor: pointer;
+}
+
+.header {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+}
+
+.body {
+  text-align: center;
+  width: 100%;
+  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;
+}
+
+.tools {
+  padding: 5px 0;
+  color: #999;
+  cursor: pointer;
+  position: relative;
+  text-align: center;
+
+  &:hover {
+    color: #409eff;
+    background-color: #eee;
+  }
+}
+</style>
diff --git a/src/views/dashboard/controls/index.ts b/src/views/dashboard/controls/index.ts
index 7fdcd85..9829a0d 100644
--- a/src/views/dashboard/controls/index.ts
+++ b/src/views/dashboard/controls/index.ts
@@ -20,5 +20,6 @@ import Widget from "./Widget.vue";
 import Trace from "./Trace.vue";
 import Profile from "./Profile.vue";
 import Log from "./Log.vue";
+import Text from "./Text.vue";
 
-export default { Tab, Widget, Trace, Topology, Profile, Log };
+export default { Tab, Widget, Trace, Topology, Profile, Log, Text };
diff --git a/src/views/dashboard/data.ts b/src/views/dashboard/data.ts
index a97fc3d..9668d76 100644
--- a/src/views/dashboard/data.ts
+++ b/src/views/dashboard/data.ts
@@ -23,10 +23,10 @@ export const ChartTypes = [
   { label: "Bar", value: "Bar" },
   { label: "Line", value: "Line" },
   { label: "Area", value: "Area" },
-  // { label: "Pie", value: "Pie" },
   { label: "Card", value: "Card" },
   { label: "Top List", value: "TopList" },
   { label: "Table", value: "Table" },
+  { label: "Text", value: "Text" },
   { label: "Heatmap", value: "Heatmap" },
   { label: "Service List", value: "ServiceList" },
   { label: "Endpoint List", value: "EndpointList" },
@@ -169,6 +169,7 @@ export const SortOrder = [
 export const AllTools = [
   { name: "playlist_add", content: "Add Widget", id: "addWidget" },
   { name: "all_inbox", content: "Add Tab", id: "addTab" },
+  { name: "library_books", content: "Add Text", id: "addText" },
   { name: "device_hub", content: "Add Topology", id: "addTopology" },
   { name: "merge", content: "Add Trace", id: "addTrace" },
   { name: "assignment", content: "Add Log", id: "addLog" },
@@ -177,6 +178,7 @@ export const AllTools = [
 export const ServiceTools = [
   { name: "playlist_add", content: "Add Widget", id: "addWidget" },
   { name: "all_inbox", content: "Add Tab", id: "addTab" },
+  { name: "library_books", content: "Add Text", id: "addText" },
   { name: "device_hub", content: "Add Topology", id: "addTopology" },
   { name: "merge", content: "Add Trace", id: "addTrace" },
   { name: "timeline", content: "Add Profile", id: "addProfile" },
@@ -186,6 +188,7 @@ export const ServiceTools = [
 export const InstanceTools = [
   { name: "playlist_add", content: "Add Widget", id: "addWidget" },
   { name: "all_inbox", content: "Add Tab", id: "addTab" },
+  { name: "library_books", content: "Add Text", id: "addText" },
   { name: "merge", content: "Add Trace", id: "addTrace" },
   { name: "assignment", content: "Add Log", id: "addLog" },
   { name: "save", content: "Apply", id: "apply" },
@@ -193,6 +196,7 @@ export const InstanceTools = [
 export const EndpointTools = [
   { name: "playlist_add", content: "Add Widget", id: "addWidget" },
   { name: "all_inbox", content: "Add Tab", id: "addTab" },
+  { name: "library_books", content: "Add Text", id: "addText" },
   { name: "device_hub", content: "Add Topology", id: "addTopology" },
   { name: "merge", content: "Add Trace", id: "addTrace" },
   { name: "assignment", content: "Add Log", id: "addLog" },
@@ -201,6 +205,7 @@ export const EndpointTools = [
 export const ServiceRelationTools = [
   { name: "playlist_add", content: "Add Widget", id: "addWidget" },
   { name: "all_inbox", content: "Add Tab", id: "addTab" },
+  { name: "library_books", content: "Add Text", id: "addText" },
   { name: "device_hub", content: "Add Topology", id: "addTopology" },
   { name: "save", content: "Apply", id: "apply" },
 ];
@@ -208,11 +213,13 @@ export const ServiceRelationTools = [
 export const EndpointRelationTools = [
   { name: "playlist_add", content: "Add Widget", id: "addWidget" },
   { name: "all_inbox", content: "Add Tab", id: "addTab" },
+  { name: "library_books", content: "Add Text", id: "addText" },
   { name: "save", content: "Apply", id: "apply" },
 ];
 export const InstanceRelationTools = [
   { name: "playlist_add", content: "Add Widget", id: "addWidget" },
   { name: "all_inbox", content: "Add Tab", id: "addTab" },
+  { name: "library_books", content: "Add Text", id: "addText" },
   { name: "device_hub", content: "Add Topology", id: "addTopology" },
   { name: "save", content: "Apply", id: "apply" },
 ];
@@ -248,3 +255,12 @@ export const QueryOrders = [
   { label: "Start Time", value: "BY_START_TIME" },
   { label: "Duration", value: "BY_DURATION" },
 ];
+export const TextColors: { [key: string]: string } = {
+  green: "#67C23A",
+  blue: "#409EFF",
+  red: "#F56C6C",
+  grey: "#909399",
+  white: "#fff",
+  black: "#000",
+  orange: "#E6A23C",
+};
diff --git a/src/views/dashboard/panel/Layout.vue b/src/views/dashboard/panel/Layout.vue
index 93d8ad1..cec440a 100644
--- a/src/views/dashboard/panel/Layout.vue
+++ b/src/views/dashboard/panel/Layout.vue
@@ -59,6 +59,9 @@ export default defineComponent({
     function clickGrid(item: LayoutConfig) {
       dashboardStore.activeGridItem(item.i);
       dashboardStore.selectWidget(item);
+      if (item.type === "Tab") {
+        dashboardStore.setActiveTabIndex(0);
+      }
     }
     onBeforeUnmount(() => {
       dashboardStore.setLayout([]);
diff --git a/src/views/dashboard/panel/Tool.vue b/src/views/dashboard/panel/Tool.vue
index 9c9e55c..d5476d0 100644
--- a/src/views/dashboard/panel/Tool.vue
+++ b/src/views/dashboard/panel/Tool.vue
@@ -367,6 +367,9 @@ function setTabControls(id: string) {
     case "addTopology":
       dashboardStore.addTabControls("Topology");
       break;
+    case "addText":
+      dashboardStore.addTabControls("Text");
+      break;
     default:
       ElMessage.info("Don't support this control");
       break;
@@ -393,6 +396,9 @@ function setControls(id: string) {
     case "addTopology":
       dashboardStore.addControl("Topology");
       break;
+    case "addText":
+      dashboardStore.addControl("Text");
+      break;
     default:
       dashboardStore.addControl("Widget");
   }
diff --git a/src/views/dashboard/related/topology/components/Graph.vue b/src/views/dashboard/related/topology/components/Graph.vue
index b682677..f48705a 100644
--- a/src/views/dashboard/related/topology/components/Graph.vue
+++ b/src/views/dashboard/related/topology/components/Graph.vue
@@ -30,7 +30,6 @@ limitations under the License. -->
           class="inputs"
           :value="depth"
           :options="DepthList"
-          placeholder="Select a option"
           @change="changeDepth"
         />
       </span>
@@ -377,10 +376,12 @@ async function handleInspect() {
   topologyStore.setNode(null);
   topologyStore.setLink(null);
   loading.value = true;
-  const resp = await topologyStore.getServicesTopology([id]);
+  const resp = await topologyStore.getDepthServiceTopology(
+    [id],
+    Number(depth.value)
+  );
   loading.value = false;
-
-  if (resp.errors) {
+  if (resp && resp.errors) {
     ElMessage.error(resp.errors);
   }
   await init();
@@ -555,17 +556,18 @@ watch(
 
   .operations-list {
     position: absolute;
-    padding: 10px;
     color: #333;
     cursor: pointer;
     background-color: #fff;
     border-radius: 3px;
+    padding: 10px 0;
 
     span {
       display: block;
       height: 30px;
       line-height: 30px;
-      text-align: center;
+      text-align: left;
+      padding: 0 15px;
     }
 
     span:hover {
diff --git a/src/views/dashboard/related/topology/components/PodTopology.vue b/src/views/dashboard/related/topology/components/PodTopology.vue
index b336279..8fc4df7 100644
--- a/src/views/dashboard/related/topology/components/PodTopology.vue
+++ b/src/views/dashboard/related/topology/components/PodTopology.vue
@@ -324,7 +324,7 @@ watch(
 
 .operations-list {
   position: absolute;
-  padding: 10px;
+  padding: 10px 0;
   color: #333;
   cursor: pointer;
   background-color: #fff;
@@ -335,7 +335,8 @@ watch(
     height: 30px;
     width: 140px;
     line-height: 30px;
-    text-align: center;
+    text-align: left;
+    padding: 0 15px;
   }
 
   span:hover {
diff --git a/src/views/dashboard/related/topology/components/Settings.vue b/src/views/dashboard/related/topology/components/Settings.vue
index 8dbe619..21e15c8 100644
--- a/src/views/dashboard/related/topology/components/Settings.vue
+++ b/src/views/dashboard/related/topology/components/Settings.vue
@@ -23,6 +23,7 @@ limitations under the License. -->
       placeholder="Please input a dashboard name for calls"
       @change="changeLinkDashboard"
       class="inputs"
+      :clearable="true"
     />
     <div class="label">{{ t("linkServerMetrics") }}</div>
     <Selector
@@ -86,7 +87,6 @@ limitations under the License. -->
       <span>
         <Icon
           class="cp mr-5"
-          v-show="items.length > 1"
           iconName="remove_circle_outline"
           size="middle"
           @click="deleteItem(index)"
@@ -144,7 +144,6 @@ limitations under the License. -->
           iconName="remove_circle_outline"
           size="middle"
           @click="deleteMetric(index)"
-          v-show="legend.metric.length > 1"
         />
         <Icon
           class="cp"
@@ -196,6 +195,10 @@ const { t } = useI18n();
 const dashboardStore = useDashboardStore();
 const topologyStore = useTopologyStore();
 const { selectedGrid } = dashboardStore;
+const nodeDashboard =
+  selectedGrid.nodeDashboard && selectedGrid.nodeDashboard.length
+    ? selectedGrid.nodeDashboard
+    : "";
 const isService = [EntityType[0].value, EntityType[1].value].includes(
   dashboardStore.entity
 );
@@ -204,7 +207,7 @@ const items = reactive<
     scope: string;
     dashboard: string;
   }[]
->((isService && selectedGrid.nodeDashboard) || [{ scope: "", dashboard: "" }]);
+>(isService && nodeDashboard ? nodeDashboard : [{ scope: "", dashboard: "" }]);
 const states = reactive<{
   linkDashboard: string;
   nodeDashboard: {
@@ -229,9 +232,12 @@ const states = reactive<{
   linkDashboards: [],
   nodeDashboards: [],
 });
+const l = selectedGrid.legend && selectedGrid.legend.length;
 const legend = reactive<{
   metric: { name: string; condition: string; value: string }[];
-}>({ metric: selectedGrid.legend || [{ name: "", condition: "", value: "" }] });
+}>({
+  metric: l ? selectedGrid.legend : [{ name: "", condition: "", value: "" }],
+});
 
 getMetricList();
 async function getMetricList() {
@@ -335,6 +341,9 @@ function addItem() {
   items.push({ scope: "", dashboard: "" });
 }
 function deleteItem(index: number) {
+  if (items.length === 1) {
+    items.push({ scope: "", dashboard: "" });
+  }
   items.splice(index, 1);
   updateSettings();
 }
@@ -384,6 +393,10 @@ async function changeNodeMetrics(options: Option[] | any) {
   topologyStore.queryNodeMetrics(states.nodeMetrics);
 }
 function deleteMetric(index: number) {
+  if (legend.metric.length === 1) {
+    legend.metric = [{ name: "", condition: "", value: "" }];
+    return;
+  }
   legend.metric.splice(index, 1);
 }
 function addMetric() {
diff --git a/src/views/dashboard/related/trace/utils/d3-trace-list.ts b/src/views/dashboard/related/trace/utils/d3-trace-list.ts
index f987e1b..dc2c16a 100644
--- a/src/views/dashboard/related/trace/utils/d3-trace-list.ts
+++ b/src/views/dashboard/related/trace/utils/d3-trace-list.ts
@@ -40,14 +40,15 @@ export default class ListGraph {
   constructor(el: HTMLDivElement, handleSelectSpan: (i: Trace) => void) {
     this.handleSelectSpan = handleSelectSpan;
     this.el = el;
-    this.width = el.clientWidth - 20;
+    this.width = el.clientWidth - 10;
     this.height = el.clientHeight;
     this.svg = d3
       .select(this.el)
       .append("svg")
       .attr("class", "trace-list-dowanload")
       .attr("width", this.width)
-      .attr("height", this.height);
+      .attr("height", this.height)
+      .attr("transform", `translate(-5, 0)`);
     this.tip = (d3tip as any)()
       .attr("class", "d3-tip")
       .offset([-8, 0])
@@ -220,7 +221,7 @@ export default class ListGraph {
     nodeEnter
       .transition()
       .duration(400)
-      .attr("transform", (d: any) => `translate(${d.y},${d.x})`)
+      .attr("transform", (d: any) => `translate(${d.y + 5},${d.x})`)
       .style("opacity", 1);
     nodeEnter
       .append("circle")
@@ -243,7 +244,7 @@ export default class ListGraph {
     node
       .transition()
       .duration(400)
-      .attr("transform", (d: any) => `translate(${d.y},${d.x})`)
+      .attr("transform", (d: any) => `translate(${d.y + 5},${d.x})`)
       .style("opacity", 1)
       .select("circle")
       .attr("fill", (d: any) =>
@@ -273,8 +274,9 @@ export default class ListGraph {
       .attr("fill", "rgba(0,0,0,0)")
       .attr("stroke", "rgba(0, 0, 0, 0.1)")
       .attr("stroke-width", 2)
+      .attr("transform", `translate(5, 0)`)
       .attr("d", () => {
-        const o = { x: source.x0 + 35, y: source.y0 };
+        const o = { x: source.x0 + 40, y: source.y0 };
         return this.diagonal({ source: o, target: o });
       })
       .transition()