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;
}