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 <34978943+muk...@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; }