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 8ad1c91  feat: Add Log widget in dashboards (#22)
8ad1c91 is described below

commit 8ad1c91082b5c58a7be39cd78ecef783e60d50c2
Author: Fine0830 <[email protected]>
AuthorDate: Tue Mar 8 14:39:42 2022 +0800

    feat: Add Log widget in dashboards (#22)
---
 src/locales/lang/en.ts                             |   3 +-
 src/locales/lang/zh.ts                             |   4 +-
 src/router/alert.ts                                |  45 ---
 src/router/index.ts                                |   5 +-
 src/store/modules/dashboard.ts                     |   6 +-
 src/store/modules/log.ts                           | 156 +++++++++
 src/styles/lib.scss                                |   8 +
 src/views/dashboard/controls/Log.vue               |  93 ++++++
 src/views/dashboard/controls/Tab.vue               |   3 +-
 src/views/dashboard/controls/index.ts              |   3 +-
 src/views/dashboard/data.ts                        |   1 +
 src/views/dashboard/panel/Tool.vue                 |   6 +
 .../related}/components/ConditionTags.vue          |  11 +-
 .../related}/components/LogTable/Index.vue         |   0
 .../related}/components/LogTable/LogBrowser.vue    |  13 +-
 .../related}/components/LogTable/LogDetail.vue     |  32 +-
 .../related}/components/LogTable/LogService.vue    |   9 +-
 .../related}/components/LogTable/data.ts           |  12 +-
 src/views/dashboard/related/log/Header.vue         | 362 +++++++++++++++++++++
 src/views/dashboard/related/log/List.vue           |  73 +++++
 src/views/dashboard/related/trace/Detail.vue       |   6 +-
 src/views/dashboard/related/trace/Filter.vue       |   4 +-
 src/views/dashboard/related/trace/TraceList.vue    |   2 +-
 .../trace/components/D3Graph/SpanDetail.vue        |   2 +-
 24 files changed, 760 insertions(+), 99 deletions(-)

diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts
index b364919..ac45f3e 100644
--- a/src/locales/lang/en.ts
+++ b/src/locales/lang/en.ts
@@ -324,11 +324,12 @@ const msg = {
   logContentEmpty: "The content of the log should not be empty.",
   debug: "Debug",
   addTraceID: "Please input a trace ID",
+  addTags: "Please input a tag",
   addKeywordsOfContent: "Please input a keyword of content",
   addExcludingKeywordsOfContent: "Please input a keyword of excluding content",
   noticeTag: "Please press Enter after inputting a tag(key=value).",
   conditionNotice:
-    "Notice: Please press enter after inputting a tag, key of content, exclude 
key of content.",
+    "Notice: Please press Enter after inputting a tag, key of content, exclude 
key of content(key=value).",
   cacheModalTitle: "Clear cache reminder",
   yes: "Yes",
   no: "No",
diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts
index d60322c..0b60578 100644
--- a/src/locales/lang/zh.ts
+++ b/src/locales/lang/zh.ts
@@ -325,10 +325,12 @@ const msg = {
   logContentEmpty: "日志数据的内容不应该是空。",
   debug: "调试",
   addTraceID: "请输入一个Trace ID",
+  addTags: "请输入一个标签",
   addKeywordsOfContent: "请输入一个内容关键词",
   addExcludingKeywordsOfContent: "请输入一个内容不包含的关键词",
   noticeTag: "请输入一个标签(key=value)之后回车",
-  conditionNotice: "请输入一个标签、内容关键词或者内容不包含的关键词之后回车",
+  conditionNotice:
+    "请输入一个标签、内容关键词或者内容不包含的关键词(key=value)之后回车",
   cacheModalTitle: "清除缓存提醒",
   yes: "是的",
   no: "不",
diff --git a/src/router/alert.ts b/src/router/alert.ts
deleted file mode 100644
index c0e27fb..0000000
--- a/src/router/alert.ts
+++ /dev/null
@@ -1,45 +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.
- */
-import { RouteRecordRaw } from "vue-router";
-import Layout from "@/layout/Index.vue";
-
-export const routesAlert: Array<RouteRecordRaw> = [
-  {
-    path: "",
-    name: "Alerts",
-    meta: {
-      title: "alerts",
-      icon: "notification_important",
-      hasGroup: false,
-      exact: false,
-    },
-    component: Layout,
-    children: [
-      {
-        path: "/alerts",
-        name: "Alerts",
-        meta: {
-          title: "alerts",
-          icon: "notification_important",
-          hasGroup: false,
-          exact: false,
-        },
-        component: () => import("@/views/Log.vue"),
-      },
-    ],
-  },
-];
diff --git a/src/router/index.ts b/src/router/index.ts
index 4ab9d19..aa6e470 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -21,9 +21,8 @@ import { routesMesh } from "./serviceMesh";
 import { routesDatabase } from "./database";
 import { routesInfra } from "./infrastructure";
 import { routesDashboard } from "./dashboard";
-import { routesLog } from "./log";
+// import { routesLog } from "./log";
 import { routesEvent } from "./event";
-import { routesAlert } from "./alert";
 import { routesSetting } from "./setting";
 import { routesAlarm } from "./alarm";
 
@@ -33,9 +32,7 @@ const routes: Array<RouteRecordRaw> = [
   ...routesDatabase,
   ...routesInfra,
   ...routesDashboard,
-  ...routesLog,
   ...routesEvent,
-  ...routesAlert,
   ...routesSetting,
   ...routesAlarm,
 ];
diff --git a/src/store/modules/dashboard.ts b/src/store/modules/dashboard.ts
index ad15f25..84af4f5 100644
--- a/src/store/modules/dashboard.ts
+++ b/src/store/modules/dashboard.ts
@@ -98,8 +98,8 @@ export const dashboardStore = defineStore({
           showDepth: true,
         };
       }
-      if (type === "Trace" || type === "Profile") {
-        newItem.h = 24;
+      if (type === "Trace" || type === "Profile" || type === "Log") {
+        newItem.h = 36;
       }
       this.activedGridItem = newItem.i;
       this.selectedGrid = newItem;
@@ -150,7 +150,7 @@ export const dashboardStore = defineStore({
           showDepth: true,
         };
       }
-      if (type === "Trace" || type === "Profile") {
+      if (type === "Trace" || type === "Profile" || type === "Log") {
         newItem.h = 24;
       }
       if (this.layout[idx].children) {
diff --git a/src/store/modules/log.ts b/src/store/modules/log.ts
new file mode 100644
index 0000000..f7846d6
--- /dev/null
+++ b/src/store/modules/log.ts
@@ -0,0 +1,156 @@
+/**
+ * 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.
+ */
+import { defineStore } from "pinia";
+import { Duration } from "@/types/app";
+import { Instance, Endpoint, Service } from "@/types/selector";
+import { store } from "@/store";
+import graphql from "@/graphql";
+import { AxiosResponse } from "axios";
+import { useAppStoreWithOut } from "@/store/modules/app";
+import { useSelectorStore } from "@/store/modules/selectors";
+import { useDashboardStore } from "@/store/modules/dashboard";
+
+interface LogState {
+  services: Service[];
+  instances: Instance[];
+  endpoints: Endpoint[];
+  conditions: any;
+  durationTime: Duration;
+  selectorStore: any;
+  supportQueryLogsByKeywords: boolean;
+  logs: any[];
+  logsTotal: number;
+  loadLogs: boolean;
+}
+
+export const logStore = defineStore({
+  id: "trace",
+  state: (): LogState => ({
+    services: [{ value: "0", label: "All" }],
+    instances: [{ value: "0", label: "All" }],
+    endpoints: [{ value: "0", label: "All" }],
+    conditions: {
+      queryDuration: useAppStoreWithOut().durationTime,
+      paging: { pageNum: 1, pageSize: 15, needTotal: true },
+    },
+    supportQueryLogsByKeywords: true,
+    durationTime: useAppStoreWithOut().durationTime,
+    selectorStore: useSelectorStore(),
+    logs: [],
+    logsTotal: 0,
+    loadLogs: false,
+  }),
+  actions: {
+    setLogCondition(data: any) {
+      this.conditions = { ...this.conditions, ...data };
+    },
+    async getServices(layer: string) {
+      const res: AxiosResponse = await graphql.query("queryServices").params({
+        layer,
+      });
+      if (res.data.errors) {
+        return res.data;
+      }
+      this.services = [
+        { value: "0", label: "All" },
+        ...res.data.data.services,
+      ] || [{ value: "0", label: "All" }];
+      return res.data;
+    },
+    async getInstances() {
+      const res: AxiosResponse = await graphql.query("queryInstances").params({
+        serviceId: this.selectorStore.currentService.id,
+        duration: this.durationTime,
+      });
+
+      if (res.data.errors) {
+        return res.data;
+      }
+      this.instances = [
+        { value: "0", label: "All" },
+        ...res.data.data.pods,
+      ] || [{ value: " 0", label: "All" }];
+      return res.data;
+    },
+    async getEndpoints() {
+      const res: AxiosResponse = await graphql.query("queryEndpoints").params({
+        serviceId: this.selectorStore.currentService.id,
+        duration: this.durationTime,
+        keyword: "",
+      });
+      if (res.data.errors) {
+        return res.data;
+      }
+      this.endpoints = [
+        { value: "0", label: "All" },
+        ...res.data.data.pods,
+      ] || [{ value: "0", label: "All" }];
+      return res.data;
+    },
+    async queryLogsByKeywords() {
+      const res: AxiosResponse = await graphql
+        .query("queryLogsByKeywords")
+        .params({});
+
+      if (res.data.errors) {
+        return res.data;
+      }
+
+      this.supportQueryLogsByKeywords = res.data.data.support;
+      return res.data;
+    },
+    async getLogs() {
+      const dashboardStore = useDashboardStore();
+      if (dashboardStore.layerId === "BROWSER") {
+        return this.getBrowserLogs();
+      }
+      return this.getServiceLogs();
+    },
+    async getServiceLogs() {
+      this.loadLogs = true;
+      const res: AxiosResponse = await graphql
+        .query("queryServiceLogs")
+        .params({ condition: this.conditions });
+      this.loadLogs = false;
+      if (res.data.errors) {
+        return res.data;
+      }
+
+      this.logs = res.data.data.queryLogs.logs;
+      this.logsTotal = res.data.data.queryLogs.total;
+      return res.data;
+    },
+    async getBrowserLogs() {
+      this.loadLogs = true;
+      const res: AxiosResponse = await graphql
+        .query("queryBrowserErrorLogs")
+        .params({ condition: this.conditions });
+
+      this.loadLogs = false;
+      if (res.data.errors) {
+        return res.data;
+      }
+      this.logs = res.data.data.queryBrowserErrorLogs.logs;
+      this.logsTotal = res.data.data.queryBrowserErrorLogs.total;
+      return res.data;
+    },
+  },
+});
+
+export function useLogStore(): any {
+  return logStore(store);
+}
diff --git a/src/styles/lib.scss b/src/styles/lib.scss
index 3462512..a589ae1 100644
--- a/src/styles/lib.scss
+++ b/src/styles/lib.scss
@@ -107,6 +107,10 @@
   margin-bottom: 5px;
 }
 
+.mt-5 {
+  margin-top: 5px;
+}
+
 .ml-5 {
   margin-left: 5px;
 }
@@ -123,6 +127,10 @@
   margin-left: 20px;
 }
 
+.mb-10 {
+  margin-bottom: 10px;
+}
+
 .mr-5 {
   margin-right: 5px;
 }
diff --git a/src/views/dashboard/controls/Log.vue 
b/src/views/dashboard/controls/Log.vue
new file mode 100644
index 0000000..d0a2240
--- /dev/null
+++ b/src/views/dashboard/controls/Log.vue
@@ -0,0 +1,93 @@
+<!-- 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="log-wrapper flex-v">
+    <el-popover placement="bottom" trigger="click" :width="100">
+      <template #reference>
+        <span class="delete cp">
+          <Icon iconName="ellipsis_v" size="middle" class="operation" />
+        </span>
+      </template>
+      <div class="tools" @click="removeWidget">
+        <span>{{ t("delete") }}</span>
+      </div>
+    </el-popover>
+    <div class="header">
+      <Header />
+    </div>
+    <div class="log">
+      <List />
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { useI18n } from "vue-i18n";
+import { useDashboardStore } from "@/store/modules/dashboard";
+import Header from "../related/log/Header.vue";
+import List from "../related/log/List.vue";
+
+/*global defineProps */
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+  activeIndex: { type: String, default: "" },
+});
+const { t } = useI18n();
+const dashboardStore = useDashboardStore();
+
+function removeWidget() {
+  dashboardStore.removeControls(props.data);
+}
+</script>
+<style lang="scss" scoped>
+.log-wrapper {
+  width: 100%;
+  height: 100%;
+  font-size: 12px;
+  position: relative;
+}
+
+.delete {
+  position: absolute;
+  top: 5px;
+  right: 3px;
+}
+
+.header {
+  padding: 10px;
+  font-size: 12px;
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.tools {
+  padding: 5px 0;
+  color: #999;
+  cursor: pointer;
+  position: relative;
+  text-align: center;
+
+  &:hover {
+    color: #409eff;
+    background-color: #eee;
+  }
+}
+
+.log {
+  width: 100%;
+  overflow: auto;
+}
+</style>
diff --git a/src/views/dashboard/controls/Tab.vue 
b/src/views/dashboard/controls/Tab.vue
index 20c106d..78f4586 100644
--- a/src/views/dashboard/controls/Tab.vue
+++ b/src/views/dashboard/controls/Tab.vue
@@ -119,6 +119,7 @@ import Topology from "./Topology.vue";
 import Widget from "./Widget.vue";
 import Trace from "./Trace.vue";
 import Profile from "./Profile.vue";
+import Log from "./Log.vue";
 
 const props = {
   data: {
@@ -129,7 +130,7 @@ const props = {
 };
 export default defineComponent({
   name: "Tab",
-  components: { Topology, Widget, Trace, Profile },
+  components: { Topology, Widget, Trace, Profile, Log },
   props,
   setup(props) {
     const { t } = useI18n();
diff --git a/src/views/dashboard/controls/index.ts 
b/src/views/dashboard/controls/index.ts
index 4f2ee20..7fdcd85 100644
--- a/src/views/dashboard/controls/index.ts
+++ b/src/views/dashboard/controls/index.ts
@@ -19,5 +19,6 @@ import Tab from "./Tab.vue";
 import Widget from "./Widget.vue";
 import Trace from "./Trace.vue";
 import Profile from "./Profile.vue";
+import Log from "./Log.vue";
 
-export default { Tab, Widget, Trace, Topology, Profile };
+export default { Tab, Widget, Trace, Topology, Profile, Log };
diff --git a/src/views/dashboard/data.ts b/src/views/dashboard/data.ts
index 396de2c..6917c97 100644
--- a/src/views/dashboard/data.ts
+++ b/src/views/dashboard/data.ts
@@ -168,6 +168,7 @@ export const ToolIcons = [
   { name: "device_hub", content: "Add Topology", id: "addTopology" },
   { name: "merge", content: "Add Trace", id: "addTrace" },
   { name: "timeline", content: "Add Profile", id: "addProfile" },
+  { name: "assignment", content: "Add Log", id: "addLog" },
   // { name: "save_alt", content: "Export", id: "export" },
   // { name: "folder_open", content: "Import", id: "import" },
   // { name: "settings", content: "Settings", id: "settings" },
diff --git a/src/views/dashboard/panel/Tool.vue 
b/src/views/dashboard/panel/Tool.vue
index b052257..ce05edd 100644
--- a/src/views/dashboard/panel/Tool.vue
+++ b/src/views/dashboard/panel/Tool.vue
@@ -309,6 +309,9 @@ function setTabControls(id: string) {
     case "addTrace":
       dashboardStore.addTabControls("Trace");
       break;
+    case "addLog":
+      dashboardStore.addTabControls("Log");
+      break;
     case "addProfile":
       dashboardStore.addTabControls("Profile");
       break;
@@ -335,6 +338,9 @@ function setControls(id: string) {
     case "addProfile":
       dashboardStore.addControl("Profile");
       break;
+    case "addLog":
+      dashboardStore.addControl("Log");
+      break;
     case "addTopology":
       dashboardStore.addControl("Topology");
       break;
diff --git a/src/views/components/ConditionTags.vue 
b/src/views/dashboard/related/components/ConditionTags.vue
similarity index 93%
rename from src/views/components/ConditionTags.vue
rename to src/views/dashboard/related/components/ConditionTags.vue
index b73ae6a..f755910 100644
--- a/src/views/components/ConditionTags.vue
+++ b/src/views/dashboard/related/components/ConditionTags.vue
@@ -14,7 +14,7 @@ See the License for the specific language governing 
permissions and
 limitations under the License. -->
 <template>
   <div class="flex-h" :class="{ light: theme === 'light' }">
-    <div class="mr-10 pt-5">
+    <div class="mr-5">
       <span class="sm grey" v-show="theme === 'dark'">{{ t("tags") }}: </span>
       <span
         class="trace-tags"
@@ -25,7 +25,12 @@ limitations under the License. -->
           <span class="remove-icon" @click="removeTags(index)">×</span>
         </span>
       </span>
-      <el-input v-model="tags" class="trace-new-tag" @change="addLabels" />
+      <el-input
+        v-model="tags"
+        class="trace-new-tag"
+        @change="addLabels"
+        :placeholder="t('addTags')"
+      />
       <span class="tags-tip">
         <a
           target="blank"
@@ -38,7 +43,7 @@ limitations under the License. -->
             <Icon class="icon-help mr-5" iconName="help" size="middle" />
           </span>
         </el-tooltip>
-        <b>{{ t("noticeTag") }}</b>
+        <b v-if="type === 'TRACE'">{{ t("noticeTag") }}</b>
       </span>
     </div>
   </div>
diff --git a/src/views/components/LogTable/Index.vue 
b/src/views/dashboard/related/components/LogTable/Index.vue
similarity index 100%
rename from src/views/components/LogTable/Index.vue
rename to src/views/dashboard/related/components/LogTable/Index.vue
diff --git a/src/views/components/LogTable/LogBrowser.vue 
b/src/views/dashboard/related/components/LogTable/LogBrowser.vue
similarity index 91%
rename from src/views/components/LogTable/LogBrowser.vue
rename to src/views/dashboard/related/components/LogTable/LogBrowser.vue
index 20fbb7d..6a1159e 100644
--- a/src/views/components/LogTable/LogBrowser.vue
+++ b/src/views/dashboard/related/components/LogTable/LogBrowser.vue
@@ -29,13 +29,16 @@ limitations under the License. -->
     >
       <span v-if="item.label === 'time'">{{ dateFormat(data.time) }}</span>
       <span v-else-if="item.label === 'errorUrl'">{{ data.pagePath }}</span>
-      <span v-else v-tooltip:bottom="data[item.label] || '-'">{{
-        data[item.label] || "-"
-      }}</span>
+      <el-tooltip v-else :content="data[item.label] || '-'">
+        <span>
+          {{ data[item.label] || "-" }}
+        </span>
+      </el-tooltip>
     </div>
   </div>
 </template>
 <script lang="ts" setup>
+import { ref } from "vue";
 import dayjs from "dayjs";
 import { BrowserLogConstants } from "./data";
 
@@ -45,6 +48,7 @@ const props = defineProps({
 });
 const columns = BrowserLogConstants;
 const emit = defineEmits(["select"]);
+const logItem = ref<any>(null);
 
 const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
   dayjs(date).format(pattern);
@@ -55,9 +59,8 @@ function showSelectSpan() {
   for (const item of items) {
     item.style.background = "#fff";
   }
-  const logItem: any = this.$refs.logItem;
 
-  logItem.style.background = "rgba(0, 0, 0, 0.1)";
+  logItem.value.style.background = "rgba(0, 0, 0, 0.1)";
   emit("select", props.data);
 }
 </script>
diff --git a/src/views/components/LogTable/LogDetail.vue 
b/src/views/dashboard/related/components/LogTable/LogDetail.vue
similarity index 71%
rename from src/views/components/LogTable/LogDetail.vue
rename to src/views/dashboard/related/components/LogTable/LogDetail.vue
index 4588132..75104fb 100644
--- a/src/views/components/LogTable/LogDetail.vue
+++ b/src/views/dashboard/related/components/LogTable/LogDetail.vue
@@ -19,22 +19,20 @@ limitations under the License. -->
       v-for="(item, index) in columns"
       :key="index"
     >
-      <template>
-        <span class="g-sm-4 grey">{{ t(item.value) }}:</span>
-        <span v-if="item.label === 'timestamp'" class="g-sm-8">
-          {{ dateFormat(currentLog[item.label]) }}
-        </span>
-        <textarea
-          class="content"
-          :readonly="true"
-          v-else-if="item.label === 'content'"
-          :value="currentLog[item.label]"
-        />
-        <span v-else-if="item.label === 'tags'" class="g-sm-8">
-          <div v-for="(d, index) in logTags" :key="index">{{ d }}</div>
-        </span>
-        <span v-else class="g-sm-8">{{ currentLog[item.label] }}</span>
-      </template>
+      <span class="g-sm-4 grey">{{ t(item.value) }}:</span>
+      <span v-if="item.label === 'timestamp'" class="g-sm-8 mb-10">
+        {{ dateFormat(currentLog[item.label]) }}
+      </span>
+      <textarea
+        class="content mb-10"
+        :readonly="true"
+        v-else-if="item.label === 'content'"
+        :value="currentLog[item.label]"
+      />
+      <span v-else-if="item.label === 'tags'" class="g-sm-8 mb-10">
+        <div v-for="(d, index) in logTags" :key="index">{{ d }}</div>
+      </span>
+      <span v-else class="g-sm-8 mb-10">{{ currentLog[item.label] }}</span>
     </div>
   </div>
 </template>
@@ -43,7 +41,7 @@ import { computed } from "vue";
 import type { PropType } from "vue";
 import { useI18n } from "vue-i18n";
 import dayjs from "dayjs";
-import { ServiceLogDetail } from "@/views/components/LogTable/data";
+import { ServiceLogDetail } from "./data";
 
 /*global defineProps */
 const props = defineProps({
diff --git a/src/views/components/LogTable/LogService.vue 
b/src/views/dashboard/related/components/LogTable/LogService.vue
similarity index 94%
rename from src/views/components/LogTable/LogService.vue
rename to src/views/dashboard/related/components/LogTable/LogService.vue
index 59943d4..3f94af0 100644
--- a/src/views/components/LogTable/LogService.vue
+++ b/src/views/dashboard/related/components/LogTable/LogService.vue
@@ -26,7 +26,7 @@ limitations under the License. -->
         v-else-if="item.label === 'traceId' && !noLink"
         :to="{ name: 'trace', query: { traceid: data[item.label] } }"
       >
-        <span>{{ data[item.label] }}</span>
+        <span :class="noLink ? '' : 'blue'">{{ data[item.label] }}</span>
       </router-link>
       <span v-else>{{ data[item.label] }}</span>
     </div>
@@ -38,7 +38,7 @@ import dayjs from "dayjs";
 import { ServiceLogConstants } from "./data";
 /*global defineProps, defineEmits */
 const props = defineProps({
-  data: { type: Array as any, default: () => [] },
+  data: { type: Object as any, default: () => ({}) },
   noLink: { type: Boolean, default: true },
 });
 const emit = defineEmits(["select"]);
@@ -63,7 +63,6 @@ function showSelectSpan() {
 
   .traceId {
     width: 390px;
-    color: #448dfe;
     cursor: pointer;
 
     span {
@@ -71,6 +70,10 @@ function showSelectSpan() {
       width: 100%;
       line-height: 30px;
     }
+
+    .blue {
+      color: #448dfe;
+    }
   }
 
   .content,
diff --git a/src/views/components/LogTable/data.ts 
b/src/views/dashboard/related/components/LogTable/data.ts
similarity index 93%
rename from src/views/components/LogTable/data.ts
rename to src/views/dashboard/related/components/LogTable/data.ts
index 9765e01..9ddd3c5 100644
--- a/src/views/components/LogTable/data.ts
+++ b/src/views/dashboard/related/components/LogTable/data.ts
@@ -48,11 +48,11 @@ export const ServiceLogConstants = [
 export const ServiceLogDetail = [
   {
     label: "serviceName",
-    value: "currentService",
+    value: "service",
   },
   {
     label: "serviceInstanceName",
-    value: "currentInstance",
+    value: "instance",
   },
   {
     label: "timestamp",
@@ -96,19 +96,15 @@ export const BrowserLogConstants = [
   {
     label: "message",
     value: "message",
-    drag: true,
+    // drag: true,
     method: 350,
   },
   {
     label: "stack",
     value: "stack",
-    drag: true,
+    // drag: true,
     method: 350,
   },
-  // {
-  //   label: 'pagePath',
-  //   value: 'Page Path',
-  // },
   {
     label: "category",
     value: "category",
diff --git a/src/views/dashboard/related/log/Header.vue 
b/src/views/dashboard/related/log/Header.vue
new file mode 100644
index 0000000..42a7e63
--- /dev/null
+++ b/src/views/dashboard/related/log/Header.vue
@@ -0,0 +1,362 @@
+<!-- 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="flex-h row">
+    <div class="mr-5" v-if="dashboardStore.entity === EntityType[1].value">
+      <span class="grey mr-5">{{ t("service") }}:</span>
+      <Selector
+        size="small"
+        :value="state.service.value"
+        :options="logStore.services"
+        placeholder="Select a service"
+        @change="changeField('service', $event)"
+      />
+    </div>
+    <div class="mr-5" v-if="dashboardStore.entity !== EntityType[3].value">
+      <span class="grey mr-5">
+        {{ isBrowser ? t("version") : t("instance") }}:
+      </span>
+      <Selector
+        size="small"
+        :value="state.instance.value"
+        :options="logStore.instances"
+        placeholder="Select a instance"
+        @change="changeField('instance', $event)"
+      />
+    </div>
+    <div class="mr-5" v-if="dashboardStore.entity !== EntityType[2].value">
+      <span class="grey mr-5"
+        >{{ isBrowser ? t("page") : t("endpoint") }}:</span
+      >
+      <Selector
+        size="small"
+        :value="state.endpoint.value"
+        :options="logStore.endpoints"
+        placeholder="Select a endpoint"
+        @change="changeField('endpoint', $event)"
+      />
+    </div>
+  </div>
+  <div class="row tips">
+    <b>{{ t("conditionNotice") }}</b>
+  </div>
+  <div class="flex-h row">
+    <div class="mr-5 traceId" v-show="!isBrowser">
+      <span class="grey mr-5">{{ t("traceID") }}:</span>
+      <el-input v-model="traceId" class="inputs-max" />
+    </div>
+    <ConditionTags :type="'LOG'" @update="updateTags" />
+  </div>
+  <div class="flex-h" v-show="!isBrowser">
+    <div class="mr-5" v-show="logStore.supportQueryLogsByKeywords">
+      <span class="mr-5 grey">{{ t("keywordsOfContent") }}:</span>
+      <span class="log-tags">
+        <span
+          class="selected"
+          v-for="(item, index) in keywordsOfContent"
+          :key="`keywordsOfContent${index}`"
+        >
+          <span>{{ item }}</span>
+          <span class="remove-icon" @click="removeContent(index)">×</span>
+        </span>
+      </span>
+      <el-input
+        class="inputs-max"
+        :placeholder="t('addKeywordsOfContent')"
+        v-model="contentStr"
+        @change="addLabels('keywordsOfContent')"
+      />
+    </div>
+    <div class="mr-5" v-show="logStore.supportQueryLogsByKeywords">
+      <span class="grey mr-5"> {{ t("excludingKeywordsOfContent") }}: </span>
+      <span class="log-tags">
+        <span
+          class="selected"
+          v-for="(item, index) in excludingKeywordsOfContent"
+          :key="`excludingKeywordsOfContent${index}`"
+        >
+          <span>{{ item }}</span>
+          <span class="remove-icon" @click="removeExcludeContent(index)">
+            ×
+          </span>
+        </span>
+      </span>
+      <el-input
+        class="inputs-max"
+        :placeholder="t('addExcludingKeywordsOfContent')"
+        v-model="excludingContentStr"
+        @change="addLabels('excludingKeywordsOfContent')"
+      />
+      <el-tooltip :content="t('keywordsOfContentLogTips')">
+        <span class="log-tips" v-show="!logStore.supportQueryLogsByKeywords">
+          <Icon icon="help" class="mr-5" />
+        </span>
+      </el-tooltip>
+    </div>
+    <el-button
+      class="search-btn"
+      size="small"
+      type="primary"
+      @click="searchLogs"
+    >
+      {{ t("search") }}
+    </el-button>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref, reactive, watch } from "vue";
+import { useI18n } from "vue-i18n";
+import { Option } from "@/types/app";
+import { useLogStore } from "@/store/modules/log";
+import { useDashboardStore } from "@/store/modules/dashboard";
+import { useAppStoreWithOut } from "@/store/modules/app";
+import { useSelectorStore } from "@/store/modules/selectors";
+import ConditionTags from 
"@/views/dashboard/related/components/ConditionTags.vue";
+import { ElMessage } from "element-plus";
+import { EntityType } from "../../data";
+
+const { t } = useI18n();
+const appStore = useAppStoreWithOut();
+const selectorStore = useSelectorStore();
+const dashboardStore = useDashboardStore();
+const logStore = useLogStore();
+const traceId = ref<string>("");
+const keywordsOfContent = ref<string[]>([]);
+const excludingKeywordsOfContent = ref<string[]>([]);
+const tagsList = ref<string[]>([]);
+const tagsMap = ref<Option[]>([]);
+const contentStr = ref<string>("");
+const excludingContentStr = ref<string>("");
+const isBrowser = ref<boolean>(dashboardStore.layerId === "BROWSER");
+const state = reactive<any>({
+  instance: { value: "0", label: "All" },
+  endpoint: { value: "0", label: "All" },
+  service: { value: "0", label: "All" },
+});
+
+init();
+async function init() {
+  const resp = await logStore.queryLogsByKeywords();
+
+  if (resp.errors) {
+    ElMessage.error(resp.errors);
+    return;
+  }
+  state.instance = { value: "0", label: "All" };
+  state.endpoint = { value: "0", label: "All" };
+  state.service = { value: "0", label: "All" };
+  searchLogs();
+  fetchSelectors();
+}
+
+function fetchSelectors() {
+  if (dashboardStore.entity === EntityType[1].value) {
+    getServices();
+    return;
+  }
+  if (dashboardStore.entity === EntityType[2].value) {
+    getInstances();
+    return;
+  }
+  if (dashboardStore.entity === EntityType[3].value) {
+    getEndpoints();
+    return;
+  }
+  if (dashboardStore.entity === EntityType[0].value) {
+    getInstances();
+    getEndpoints();
+  }
+}
+
+async function getServices() {
+  const resp = await logStore.getServices(dashboardStore.layerId);
+  if (resp.errors) {
+    ElMessage.error(resp.errors);
+    return;
+  }
+  state.service = logStore.services[0];
+}
+
+async function getEndpoints() {
+  const resp = await logStore.getEndpoints();
+  if (resp.errors) {
+    ElMessage.error(resp.errors);
+    return;
+  }
+  state.endpoint = logStore.endpoints[0];
+}
+async function getInstances() {
+  const resp = await logStore.getInstances();
+  if (resp.errors) {
+    ElMessage.error(resp.errors);
+    return;
+  }
+  state.instance = logStore.instances[0];
+}
+function searchLogs() {
+  let endpoint = "",
+    instance = "";
+  if (dashboardStore.entity === EntityType[2].value) {
+    endpoint = selectorStore.currentPod.id;
+  }
+  if (dashboardStore.entity === EntityType[3].value) {
+    instance = selectorStore.currentPod.id;
+  }
+  logStore.setLogCondition({
+    serviceId: selectorStore.currentService
+      ? selectorStore.currentService.id
+      : state.service.id,
+    endpointId: endpoint || state.endpoint.id || undefined,
+    serviceInstanceId: instance || state.instance.id || undefined,
+    queryDuration: appStore.durationTime,
+    keywordsOfContent: keywordsOfContent.value,
+    excludingKeywordsOfContent: excludingKeywordsOfContent.value,
+    tags: tagsMap.value.length ? tagsMap.value : undefined,
+    paging: { pageNum: 1, pageSize: 15, needTotal: true },
+    relatedTrace: traceId.value ? { traceId: traceId.value } : undefined,
+  });
+  queryLogs();
+}
+async function queryLogs() {
+  const res = await logStore.getLogs();
+  if (res && res.errors) {
+    ElMessage.error(res.errors);
+  }
+}
+function changeField(type: string, opt: any) {
+  state[type] = opt[0];
+  if (type === "service") {
+    getEndpoints();
+    getInstances();
+  }
+}
+function updateTags(data: { tagsMap: Array<Option>; tagsList: string[] }) {
+  tagsList.value = data.tagsList;
+  tagsMap.value = data.tagsMap;
+}
+function removeContent(index: number) {
+  const keywordsOfContentList = keywordsOfContent.value || [];
+  keywordsOfContentList.splice(index, 1);
+  logStore.setLogCondition({
+    keywordsOfContent: keywordsOfContentList,
+  });
+  contentStr.value = "";
+}
+function addLabels(type: string) {
+  if (type === "keywordsOfContent" && !contentStr.value) {
+    return;
+  }
+  if (type === "excludingKeywordsOfContent" && !excludingContentStr.value) {
+    return;
+  }
+  if (type === "keywordsOfContent") {
+    keywordsOfContent.value.push(contentStr.value);
+    logStore.setLogCondition({
+      [type]: keywordsOfContent.value,
+    });
+    contentStr.value = "";
+  } else if (type === "excludingKeywordsOfContent") {
+    excludingKeywordsOfContent.value.push(excludingContentStr.value);
+    logStore.setLogCondition({
+      [type]: excludingKeywordsOfContent.value,
+    });
+    excludingContentStr.value = "";
+  }
+}
+function removeExcludeContent(index: number) {
+  excludingKeywordsOfContent.value.splice(index, 1);
+  logStore.setLogCondition({
+    excludingKeywordsOfContent: excludingKeywordsOfContent.value,
+  });
+  excludingContentStr.value = "";
+}
+watch(
+  () => selectorStore.currentService,
+  () => {
+    if (dashboardStore.entity === EntityType[0].value) {
+      init();
+    }
+  }
+);
+watch(
+  () => [selectorStore.currentPod],
+  () => {
+    if (dashboardStore.entity === EntityType[0].value) {
+      return;
+    }
+    init();
+  }
+);
+</script>
+<style lang="scss" scoped>
+.inputs {
+  width: 120px;
+}
+
+.row {
+  margin-bottom: 5px;
+}
+
+.inputs-max {
+  width: 270px;
+}
+
+.traceId {
+  margin-top: 2px;
+}
+
+.search-btn {
+  margin-left: 20px;
+  cursor: pointer;
+}
+
+.tips {
+  color: #888;
+}
+
+.log-tag {
+  width: 30%;
+  border-style: unset;
+  outline: 0;
+  border: 1px solid #ccc;
+  height: 30px;
+  padding: 0 5px;
+}
+
+.log-tags {
+  padding: 1px 5px 0 0;
+  border-radius: 3px;
+  height: 24px;
+  display: inline-block;
+  vertical-align: top;
+}
+
+.selected {
+  display: inline-block;
+  padding: 0 3px;
+  border-radius: 3px;
+  overflow: hidden;
+  color: #3d444f;
+  border: 1px dashed #aaa;
+  font-size: 12px;
+  margin: 0 2px;
+}
+
+.remove-icon {
+  display: inline-block;
+  margin-left: 3px;
+  cursor: pointer;
+}
+</style>
diff --git a/src/views/dashboard/related/log/List.vue 
b/src/views/dashboard/related/log/List.vue
new file mode 100644
index 0000000..8cbcb50
--- /dev/null
+++ b/src/views/dashboard/related/log/List.vue
@@ -0,0 +1,73 @@
+<!-- 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>
+    <div class="log-t-loading" v-show="logStore.loadLogs">
+      <Icon iconName="spinner" size="lg" />
+    </div>
+    <LogTable :tableData="logStore.logs || []" :type="type" :noLink="true">
+      <div class="log-tips" v-if="!logStore.logs.length">{{ t("noData") 
}}</div>
+    </LogTable>
+    <div class="mt-5 mb-5">
+      <el-pagination
+        v-model:currentPage="logStore.conditions.paging.pageNum"
+        v-model:page-size="pageSize"
+        layout="prev, pager, next, jumper"
+        :total="logStore.logsTotal"
+        @current-change="updatePage"
+        :style="`float: right`"
+      />
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref } from "vue";
+import { useI18n } from "vue-i18n";
+import LogTable from "@/views/dashboard/related/components/LogTable/Index.vue";
+import { useLogStore } from "@/store/modules/log";
+import { useDashboardStore } from "@/store/modules/dashboard";
+import { ElMessage } from "element-plus";
+
+const { t } = useI18n();
+const logStore = useLogStore();
+const dashboardStore = useDashboardStore();
+const type = ref<string>(
+  dashboardStore.layerId === "BROWSER" ? "browser" : "service"
+);
+const pageSize = ref<number>(15);
+function updatePage(p: number) {
+  logStore.setLogCondition({
+    paging: { pageNum: p, pageSize: pageSize.value, needTotal: true },
+  });
+  queryLogs();
+}
+async function queryLogs() {
+  const res = await logStore.getLogs();
+  if (res && res.errors) {
+    ElMessage.error(res.errors);
+  }
+}
+</script>
+<style lang="scss" scoped>
+.log-tips {
+  width: 100%;
+  text-align: center;
+  margin: 50px 0;
+}
+
+.log-t-loading {
+  text-align: center;
+}
+</style>
diff --git a/src/views/dashboard/related/trace/Detail.vue 
b/src/views/dashboard/related/trace/Detail.vue
index ade0f74..c622efb 100644
--- a/src/views/dashboard/related/trace/Detail.vue
+++ b/src/views/dashboard/related/trace/Detail.vue
@@ -144,7 +144,7 @@ import { Option } from "@/types/app";
 import copy from "@/utils/copy";
 import List from "./components/List.vue";
 import graphs from "./components/index";
-import LogTable from "@/views/components/LogTable/Index.vue";
+import LogTable from "@/views/dashboard/related/components/LogTable/Index.vue";
 import { ElMessage } from "element-plus";
 
 export default defineComponent({
@@ -166,7 +166,7 @@ export default defineComponent({
       dayjs(date).format(pattern);
     const showTraceLogs = ref<boolean>(false);
 
-    function handleClick(ids: string[]) {
+    function handleClick(ids: string[] | any) {
       let copyValue = null;
       if (ids.length === 1) {
         copyValue = ids[0];
@@ -176,7 +176,7 @@ export default defineComponent({
       copy(copyValue);
     }
 
-    async function changeTraceId(opt: Option[]) {
+    async function changeTraceId(opt: Option[] | any) {
       traceId.value = opt[0].value;
       loading.value = true;
       const res = await traceStore.getTraceSpans({ traceId: opt[0].value });
diff --git a/src/views/dashboard/related/trace/Filter.vue 
b/src/views/dashboard/related/trace/Filter.vue
index 236697b..342a225 100644
--- a/src/views/dashboard/related/trace/Filter.vue
+++ b/src/views/dashboard/related/trace/Filter.vue
@@ -95,7 +95,7 @@ import { useTraceStore } from "@/store/modules/trace";
 import { useDashboardStore } from "@/store/modules/dashboard";
 import { useAppStoreWithOut } from "@/store/modules/app";
 import { useSelectorStore } from "@/store/modules/selectors";
-import ConditionTags from "@/views/components/ConditionTags.vue";
+import ConditionTags from 
"@/views/dashboard/related/components/ConditionTags.vue";
 import { ElMessage } from "element-plus";
 import { EntityType } from "../../data";
 
@@ -198,7 +198,7 @@ async function queryTraces() {
     ElMessage.error(res.errors);
   }
 }
-function changeField(type: string, opt: any[]) {
+function changeField(type: string, opt: any) {
   state[type] = opt[0];
   if (type === "service") {
     getEndpoints();
diff --git a/src/views/dashboard/related/trace/TraceList.vue 
b/src/views/dashboard/related/trace/TraceList.vue
index 8f863e5..9240e5d 100644
--- a/src/views/dashboard/related/trace/TraceList.vue
+++ b/src/views/dashboard/related/trace/TraceList.vue
@@ -101,7 +101,7 @@ function updatePage(p: number) {
   searchTrace();
 }
 
-function changeSort(opt: Option[]) {
+function changeSort(opt: Option[] | any) {
   traceStore.setTraceCondition({
     queryOrder: opt[0].value,
     paging: { pageNum: 1, pageSize: pageSize.value, needTotal: true },
diff --git 
a/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue 
b/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue
index 69f154c..e6390ff 100644
--- a/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue
+++ b/src/views/dashboard/related/trace/components/D3Graph/SpanDetail.vue
@@ -113,7 +113,7 @@ import dayjs from "dayjs";
 import { useTraceStore } from "@/store/modules/trace";
 import copy from "@/utils/copy";
 import { ElMessage } from "element-plus";
-import LogTable from "@/views/components/LogTable/Index.vue";
+import LogTable from "@/views/dashboard/related/components/LogTable/Index.vue";
 
 /* global defineProps */
 const props = defineProps({

Reply via email to