Support clicking the service name in the chart to link to the tracking page. (#530)

* Support service metrics link to trace query

* Support service metrics link to trace query

* Modify the hard code.

* Support service metrics link to log or trace

* Clean up useless code and modify styles

* Clean up useless code and modify styles

* Clean up useless code

* Modify style and copywriting

* fix undefined bug

* fix undefined bug

Co-authored-by: 吴晟 Wu Sheng <wu.sheng@foxmail.com>
diff --git a/src/assets/lang/en.ts b/src/assets/lang/en.ts
index 8529304..0e0ced9 100644
--- a/src/assets/lang/en.ts
+++ b/src/assets/lang/en.ts
@@ -230,6 +230,8 @@
   destService: 'Destination Service',
   destServiceInstance: 'Destination Service Instance',
   eventSource: 'Event Source',
+  modalTitle: 'Inspection',
+  selectRedirectPage: 'Do you want to inspect Traces or Logs of %s service?',
 };
 
 export default m;
diff --git a/src/assets/lang/zh.ts b/src/assets/lang/zh.ts
index 84ec613..5e13722 100644
--- a/src/assets/lang/zh.ts
+++ b/src/assets/lang/zh.ts
@@ -228,6 +228,8 @@
   destService: '终点服务',
   destServiceInstance: '终点实例',
   eventSource: '事件资源',
+  modalTitle: '查看',
+  selectRedirectPage: '查看 %s 服务的追踪或日志?',
 };
 
 export default m;
diff --git a/src/components/index.ts b/src/components/index.ts
index 67d83ec..a5f02e8 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -24,6 +24,7 @@
 import RkDate from './rk-date.vue';
 import RkPanel from './rk-panel.vue';
 import RkSidebox from './rk-sidebox.vue';
+import RkModal from './rk-modal.vue';
 import RkEcharts from './rk-echarts.vue';
 import RkSelect from './rk-select.vue';
 import RkPopper from './rk-popper.vue';
@@ -42,6 +43,7 @@
   RkEcharts,
   RkPage,
   RkSidebox,
+  RkModal,
   RkFooterTime,
   RkSelect,
   RkPopper,
diff --git a/src/components/rk-modal.vue b/src/components/rk-modal.vue
new file mode 100644
index 0000000..93357f2
--- /dev/null
+++ b/src/components/rk-modal.vue
@@ -0,0 +1,164 @@
+<!-- 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="rk-modal-bg" v-show="show" @mousemove="modalMove" @mouseup="cancelMove">
+        <div class="rk-modal-container">
+            <div class="rk-modal-header" @mousedown="setStartingPoint">
+                <span class="title">{{ this.title }}</span>
+                <div class="r rk-modal-close" @click="cancel">
+                    <svg class="icon">
+                        <use xlink:href="#close"></use>
+                    </svg>
+                </div>
+            </div>
+            <div class="rk-modal-main">
+                <slot></slot>
+            </div>
+            <div v-if="showButton" class="rk-modal-footer">
+                <rk-button class="cancel" @click="cancel">{{ $t('cancel') }}</rk-button>
+                <rk-button @click="confirm">{{ $t('confirm') }}</rk-button>
+            </div>
+        </div>
+    </div>
+</template>
+<script lang="js">
+    import RkButton from '@/components/rk-button';
+    export default {
+        name: 'RkModal',
+        components: {RkButton},
+        props: {
+            show: {
+                type: Boolean,
+                default: false,
+            },
+            showButton: {
+                type: Boolean,
+                default: false,
+            },
+            title: {
+                type: String,
+                default: '',
+            },
+        },
+        data() {
+            return {
+                x: 0,
+                y: 0,
+                node: null,
+                isCanMove: false,
+            };
+        },
+        mounted() {
+            this.node = document.querySelector('.rk-modal-container');
+        },
+        methods: {
+            cancel() {
+                this.$emit('update:show', false);
+                this.$emit('cancelModalCallback');
+            },
+
+            confirm() {
+                this.$emit('update:show', false);
+                this.$emit('confirmModalCallback');
+            },
+
+            setStartingPoint(e) {
+                this.x = e.clientX - this.node.offsetLeft;
+                this.y = e.clientY - this.node.offsetTop;
+                this.isCanMove = true;
+            },
+
+            modalMove(e) {
+                if (this.isCanMove) {
+                    this.node.style.left = e.clientX - this.x + 'px';
+                    this.node.style.top = e.clientY - this.y + 'px';
+                }
+            },
+
+            cancelMove() {
+                this.isCanMove = false;
+            },
+        },
+    };
+</script>
+
+<style lang="scss">
+    .rk-modal-bg {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background: rgba(0, 0, 0, .5);
+        z-index: 10;
+    }
+
+    .rk-modal-container {
+        background: #fff;
+        border-radius: 10px;
+        overflow: hidden;
+        position: fixed;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+    }
+
+    .rk-modal-header {
+        height: 35px;
+        background: #333840;
+        color: #efefef;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        cursor: move;
+    }
+
+    .rk-modal-close {
+        position: absolute;
+        right: 10px;
+        top: 5px;
+        cursor: pointer;
+        color: #d8d8d8;
+        transition: color 0.3s;
+
+        .icon {
+            width: 18px;
+            height: 20px;
+        }
+
+        &:hover {
+            color: #3d92ff;
+        }
+    }
+
+    .rk-modal-main {
+        padding: 15px 40px;
+    }
+
+    .rk-modal-footer {
+        display: flex;
+        align-items: center;
+        justify-content: right;
+        height: 35px;
+        border-top: 1px solid #ddd;
+        padding-right: 10px;
+    }
+    .rk-modal-footer .cancel{
+        margin-right: 10px;
+        border: 1px solid #ddd;
+        color: #333;
+        background-color: #fff;
+    }
+</style>
diff --git a/src/views/components/dashboard/charts/chart-slow.vue b/src/views/components/dashboard/charts/chart-slow.vue
index 9f905e5..23bf084 100644
--- a/src/views/components/dashboard/charts/chart-slow.vue
+++ b/src/views/components/dashboard/charts/chart-slow.vue
@@ -22,11 +22,20 @@
         </svg>
         <div class="mb-5 ell" v-tooltip:top.ellipsis="i.name || ''">
           <span class="calls sm mr-10">{{ i.value }}</span>
