'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI * (C) Copyright IBM Corp. 2016, 2020 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['../../lib/@waca/core-client/js/core-client/ui/core/Events', '../../lib/@waca/core-client/js/core-client/nls/StringResources', '../../lib/@waca/core-client/js/core-client/ui/ProgressToast', '../../lib/@waca/core-client/js/core-client/utils/Deferred', 'underscore'], function (Events, StringResources, ProgressToast, Deferred, _) { 'use strict'; var CONTENT_TYPE = 'application/vnd.ibm.bi.platform.execution+json; charset=UTF-8'; var APPLICATION_JSON = 'application/json'; var DatasetExecutionService = Events.extend({ init: function init() { DatasetExecutionService.inherited('init', this, arguments); this._backgroundExecutions = {}; // We want to ping a lot at the start and then slow down if the dataset take a long time this._pingTimeouts = [300, 600, 1000, 1500, 2000, 4000, 6000, 10000]; this._defaultOptions = { showToastWhenDone: true }; }, /** Will execute a dataset in the background. If the dataset is already running, then it will be canceled before the new run is triggered. @param options.id {string} - store ID of the dataset to run @param options.name {stirng} - the name of the data set to execute. Will be shown in the toast @param options.showToastWhenDone {boolean} - default is true, set to false if you don't want a toast to be displayed when the dataset is done running @param options.glassContext {object} - the glassContext object @param options.isRefresh {boolean} - default false. Set to true if doing a refresh from the context menu **/ execute: function execute(options) { var _this = this; _.defaults(options, this._defaultOptions); this._initLogger(options.glassContext); // If we're already running the dataset then cancel it before we start another run if (this.isExecuting(options.id)) { this.cancel(options, false); } var timestamp = Date.now(); options.timestamp = timestamp; this._backgroundExecutions[options.id] = { 'status': 'executing', 'timestamp': timestamp, 'isRefresh': options.isRefresh }; if (options.showToastWhenDone) { this._createProgressToast(options); } var data = JSON.stringify({ 'options': { 'delivery': { 'save': { 'notify': false } } } }); return Promise.resolve().then(function () { return options.glassContext.services.ajax.post('v1/datasets/' + options.id + '/executions', { 'headers': { 'Content-Type': CONTENT_TYPE, 'Accept': APPLICATION_JSON }, 'datatype': 'json', 'data': data }).then(function (result, status, xhr) { this._backgroundExecutions[options.id].executionURL = xhr.getResponseHeader('location'); this._backgroundExecutions[options.id].eventID = result.eventID; this._pingForStatus(options); }.bind(_this), function (error) { _this._rejectDeferredObjects(options, 'failed'); throw new Error('Execution failed: ' + error.message); }); }); }, _initLogger: function _initLogger(glassContext) { var _this2 = this; if (!this.logger && glassContext) { glassContext.getSvc('.Logger').then(function (logger) { return _this2.logger = logger; }); } }, /** Will create the progress toast to show the user that the data set is being loaded **/ _createProgressToast: function _createProgressToast(options) { var progressToast = new ProgressToast(); progressToast.show(this._getToastMessage(options)); progressToast.indefinite(this._getToastMessage(options)); progressToast.onCancel(function (options) { this.cancel(options, true); }.bind(this, options)); progressToast.onHide(function (options) { this._hideProgressToast(options.id); }.bind(this, options)); this._backgroundExecutions[options.id].progressToast = progressToast; }, /** Hide the progress toast **/ _hideProgressToast: function _hideProgressToast(id) { var execution = this._backgroundExecutions[id]; if (execution && execution.progressToast) { execution.progressToast.remove(0); execution.progressToast = null; } }, _showErrorToast: function _showErrorToast(options, errorMessage) { var execution = this._backgroundExecutions[options.id]; if (options.showToastWhenDone) { if (execution && execution.progressToast) { // We don't want the error progress toast to get cleaned up, so null it out here // so our cleanup code doesn't know to hide it. Sneaky sneaky. var progressToast = execution.progressToast; execution.progressToast = null; progressToast.fail(this._getToastMessage(options, errorMessage)); progressToast.hideButton('cancel'); } else { options.glassContext.appController.showToast(this._getToastMessage(options), { 'type': 'error' }); } } }, _showCancelledRefreshToast: function _showCancelledRefreshToast(options) { options.glassContext.appController.showToast(this._getToastMessage(options), { 'type': 'info' }); }, /** Will query the server for the status of the execution every once in a while until the status of the execution is complete|failed. This method will query a lot at the begining and slow down the longer the dataset is executing. @param options.id {string} - store ID of the dataset to run @param options.type {string} - type of the object being executed @param options.showToastWhenDone {boolean} - default is true, set to false if you don't want a toast to be displayed when the dataset is done running @param options.glassContext {object} - the glassContext object **/ _pingForStatus: function _pingForStatus(options) { var execution = this._backgroundExecutions[options.id]; // If the timestamp in the options doesn't match the timestamp on the execution object then another execution for the same dataset // has started and the one we're currently pinging for has been cancelled. if (!execution || options.timestamp !== execution.timestamp || !execution.executionURL || execution.status === 'cancelled') { return; } else if (execution.status === 'failed') { this._rejectDeferredObjects(options, 'failed'); this._cleanupAfterExecutionFinished(options.id); return; } else if (!this.isExecuting(options.id)) { this._processExecutionStatus(execution.status, options); return; } options.glassContext.services.ajax.get(execution.executionURL, { 'headers': { 'Content-Type': CONTENT_TYPE, 'Accept': APPLICATION_JSON }, 'datatype': 'json' }).then(function (response) { execution.status = response.status; this._processExecutionStatus(execution.status, options); }.bind(this), function () { // The ping failed, not much we can do this._rejectDeferredObjects(options, 'statusPingFailed'); }); }, /** Deal with the status of the execution **/ _processExecutionStatus: function _processExecutionStatus(status, options) { var _this3 = this; var execution = this._backgroundExecutions[options.id]; switch (status) { case 'complete': case 'succeeded': // We could get a status of complete back for a cancelled execution. Double check here to make sure we didn't cancel anything if (execution.status === 'cancelled') { this._rejectDeferredObjects(options, 'cancelled'); } else { this._resolveDefferedObjects(options.id); if (options.showToastWhenDone) { options.glassContext.appController.showToast(this._getToastMessage(options)); } this.trigger('loadComplete', { id: options.id }); } this._cleanupAfterExecutionFinished(options.id); break; case 'cancelled': this._rejectDeferredObjects(options, 'cancelled'); this._cleanupAfterExecutionFinished(options.id); break; case 'failed': this._getErrorMessage(options).then(function (errorMessage) { _this3._rejectDeferredObjects(options, 'failed', errorMessage); _this3._cleanupAfterExecutionFinished(options.id); }); break; case 'executing': case 'pending': var pingTimeoutIndex = options.pingTimeoutIndex || 0; pingTimeoutIndex += 1; if (pingTimeoutIndex >= this._pingTimeouts.length) { pingTimeoutIndex = this._pingTimeouts.length - 1; } options.pingTimeoutIndex = pingTimeoutIndex; setTimeout(function () { this._pingForStatus(options); }.bind(this), this._pingTimeouts[pingTimeoutIndex]); break; default: this._cleanupAfterExecutionFinished(options.id); console.debug('Unknown status returned by ' + execution.executionURL + '. Status of: ' + status); } }, /** Will reject all deferred objects associated to a running dataset. This will let the callers know that something went wrong - request got cancelled or a fault happened. **/ _rejectDeferredObjects: function _rejectDeferredObjects(options, status, errorMessage) { var execution = this._backgroundExecutions[options.id]; if (!execution) { return; } execution.status = status; if (status === 'failed') { this._showErrorToast(options, errorMessage); } // Reject any deferred objects that were waiting until the request was complete if (execution.deferredObjects) { execution.deferredObjects.forEach(function (deferred) { deferred.reject({ 'status': status }); }); } }, _logError: function _logError() { if (this.logger) { var _logger; (_logger = this.logger).error.apply(_logger, arguments); } }, _getErrorMessage: function _getErrorMessage(options) { var _this4 = this; return this._getItemLastHistoryEntry(options).catch(function (error) { return _this4._logError('Error trying to read history of asset ' + options.id, error); }).then(function (historyEntry) { return historyEntry && _this4._getHistoryEntryDetailMessage(options, historyEntry); }).catch(function (error) { return _this4._logError('Error trying to read history details of asset ' + options.id, error); }); }, _getItemLastHistoryEntry: function _getItemLastHistoryEntry(options) { return options.glassContext.services.ajax.get('v1/objects/' + options.id + '/items?types=history').then(function (result) { return result && result.data && _.max(result.data, function (_ref) { var modificationTime = _ref.modificationTime; return Date.parse(modificationTime); }); }); }, _getHistoryEntryDetailMessage: function _getHistoryEntryDetailMessage(options, historyEntry) { var historyDetailsUrl = historyEntry._meta && historyEntry._meta.links && historyEntry._meta.links.details && historyEntry._meta.links.details.url; return historyDetailsUrl && options.glassContext.services.ajax.get(historyDetailsUrl).then(function (result) { //Get first (only?) message, assume it contains the error message of interest var message = result && result.data && result.data.messages && result.data.messages.length && result.data.messages[0]; return message && message.detail; }); }, /** Will resolve all deferred objects associated to a running dataset. **/ _resolveDefferedObjects: function _resolveDefferedObjects(id) { var execution = this._backgroundExecutions[id]; // Reject any deferred objects that were waiting until the request was complete if (execution.deferredObjects) { execution.deferredObjects.forEach(function (deferred) { deferred.resolve(); }); } }, _cleanupAfterExecutionFinished: function _cleanupAfterExecutionFinished(id) { this._hideProgressToast(id); this._backgroundExecutions[id] = { 'status': this._backgroundExecutions[id].status }; }, /** Cancels a currently running dataset. There's no reason to wait for the cancel to finish, so this method will simply return right after it sends the request. @param options.id {string} - store ID of the dataset to run @param options.glassContext {object} - the glassContext object @param showCancelToast {boolean} - default true, should we show a toast after the cancel **/ cancel: function cancel(options, showCancelToast) { this._hideProgressToast(options.id); var execution = this._backgroundExecutions[options.id]; if (!execution || !this.isExecuting(options.id)) { return; } execution.status = 'cancelled'; if (showCancelToast !== false) { this._showCancelledRefreshToast(options); } // Reject all deferred object waiting for the completion since we're being asked to cancel this._rejectDeferredObjects(options, 'cancelled'); if (execution.executionURL) { options.glassContext.services.ajax['delete'](execution.executionURL, { 'headers': { 'Content-Type': CONTENT_TYPE, 'Accept': APPLICATION_JSON }, 'datatype': 'json' }); } }, /** * @param id {string} - store ID of the dataset * * @returns {promise} * - will be resolved when the dataset specified by options.id is complete. * - will be rejected if the dataset execution fails or gets canceled. The reason returns will be an object * reason.status {string} : the status of request (fault | cancelled) * reason.msg {string}: In the case of a fault, the error message **/ whenComplete: function whenComplete(id) { var deferred = new Deferred(); var status = this.getStatus(id); var execution = this._backgroundExecutions[id]; // If the dataset is already done then resolve the promise right away if (!execution || status === 'complete') { deferred.resolve(); } else if (status === 'failed' || status === 'cancelled') { // The dataset already failed, reject the promise deferred.reject({ 'status': status }); } else { if (!execution.deferredObjects) { execution.deferredObjects = []; } execution.deferredObjects.push(deferred); } return deferred.promise; }, /** Will return the last known status for the id provided @param id {string} - store ID of the dataset @return {string} - complete | pending | executing | failed | null (if id isn't found) **/ getStatus: function getStatus(id) { return this._backgroundExecutions[id] ? this._backgroundExecutions[id].status : null; }, /** The UI doesn't distinguish between pending and executing, so treat both of them as 'executing' **/ isExecuting: function isExecuting(id) { var status = this.getStatus(id); return status === 'pending' || status === 'executing'; }, _getToastMessage: function _getToastMessage(options, errorMessage) { var execution = this._backgroundExecutions[options.id]; if (!execution) { return ''; } var stringId = ''; if (execution.isRefresh) { switch (execution.status) { case 'executing': case 'pending': stringId = 'datasetRefreshing'; break; case 'complete': case 'succeeded': stringId = 'datasetFinishedRefreshing'; break; case 'failed': stringId = 'datasetRefreshFailed'; break; case 'cancelled': stringId = 'datasetRefreshCancelled'; break; } } else { switch (execution.status) { case 'executing': case 'pending': stringId = 'datasetLoading'; break; case 'complete': case 'succeeded': stringId = 'datasetFinishedLoading'; break; case 'failed': stringId = 'datasetLoadingFailed'; break; case 'cancelled': stringId = 'datasetLoadingCancelled'; break; } } return [StringResources.get(stringId, { 'name': options.name }), errorMessage].filter(function (string) { return !!string; }).join('\n'); } }); return DatasetExecutionService; }); //# sourceMappingURL=DatasetExecutionService.js.map