You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by xx...@apache.org on 2022/09/06 01:19:12 UTC

[kylin] branch kylin5 updated: KYLIN-5249 add node status notification on web ui

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

xxyu pushed a commit to branch kylin5
in repository https://gitbox.apache.org/repos/asf/kylin.git


The following commit(s) were added to refs/heads/kylin5 by this push:
     new 2e294b5eea KYLIN-5249 add node status notification on web ui
2e294b5eea is described below

commit 2e294b5eea0d3f3b522ccb726aa75d6918ac5ab3
Author: Tengting Xu <34...@users.noreply.github.com>
AuthorDate: Tue Sep 6 09:19:05 2022 +0800

    KYLIN-5249 add node status notification on web ui
    
    * Minor fix email notification
    
    * KYLIN-5249 add node status notification on web ui
---
 .../admin/SystemCapacity/CapacityTopBar.vue        | 269 +++++++++++++++++++++
 .../src/components/admin/SystemCapacity/locales.js |  10 +
 .../components/layout/layout_left_right_top.vue    |  41 +++-
 kystudio/src/main.js                               |   4 +-
 kystudio/src/store/capacity.js                     |  46 ++++
 kystudio/src/store/index.js                        |   2 +
 .../common/util/BasicEmailNotificationContent.java |  15 +-
 .../java/org/apache/kylin/tool/RollbackTool.java   |   3 +-
 8 files changed, 377 insertions(+), 13 deletions(-)