-          <span class="cp link-hover">{{ i.name + getTraceId(i) }}</span>
+          <span class="cp link-hover" @click="handleLink(i)">{{ i.name + getTraceId(i) }}</span>
         </div>
         <RkProgress :precent="(i.value / maxValue) * 100" color="#bf99f8" />
       </div>
     </div>
+    <rk-modal :show.sync="showModal" :title="$t('modalTitle')">
+      <div>
+        {{ $t('selectRedirectPage').replace('%s', redirectData.name) }}
+      </div>
+      <div class="mt-15">
+        <router-link :to="redirectData.log" class="rk-chart-slow-link mr-20">{{ $t('log') }}</router-link>
+        <router-link :to="redirectData.trace" class="rk-chart-slow-link mr-20">{{ $t('trace') }}</router-link>
+      </div>
+    </rk-modal>
   </div>
 </template>
 
@@ -34,6 +43,7 @@
   import Vue from 'vue';
   import { Component, Prop } from 'vue-property-decorator';
   import copy from '@/utils/copy';
+  import {MetricsName} from '@/views/components/dashboard/charts/constant';
 
   @Component({})
   export default class ChartSlow extends Vue {
@@ -41,6 +51,37 @@
     @Prop() private item!: any;
     @Prop() private type!: any;
     @Prop() private intervalTime!: any;
+    private showModal: boolean = false;
+    private redirectData: any = {
+      name: '',
+      log: {
+        path: '',
+        query: {
+          service: '',
+        },
+      },
+      trace: {
+        path: '',
+        query: {
+          service: '',
+        },
+      },
+    };
+    private isServiceChart: boolean = false;
+
+    private created() {
+      const serviceMetricNames = [
+              MetricsName.SERVICE_RESP_TIME,
+              MetricsName.SERVICE_SLA,
+              MetricsName.SERVICE_CPM,
+              MetricsName.SERVICE_PERCENTILE,
+              MetricsName.SERVICE_APDEX,
+      ];
+      this.isServiceChart = 'Service' === this.item.entityType
+              && 'sortMetrics' === this.item.queryMetricType
+              && serviceMetricNames.includes(this.item.metricName);
+    }
+
     get maxValue() {
       if (!this.data.length) {
         return null;
@@ -54,6 +95,24 @@
     private handleClick(i: any) {
       copy(i);
     }
+    private handleLink(i: any) {
+      if (this.isServiceChart && i.name) {
+        this.redirectData.name = i.name;
+        this.redirectData.log = {
+          path: 'log',
+          query: {
+            service: encodeURIComponent(i.name),
+          },
+        };
+        this.redirectData.trace = {
+          path: 'trace',
+          query: {
+            service: encodeURIComponent(i.name),
+          },
+        };
+        this.showModal = true;
+      }
+    }
     get datas() {
       if (!this.data.length) {
         return [];
@@ -88,4 +147,13 @@
   .rk-chart-slow-i {
     padding: 6px 0;
   }
+  .rk-chart-slow-link {
+    padding: 4px 10px 7px 10px;
+    border-radius: 4px;
+    border: 1px solid #ddd;
+    color: #333;
+    background-color: #fff;
+    will-change: opacity, background-color;
+    transition: opacity 0.3s, background-color 0.3s;
+  }
 </style>
diff --git a/src/views/components/dashboard/charts/constant.ts b/src/views/components/dashboard/charts/constant.ts
index 5aa812f..ef2f80a 100644
--- a/src/views/components/dashboard/charts/constant.ts
+++ b/src/views/components/dashboard/charts/constant.ts
@@ -86,3 +86,11 @@
 ];
 
 export const MaxItemNum = 10;
+
+export enum MetricsName {
+  SERVICE_RESP_TIME = 'service_resp_time',
+  SERVICE_SLA = 'service_sla',
+  SERVICE_CPM = 'service_cpm',
+  SERVICE_PERCENTILE = 'service_percentile',
+  SERVICE_APDEX = 'service_apdex',
+}
diff --git a/src/views/components/log/log-bar.vue b/src/views/components/log/log-bar.vue
index 8216294..fe369e5 100644
--- a/src/views/components/log/log-bar.vue
+++ b/src/views/components/log/log-bar.vue
@@ -127,6 +127,15 @@
           this.QUERY_LOGS_BYKEYWORDS();
         })
         .then(() => {
+          const serviceName = this.$route.query.service ? this.$route.query.service.toString() : undefined;
+          if (serviceName) {
+            for (const s of this.rocketOption.services) {
+              if (s.label === serviceName) {
+                this.selectService(s);
+                break;
+              }
+            }
+          }
           this.queryLogs();
         });
     }
diff --git a/src/views/components/trace/trace-search.vue b/src/views/components/trace/trace-search.vue
index 92b0c4f..1882460 100644
--- a/src/views/components/trace/trace-search.vue
+++ b/src/views/components/trace/trace-search.vue
@@ -132,19 +132,32 @@
     private tagsMap: Array<{ key: string; value: string }> = [];
     private tagsList: string[] = [];
     private clearTags: boolean = false;
+    private serviceName: string = '';
 
     private created() {
       this.traceId = this.$route.query.traceid ? this.$route.query.traceid.toString() : this.traceId;
+      this.serviceName = this.$route.query.service ? this.$route.query.service.toString() : this.serviceName;
       this.time = [this.rocketbotGlobal.durationRow.start, this.rocketbotGlobal.durationRow.end];
     }
     private mounted() {
-      this.getTraceList();
-      if (this.service && this.service.key) {
-        this.GET_INSTANCES({
-          duration: this.durationTime,
-          serviceId: this.service.key,
+      this.GET_SERVICES({ duration: this.durationTime })
+        .then(() => {
+          if (this.serviceName) {
+            for (const s of this.rocketTrace.services) {
+              if (s.label === this.serviceName) {
+                this.service = s;
+                break;
+              }
+            }
+          }
+          this.getTraceList();
+          if (this.service && this.service.key) {
+            this.GET_INSTANCES({
+              duration: this.durationTime,
+              serviceId: this.service.key,
+            });
+          }
         });
-      }
     }
     private globalTimeFormat(time: Date[]) {
       const step = 'SECOND';
@@ -195,7 +208,6 @@
       this.tagsMap = data.tagsMap;
     }
     private getTraceList() {
-      this.GET_SERVICES({ duration: this.durationTime });
       const temp: any = {
         queryDuration: this.globalTimeFormat([
           new Date(
@@ -259,6 +271,7 @@
       localStorage.removeItem('traceId');
       this.traceState = { label: 'All', key: 'ALL' };
       this.SET_TRACE_FORM_ITEM({ type: 'queryOrder', data: '' });
+      this.GET_SERVICES({ duration: this.durationTime });
       this.getTraceList();
     }