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 8ea50c86 feat: visualize `Snapshot` on Alerting page (#444)
8ea50c86 is described below

commit 8ea50c8680add858f54ed16e696d87eb95166216
Author: Fine0830 <[email protected]>
AuthorDate: Mon Jan 13 17:24:32 2025 +0800

    feat: visualize `Snapshot` on Alerting page (#444)
---
 src/graphql/fragments/alarm.ts          |  30 +++++++
 src/hooks/useSnapshot.ts                |  47 +++++++++++
 src/locales/lang/en.ts                  |   2 +
 src/locales/lang/es.ts                  |   2 +
 src/locales/lang/zh.ts                  |   2 +
 src/types/alarm.d.ts                    |   1 +
 src/types/dashboard.d.ts                |  16 ++++
 src/views/alarm/Content.vue             |  48 ++++++-----
 src/views/alarm/components/Line.vue     | 141 ++++++++++++++++++++++++++++++++
 src/views/alarm/components/Snapshot.vue |  35 ++++++++
 src/views/alarm/data.ts                 |   8 ++
 src/views/dashboard/graphs/Line.vue     |  15 ++--
 12 files changed, 323 insertions(+), 24 deletions(-)

diff --git a/src/graphql/fragments/alarm.ts b/src/graphql/fragments/alarm.ts
index a2e9ba0c..76ed4672 100644
--- a/src/graphql/fragments/alarm.ts
+++ b/src/graphql/fragments/alarm.ts
@@ -24,6 +24,7 @@ export const Alarm = {
         message
         startTime
         scope
+        name
         tags {
           key
           value
@@ -43,6 +44,35 @@ export const Alarm = {
           startTime
           endTime
         }
+        snapshot {
+          expression
+          metrics {
+            name
+            results {
+              metric {
+                labels {
+                  key
+                  value
+                }
+              }
+              values {
+                id
+                owner {
+                  scope
+                  serviceID
+                  serviceName
+                  normal
+                  serviceInstanceID
+                  serviceInstanceName
+                  endpointID
+                  endpointName
+                }
+                value
+                traceID
+              }
+            }
+          }
+        }
       }
     }`,
 };
diff --git a/src/hooks/useSnapshot.ts b/src/hooks/useSnapshot.ts
new file mode 100644
index 00000000..2ddfc9b7
--- /dev/null
+++ b/src/hooks/useSnapshot.ts
@@ -0,0 +1,47 @@
+/**
+ * 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 type { MetricsResults } from "@/types/dashboard";
+
+export function useSnapshot(metrics: { name: string; results: MetricsResults[] 
}[]) {
+  function processResults() {
+    const sources = metrics.map((metric: { name: string; results: 
MetricsResults[] }) => {
+      const values = metric.results.map(
+        (r: { values: { value: string }[]; metric: { labels: { key: string; 
value: string }[] } }) => {
+          const arr = r.values.map((v: { value: string }) => Number(v.value));
+          if (!r.metric.labels.length) {
+            return { values: arr };
+          }
+          const name = r.metric.labels
+            .map(
+              (label: { key: string; value: string }) =>
+                `${metric.name}${label ? "{" : 
""}${label.key}=${label.value}${label ? "}" : ""}`,
+            )
+            .join(",");
+          return { name, values: arr };
+        },
+      );
+
+      return { name: metric.name, values };
+    });
+
+    return sources;
+  }
+
+  return {
+    processResults,
+  };
+}
diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts
index 5640a21d..98764453 100644
--- a/src/locales/lang/en.ts
+++ b/src/locales/lang/en.ts
@@ -395,5 +395,7 @@ const msg = {
   profilingEvents: "Async Profiling Events",
   execArgs: "Exec Args",
   instances: "Instances",
+  snapshot: "Snapshot",
+  expression: "Expression",
 };
 export default msg;
diff --git a/src/locales/lang/es.ts b/src/locales/lang/es.ts
index 4496b367..bc030873 100644
--- a/src/locales/lang/es.ts
+++ b/src/locales/lang/es.ts
@@ -395,5 +395,7 @@ const msg = {
   profilingEvents: "Async Profiling Events",
   execArgs: "Exec Args",
   instances: "Instances",
+  snapshot: "Snapshot",
+  expression: "Expression",
 };
 export default msg;
diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts
index 800d8c78..359ebb97 100644
--- a/src/locales/lang/zh.ts
+++ b/src/locales/lang/zh.ts
@@ -393,5 +393,7 @@ const msg = {
   profilingEvents: "异步分析事件",
   execArgs: "String任务扩展",
   instances: "实例",
+  snapshot: "快照",
+  expression: "表达式",
 };
 export default msg;
diff --git a/src/types/alarm.d.ts b/src/types/alarm.d.ts
index 06c09958..df791ca2 100644
--- a/src/types/alarm.d.ts
+++ b/src/types/alarm.d.ts
@@ -27,6 +27,7 @@ export interface Alarm {
   scope: string;
   tags: Array<{ key: string; value: string }>;
   events: Event[];
+  snapshot: Indexable;
 }
 
 export interface Event {
diff --git a/src/types/dashboard.d.ts b/src/types/dashboard.d.ts
index fa72ce4a..d021e48d 100644
--- a/src/types/dashboard.d.ts
+++ b/src/types/dashboard.d.ts
@@ -111,6 +111,8 @@ export interface LineConfig extends AreaConfig {
   showYAxis?: boolean;
   smallTips?: boolean;
   showlabels?: boolean;
+  noTooltips?: boolean;
+  showLegend?: boolean;
 }
 
 export interface AreaConfig {
@@ -197,3 +199,17 @@ export type LegendOptions = {
   toTheRight: boolean;
   width: number;
 };
+export type MetricsResults = {
+  metric: { labels: MetricLabel[] };
+  values: MetricValue[];
+};
+type MetricLabel = {
+  key: string;
+  value: string;
+};
+type MetricValue = {
+  name: string;
+  value: string;
+  owner: null | string;
+  refId: null | string;
+};
diff --git a/src/views/alarm/Content.vue b/src/views/alarm/Content.vue
index 0a8988ae..aee06071 100644
--- a/src/views/alarm/Content.vue
+++ b/src/views/alarm/Content.vue
@@ -22,15 +22,17 @@ limitations under the License. -->
         <div class="message mb-5 b">
           {{ i.message }}
         </div>
-        <div
-          class="timeline-table-i-scope mr-10 l sm"
-          :class="{
-            blue: i.scope === 'Service',
-            green: i.scope === 'Endpoint',
-            yellow: i.scope === 'ServiceInstance',
-          }"
-        >
-          {{ t(i.scope.toLowerCase()) }}
+        <div class="flex-h">
+          <div
+            class="timeline-table-i-scope"
+            :class="{
+              blue: i.scope === 'Service',
+              green: i.scope === 'Endpoint',
+              yellow: i.scope === 'ServiceInstance',
+            }"
+          >
+            {{ t(i.scope.toLowerCase()) }}
+          </div>
         </div>
         <div class="grey sm show-xs">
           {{ dateFormat(parseInt(i.startTime)) }}
@@ -46,7 +48,7 @@ limitations under the License. -->
     :destroy-on-close="true"
     @closed="isShowDetails = false"
   >
-    <div class="mb-10 clear alarm-detail" v-for="(item, index) in 
AlarmDetailCol" :key="index">
+    <div class="mb-20 clear alarm-detail" v-for="(item, index) in 
AlarmDetailCol" :key="index">
       <span class="g-sm-2 grey">{{ t(item.value) }}:</span>
       <span v-if="item.label === 'startTime'">
         {{ dateFormat(currentDetail[item.label]) }}
@@ -54,7 +56,7 @@ limitations under the License. -->
       <span v-else-if="item.label === 'tags'">
         <div v-for="(d, index) in alarmTags" :key="index">{{ d }}</div>
       </span>
-      <span v-else-if="item.label === 'events'" class="event-detail">
+      <span v-else-if="item.label === 'events'">
         <div>
           <ul>
             <li>
@@ -75,6 +77,12 @@ limitations under the License. -->
           </ul>
         </div>
       </span>
+      <span v-else-if="item.label === 'expression'">
+        {{ currentDetail.snapshot.expression }}
+      </span>
+      <span v-else-if="item.label === 'snapshot'">
+        <Snapshot :snapshot="currentDetail.snapshot" />
+      </span>
       <span v-else>{{ currentDetail[item.label] }}</span>
     </div>
   </el-dialog>
@@ -85,7 +93,7 @@ limitations under the License. -->
     :destroy-on-close="true"
     @closed="showEventDetails = false"
   >
-    <div class="event-detail">
+    <div>
       <div class="mb-10" v-for="(eventKey, index) in EventsDetailKeys" 
:key="index">
         <span class="keys">{{ t(eventKey.text) }}</span>
         <span v-if="eventKey.class === 'parameters'">
@@ -117,6 +125,7 @@ limitations under the License. -->
   import { useAlarmStore } from "@/store/modules/alarm";
   import { EventsDetailHeaders, AlarmDetailCol, EventsDetailKeys } from 
"./data";
   import { dateFormat } from "@/utils/dateFormat";
+  import Snapshot from "./components/Snapshot.vue";
 
   const { t } = useI18n();
   const alarmStore = useAlarmStore();
@@ -186,11 +195,10 @@ limitations under the License. -->
   }
 
   .timeline-table-i-scope {
-    display: inline-block;
-    padding: 0 8px;
+    padding: 0 5px;
     border: 1px solid;
-    margin-top: -1px;
-    border-radius: 4px;
+    border-radius: 3px;
+    display: inline-block;
   }
 
   .timeline-item {
@@ -224,9 +232,6 @@ limitations under the License. -->
   }
 
   .alarm-detail {
-    max-height: 600px;
-    overflow: auto;
-
     ul {
       min-height: 100px;
       overflow: auto;
@@ -247,4 +252,9 @@ limitations under the License. -->
       }
     }
   }
+
+  .mini-chart {
+    height: 20px;
+    width: 400px;
+  }
 </style>
diff --git a/src/views/alarm/components/Line.vue 
b/src/views/alarm/components/Line.vue
new file mode 100644
index 00000000..da8be69f
--- /dev/null
+++ b/src/views/alarm/components/Line.vue
@@ -0,0 +1,141 @@
+<!-- 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>
+  <Graph :option="option" @select="clickEvent" />
+</template>
+<script lang="ts" setup>
+  import { computed } from "vue";
+  import type { PropType } from "vue";
+  import type { EventParams } from "@/types/dashboard";
+  import useLegendProcess from "@/hooks/useLegendProcessor";
+  import { useAppStoreWithOut } from "@/store/modules/app";
+  import { Themes } from "@/constants/data";
+
+  /*global defineProps, defineEmits */
+  const emits = defineEmits(["click"]);
+  const props = defineProps({
+    data: {
+      type: Array as PropType<any>,
+      default: () => [],
+    },
+    intervalTime: { type: Array as PropType<string[]>, default: () => [] },
+  });
+  const appStore = useAppStoreWithOut();
+  const option = computed(() => getOption());
+  function getOption() {
+    const { chartColors } = useLegendProcess();
+    const color: string[] = chartColors();
+    const series = [];
+    const grid = [];
+    const xAxis = [];
+    const yAxis = [];
+    for (const [index, metric] of props.data.entries()) {
+      grid.push({
+        top: 300 * index + 30,
+        left: 0,
+        right: 10,
+        bottom: 5,
+        containLabel: true,
+        height: 260,
+      });
+      xAxis.push({
+        type: "category",
+        show: true,
+        axisTick: {
+          lineStyle: { color: "#c1c5ca41" },
+          alignWithLabel: true,
+        },
+        splitLine: { show: false },
+        axisLine: { lineStyle: { color: "rgba(0,0,0,0)" } },
+        axisLabel: { color: "#9da5b2", fontSize: "11" },
+        gridIndex: index,
+      });
+      yAxis.push({
+        type: "value",
+        axisLine: { show: false },
+        axisTick: { show: false },
+        splitLine: { lineStyle: { color: "#c1c5ca41", type: "dashed" } },
+        axisLabel: {
+          color: "#9da5b2",
+          fontSize: "11",
+          show: true,
+        },
+        gridIndex: index,
+      });
+      for (const item of metric.values) {
+        series.push({
+          data: item.values.map((item: number, itemIndex: number) => 
[props.intervalTime[itemIndex], item]),
+          name: item.name || metric.name,
+          type: "line",
+          symbol: "circle",
+          symbolSize: 4,
+          xAxisIndex: index,
+          yAxisIndex: index,
+          lineStyle: {
+            width: 2,
+            type: "solid",
+          },
+        });
+      }
+    }
+    const tooltip = {
+      trigger: "axis",
+      backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
+      borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
+      textStyle: {
+        fontSize: 12,
+        color: appStore.theme === Themes.Dark ? "#eee" : "#333",
+      },
+      enterable: true,
+      confine: true,
+      extraCssText: "max-height:85%; overflow: auto;",
+      axisPointer: {
+        animation: false,
+      },
+    };
+
+    return {
+      color,
+      tooltip,
+      axisPointer: {
+        link: { xAxisIndex: "all" },
+      },
+      legend: {
+        type: "scroll",
+        icon: "circle",
+        top: -5,
+        left: 0,
+        itemWidth: 12,
+        textStyle: {
+          color: appStore.theme === Themes.Dark ? "#fff" : "#333",
+        },
+      },
+      grid,
+      xAxis,
+      yAxis,
+      series,
+    };
+  }
+
+  function clickEvent(params: EventParams) {
+    emits("click", params);
+  }
+</script>
+<style lang="scss" scoped>
+  .snapshot-charts {
+    width: 100%;
+    height: 100%;
+  }
+</style>
diff --git a/src/views/alarm/components/Snapshot.vue 
b/src/views/alarm/components/Snapshot.vue
new file mode 100644
index 00000000..5b7fad33
--- /dev/null
+++ b/src/views/alarm/components/Snapshot.vue
@@ -0,0 +1,35 @@
+<!-- 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>
+  <LineChart
+    :intervalTime="appStore.intervalTime"
+    :data="metrics"
+    :style="{ width: `800px`, height: `${metrics.length * 300}px` }"
+  />
+</template>
+<script lang="ts" setup>
+  import { computed } from "vue";
+  import { useSnapshot } from "@/hooks/useSnapshot";
+  import { useAppStoreWithOut } from "@/store/modules/app";
+  import LineChart from "./Line.vue";
+
+  /*global defineProps */
+  const props = defineProps({
+    snapshot: { type: Object, default: () => {} },
+  });
+  const { processResults } = useSnapshot(props.snapshot.metrics);
+  const metrics = computed(() => processResults());
+  const appStore = useAppStoreWithOut();
+</script>
diff --git a/src/views/alarm/data.ts b/src/views/alarm/data.ts
index d78f7ea8..22701308 100644
--- a/src/views/alarm/data.ts
+++ b/src/views/alarm/data.ts
@@ -52,6 +52,14 @@ export const AlarmDetailCol = [
     label: "events",
     value: "eventDetail",
   },
+  {
+    label: "expression",
+    value: "expression",
+  },
+  {
+    label: "snapshot",
+    value: "snapshot",
+  },
 ];
 
 export const EventsDetailKeys = [
diff --git a/src/views/dashboard/graphs/Line.vue 
b/src/views/dashboard/graphs/Line.vue
index 11d23515..d91b31c2 100644
--- a/src/views/dashboard/graphs/Line.vue
+++ b/src/views/dashboard/graphs/Line.vue
@@ -60,6 +60,8 @@ limitations under the License. -->
         showYAxis: true,
         smallTips: false,
         showlabels: true,
+        noTooltips: false,
+        showLegend: true,
       }),
     },
   });
@@ -69,10 +71,12 @@ limitations under the License. -->
   function getOption() {
     const { showEchartsLegend, isRight, chartColors } = 
useLegendProcess(props.config.legend);
     setRight.value = isRight;
-    const keys = Object.keys(props.data || {}).filter((i: any) => 
Array.isArray(props.data[i]) && props.data[i].length);
-    const temp = keys.map((i: any) => {
+    const keys = Object.keys(props.data || {}).filter(
+      (i: string) => Array.isArray(props.data[i]) && props.data[i].length,
+    );
+    const temp = keys.map((i: string) => {
       const serie: any = {
-        data: props.data[i].map((item: any, itemIndex: number) => 
[props.intervalTime[itemIndex], item]),
+        data: props.data[i].map((item: number, itemIndex: number) => 
[props.intervalTime[itemIndex], item]),
         name: i,
         type: "line",
         symbol: "circle",
@@ -95,6 +99,7 @@ limitations under the License. -->
     const color: string[] = chartColors();
     const tooltip = {
       trigger: "axis",
+      show: !props.config.noTooltips,
       backgroundColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
       borderColor: appStore.theme === Themes.Dark ? "#333" : "#fff",
       textStyle: {
@@ -106,10 +111,10 @@ limitations under the License. -->
       extraCssText: "max-height:85%; overflow: auto;",
     };
     const tips = {
+      show: !props.config.noTooltips,
       formatter(params: any) {
         return `${params[0].value[1]}`;
       },
-      confine: true,
       extraCssText: `height: 20px; padding:0 2px;`,
       trigger: "axis",
       backgroundColor: appStore.theme === Themes.Dark ? "#666" : "#eee",
@@ -125,7 +130,7 @@ limitations under the License. -->
       tooltip: props.config.smallTips ? tips : tooltip,
       legend: {
         type: "scroll",
-        show: showEchartsLegend(keys),
+        show: props.config.showLegend ? showEchartsLegend(keys) : false,
         icon: "circle",
         top: 0,
         left: 0,

Reply via email to