diff --git a/kystudio/src/components/admin/SystemCapacity/CapacityTopBar.vue b/kystudio/src/components/admin/SystemCapacity/CapacityTopBar.vue
new file mode 100644
index 0000000000..2ed5ca54c5
--- /dev/null
+++ b/kystudio/src/components/admin/SystemCapacity/CapacityTopBar.vue
@@ -0,0 +1,269 @@
+<template>
+  <div class="capacity-top-bar">
+    <el-popover ref="activeNodes" width="290" popper-class="nodes-popover" v-model="showNodes">
+      <div class="contain" @mouseover="showNodeDetails = false">
+        <div class="lastest-update-time">
+          <el-tooltip :content="$t('lastUpdateTime')" effect="dark" placement="top"><i class="icon el-icon-ksd-type_time"></i></el-tooltip>{{latestUpdateTime | timeFormatHasTimeZone}}</div>
+        <div class="data-valumns">
+          <p :class="['label', 'node-item']" @mouseover.stop @mouseenter.stop="showNodeDetails = true">
+            <span>{{$t('usedNodes')}}:<span :class="['font-medium']">{{nodeList.length}}</span></span>
+            <template>
+              <!-- <span class="font-disabled" v-if="systemNodeInfo.fail">{{$t('failApi')}}</span> -->
+              <!-- <el-tooltip :content="$t('failedTagTip')" effect="dark" placement="top">
+                <el-tag size="mini" type="danger">{{$t('failApi')}}</el-tag>
+              </el-tooltip> -->
+              <el-tag size="mini" type="danger" v-if="isOnlyQueryNode">{{$t('noActiveAllNode')}}</el-tag>
+              <!-- <el-tag size="mini" type="danger" v-if="systemNodeInfo.node_status === 'OVERCAPACITY'">{{$t('excess')}}</el-tag> -->
+            </template>
+            <span class="icon el-icon-ksd-more_02 node-list-icon"></span></p>
+        </div>
+      </div>
+      <div class="nodes" v-if="showNodeDetails && isNodeLoadingSuccess && !isNodeLoading" @mouseenter="showNodeDetails = true" @mouseleave="showNodeDetails = false">
+        <!-- <p class="error-text" v-if="!nodeList.filter(it => it.mode === 'all').length">{{$t('noNodesTip1')}}</p> -->
+        <div class="node-details" v-if="nodeList.length > 0">
+          <div class="node-list" v-for="(node, index) in nodeList" :key="index">
+            <span v-custom-tooltip="{text: `${node.host}(${node.mode === 'All' ? 'All' : $t(`kylinLang.common.${node.mode.toLocaleLowerCase()}Node`)})`, w: 20}">{{`${node.host}(${node.mode === 'All' ? 'All' : $t(`kylinLang.common.${node.mode.toLocaleLowerCase()}Node`)})`}}</span>
+          </div>
+        </div>
+        <div class="node-details nodata" v-else>{{$t('kylinLang.common.noData')}}</div>
+      </div>
+    </el-popover>
+    <p class="active-nodes" v-popover:activeNodes @click="showNodes = !showNodes">
+      <span :class="['flag', getNodesNumColor]"></span>
+      <span class="server-status">{{$t('serverStatus')}}</span>
+    </p>
+  </div>
+</template>
+<script>
+  import Vue from 'vue'
+  import { Component } from 'vue-property-decorator'
+  import { mapActions, mapState, mapGetters } from 'vuex'
+  import locales from './locales'
+  import filterElements from '../../../filter/index'
+  import { handleError } from '../../../util/business'
+
+  @Component({
+    methods: {
+      ...mapActions({
+        getNodeList: 'GET_NODES_LIST'
+      })
+    },
+    computed: {
+      ...mapState({
+        latestUpdateTime: state => state.capacity.latestUpdateTime
+      }),
+      ...mapGetters([
+        'isOnlyQueryNode',
+        'isOnlyJobNode',
+        'isAdminRole'
+      ])
+    },
+    locales
+  })
+  export default class CapacityTopBar extends Vue {
+    showNodes = false
+    isNodeLoadingSuccess = false
+    nodeList = []
+    isNodeLoading = true
+    showNodeDetails = false
+    filterElements = filterElements
+    nodesTimer = null
+    modelObj = {
+      all: 'All',
+      job: 'Job',
+      query: 'Query'
+    }
+
+    get getNodesNumColor () {
+      return 'is-success'
+    }
+
+    created () {
+      this.getHANodes()
+    }
+
+    getHANodes () {
+      if (this._isDestroyed) {
+        return
+      }
+      this.isNodeLoading = true
+      const data = {ext: true}
+      if (this.nodesTimer) {
+        data.isAuto = true
+      }
+      this.getNodeList(data).then((res) => {
+        if (this._isDestroyed) {
+          return
+        }
+        this.isNodeLoadingSuccess = true
+        res.servers.length && (this.nodeList = res.servers.map(it => ({...it, mode: this.modelObj[it.mode]})))
+        this.isNodeLoading = false
+        clearTimeout(this.nodesTimer)
+        this.nodesTimer = setTimeout(() => {
+          this.getHANodes()
+        }, 1000 * 60)
+      }).catch((e) => {
+        if (e.status === 401) {
+          handleError(e)
+        } else {
+          clearTimeout(this.nodesTimer)
+          this.timer = setTimeout(() => {
+            this.getHANodes()
+          }, 1000 * 60)
+        }
+      })
+    }
+  }
+</script>
+<style lang="less" scoped>
+  @import '../../../assets/styles/variables.less';
+  
+  .capacity-top-bar {
+    position: relative;
+    min-width: 65px;
+    // padding-right: 20px;
+    .active-nodes {
+      position: relative;
+      &:hover {
+        color: @text-normal-color !important;
+      }
+      .server-status {
+        font-weight: @font-regular;
+        &:hover {
+          color: @base-color;
+        }
+      }
+      .flag {
+        width: 10px;
+        height: 10px;
+        // position: absolute;
+        // left: -8px;
+        display: inline-block;
+        border-radius: 100%;
+        &.is-danger {
+          background-color: @error-color-1;
+        }
+        &.is-warning {
+          background-color: @warning-color-1;
+        }
+        &.is-success {
+          background-color: @normal-color-1;
+        }
+      }
+      .el-icon-ksd-restart {
+        color: @base-color;
+      }
+    }
+    .font-disabled {
+      color: @text-disabled-color;
+    }
+  }
+  .is-danger {
+    color: @error-color-1;
+  }
+  .is-warning {
+    color: @warning-color-1;
+  }
+  .is-success {
+    color: @normal-color-1;
+  }
+  .error-text {
+    color: @error-color-1;
+    font-size: 12px;
+    margin-bottom: 13px;
+  }
+</style>
+
+<style lang="less">
+  @import '../../../assets/styles/variables.less';
+  .nodes-popover {
+    padding: 0 !important;
+    margin-left: -200px;
+    position: relative;
+    .popper__arrow {
+      // margin-left: 50px;
+      left: initial !important;
+      right: 38px;
+    }
+    .font-disabled {
+      color: @text-disabled-color;
+    }
+    .contain {
+      .lastest-update-time {
+        height: 30px;
+        padding: 5px 10px;
+        line-height: 20px;
+        box-sizing: border-box;
+        color: @text-normal-color;
+        border-bottom: 1px solid @line-border-color3;
+        .icon {
+          margin-right: 5px;
+          color: @text-disabled-color;
+        }
+      }
+      .data-valumns {
+        padding: 10px 0;
+        box-sizing: border-box;
+        .label {
+          line-height: 28px;
+          padding: 0 10px;
+          box-sizing: border-box;
+          .over-thirty-days {
+            cursor: pointer;
+          }
+        }
+        .node-item {
+          position: relative;
+          cursor: pointer;
+          .node-list-icon {
+            position: absolute;
+            right: 10px;
+            top: 10px;
+            font-size: 9px;
+          }
+          &:hover {
+            background: @base-color-9;
+          }
+          &.is-disabled {
+            pointer-events: none;
+          }
+        }
+      }
+    }
+    .nodes {
+      max-height: 170px;
+      overflow: auto;
+      text-align: left;
+      position: absolute;
+      background: #ffffff;
+      transform: translate(-105%, 0);
+      margin-top: -39px;
+      width: 208px;
+      padding: 10px;
+      box-sizing: border-box;
+      box-shadow: 0 0px 6px 0px #E5E5E5;
+      .node-details {
+        text-align: left;
+        width: 100%;
+        &.nodata {
+          color: @text-disabled-color;
+          text-align: center;
+        }
+      }
+      .node-list {
+        color: @text-normal-color;
+        margin-top: 8px;
+        width: 100%;
+        display: inline-block;
+        &:first-child {
+          margin-top: 0;
+        }
+        .custom-tooltip-layout {
+          vertical-align: middle;
+          line-height: 1;
+          width: 100%;
+        }
+      }
+    }
+  }
+</style>
diff --git a/kystudio/src/components/admin/SystemCapacity/locales.js b/kystudio/src/components/admin/SystemCapacity/locales.js
new file mode 100644
index 0000000000..adfe60e9c3
--- /dev/null
+++ b/kystudio/src/components/admin/SystemCapacity/locales.js
@@ -0,0 +1,10 @@
+export default {
+  'en': {
+    usedNodes: 'Node Used',
+    nodeList: 'Node List',
+    node: 'Node',
+    type: 'Type',
+    serverStatus: 'Service Status',
+    lastUpdateTime: 'Last Updated Time'
+  }
+}
diff --git a/kystudio/src/components/layout/layout_left_right_top.vue b/kystudio/src/components/layout/layout_left_right_top.vue
index 3dbbd337d9..9165d03baa 100644
--- a/kystudio/src/components/layout/layout_left_right_top.vue
+++ b/kystudio/src/components/layout/layout_left_right_top.vue
@@ -51,6 +51,9 @@
           </template>
 
           <ul class="top-ul ksd-fright">
