/** * 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. */ var App = require('app'); var date = require('utils/date/date'); var dataUtils = require('utils/data_manipulation'); /** * Host information shown in the operations popup * @typedef {Em.Object} wrappedHost * @property {string} name * @property {string} publicName * @property {string} displayName * @property {number} progress * @property {boolean} isInProgress * @property {string} serviceName * @property {string} status * @property {number} isVisible * @property {string} icon * @property {string} barColor * @property {string} barWidth */ /** * Task information shown in the operations popup * @typedef {Em.Object} wrappedTask * @property {string} id * @property {string} hostName * @property {string} command * @property {string} commandDetail * @property {string} status * @property {string} role * @property {string} stderr * @property {string} stdout * @property {number} request_id * @property {boolean} isVisible * @property {string} startTime * @property {string} duration * @property {string} icon */ /** * Service information shown in the operations popup * @typedef {Em.Object} wrappedService * @property {string} id * @property {string} displayName * @property {string} progress * @property {string} status * @property {boolean} isRunning * @property {string} name * @property {boolean} isVisible * @property {string} startTime * @property {string} duration * @property {string} icon * @property {string} barColor * @property {boolean} isInProgress * @property {string} barWidth * @property {number} sourceRequestScheduleId * @property {string} contextCommand */ /** * App.HostPopup is for the popup that shows up upon clicking already-performed or currently-in-progress operations * Allows to abort executing operations * * @type {Em.Object} * @class {HostPopup} */ App.HostPopup = Em.Object.create({ name: 'hostPopup', /** * @type {object[]} */ servicesInfo: [], /** * @type {?wrappedHost[]} */ hosts: null, /** * @type {?object[]} */ inputData: null, /** * @type {string} */ serviceName: '', /** * @type {?Number} */ currentServiceId: null, /** * @type {?Number} */ previousServiceId: null, /** * @type {string} */ popupHeaderName: '', operationInfo: null, /** * @type {?App.Controller} */ dataSourceController: null, /** * @type {bool} */ isBackgroundOperations: false, /** * @type {?string} */ currentHostName: null, /** * @type {?App.ModalPopup} */ isPopup: null, /** * @type {object} */ detailedProperties: { stdout: 'stdout', stderr: 'stderr', outputLog: 'output_log', errorLog: 'error_log' }, /** * @type {object} */ barColorMap: { 'FAILED': 'progress-danger', 'ABORTED': 'progress-warning', 'TIMEDOUT': 'progress-warning', 'IN_PROGRESS': 'progress-info', 'COMPLETED': 'progress-success' }, /** * map to get css class with styles by service status * * @type {object} */ statusesStyleMap: { 'FAILED': ['FAILED', 'icon-exclamation-sign', 'progress-danger', false], 'ABORTED': ['ABORTED', 'icon-minus', 'progress-warning', false], 'TIMEDOUT': ['TIMEDOUT', 'icon-time', 'progress-warning', false], 'IN_PROGRESS': ['IN_PROGRESS', 'icon-cogs', 'progress-info', true], 'COMPLETED': ['SUCCESS', 'icon-ok', 'progress-success', false] }, /** * View with "Abort Request"-button * * @type {Em.View} */ abortIcon: Em.View.extend({ tagName: 'i', classNames: ['abort-icon', 'icon-remove-circle', 'pointer'], click: function () { this.get('controller').abortRequest(this.get('servicesInfo')); return false; }, didInsertElement: function () { App.tooltip($(this.get('element')), { placement: "top", title: Em.I18n.t('hostPopup.bgop.abortRequest.title') }); }, willDestroyElement: function () { $(this.get('element')).tooltip('destroy'); } }), /** * View with status icon (and tooltip on it) * * @type {Em.View} */ statusIcon: Em.View.extend({ tagName: 'i', classNames: ["service-status"], classNameBindings: ['servicesInfo.status', 'servicesInfo.icon', 'additionalClass'], attributeBindings: ['data-original-title'], 'data-original-title': function () { return this.get('servicesInfo.status'); }.property('servicesInfo.status'), didInsertElement: function () { App.tooltip($(this.get('element'))); }, willDestroyElement: function () { $(this.get('element')).tooltip('destroy'); } }), /** * Determines if background operation can be aborted depending on its status * * @param status * @returns {boolean} */ isAbortableByStatus: function (status) { var statuses = this.get('statusesStyleMap'); return !Em.keys(statuses).contains(status) || status === 'IN_PROGRESS'; }, /** * Send request to abort operation * * @method abortRequest */ abortRequest: function (serviceInfo) { var requestName = serviceInfo.get('name'); var self = this; App.showConfirmationPopup(function () { serviceInfo.set('isAbortable', false); return App.ajax.send({ name: 'background_operations.abort_request', sender: self, data: { requestId: serviceInfo.get('id'), requestName: requestName, serviceInfo: serviceInfo }, success: 'abortRequestSuccessCallback', error: 'abortRequestErrorCallback' }); }, Em.I18n.t('hostPopup.bgop.abortRequest.confirmation.body').format(requestName)); return false; }, /** * Method called on successful sending request to abort operation * * @return {App.ModalPopup} * @method abortRequestSuccessCallback */ abortRequestSuccessCallback: function (response, request, data) { return App.ModalPopup.show({ header: Em.I18n.t('hostPopup.bgop.abortRequest.modal.header'), encodeBody: false, body: Em.I18n.t('hostPopup.bgop.abortRequest.modal.body').format(data.requestName), secondary: null }); }, /** * Method called on unsuccessful sending request to abort operation * * @method abortRequestErrorCallback */ abortRequestErrorCallback: function (xhr, textStatus, error, opt, data) { data.serviceInfo.set('isAbortable', this.isAbortableByStatus(data.serviceInfo.status)); App.ajax.defaultErrorHandler(xhr, opt.url, 'PUT', xhr.status); }, /** * Entering point of this component * * @param {String} serviceName * @param {Object} controller * @param {Boolean} isBackgroundOperations * @param {Integer} requestId * @method initPopup */ initPopup: function (serviceName, controller, isBackgroundOperations, requestId) { if (!isBackgroundOperations) { this.clearHostPopup(); this.set("popupHeaderName", serviceName); } this.setProperties({ currentServiceId: requestId, serviceName: serviceName, dataSourceController: controller, isBackgroundOperations: isBackgroundOperations, inputData: this.get("dataSourceController.services") }); if (isBackgroundOperations) { this.onServiceUpdate(); } else { this.onHostUpdate(); } return this.createPopup(); }, /** * clear info popup data * * @method clearHostPopup */ clearHostPopup: function () { this.setProperties({ servicesInfo: [], host: null, inputData: null, serviceName: '', currentServiceId: null, previousServiceId: null, popupHeaderName: '', dataSourceController: null, currentHostName: null }); if(this.get('isPopup')) { this.get('isPopup').remove(); } }, /** * Depending on tasks status * * @param {object[]} tasks * @return {*[]} [Status, Icon type, Progressbar color, is IN_PROGRESS] * @method getStatus */ getStatus: function (tasks) { var isCompleted = true; var tasksLength = tasks.length; for (var i = 0; i < tasksLength; i++) { var taskStatus = tasks[i].Tasks.status; if (taskStatus !== 'COMPLETED') { isCompleted = false; } if (taskStatus === 'FAILED') { return ['FAILED', 'icon-exclamation-sign', 'progress-danger', false]; } if (taskStatus === 'ABORTED') { return ['ABORTED', 'icon-minus', 'progress-warning', false]; } if (taskStatus === 'TIMEDOUT') { return ['TIMEDOUT', 'icon-time', 'progress-warning', false]; } if (taskStatus === 'IN_PROGRESS') { return ['IN_PROGRESS', 'icon-cogs', 'progress-info', true] } } if (isCompleted) { return ['SUCCESS', 'icon-ok', 'progress-success', false]; } return ['PENDING', 'icon-cog', 'progress-info', true]; }, /** * Progress of host or service depending on tasks status * If no tasks, progress is 0 * * @param {?object[]} tasks * @return {Number} percent of completion * @method getProgress */ getProgress: function (tasks) { if (!tasks || !tasks.length) { return 0; } var groupedByStatus = dataUtils.groupPropertyValues(tasks, 'Tasks.status'); var completedActions = Em.getWithDefault(groupedByStatus, 'COMPLETED.length', 0) + Em.getWithDefault(groupedByStatus, 'FAILED.length', 0) + Em.getWithDefault(groupedByStatus, 'ABORTED.length', 0) + Em.getWithDefault(groupedByStatus, 'TIMEDOUT.length', 0); var queuedActions = Em.getWithDefault(groupedByStatus, 'QUEUED.length', 0); var inProgressActions = Em.getWithDefault(groupedByStatus, 'IN_PROGRESS.length', 0); return Math.ceil((queuedActions * 0.09 + inProgressActions * 0.35 + completedActions ) / tasks.length * 100); }, /** * Count number of operations for select box options * * @param {?Object[]} obj * @param {progressPopupCategoryObject[]} categories * @method setSelectCount */ setSelectCount: function (obj, categories) { if (!obj) { return; } var groupedByStatus = dataUtils.groupPropertyValues(obj, 'status'); categories.findProperty("value", 'all').set("count", obj.length); categories.findProperty("value", 'pending').set("count", Em.getWithDefault(groupedByStatus, 'pending.length', 0) + Em.getWithDefault(groupedByStatus, 'queued.length', 0)); categories.findProperty("value", 'in_progress').set("count", Em.getWithDefault(groupedByStatus, 'in_progress.length', 0)); categories.findProperty("value", 'failed').set("count", Em.getWithDefault(groupedByStatus, 'failed.length', 0)); categories.findProperty("value", 'completed').set("count", Em.getWithDefault(groupedByStatus, 'success.length', 0) + Em.getWithDefault(groupedByStatus, 'completed.length', 0)); categories.findProperty("value", 'aborted').set("count", Em.getWithDefault(groupedByStatus, 'aborted.length', 0)); categories.findProperty("value", 'timedout').set("count", Em.getWithDefault(groupedByStatus, 'timedout.length', 0)); }, /** * For Background operation popup calculate number of running Operations, and set popup header * * @param {bool} isServiceListHidden * @method setBackgroundOperationHeader */ setBackgroundOperationHeader: function (isServiceListHidden) { if (this.get('isBackgroundOperations') && !isServiceListHidden) { var numRunning = App.router.get('backgroundOperationsController.allOperationsCount'); this.set("popupHeaderName", numRunning + Em.I18n.t('hostPopup.header.postFix').format(numRunning === 1 ? "" : "s")); } }, /** * Create services obj data structure for popup * Set data for services * * @param {bool} isServiceListHidden * @method onServiceUpdate */ onServiceUpdate: function (isServiceListHidden) { if (this.get('isBackgroundOperations') && this.get("inputData")) { var servicesInfo = this.get("servicesInfo"); var currentServices = []; this.get("inputData").forEach(function (service, index) { var updatedService; var id = service.id; currentServices.push(id); var existedService = servicesInfo.findProperty('id', id); updatedService = existedService; if (existedService) { updatedService = this.updateService(existedService, service); } else { updatedService = this.createService(service); servicesInfo.insertAt(index, updatedService); } updatedService.set('isAbortable', App.isAuthorized('SERVICE.START_STOP') && this.isAbortableByStatus(service.status)); }, this); this.removeOldServices(servicesInfo, currentServices); this.setBackgroundOperationHeader(isServiceListHidden); } }, /** * Create service object from transmitted data * * @param {object} service * @return {wrappedService} * @method createService */ createService: function (service) { var statuses = this.get('statusesStyleMap'); var pendingStatus = ['PENDING', 'icon-cog', 'progress-info', true]; var status = statuses[service.status] || pendingStatus; return Em.Object.create({ id: service.id, displayName: service.displayName, progress: service.progress, status: App.format.taskStatus(status[0]), isRunning: service.isRunning, name: service.name, isVisible: true, startTime: date.startTime(service.startTime), duration: date.durationSummary(service.startTime, service.endTime), icon: status[1], barColor: status[2], isInProgress: status[3], barWidth: "width:" + service.progress + "%;", sourceRequestScheduleId: service.sourceRequestScheduleId, contextCommand: service.contextCommand }); }, /** * Update properties of existed service with new data * * @param {wrappedService} service * @param {object} newData * @returns {wrappedService} * @method updateService */ updateService: function (service, newData) { var statuses = this.get('statusesStyleMap'); var pendingStatus = ['PENDING', 'icon-cog', 'progress-info', true]; var status = statuses[newData.status] || pendingStatus; return service.setProperties({ progress: newData.progress, status: App.format.taskStatus(status[0]), isRunning: newData.isRunning, startTime: date.startTime(newData.startTime), duration: date.durationSummary(newData.startTime, newData.endTime), icon: status[1], barColor: status[2], isInProgress: status[3], barWidth: "width:" + newData.progress + "%;", sourceRequestScheduleId: newData.get('sourceRequestScheduleId'), contextCommand: newData.get('contextCommand') }); }, /** * remove old requests * as API returns 10, or 20 , or 30 ...etc latest request, the requests that absent in response should be removed * * @param {wrappedService[]} services * @param {number[]} currentServicesIds * @method removeOldServices */ removeOldServices: function (services, currentServicesIds) { for (var i = 0, l = services.length; i < l; i++) { if (!currentServicesIds.contains(services[i].id)) { services.splice(i, 1); i--; l--; } } }, /** * Wrap task as Ember-object * * @param {Object} _task * @return {wrappedTask} * @method createTask */ createTask: function (_task) { return Em.Object.create({ id: _task.Tasks.id, hostName: _task.Tasks.host_name, command: _task.Tasks.command.toLowerCase() === 'service_check' ? '' : _task.Tasks.command.toLowerCase(), commandDetail: App.format.commandDetail(_task.Tasks.command_detail, _task.Tasks.request_inputs), status: App.format.taskStatus(_task.Tasks.status), role: App.format.role(_task.Tasks.role, false), stderr: _task.Tasks.stderr, stdout: _task.Tasks.stdout, request_id: _task.Tasks.request_id, isVisible: true, startTime: date.startTime(_task.Tasks.start_time), duration: date.durationSummary(_task.Tasks.start_time, _task.Tasks.end_time), icon: function () { var statusIconMap = { 'pending': 'icon-cog', 'queued': 'icon-cog', 'in_progress': 'icon-cogs', 'completed': 'icon-ok', 'failed': 'icon-exclamation-sign', 'aborted': 'icon-minus', 'timedout': 'icon-time' }; return statusIconMap[this.get('status')] || 'icon-cog'; }.property('status') }); }, /** * Create hosts and tasks data structure for popup * Set data for hosts and tasks * * @method onHostUpdate */ onHostUpdate: function () { var self = this; if (this.get("inputData")) { var hostsMap = this._getHostsMap(); var existedHosts = self.get('hosts'); if (existedHosts && existedHosts.length && this.get('currentServiceId') === this.get('previousServiceId')) { this._processingExistingHostsWithSameService(hostsMap); } else { var hostsArr = this._hostMapProcessing(hostsMap); hostsArr = hostsArr.sortProperty('name'); hostsArr.setEach("serviceName", this.get("serviceName")); self.set("hosts", hostsArr); self.set('previousServiceId', this.get('currentServiceId')); } } var operation = this.get('servicesInfo').findProperty('name', this.get('serviceName')); this.set('operationInfo', !operation || operation && operation.get('progress') === 100 ? null : operation); }, /** * Generate hosts map for further processing inputData * * @returns {object} * @private * @method _getHostsMap */ _getHostsMap: function () { var hostsData; var hostsMap = {}; var inputData = this.get('inputData'); if (this.get('isBackgroundOperations') && this.get("currentServiceId")) { //hosts popup for Background Operations hostsData = inputData.findProperty("id", this.get("currentServiceId")); } else { if (this.get("serviceName")) { //hosts popup for Wizards hostsData = inputData.findProperty("name", this.get("serviceName")); } } if (hostsData) { if (hostsData.hostsMap) { //hosts data come from Background Operations as object map hostsMap = hostsData.hostsMap; } else { if (hostsData.hosts) { //hosts data come from Wizard as array hostsMap = hostsData.hosts.toMapByProperty('name'); } } } return hostsMap; }, /** * * @param {object} hostsMap * @returns {wrappedHost[]} * @private * @method _hostMapProcessing */ _hostMapProcessing: function (hostsMap) { var self = this; var hostsArr = []; for (var hostName in hostsMap) { if (!hostsMap.hasOwnProperty(hostName)) { continue; } var _host = hostsMap[hostName]; var tasks = _host.logTasks; var hostInfo = Em.Object.create({ name: hostName, publicName: _host.publicName, displayName: Em.computed.truncate('name', 43, 40), progress: 0, status: App.format.taskStatus("PENDING"), serviceName: _host.serviceName, isVisible: true, icon: "icon-cog", barColor: "progress-info", barWidth: "width:0%;" }); if (tasks.length) { tasks = tasks.sortProperty('Tasks.id'); var hostStatus = self.getStatus(tasks); var hostProgress = self.getProgress(tasks); hostInfo.setProperties({ status: App.format.taskStatus(hostStatus[0]), icon: hostStatus[1], barColor: hostStatus[2], isInProgress: hostStatus[3], progress: hostProgress, barWidth: "width:" + hostProgress + "%;" }); } hostInfo.set('logTasks', tasks); hostsArr.push(hostInfo); } return hostsArr; }, /** * * @param {object} hostsMap * @private * @method _processingExistingHostsWithSameService */ _processingExistingHostsWithSameService: function (hostsMap) { var self = this; var existedHosts = self.get('hosts'); var detailedProperties = this.get('detailedProperties'); var detailedPropertiesKeys = Em.keys(detailedProperties); existedHosts.forEach(function (host) { var newHostInfo = hostsMap[host.get('name')]; //update only hosts with changed tasks or currently opened tasks of host var hostShouldBeUpdated = !this.get('isBackgroundOperations') || newHostInfo.isModified || this.get('currentHostName') === host.get('name'); if (newHostInfo && hostShouldBeUpdated) { var hostStatus = self.getStatus(newHostInfo.logTasks); var hostProgress = self.getProgress(newHostInfo.logTasks); host.setProperties({ status: App.format.taskStatus(hostStatus[0]), icon: hostStatus[1], barColor: hostStatus[2], isInProgress: hostStatus[3], progress: hostProgress, barWidth: "width:" + hostProgress + "%;", logTasks: newHostInfo.logTasks }); var existTasks = host.get('tasks'); if (existTasks) { newHostInfo.logTasks.forEach(function (_task) { var existTask = existTasks.findProperty('id', _task.Tasks.id); if (existTask) { var status = _task.Tasks.status; detailedPropertiesKeys.forEach(function (key) { var name = detailedProperties[key]; var value = _task.Tasks[name]; if (!Em.isNone(value)) { existTask.set(key, value); } }, this); existTask.setProperties({ status: App.format.taskStatus(status), startTime: date.startTime(_task.Tasks.start_time), duration: date.durationSummary(_task.Tasks.start_time, _task.Tasks.end_time) }); existTask = self._handleRebalanceHDFS(_task, existTask); } else { existTasks.pushObject(this.createTask(_task)); } }, this); } } }, this); }, /** * Custom processing for "Rebalance HDFS"-task * * @param {object} task * @param {object} existTask * @returns {object} * @private * @method _handleRebalanceHDFS */ _handleRebalanceHDFS: function (task, existTask) { var barColorMap = this.get('barColorMap'); var isRebalanceHDFSTask = task.Tasks.command === 'CUSTOM_COMMAND' && task.Tasks.custom_command_name === 'REBALANCEHDFS'; existTask.set('isRebalanceHDFSTask', isRebalanceHDFSTask); if (isRebalanceHDFSTask) { var structuredOut = task.Tasks.structured_out || {}; var status = task.Tasks.status; existTask.setProperties({ dataMoved: structuredOut.dataMoved || '0', dataLeft: structuredOut.dataLeft || '0', dataBeingMoved: structuredOut.dataBeingMoved || '0', barColor: barColorMap[status], isInProgress: status === 'IN_PROGRESS', isNotComplete: ['QUEUED', 'IN_PROGRESS'].contains(status), completionProgressStyle: 'width:' + (structuredOut.completePercent || 0) * 100 + '%;', command: task.Tasks.command, custom_command_name: task.Tasks.custom_command_name }); } return existTask; }, /** * Show popup * * @return {App.ModalPopup} PopupObject For testing purposes */ createPopup: function () { var self = this; var isBackgroundOperations = this.get('isBackgroundOperations'); this.set('isPopup', App.ModalPopup.show({ /** * @type {boolean} */ isLogWrapHidden: true, /** * @type {boolean} */ isTaskListHidden: true, /** * @type {boolean} */ isHostListHidden: true, /** * @type {boolean} */ isServiceListHidden: false, /** * @type {boolean} */ isHideBodyScroll: true, /** * no need to track is it loaded when popup contain only list of hosts * @type {bool} */ isLoaded: !isBackgroundOperations, /** * is BG-popup opened * @type {bool} */ isOpen: false, /** * @type {object} */ detailedProperties: self.get('detailedProperties'), didInsertElement: function () { this._super(); this.set('isOpen', true); }, /** * @type {Em.View} */ headerClass: Em.View.extend({ controller: this, template: Em.Handlebars.compile('{{popupHeaderName}} ' + '{{#unless view.parentView.isHostListHidden}}{{#if controller.operationInfo.isAbortable}}' + '{{view controller.abortIcon servicesInfoBinding="controller.operationInfo"}}' + '{{/if}}{{/unless}}') }), /** * @type {String[]} */ classNames: ['sixty-percent-width-modal', 'host-progress-popup'], /** * for the checkbox: do not show this dialog again * * @type {bool} */ hasFooterCheckbox: true, /** * Auto-display BG-popup * * @type {bool} */ isNotShowBgChecked: null, /** * Save user pref about auto-display BG-popup * * @method updateNotShowBgChecked */ updateNotShowBgChecked: function () { var curVal = !this.get('isNotShowBgChecked'); if (!App.get('testMode')) { App.router.get('userSettingsController').postUserPref('show_bg', curVal); } }.observes('isNotShowBgChecked'), autoHeight: false, /** * @method closeModelPopup */ closeModelPopup: function () { this.set('isOpen', false); if (isBackgroundOperations) { $(this.get('element')).detach(); App.router.get('backgroundOperationsController').set('levelInfo.name', 'REQUESTS_LIST'); } else { this.hide(); self.set('isPopup', null); } }, onPrimary: function () { this.closeModelPopup(); }, onClose: function () { this.closeModelPopup(); }, secondary: null, bodyClass: App.HostProgressPopupBodyView.extend({ controller: self }) })); return this.get('isPopup'); } });