+            <li class="capacity-li">
+              <capacity/>
+            </li>
             <li v-if="showMenuByRole('admin')" style="margin-right: 1px;">
               <el-tooltip :content="$t('kylinLang.menu.admin')" placement="bottom">
                 <el-button
@@ -78,8 +81,27 @@
             </li>
           </ul>
         </div>
-        <div class="panel-content" id="scrollBox">
-          <div class="grid-content bg-purple-light" id="scrollContent">
+        <div class="panel-content" id="scrollBox" :class="{'ksd-pt-38': isShowAlter && !isFullScreen}">
+          <div class="alter-block" v-if="isShowAlter && !isFullScreen">
+            <el-alert :type="globalAlterTips.flag === 0 ? 'error' : 'warning'" :closable="globalAlterTips.flag !== 0" show-icon>
+              <span slot="title">{{globalAlterTips.text}} <a href="javascript:void(0);" @click="jumpToDetails" v-if="globalAlterTips.detailPath&&$route.name!=='SystemCapacity'">{{$t('viewDetails')}}</a></span>
+            </el-alert>
+          </div>
+          <div :class="['grid-content', 'bg-purple-light']" id="scrollContent">
+            <!-- <el-col :span="24" v-show="gloalProjectSelectShow" class="bread-box"> -->
+              <!-- 面包屑在dashboard页面不显示 -->
+              <!-- <el-breadcrumb separator="/" class="ksd-ml-30">
+                <el-breadcrumb-item>
+                  <span>{{$t('kylinLang.menu.' + currentRouterNameArr[0])}}</span>
+                </el-breadcrumb-item>
+                <el-breadcrumb-item v-if="currentRouterNameArr[1]" :to="{ path: '/' + currentRouterNameArr[0] + '/' + currentRouterNameArr[1]}">
+                  <span>{{$t('kylinLang.menu.' + currentRouterNameArr[1])}}</span>
+                </el-breadcrumb-item>
+                <el-breadcrumb-item v-if="currentRouterNameArr[2]" >
+                  {{currentRouterNameArr[2]}}
+                </el-breadcrumb-item>
+              </el-breadcrumb> -->
+            <!-- </el-col> -->
             <el-col :span="24" class="main-content">
               <transition :name="isAnimation ? 'slide' : null" v-bind:css="isAnimation">
                 <router-view v-on:addProject="addProject" v-if="isShowRouterView"></router-view>
@@ -120,6 +142,7 @@ import projectSelect from '../project/project_select'
 import help from '../common/help'
 import KapDetailDialogModal from '../common/GlobalDialog/dialog/detail_dialog'
 import Diagnostic from '../admin/Diagnostic/index'
+import Capacity from '../admin/SystemCapacity/CapacityTopBar'
 import $ from 'jquery'
 import ElementUI from 'kyligence-kylin-ui'
 import GuideModal from '../studio/StudioModel/ModelList/GuideModal/GuideModal.vue'
@@ -143,6 +166,7 @@ let MessageBox = ElementUI.MessageBox
       cacheHistory: 'CACHE_HISTORY',
       saveTabs: 'SET_QUERY_TABS',
       resetSpeedInfo: 'CACHE_SPEED_INFO',
+      setGlobalAlter: 'SET_GLOBAL_ALTER',
       setProject: 'SET_PROJECT'
     }),
     ...mapActions('UserEditModal', {
@@ -158,7 +182,7 @@ let MessageBox = ElementUI.MessageBox
     help,
     KapDetailDialogModal,
     Diagnostic,
-    // Capacity,
+    Capacity,
     GuideModal
   },
   computed: {
@@ -181,6 +205,16 @@ let MessageBox = ElementUI.MessageBox
     canAddProject () {
       // 模型编辑页面的时候,新增项目的按钮不可点
       return this.$route.name !== 'ModelEdit'
+    },
+    isShowAlter () {
+      const isGlobalAlter = this.$store.state.capacity.maintenance_mode || this.capacityAlert
+      if (this.$store.state.capacity.maintenance_mode) {
+        this.globalAlterTips = { text: this.$t('systemUprade'), flag: 0 }
+      } else if (this.capacityAlert) {
+        this.globalAlterTips = { ...this.capacityAlert, text: this.$t(`kylinLang.capacity.${this.capacityAlert.text}`, this.capacityAlert.query ? this.capacityAlert.query : {}), detailPath: this.capacityAlert.detailPath }
+      }
+      this.setGlobalAlter(isGlobalAlter)
+      return isGlobalAlter
     }
   },
   locales: {
@@ -249,6 +283,7 @@ export default class LayoutLeftRightTop extends Vue {
   isGlobalMaskShow = false
   showDiagnostic = false
   showChangePassword = false
+  globalAlterTips = {}
 
   get isAdminView () {
     const adminRegex = /^\/admin/
diff --git a/kystudio/src/main.js b/kystudio/src/main.js
index f93ea8979d..fdc85f184c 100644
--- a/kystudio/src/main.js
+++ b/kystudio/src/main.js
@@ -55,7 +55,9 @@ Vue.use(VueKonva)
 Vue.http.headers.common['Accept-Language'] = localStorage.getItem('kystudio_lang') === 'en' ? 'en' : 'cn'
 Vue.http.options.xhr = { withCredentials: true }
 const skipUpdateApiList = [
-  'kylin/api/jobs'
+  'kylin/api/jobs',
+  'kylin/api/system/servers',
+  'kylin/api/jobs/waiting_jobs'
 ]
 Vue.http.interceptors.push(function (request, next) {
   const isProgressVisiable = !request.headers.get('X-Progress-Invisiable')
diff --git a/kystudio/src/store/capacity.js b/kystudio/src/store/capacity.js
new file mode 100644
index 0000000000..2ff618adec
--- /dev/null
+++ b/kystudio/src/store/capacity.js
@@ -0,0 +1,46 @@
+import api from './../service/api'
+import * as types from './types'
+
+export default {
+  state: {
+    nodeList: [],
+    maintenance_mode: false,
+    latestUpdateTime: 0
+  },
+  mutations: {
+    [types.SET_NODES_LIST] (state, data) {
+      state.nodeList = data.servers
+      state.maintenance_mode = data.status.maintenance_mode
+    },
+    'LATEST_UPDATE_TIME' (state) {
+      state.latestUpdateTime = new Date().getTime()
+    }
+  },
+  actions: {
+    // 获取节点列表
+    [types.GET_NODES_LIST] ({ commit, dispatch }, paras) {
+      return new Promise((resolve, reject) => {
+        api.system.loadOnlineNodes(paras).then(res => {
+          const { data, code } = res.data
+          if (code === '000') {
+            commit(types.SET_NODES_LIST, data)
+            commit('LATEST_UPDATE_TIME')
+            resolve(data)
+          } else {
+            reject()
+          }
+        }).catch((e) => {
+          reject(e)
+        })
+      })
+    }
+  },
+  getters: {
+    isOnlyQueryNode (state) {
+      return state.nodeList.length && state.nodeList.filter(it => it.mode === 'query').length === state.nodeList.length
+    },
+    isOnlyJobNode (state) {
+      return state.nodeList.length && state.nodeList.filter(it => it.mode === 'job').length === state.nodeList.length
+    }
+  }
+}
diff --git a/kystudio/src/store/index.js b/kystudio/src/store/index.js
index d38d0c9202..ec0956b973 100644
--- a/kystudio/src/store/index.js
+++ b/kystudio/src/store/index.js
@@ -10,6 +10,7 @@ import user from './user'
 import datasource from './datasource'
 import system from './system'
 import monitor from './monitor'
+import capacity from './capacity'
 import * as actionTypes from './types'
 
 export default new Vuex.Store({
@@ -22,6 +23,7 @@ export default new Vuex.Store({
     datasource: datasource,
     system: system,
     monitor: monitor,
+    capacity: capacity,
     modals: {}
   }
 })
diff --git a/src/core-common/src/main/java/org/apache/kylin/common/util/BasicEmailNotificationContent.java b/src/core-common/src/main/java/org/apache/kylin/common/util/BasicEmailNotificationContent.java
index d34a6eef88..e164197324 100644
--- a/src/core-common/src/main/java/org/apache/kylin/common/util/BasicEmailNotificationContent.java
+++ b/src/core-common/src/main/java/org/apache/kylin/common/util/BasicEmailNotificationContent.java
@@ -25,15 +25,15 @@ import lombok.Setter;
 @Setter
 public class BasicEmailNotificationContent {
 
-    public static final String NOTIFY_EMAIL_TITLE_TEMPLATE = "[Kyligence System Notification] ${issue}";
-    public static final String NOTIFY_EMAIL_BODY_TEMPLATE = "<div style='display:block;word-wrap:break-word;width:80%;font-size:16px;font-family:Microsoft YaHei;'><b>Dear Kyligence Customer,</b><pre><p>"
+    public static final String NOTIFY_EMAIL_TITLE_TEMPLATE = "[Apache Kylin System Notification] ${issue}";
+    public static final String NOTIFY_EMAIL_BODY_TEMPLATE = "<div style='display:block;word-wrap:break-word;width:80%;font-size:16px;font-family:Microsoft YaHei;'><b>Dear Apache Kylin User,</b><pre><p>"
             + "<p>${conclusion}</p>" + "<p>Issue: ${issue}<br>" + "Type: ${type}<br>" + "Time: ${time}<br>"
-            + "Project: ${project}<br>" + "Solution: ${solution}</p>" + "<p>Yours sincerely,<br>" + "Kyligence team</p>"
+            + "Project: ${project}<br>" + "Solution: ${solution}</p>" + "<p>Yours sincerely,<br>" + "Apache Kylin Community</p>"
             + "</pre><div/>";
 
-    public static final String CONCLUSION_FOR_JOB_ERROR = "We found an error job happened in your Kyligence system as below. It won't affect your system stability and you may repair it by following instructions.";
-    public static final String CONCLUSION_FOR_LOAD_EMPTY_DATA = "We found a job has loaded empty data in your Kyligence system as below. It won't affect your system stability and you may reload data by following instructions.";
-    public static final String CONCLUSION_FOR_SOURCE_RECORDS_CHANGE = "We found some source records updated in your Kyligence system. You can reload updated records by following instructions. Ignore this issue may cause query result inconsistency over different indexes.";
+    public static final String CONCLUSION_FOR_JOB_ERROR = "We found an error job happened in your Apache Kylin system as below. It won't affect your system stability and you may repair it by following instructions.";
+    public static final String CONCLUSION_FOR_LOAD_EMPTY_DATA = "We found a job has loaded empty data in your Apache Kylin system as below. It won't affect your system stability and you may reload data by following instructions.";
+    public static final String CONCLUSION_FOR_SOURCE_RECORDS_CHANGE = "We found some source records updated in your Apache Kylin system. You can reload updated records by following instructions. Ignore this issue may cause query result inconsistency over different indexes.";
     public static final String CONCLUSION_FOR_OVER_CAPACITY_THRESHOLD = "The amount of data volume used (${volume_used}/${volume_total}) has reached ${capacity_threshold}% of the license’s limit.";
 
     public static final String SOLUTION_FOR_JOB_ERROR = "You may resume the job first. If still won't work, please send the job's diagnostic package to kyligence technical support.";
@@ -59,5 +59,4 @@ public class BasicEmailNotificationContent {
                 .replaceAll("\\$\\{time\\}", time).replaceAll("\\$\\{project\\}", project)
                 .replaceAll("\\$\\{solution\\}", solution);
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/tool/src/main/java/org/apache/kylin/tool/RollbackTool.java b/src/tool/src/main/java/org/apache/kylin/tool/RollbackTool.java
index da7362eede..0f3d737145 100644
--- a/src/tool/src/main/java/org/apache/kylin/tool/RollbackTool.java
+++ b/src/tool/src/main/java/org/apache/kylin/tool/RollbackTool.java
@@ -133,6 +133,7 @@ public class RollbackTool extends ExecutableApplication {
         return options;
     }
 
+    @Override
     protected void execute(OptionsHelper optionsHelper) throws Exception {
         log.info("start roll back");
         log.info("start to init ResourceStore");
@@ -226,7 +227,7 @@ public class RollbackTool extends ExecutableApplication {
         long userTargetTimeMillis = formatter.parseDateTime(userTargetTime).getMillis();
         long protectionTime = System.currentTimeMillis() - kylinConfig.getStorageResourceSurvivalTimeThreshold();
         if (userTargetTimeMillis < protectionTime) {
-            log.error("user specified time  is less than protection time");
+            log.error("user specified time is less than protection time");
             return false;
         }