From 749f4e9be16e63b27c35052c0535546ad9853341 Mon Sep 17 00:00:00 2001 From: Matt Gilman Date: Fri, 13 Nov 2015 16:19:05 -0500 Subject: [PATCH] NIFI-655: - Adding automatic client side token renewal. --- .../webapp/WEB-INF/pages/bulletin-board.jsp | 1 + .../src/main/webapp/WEB-INF/pages/canvas.jsp | 1 + .../src/main/webapp/WEB-INF/pages/cluster.jsp | 1 + .../main/webapp/WEB-INF/pages/counters.jsp | 1 + .../src/main/webapp/WEB-INF/pages/history.jsp | 1 + .../main/webapp/WEB-INF/pages/provenance.jsp | 1 + .../src/main/webapp/WEB-INF/pages/summary.jsp | 1 + .../main/webapp/WEB-INF/pages/templates.jsp | 1 + .../src/main/webapp/WEB-INF/pages/users.jsp | 1 + .../src/main/webapp/js/nf/login/nf-login.js | 45 +- .../src/main/webapp/js/nf/nf-common.js | 1941 +++++++++-------- .../src/main/webapp/js/nf/nf-storage.js | 100 +- 12 files changed, 1117 insertions(+), 978 deletions(-) diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp index 6df0e51ae0..3eaa2258e4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/bulletin-board.jsp @@ -28,6 +28,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp index dcfb47bde7..55c2b86241 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/canvas.jsp @@ -40,6 +40,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp index 55ffabe3a4..acf2e55629 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp @@ -30,6 +30,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp index 7362cf2272..e3113f9534 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/counters.jsp @@ -30,6 +30,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp index 935512a3f7..ac28bb7662 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/history.jsp @@ -30,6 +30,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp index e02a7cd3c7..496bfd1775 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/provenance.jsp @@ -31,6 +31,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp index e6f330582d..45dff93380 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/summary.jsp @@ -38,6 +38,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp index 126c3885a3..452064d6cd 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/templates.jsp @@ -30,6 +30,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp index a5f422c21d..b3e09681da 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/users.jsp @@ -31,6 +31,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js index f844d91d30..9e1dab60a3 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js @@ -98,7 +98,7 @@ nf.Login = (function () { } }).done(function (jwt) { // store the jwt and reload the page - nf.Storage.setItem('jwt', jwt); + nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt)); // check to see if they actually have access now $.ajax({ @@ -109,15 +109,18 @@ nf.Login = (function () { if (response.identity === 'anonymous') { showLogoutLink(); + // schedule automatic token refresh + nf.Common.scheduleTokenRefresh(); + // show the user - var user = getJwtSubject(jwt); + var user = nf.Common.getJwtSubject(jwt); $('#nifi-user-submit-justification').text(user); // show the registration form initializeNiFiRegistration(); showNiFiRegistration(); } else { - // reload as appropriate + // reload as appropriate - no need to schedule token refresh as the page is reloading if (top !== window) { parent.window.location = '/nifi'; } else { @@ -127,8 +130,11 @@ nf.Login = (function () { }).fail(function (xhr, status, error) { showLogoutLink(); + // schedule automatic token refresh + nf.Common.scheduleTokenRefresh(); + // show the user - var user = getJwtSubject(jwt); + var user = nf.Common.getJwtSubject(jwt); $('#nifi-user-submit-justification').text(user); if (xhr.status === 401) { @@ -189,33 +195,6 @@ nf.Login = (function () { }); }; - /** - * Extracts the subject from the specified jwt. If the jwt is not as expected - * an empty string is returned. - * - * @param {string} jwt - * @returns {string} - */ - var getJwtSubject = function (jwt) { - if (nf.Common.isDefinedAndNotNull(jwt)) { - var segments = jwt.split(/\./); - if (segments.length !== 3) { - return ''; - } - - var rawPayload = $.base64.atob(segments[1]); - var payload = JSON.parse(rawPayload); - - if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) { - return payload['preferred_username']; - } else { - ''; - } - } - - return ''; - }; - var logout = function () { nf.Storage.removeItem('jwt'); }; @@ -272,7 +251,7 @@ nf.Login = (function () { $('#login-message').text('Your account is active and you are already logged in.'); }).fail(function (xhr, status, error) { if (xhr.status === 401) { - var user = getJwtSubject(jwt); + var user = nf.Common.getJwtSubject(jwt); // show the user $('#nifi-user-submit-justification').text(user); @@ -316,7 +295,7 @@ nf.Login = (function () { if (xhr.status === 401) { // attempt to get a token for the current user without passing login credentials token.done(function (jwt) { - var user = getJwtSubject(jwt); + var user = nf.Common.getJwtSubject(jwt); // show the user $('#nifi-user-submit-justification').text(user); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js index 1c828c8e90..a2ff680729 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js @@ -74,952 +74,1055 @@ $(document).ready(function () { nf.Storage.removeItem('jwt'); window.location = '/nifi/login'; }); + + // schedule token refresh when a token is present + if (nf.Storage.getItem('jwt') !== null) { + nf.Common.scheduleTokenRefresh(); + } }); // Define a common utility class used across the entire application. -nf.Common = { - config: { - sensitiveText: 'Sensitive value set', - tooltipConfig: { - style: { - classes: 'nifi-tooltip' - }, - show: { - solo: true, - effect: false - }, - hide: { - effect: false - }, - position: { - at: 'top right', - my: 'bottom left' - } - } - }, +nf.Common = (function () { + // interval for cancelling token refresh when necessary + var tokenRefreshInterval = null; - /** - * Determines if the current broswer supports SVG. - */ - SUPPORTS_SVG: !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect, - - /** - * The authorities for the current user. - */ - authorities: undefined, - - /** - * Sets the authorities for the current user. - * - * @argument {array} roles The current users authorities - */ - setAuthorities: function (roles) { - nf.Common.authorities = roles; - }, - - /** - * Loads a script at the specified URL. Supports caching the script on the browser. - * - * @param {string} url - */ - cachedScript: function (url) { - return $.ajax({ - dataType: 'script', - cache: true, - url: url - }); - }, - - /** - * Determines whether the current user can access provenance. - * - * @returns {boolean} - */ - canAccessProvenance: function () { - var canAccessProvenance = false; - if (nf.Common.isDefinedAndNotNull(nf.Common.authorities)) { - $.each(nf.Common.authorities, function (i, authority) { - if (authority === 'ROLE_PROVENANCE') { - canAccessProvenance = true; - return false; + return { + config: { + sensitiveText: 'Sensitive value set', + tooltipConfig: { + style: { + classes: 'nifi-tooltip' + }, + show: { + solo: true, + effect: false + }, + hide: { + effect: false + }, + position: { + at: 'top right', + my: 'bottom left' } - }); - } - return canAccessProvenance; - }, - - /** - * Returns whether or not the current user is a DFM. - */ - isDFM: function () { - var dfm = false; - if (nf.Common.isDefinedAndNotNull(nf.Common.authorities)) { - $.each(nf.Common.authorities, function (i, authority) { - if (authority === 'ROLE_DFM') { - dfm = true; - return false; - } - }); - } - return dfm; - }, - - /** - * Returns whether or not the current user is a DFM. - */ - isAdmin: function () { - var admin = false; - if (nf.Common.isDefinedAndNotNull(nf.Common.authorities)) { - $.each(nf.Common.authorities, function (i, authority) { - if (authority === 'ROLE_ADMIN') { - admin = true; - return false; - } - }); - } - return admin; - }, - - /** - * Adds a mouse over effect for the specified selector using - * the specified styles. - * - * @argument {string} selector The selector for the element to add a hover effect for - * @argument {string} normalStyle The css style for the normal state - * @argument {string} overStyle The css style for the over state - */ - addHoverEffect: function (selector, normalStyle, overStyle) { - $(document).on('mouseenter', selector, function () { - $(this).removeClass(normalStyle).addClass(overStyle); - }).on('mouseleave', selector, function () { - $(this).removeClass(overStyle).addClass(normalStyle); - }); - return $(selector).addClass(normalStyle); - }, - - /** - * Method for handling ajax errors. - * - * @argument {object} xhr The XmlHttpRequest - * @argument {string} status The status of the request - * @argument {string} error The error - */ - handleAjaxError: function (xhr, status, error) { - // show the account registration page if necessary -// if (xhr.status === 401 && $('#registration-pane').length) { -// // show the registration pane -// $('#registration-pane').show(); -// -// // close the canvas -// nf.Common.closeCanvas(); -// return; -// } - - // if an error occurs while the splash screen is visible close the canvas show the error message - if ($('#splash').is(':visible')) { - if (xhr.status === 401) { - $('#message-title').text('Unauthorized'); - } else if (xhr.status === 403) { - $('#message-title').text('Access Denied'); - } else { - $('#message-title').text('An unexpected error has occurred'); + }, + urls: { + token: '../nifi-api/token' } - - if ($.trim(xhr.responseText) === '') { - $('#message-content').text('Please check the logs.'); - } else { - $('#message-content').text(xhr.responseText); - } - - // show the error pane - $('#message-pane').show(); + }, - // close the canvas - nf.Common.closeCanvas(); - return; - } - - // status code 400, 404, and 409 are expected response codes for common errors. - if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) { - nf.Dialog.showOkDialog({ - dialogContent: nf.Common.escapeHtml(xhr.responseText), - overlayBackground: false + /** + * Determines if the current broswer supports SVG. + */ + SUPPORTS_SVG: !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect, + + /** + * The authorities for the current user. + */ + authorities: undefined, + + /** + * Sets the authorities for the current user. + * + * @argument {array} roles The current users authorities + */ + setAuthorities: function (roles) { + nf.Common.authorities = roles; + }, + + /** + * Loads a script at the specified URL. Supports caching the script on the browser. + * + * @param {string} url + */ + cachedScript: function (url) { + return $.ajax({ + dataType: 'script', + cache: true, + url: url }); - } else { - if (xhr.status < 99 || xhr.status === 12007 || xhr.status === 12029) { - var content = 'Please ensure the application is running and check the logs for any errors.'; - if (nf.Common.isDefinedAndNotNull(status)) { - if (status === 'timeout') { - content = 'Request has timed out. Please ensure the application is running and check the logs for any errors.'; - } else if (status === 'abort') { - content = 'Request has been aborted.'; - } else if (status === 'No Transport') { - content = 'Request transport mechanism failed. Please ensure the host where the application is running is accessible.'; + }, + + /** + * Automatically refresh tokens by checking once an hour if its going to expire soon. + */ + scheduleTokenRefresh: function () { + // if we are currently polling for token refresh, cancel it + if (tokenRefreshInterval !== null) { + clearInterval(tokenRefreshInterval); + } + + // set the interval to one hour + var interval = nf.Common.MILLIS_PER_HOUR; + + var checkExpiration = function () { + var expiration = nf.Storage.getItemExpiration('jwt'); + + // ensure there is an expiration and token present + if (expiration !== null) { + var expirationDate = new Date(expiration); + var now = new Date(); + + // get the time remainging plus a little bonus time to reload the token + var timeRemaining = expirationDate.valueOf() - now.valueOf() - nf.Common.MILLIS_PER_MINUTE; + if (timeRemaining < interval) { + // if the token will expire before the next interval minus some bonus time, refresh now + $.ajax({ + type: 'GET', + url: nf.Common.config.urls.token + }).done(function (jwt) { + nf.Storage.setItem('jwt', jwt, nf.Common.getJwtExpiration(jwt)); + }); } } - $('#message-title').text('Unable to communicate with NiFi'); - $('#message-content').text(content); - } else if (xhr.status === 401) { - $('#message-title').text('Unauthorized'); - if ($.trim(xhr.responseText) === '') { - $('#message-content').text('Authorization is required to use this NiFi.'); - } else { - $('#message-content').text(xhr.responseText); + }; + + // perform initial check + checkExpiration(); + + // schedule subsequent checks + tokenRefreshInterval = setInterval(checkExpiration, interval); + }, + + /** + * Extracts the subject from the specified jwt. If the jwt is not as expected + * an empty string is returned. + * + * @param {string} jwt + * @returns {string} + */ + getJwtSubject: function (jwt) { + if (nf.Common.isDefinedAndNotNull(jwt)) { + var segments = jwt.split(/\./); + if (segments.length !== 3) { + return ''; } - } else if (xhr.status === 403) { - $('#message-title').text('Access Denied'); - if ($.trim(xhr.responseText) === '') { - $('#message-content').text('Unable to authorize you to use this NiFi.'); + + var rawPayload = $.base64.atob(segments[1]); + var payload = JSON.parse(rawPayload); + + if (nf.Common.isDefinedAndNotNull(payload['preferred_username'])) { + return payload['preferred_username']; } else { - $('#message-content').text(xhr.responseText); + ''; } - } else if (xhr.status === 500) { - $('#message-title').text('An unexpected error has occurred'); - if ($.trim(xhr.responseText) === '') { - $('#message-content').text('An error occurred communicating with the application core. Please check the logs and fix any configuration issues before restarting.'); - } else { - $('#message-content').text(xhr.responseText); - } - } else if (xhr.status === 200 || xhr.status === 201) { - $('#message-title').text('Parse Error'); - if ($.trim(xhr.responseText) === '') { - $('#message-content').text('Unable to interpret response from NiFi.'); - } else { - $('#message-content').text(xhr.responseText); - } - } else { - $('#message-title').text(xhr.status + ': Unexpected Response'); - $('#message-content').text('An unexpected error has occurred. Please check the logs.'); } - // show the error pane - $('#message-pane').show(); + return ''; + }, - // close the canvas - nf.Common.closeCanvas(); - } - }, - - /** - * Closes the canvas by removing the splash screen and stats poller. - */ - closeCanvas: function () { - // ensure this javascript has been loaded in the nf canvas page - if (nf.Common.isDefinedAndNotNull(nf.Canvas)) { - // hide the splash screen if required + /** + * Extracts the expiration from the specified jwt. If the jwt is not as expected + * a null value is returned. + * + * @param {string} jwt + * @returns {integer} + */ + getJwtExpiration: function (jwt) { + if (nf.Common.isDefinedAndNotNull(jwt)) { + var segments = jwt.split(/\./); + if (segments.length !== 3) { + return null; + } + + var rawPayload = $.base64.atob(segments[1]); + var payload = JSON.parse(rawPayload); + + if (nf.Common.isDefinedAndNotNull(payload['exp'])) { + try { + // jwt exp is in seconds + return parseInt(payload['exp'], 10) * nf.Common.MILLIS_PER_SECOND; + } catch (e) { + return null; + } + } else { + return null; + } + } + + return null; + }, + + /** + * Determines whether the current user can access provenance. + * + * @returns {boolean} + */ + canAccessProvenance: function () { + var canAccessProvenance = false; + if (nf.Common.isDefinedAndNotNull(nf.Common.authorities)) { + $.each(nf.Common.authorities, function (i, authority) { + if (authority === 'ROLE_PROVENANCE') { + canAccessProvenance = true; + return false; + } + }); + } + return canAccessProvenance; + }, + + /** + * Returns whether or not the current user is a DFM. + */ + isDFM: function () { + var dfm = false; + if (nf.Common.isDefinedAndNotNull(nf.Common.authorities)) { + $.each(nf.Common.authorities, function (i, authority) { + if (authority === 'ROLE_DFM') { + dfm = true; + return false; + } + }); + } + return dfm; + }, + + /** + * Returns whether or not the current user is a DFM. + */ + isAdmin: function () { + var admin = false; + if (nf.Common.isDefinedAndNotNull(nf.Common.authorities)) { + $.each(nf.Common.authorities, function (i, authority) { + if (authority === 'ROLE_ADMIN') { + admin = true; + return false; + } + }); + } + return admin; + }, + + /** + * Adds a mouse over effect for the specified selector using + * the specified styles. + * + * @argument {string} selector The selector for the element to add a hover effect for + * @argument {string} normalStyle The css style for the normal state + * @argument {string} overStyle The css style for the over state + */ + addHoverEffect: function (selector, normalStyle, overStyle) { + $(document).on('mouseenter', selector, function () { + $(this).removeClass(normalStyle).addClass(overStyle); + }).on('mouseleave', selector, function () { + $(this).removeClass(overStyle).addClass(normalStyle); + }); + return $(selector).addClass(normalStyle); + }, + + /** + * Method for handling ajax errors. + * + * @argument {object} xhr The XmlHttpRequest + * @argument {string} status The status of the request + * @argument {string} error The error + */ + handleAjaxError: function (xhr, status, error) { + // if an error occurs while the splash screen is visible close the canvas show the error message if ($('#splash').is(':visible')) { - nf.Canvas.hideSplash(); - } - - // hide the context menu - nf.ContextMenu.hide(); - - // shut off the auto refresh - nf.Canvas.stopRevisionPolling(); - nf.Canvas.stopStatusPolling(); - } - }, - - /** - * Populates the specified field with the specified value. If the value is - * undefined, the field will read 'No value set.' If the value is an empty - * string, the field will read 'Empty string set.' - * - * @argument {string} target The dom Id of the target - * @argument {string} value The value - */ - populateField: function (target, value) { - if (nf.Common.isUndefined(value) || nf.Common.isNull(value)) { - return $('#' + target).addClass('unset').text('No value set'); - } else if (value === '') { - return $('#' + target).addClass('blank').text('Empty string set'); - } else { - return $('#' + target).text(value); - } - }, - - /** - * Clears the specified field. Removes any style that may have been applied - * by a preceeding call to populateField. - * - * @argument {string} target The dom Id of the target - */ - clearField: function (target) { - return $('#' + target).removeClass('unset blank').text(''); - }, - - /** - * Cleans up any tooltips that have been created for the specified container. - * - * @param {jQuery} container - * @param {string} tooltipTarget - */ - cleanUpTooltips: function(container, tooltipTarget) { - container.find(tooltipTarget).each(function () { - var tip = $(this); - if (tip.data('qtip')) { - var api = tip.qtip('api'); - api.destroy(true); - } - }); - }, - - /** - * Removes all read only property detail dialogs. - */ - removeAllPropertyDetailDialogs: function () { - var propertyDetails = $('body').children('div.property-detail'); - propertyDetails.find('div.nfel-editor').nfeditor('destroy'); - propertyDetails.hide().remove(); - }, - - /** - * Formats the tooltip for the specified property. - * - * @param {object} propertyDescriptor The property descriptor - * @param {object} propertyHistory The property history - * @returns {string} - */ - formatPropertyTooltip: function (propertyDescriptor, propertyHistory) { - var tipContent = []; - - // show the property description if applicable - if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { - if (!nf.Common.isBlank(propertyDescriptor.description)) { - tipContent.push(nf.Common.escapeHtml(propertyDescriptor.description)); - } - if (!nf.Common.isBlank(propertyDescriptor.defaultValue)) { - tipContent.push('Default value: ' + nf.Common.escapeHtml(propertyDescriptor.defaultValue)); - } - if (!nf.Common.isBlank(propertyDescriptor.supportsEl)) { - tipContent.push('Supports expression language: ' + nf.Common.escapeHtml(propertyDescriptor.supportsEl)); - } - } - - if (nf.Common.isDefinedAndNotNull(propertyHistory)) { - if (!nf.Common.isEmpty(propertyHistory.previousValues)) { - var history = []; - $.each(propertyHistory.previousValues, function (_, previousValue) { - history.push('
  • ' + nf.Common.escapeHtml(previousValue.previousValue) + ' - ' + nf.Common.escapeHtml(previousValue.timestamp) + ' (' + nf.Common.escapeHtml(previousValue.userName) + ')
  • '); - }); - tipContent.push('History:'); - } - } - - if (tipContent.length > 0) { - return tipContent.join('

    '); - } else { - return null; - } - }, - - /** - * Formats the specified property (name and value) accordingly. - * - * @argument {string} name The name of the property - * @argument {string} value The value of the property - */ - formatProperty: function (name, value) { - return '
    ' + nf.Common.formatValue(name) + ': ' + nf.Common.formatValue(value) + '
    '; - }, - - /** - * Formats the specified value accordingly. - * - * @argument {string} value The value of the property - */ - formatValue: function (value) { - if (nf.Common.isDefinedAndNotNull(value)) { - if (value === '') { - return 'Empty string set'; - } else { - return nf.Common.escapeHtml(value); - } - } else { - return 'No value set'; - } - }, - - /** - * HTML escapes the specified string. If the string is null - * or undefined, an empty string is returned. - * - * @returns {string} - */ - escapeHtml: (function () { - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' - }; - - return function (string) { - if (nf.Common.isDefinedAndNotNull(string)) { - return String(string).replace(/[&<>"'\/]/g, function (s) { - return entityMap[s]; - }); - } else { - return ''; - } - }; - }()), - - /** - * Determines if the specified property is sensitive. - * - * @argument {object} propertyDescriptor The property descriptor - */ - isSensitiveProperty: function (propertyDescriptor) { - if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { - return propertyDescriptor.sensitive === true; - } else { - return false; - } - }, - - /** - * Determines if the specified property is required. - * - * @param {object} propertyDescriptor The property descriptor - */ - isRequiredProperty: function (propertyDescriptor) { - if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { - return propertyDescriptor.required === true; - } else { - return false; - } - }, - - /** - * Determines if the specified property is required. - * - * @param {object} propertyDescriptor The property descriptor - */ - isDynamicProperty: function (propertyDescriptor) { - if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { - return propertyDescriptor.dynamic === true; - } else { - return false; - } - }, - - /** - * Gets the allowable values for the specified property. - * - * @argument {object} propertyDescriptor The property descriptor - */ - getAllowableValues: function (propertyDescriptor) { - if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { - return propertyDescriptor.allowableValues; - } else { - return null; - } - }, - - /** - * Returns whether the specified property supports EL. - * - * @param {object} propertyDescriptor The property descriptor - */ - supportsEl: function (propertyDescriptor) { - if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { - return propertyDescriptor.supportsEl === true; - } else { - return false; - } - }, - - /** - * Creates a form inline in order to submit the specified params to the specified URL - * using the specified method. - * - * @param {string} url The URL - * @param {object} params An object with the params to include in the submission - */ - post: function (url, params) { - // temporarily override beforeunload - var previousBeforeUnload = window.onbeforeunload; - window.onbeforeunload = null; - - // create a form for submission - var form = $('
    ').attr({ - 'method': 'POST', - 'action': url, - 'style': 'display: none;' - }); - - // add each parameter when specified - if (nf.Common.isDefinedAndNotNull(params)) { - $.each(params, function (name, value) { - $('').attr('name', name).val(value).appendTo(form); - }); - } - - // submit the form and clean up - form.appendTo('body').submit().remove(); - - // restore previous beforeunload if necessary - if (previousBeforeUnload !== null) { - window.onbeforeunload = previousBeforeUnload; - } - }, - - /** - * Formats the specified array as an unordered list. If the array is not an - * array, null is returned. - * - * @argument {array} array The array to convert into an unordered list - */ - formatUnorderedList: function (array) { - if ($.isArray(array)) { - var ul = $(''); - $.each(array, function (_, item) { - var li = $('
  • ').appendTo(ul); - if (item instanceof jQuery) { - li.append(item); + if (xhr.status === 401) { + $('#message-title').text('Unauthorized'); + } else if (xhr.status === 403) { + $('#message-title').text('Access Denied'); } else { - li.text(item); + $('#message-title').text('An unexpected error has occurred'); + } + + if ($.trim(xhr.responseText) === '') { + $('#message-content').text('Please check the logs.'); + } else { + $('#message-content').text(xhr.responseText); + } + + // show the error pane + $('#message-pane').show(); + + // close the canvas + nf.Common.closeCanvas(); + return; + } + + // status code 400, 404, and 409 are expected response codes for common errors. + if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) { + nf.Dialog.showOkDialog({ + dialogContent: nf.Common.escapeHtml(xhr.responseText), + overlayBackground: false + }); + } else { + if (xhr.status < 99 || xhr.status === 12007 || xhr.status === 12029) { + var content = 'Please ensure the application is running and check the logs for any errors.'; + if (nf.Common.isDefinedAndNotNull(status)) { + if (status === 'timeout') { + content = 'Request has timed out. Please ensure the application is running and check the logs for any errors.'; + } else if (status === 'abort') { + content = 'Request has been aborted.'; + } else if (status === 'No Transport') { + content = 'Request transport mechanism failed. Please ensure the host where the application is running is accessible.'; + } + } + $('#message-title').text('Unable to communicate with NiFi'); + $('#message-content').text(content); + } else if (xhr.status === 401) { + $('#message-title').text('Unauthorized'); + if ($.trim(xhr.responseText) === '') { + $('#message-content').text('Authorization is required to use this NiFi.'); + } else { + $('#message-content').text(xhr.responseText); + } + } else if (xhr.status === 403) { + $('#message-title').text('Access Denied'); + if ($.trim(xhr.responseText) === '') { + $('#message-content').text('Unable to authorize you to use this NiFi.'); + } else { + $('#message-content').text(xhr.responseText); + } + } else if (xhr.status === 500) { + $('#message-title').text('An unexpected error has occurred'); + if ($.trim(xhr.responseText) === '') { + $('#message-content').text('An error occurred communicating with the application core. Please check the logs and fix any configuration issues before restarting.'); + } else { + $('#message-content').text(xhr.responseText); + } + } else if (xhr.status === 200 || xhr.status === 201) { + $('#message-title').text('Parse Error'); + if ($.trim(xhr.responseText) === '') { + $('#message-content').text('Unable to interpret response from NiFi.'); + } else { + $('#message-content').text(xhr.responseText); + } + } else { + $('#message-title').text(xhr.status + ': Unexpected Response'); + $('#message-content').text('An unexpected error has occurred. Please check the logs.'); + } + + // show the error pane + $('#message-pane').show(); + + // close the canvas + nf.Common.closeCanvas(); + } + }, + + /** + * Closes the canvas by removing the splash screen and stats poller. + */ + closeCanvas: function () { + // ensure this javascript has been loaded in the nf canvas page + if (nf.Common.isDefinedAndNotNull(nf.Canvas)) { + // hide the splash screen if required + if ($('#splash').is(':visible')) { + nf.Canvas.hideSplash(); + } + + // hide the context menu + nf.ContextMenu.hide(); + + // shut off the auto refresh + nf.Canvas.stopRevisionPolling(); + nf.Canvas.stopStatusPolling(); + } + }, + + /** + * Populates the specified field with the specified value. If the value is + * undefined, the field will read 'No value set.' If the value is an empty + * string, the field will read 'Empty string set.' + * + * @argument {string} target The dom Id of the target + * @argument {string} value The value + */ + populateField: function (target, value) { + if (nf.Common.isUndefined(value) || nf.Common.isNull(value)) { + return $('#' + target).addClass('unset').text('No value set'); + } else if (value === '') { + return $('#' + target).addClass('blank').text('Empty string set'); + } else { + return $('#' + target).text(value); + } + }, + + /** + * Clears the specified field. Removes any style that may have been applied + * by a preceeding call to populateField. + * + * @argument {string} target The dom Id of the target + */ + clearField: function (target) { + return $('#' + target).removeClass('unset blank').text(''); + }, + + /** + * Cleans up any tooltips that have been created for the specified container. + * + * @param {jQuery} container + * @param {string} tooltipTarget + */ + cleanUpTooltips: function(container, tooltipTarget) { + container.find(tooltipTarget).each(function () { + var tip = $(this); + if (tip.data('qtip')) { + var api = tip.qtip('api'); + api.destroy(true); } }); - return ul; - } else { - return null; - } - }, - - /** - * Extracts the contents of the specified str after the strToFind. If the - * strToFind is not found or the last part of the str, an empty string is - * returned. - * - * @argument {string} str The full string - * @argument {string} strToFind The substring to find - */ - substringAfterLast: function (str, strToFind) { - var result = ''; - var indexOfStrToFind = str.lastIndexOf(strToFind); - if (indexOfStrToFind >= 0) { - var indexAfterStrToFind = indexOfStrToFind + strToFind.length; - if (indexAfterStrToFind < str.length) { - result = str.substr(indexAfterStrToFind); + }, + + /** + * Removes all read only property detail dialogs. + */ + removeAllPropertyDetailDialogs: function () { + var propertyDetails = $('body').children('div.property-detail'); + propertyDetails.find('div.nfel-editor').nfeditor('destroy'); + propertyDetails.hide().remove(); + }, + + /** + * Formats the tooltip for the specified property. + * + * @param {object} propertyDescriptor The property descriptor + * @param {object} propertyHistory The property history + * @returns {string} + */ + formatPropertyTooltip: function (propertyDescriptor, propertyHistory) { + var tipContent = []; + + // show the property description if applicable + if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { + if (!nf.Common.isBlank(propertyDescriptor.description)) { + tipContent.push(nf.Common.escapeHtml(propertyDescriptor.description)); + } + if (!nf.Common.isBlank(propertyDescriptor.defaultValue)) { + tipContent.push('Default value: ' + nf.Common.escapeHtml(propertyDescriptor.defaultValue)); + } + if (!nf.Common.isBlank(propertyDescriptor.supportsEl)) { + tipContent.push('Supports expression language: ' + nf.Common.escapeHtml(propertyDescriptor.supportsEl)); + } } - } - return result; - }, - - /** - * Updates the mouse pointer. - * - * @argument {string} domId The id of the element for the new cursor style - * @argument {boolean} isMouseOver Whether or not the mouse is over the element - */ - setCursor: function (domId, isMouseOver) { - if (isMouseOver) { - $('#' + domId).addClass('pointer'); - } else { - $('#' + domId).removeClass('pointer'); - } - }, - - /** - * Constants for time duration formatting. - */ - MILLIS_PER_DAY: 86400000, - MILLIS_PER_HOUR: 3600000, - MILLIS_PER_MINUTE: 60000, - MILLIS_PER_SECOND: 1000, - - /** - * Formats the specified duration. - * - * @param {integer} duration in millis - */ - formatDuration: function (duration) { - // don't support sub millisecond resolution - duration = duration < 1 ? 0 : duration; - // determine the number of days in the specified duration - var days = duration / nf.Common.MILLIS_PER_DAY; - days = days >= 1 ? parseInt(days, 10) : 0; - duration %= nf.Common.MILLIS_PER_DAY; + if (nf.Common.isDefinedAndNotNull(propertyHistory)) { + if (!nf.Common.isEmpty(propertyHistory.previousValues)) { + var history = []; + $.each(propertyHistory.previousValues, function (_, previousValue) { + history.push('
  • ' + nf.Common.escapeHtml(previousValue.previousValue) + ' - ' + nf.Common.escapeHtml(previousValue.timestamp) + ' (' + nf.Common.escapeHtml(previousValue.userName) + ')
  • '); + }); + tipContent.push('History:'); + } + } - // remaining duration should be less than 1 day, get number of hours - var hours = duration / nf.Common.MILLIS_PER_HOUR; - hours = hours >= 1 ? parseInt(hours, 10) : 0; - duration %= nf.Common.MILLIS_PER_HOUR; + if (tipContent.length > 0) { + return tipContent.join('

    '); + } else { + return null; + } + }, - // remaining duration should be less than 1 hour, get number of minutes - var minutes = duration / nf.Common.MILLIS_PER_MINUTE; - minutes = minutes >= 1 ? parseInt(minutes, 10) : 0; - duration %= nf.Common.MILLIS_PER_MINUTE; + /** + * Formats the specified property (name and value) accordingly. + * + * @argument {string} name The name of the property + * @argument {string} value The value of the property + */ + formatProperty: function (name, value) { + return '
    ' + nf.Common.formatValue(name) + ': ' + nf.Common.formatValue(value) + '
    '; + }, - // remaining duration should be less than 1 minute, get number of seconds - var seconds = duration / nf.Common.MILLIS_PER_SECOND; - seconds = seconds >= 1 ? parseInt(seconds, 10) : 0; - - // remaining duration is the number millis (don't support sub millisecond resolution) - duration = Math.floor(duration % nf.Common.MILLIS_PER_SECOND); - - // format the time - var time = nf.Common.pad(hours, 2, '0') + - ':' + - nf.Common.pad(minutes, 2, '0') + - ':' + - nf.Common.pad(seconds, 2, '0') + - '.' + - nf.Common.pad(duration, 3, '0'); - - // only include days if appropriate - if (days > 0) { - return days + ' days and ' + time; - } else { - return time; - } - }, - - /** - * Constants for formatting data size. - */ - BYTES_IN_KILOBYTE: 1024, - BYTES_IN_MEGABYTE: 1048576, - BYTES_IN_GIGABYTE: 1073741824, - BYTES_IN_TERABYTE: 1099511627776, - - /** - * Formats the specified number of bytes into a human readable string. - * - * @param {integer} dataSize - * @returns {string} - */ - formatDataSize: function (dataSize) { - // check terabytes - var dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_TERABYTE); - if (dataSizeToFormat > 1) { - return dataSizeToFormat.toFixed(2) + " TB"; - } - - // check gigabytes - dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_GIGABYTE); - if (dataSizeToFormat > 1) { - return dataSizeToFormat.toFixed(2) + " GB"; - } - - // check megabytes - dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_MEGABYTE); - if (dataSizeToFormat > 1) { - return dataSizeToFormat.toFixed(2) + " MB"; - } - - // check kilobytes - dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_KILOBYTE); - if (dataSizeToFormat > 1) { - return dataSizeToFormat.toFixed(2) + " KB"; - } - - // default to bytes - return parseFloat(dataSize).toFixed(2) + " bytes"; - }, - - /** - * Formats the specified integer as a string (adding commas). At this - * point this does not take into account any locales. - * - * @param {integer} integer - */ - formatInteger: function (integer) { - var string = integer + ''; - var regex = /(\d+)(\d{3})/; - while (regex.test(string)) { - string = string.replace(regex, '$1' + ',' + '$2'); - } - return string; - }, - - /** - * Formats the specified float using two demical places. - * - * @param {float} f - */ - formatFloat: function (f) { - if (nf.Common.isUndefinedOrNull(f)) { - return 0.00 + ''; - } - return f.toFixed(2) + ''; - }, - - /** - * Pads the specified value to the specified width with the specified character. - * If the specified value is already wider than the specified width, the original - * value is returned. - * - * @param {integer} value - * @param {integer} width - * @param {string} character - * @returns {string} - */ - pad: function (value, width, character) { - var s = value + ''; - - // pad until wide enough - while (s.length < width) { - s = character + s; - } - - return s; - }, - - /** - * Formats the specified DateTime. - * - * @param {Date} date - * @returns {String} - */ - formatDateTime: function (date) { - return nf.Common.pad(date.getMonth() + 1, 2, '0') + - '/' + - nf.Common.pad(date.getDate(), 2, '0') + - '/' + - nf.Common.pad(date.getFullYear(), 2, '0') + - ' ' + - nf.Common.pad(date.getHours(), 2, '0') + - ':' + - nf.Common.pad(date.getMinutes(), 2, '0') + - ':' + - nf.Common.pad(date.getSeconds(), 2, '0') + - '.' + - nf.Common.pad(date.getMilliseconds(), 3, '0'); - }, - - /** - * Parses the specified date time into a Date object. The resulting - * object does not account for timezone and should only be used for - * performing relative comparisons. - * - * @param {string} rawDateTime - * @returns {Date} - */ - parseDateTime: function (rawDateTime) { - // handle non date values - if (!nf.Common.isDefinedAndNotNull(rawDateTime)) { - return new Date(); - } - if (rawDateTime === 'No value set') { - return new Date(); - } - if (rawDateTime === 'Empty string set') { - return new Date(); - } - - // parse the date time - var dateTime = rawDateTime.split(/ /); - - // ensure the correct number of tokens - if (dateTime.length !== 3) { - return new Date(); - } - - // get the date and time - var date = dateTime[0].split(/\//); - var time = dateTime[1].split(/:/); - - // ensure the correct number of tokens - if (date.length !== 3 || time.length !== 3) { - return new Date(); - } - - // detect if there is millis - var seconds = time[2].split(/\./); - if (seconds.length === 2) { - return new Date(parseInt(date[2], 10), parseInt(date[0], 10), parseInt(date[1], 10), parseInt(time[0], 10), parseInt(time[1], 10), parseInt(seconds[0], 10), parseInt(seconds[1], 10)); - } else { - return new Date(parseInt(date[2], 10), parseInt(date[0], 10), parseInt(date[1], 10), parseInt(time[0], 10), parseInt(time[1], 10), parseInt(time[2], 10), 0); - } - }, - - /** - * Parses the specified duration and returns the total number of millis. - * - * @param {string} rawDuration - * @returns {number} The number of millis - */ - parseDuration: function (rawDuration) { - var duration = rawDuration.split(/:/); - - // ensure the appropriate number of tokens - if (duration.length !== 3) { - return 0; - } - - // detect if there is millis - var seconds = duration[2].split(/\./); - if (seconds.length === 2) { - return new Date(1970, 0, 1, parseInt(duration[0], 10), parseInt(duration[1], 10), parseInt(seconds[0], 10), parseInt(seconds[1], 10)).getTime(); - } else { - return new Date(1970, 0, 1, parseInt(duration[0], 10), parseInt(duration[1], 10), parseInt(duration[2], 10), 0).getTime(); - } - }, - - /** - * Parses the specified size. - * - * @param {string} rawSize - * @returns {int} - */ - parseSize: function (rawSize) { - var tokens = rawSize.split(/ /); - var size = parseFloat(tokens[0].replace(/,/g, '')); - var units = tokens[1]; - - if (units === 'KB') { - return size * 1024; - } else if (units === 'MB') { - return size * 1024 * 1024; - } else if (units === 'GB') { - return size * 1024 * 1024 * 1024; - } else if (units === 'TB') { - return size * 1024 * 1024 * 1024 * 1024; - } else { - return size; - } - }, - - /** - * Parses the specified count. - * - * @param {string} rawCount - * @returns {int} - */ - parseCount: function (rawCount) { - // extract the count - var count = rawCount.split(/ /, 1); - - // ensure the string was split successfully - if (count.length !== 1) { - return 0; - } - - // convert the count to an integer - var intCount = parseInt(count[0].replace(/,/g, ''), 10); - - // ensure it was parsable as an integer - if (isNaN(intCount)) { - return 0; - } - return intCount; - }, - - /** - * Determines if the specified object is defined and not null. - * - * @argument {object} obj The object to test - */ - isDefinedAndNotNull: function (obj) { - return !nf.Common.isUndefined(obj) && !nf.Common.isNull(obj); - }, - - /** - * Determines if the specified object is undefined or null. - * - * @param {object} obj The object to test - */ - isUndefinedOrNull: function (obj) { - return nf.Common.isUndefined(obj) || nf.Common.isNull(obj); - }, - - /** - * Determines if the specified object is undefined. - * - * @argument {object} obj The object to test - */ - isUndefined: function (obj) { - return typeof obj === 'undefined'; - }, - - /** - * Determines whether the specified string is blank (or null or undefined). - * - * @argument {string} str The string to test - */ - isBlank: function (str) { - return nf.Common.isUndefined(str) || nf.Common.isNull(str) || $.trim(str) === ''; - }, - - /** - * Determines if the specified object is null. - * - * @argument {object} obj The object to test - */ - isNull: function (obj) { - return obj === null; - }, - - /** - * Determines if the specified array is empty. If the specified arg is not an - * array, then true is returned. - * - * @argument {array} arr The array to test - */ - isEmpty: function (arr) { - return $.isArray(arr) ? arr.length === 0 : true; - }, - - /** - * Determines if these are the same bulletins. If both arguments are not - * arrays, false is returned. - * - * @param {array} bulletins - * @param {array} otherBulletins - * @returns {boolean} - */ - doBulletinsDiffer: function (bulletins, otherBulletins) { - if ($.isArray(bulletins) && $.isArray(otherBulletins)) { - if (bulletins.length === otherBulletins.length) { - for (var i = 0; i < bulletins.length; i++) { - if (bulletins[i].id !== otherBulletins[i].id) { - return true; - } + /** + * Formats the specified value accordingly. + * + * @argument {string} value The value of the property + */ + formatValue: function (value) { + if (nf.Common.isDefinedAndNotNull(value)) { + if (value === '') { + return 'Empty string set'; + } else { + return nf.Common.escapeHtml(value); } } else { + return 'No value set'; + } + }, + + /** + * HTML escapes the specified string. If the string is null + * or undefined, an empty string is returned. + * + * @returns {string} + */ + escapeHtml: (function () { + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + }; + + return function (string) { + if (nf.Common.isDefinedAndNotNull(string)) { + return String(string).replace(/[&<>"'\/]/g, function (s) { + return entityMap[s]; + }); + } else { + return ''; + } + }; + }()), + + /** + * Determines if the specified property is sensitive. + * + * @argument {object} propertyDescriptor The property descriptor + */ + isSensitiveProperty: function (propertyDescriptor) { + if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { + return propertyDescriptor.sensitive === true; + } else { + return false; + } + }, + + /** + * Determines if the specified property is required. + * + * @param {object} propertyDescriptor The property descriptor + */ + isRequiredProperty: function (propertyDescriptor) { + if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { + return propertyDescriptor.required === true; + } else { + return false; + } + }, + + /** + * Determines if the specified property is required. + * + * @param {object} propertyDescriptor The property descriptor + */ + isDynamicProperty: function (propertyDescriptor) { + if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { + return propertyDescriptor.dynamic === true; + } else { + return false; + } + }, + + /** + * Gets the allowable values for the specified property. + * + * @argument {object} propertyDescriptor The property descriptor + */ + getAllowableValues: function (propertyDescriptor) { + if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { + return propertyDescriptor.allowableValues; + } else { + return null; + } + }, + + /** + * Returns whether the specified property supports EL. + * + * @param {object} propertyDescriptor The property descriptor + */ + supportsEl: function (propertyDescriptor) { + if (nf.Common.isDefinedAndNotNull(propertyDescriptor)) { + return propertyDescriptor.supportsEl === true; + } else { + return false; + } + }, + + /** + * Creates a form inline in order to submit the specified params to the specified URL + * using the specified method. + * + * @param {string} url The URL + * @param {object} params An object with the params to include in the submission + */ + post: function (url, params) { + // temporarily override beforeunload + var previousBeforeUnload = window.onbeforeunload; + window.onbeforeunload = null; + + // create a form for submission + var form = $('
    ').attr({ + 'method': 'POST', + 'action': url, + 'style': 'display: none;' + }); + + // add each parameter when specified + if (nf.Common.isDefinedAndNotNull(params)) { + $.each(params, function (name, value) { + $('').attr('name', name).val(value).appendTo(form); + }); + } + + // submit the form and clean up + form.appendTo('body').submit().remove(); + + // restore previous beforeunload if necessary + if (previousBeforeUnload !== null) { + window.onbeforeunload = previousBeforeUnload; + } + }, + + /** + * Formats the specified array as an unordered list. If the array is not an + * array, null is returned. + * + * @argument {array} array The array to convert into an unordered list + */ + formatUnorderedList: function (array) { + if ($.isArray(array)) { + var ul = $(''); + $.each(array, function (_, item) { + var li = $('
  • ').appendTo(ul); + if (item instanceof jQuery) { + li.append(item); + } else { + li.text(item); + } + }); + return ul; + } else { + return null; + } + }, + + /** + * Extracts the contents of the specified str after the strToFind. If the + * strToFind is not found or the last part of the str, an empty string is + * returned. + * + * @argument {string} str The full string + * @argument {string} strToFind The substring to find + */ + substringAfterLast: function (str, strToFind) { + var result = ''; + var indexOfStrToFind = str.lastIndexOf(strToFind); + if (indexOfStrToFind >= 0) { + var indexAfterStrToFind = indexOfStrToFind + strToFind.length; + if (indexAfterStrToFind < str.length) { + result = str.substr(indexAfterStrToFind); + } + } + return result; + }, + + /** + * Updates the mouse pointer. + * + * @argument {string} domId The id of the element for the new cursor style + * @argument {boolean} isMouseOver Whether or not the mouse is over the element + */ + setCursor: function (domId, isMouseOver) { + if (isMouseOver) { + $('#' + domId).addClass('pointer'); + } else { + $('#' + domId).removeClass('pointer'); + } + }, + + /** + * Constants for time duration formatting. + */ + MILLIS_PER_DAY: 86400000, + MILLIS_PER_HOUR: 3600000, + MILLIS_PER_MINUTE: 60000, + MILLIS_PER_SECOND: 1000, + + /** + * Formats the specified duration. + * + * @param {integer} duration in millis + */ + formatDuration: function (duration) { + // don't support sub millisecond resolution + duration = duration < 1 ? 0 : duration; + + // determine the number of days in the specified duration + var days = duration / nf.Common.MILLIS_PER_DAY; + days = days >= 1 ? parseInt(days, 10) : 0; + duration %= nf.Common.MILLIS_PER_DAY; + + // remaining duration should be less than 1 day, get number of hours + var hours = duration / nf.Common.MILLIS_PER_HOUR; + hours = hours >= 1 ? parseInt(hours, 10) : 0; + duration %= nf.Common.MILLIS_PER_HOUR; + + // remaining duration should be less than 1 hour, get number of minutes + var minutes = duration / nf.Common.MILLIS_PER_MINUTE; + minutes = minutes >= 1 ? parseInt(minutes, 10) : 0; + duration %= nf.Common.MILLIS_PER_MINUTE; + + // remaining duration should be less than 1 minute, get number of seconds + var seconds = duration / nf.Common.MILLIS_PER_SECOND; + seconds = seconds >= 1 ? parseInt(seconds, 10) : 0; + + // remaining duration is the number millis (don't support sub millisecond resolution) + duration = Math.floor(duration % nf.Common.MILLIS_PER_SECOND); + + // format the time + var time = nf.Common.pad(hours, 2, '0') + + ':' + + nf.Common.pad(minutes, 2, '0') + + ':' + + nf.Common.pad(seconds, 2, '0') + + '.' + + nf.Common.pad(duration, 3, '0'); + + // only include days if appropriate + if (days > 0) { + return days + ' days and ' + time; + } else { + return time; + } + }, + + /** + * Constants for formatting data size. + */ + BYTES_IN_KILOBYTE: 1024, + BYTES_IN_MEGABYTE: 1048576, + BYTES_IN_GIGABYTE: 1073741824, + BYTES_IN_TERABYTE: 1099511627776, + + /** + * Formats the specified number of bytes into a human readable string. + * + * @param {integer} dataSize + * @returns {string} + */ + formatDataSize: function (dataSize) { + // check terabytes + var dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_TERABYTE); + if (dataSizeToFormat > 1) { + return dataSizeToFormat.toFixed(2) + " TB"; + } + + // check gigabytes + dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_GIGABYTE); + if (dataSizeToFormat > 1) { + return dataSizeToFormat.toFixed(2) + " GB"; + } + + // check megabytes + dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_MEGABYTE); + if (dataSizeToFormat > 1) { + return dataSizeToFormat.toFixed(2) + " MB"; + } + + // check kilobytes + dataSizeToFormat = parseFloat(dataSize / nf.Common.BYTES_IN_KILOBYTE); + if (dataSizeToFormat > 1) { + return dataSizeToFormat.toFixed(2) + " KB"; + } + + // default to bytes + return parseFloat(dataSize).toFixed(2) + " bytes"; + }, + + /** + * Formats the specified integer as a string (adding commas). At this + * point this does not take into account any locales. + * + * @param {integer} integer + */ + formatInteger: function (integer) { + var string = integer + ''; + var regex = /(\d+)(\d{3})/; + while (regex.test(string)) { + string = string.replace(regex, '$1' + ',' + '$2'); + } + return string; + }, + + /** + * Formats the specified float using two demical places. + * + * @param {float} f + */ + formatFloat: function (f) { + if (nf.Common.isUndefinedOrNull(f)) { + return 0.00 + ''; + } + return f.toFixed(2) + ''; + }, + + /** + * Pads the specified value to the specified width with the specified character. + * If the specified value is already wider than the specified width, the original + * value is returned. + * + * @param {integer} value + * @param {integer} width + * @param {string} character + * @returns {string} + */ + pad: function (value, width, character) { + var s = value + ''; + + // pad until wide enough + while (s.length < width) { + s = character + s; + } + + return s; + }, + + /** + * Formats the specified DateTime. + * + * @param {Date} date + * @returns {String} + */ + formatDateTime: function (date) { + return nf.Common.pad(date.getMonth() + 1, 2, '0') + + '/' + + nf.Common.pad(date.getDate(), 2, '0') + + '/' + + nf.Common.pad(date.getFullYear(), 2, '0') + + ' ' + + nf.Common.pad(date.getHours(), 2, '0') + + ':' + + nf.Common.pad(date.getMinutes(), 2, '0') + + ':' + + nf.Common.pad(date.getSeconds(), 2, '0') + + '.' + + nf.Common.pad(date.getMilliseconds(), 3, '0'); + }, + + /** + * Parses the specified date time into a Date object. The resulting + * object does not account for timezone and should only be used for + * performing relative comparisons. + * + * @param {string} rawDateTime + * @returns {Date} + */ + parseDateTime: function (rawDateTime) { + // handle non date values + if (!nf.Common.isDefinedAndNotNull(rawDateTime)) { + return new Date(); + } + if (rawDateTime === 'No value set') { + return new Date(); + } + if (rawDateTime === 'Empty string set') { + return new Date(); + } + + // parse the date time + var dateTime = rawDateTime.split(/ /); + + // ensure the correct number of tokens + if (dateTime.length !== 3) { + return new Date(); + } + + // get the date and time + var date = dateTime[0].split(/\//); + var time = dateTime[1].split(/:/); + + // ensure the correct number of tokens + if (date.length !== 3 || time.length !== 3) { + return new Date(); + } + + // detect if there is millis + var seconds = time[2].split(/\./); + if (seconds.length === 2) { + return new Date(parseInt(date[2], 10), parseInt(date[0], 10), parseInt(date[1], 10), parseInt(time[0], 10), parseInt(time[1], 10), parseInt(seconds[0], 10), parseInt(seconds[1], 10)); + } else { + return new Date(parseInt(date[2], 10), parseInt(date[0], 10), parseInt(date[1], 10), parseInt(time[0], 10), parseInt(time[1], 10), parseInt(time[2], 10), 0); + } + }, + + /** + * Parses the specified duration and returns the total number of millis. + * + * @param {string} rawDuration + * @returns {number} The number of millis + */ + parseDuration: function (rawDuration) { + var duration = rawDuration.split(/:/); + + // ensure the appropriate number of tokens + if (duration.length !== 3) { + return 0; + } + + // detect if there is millis + var seconds = duration[2].split(/\./); + if (seconds.length === 2) { + return new Date(1970, 0, 1, parseInt(duration[0], 10), parseInt(duration[1], 10), parseInt(seconds[0], 10), parseInt(seconds[1], 10)).getTime(); + } else { + return new Date(1970, 0, 1, parseInt(duration[0], 10), parseInt(duration[1], 10), parseInt(duration[2], 10), 0).getTime(); + } + }, + + /** + * Parses the specified size. + * + * @param {string} rawSize + * @returns {int} + */ + parseSize: function (rawSize) { + var tokens = rawSize.split(/ /); + var size = parseFloat(tokens[0].replace(/,/g, '')); + var units = tokens[1]; + + if (units === 'KB') { + return size * 1024; + } else if (units === 'MB') { + return size * 1024 * 1024; + } else if (units === 'GB') { + return size * 1024 * 1024 * 1024; + } else if (units === 'TB') { + return size * 1024 * 1024 * 1024 * 1024; + } else { + return size; + } + }, + + /** + * Parses the specified count. + * + * @param {string} rawCount + * @returns {int} + */ + parseCount: function (rawCount) { + // extract the count + var count = rawCount.split(/ /, 1); + + // ensure the string was split successfully + if (count.length !== 1) { + return 0; + } + + // convert the count to an integer + var intCount = parseInt(count[0].replace(/,/g, ''), 10); + + // ensure it was parsable as an integer + if (isNaN(intCount)) { + return 0; + } + return intCount; + }, + + /** + * Determines if the specified object is defined and not null. + * + * @argument {object} obj The object to test + */ + isDefinedAndNotNull: function (obj) { + return !nf.Common.isUndefined(obj) && !nf.Common.isNull(obj); + }, + + /** + * Determines if the specified object is undefined or null. + * + * @param {object} obj The object to test + */ + isUndefinedOrNull: function (obj) { + return nf.Common.isUndefined(obj) || nf.Common.isNull(obj); + }, + + /** + * Determines if the specified object is undefined. + * + * @argument {object} obj The object to test + */ + isUndefined: function (obj) { + return typeof obj === 'undefined'; + }, + + /** + * Determines whether the specified string is blank (or null or undefined). + * + * @argument {string} str The string to test + */ + isBlank: function (str) { + return nf.Common.isUndefined(str) || nf.Common.isNull(str) || $.trim(str) === ''; + }, + + /** + * Determines if the specified object is null. + * + * @argument {object} obj The object to test + */ + isNull: function (obj) { + return obj === null; + }, + + /** + * Determines if the specified array is empty. If the specified arg is not an + * array, then true is returned. + * + * @argument {array} arr The array to test + */ + isEmpty: function (arr) { + return $.isArray(arr) ? arr.length === 0 : true; + }, + + /** + * Determines if these are the same bulletins. If both arguments are not + * arrays, false is returned. + * + * @param {array} bulletins + * @param {array} otherBulletins + * @returns {boolean} + */ + doBulletinsDiffer: function (bulletins, otherBulletins) { + if ($.isArray(bulletins) && $.isArray(otherBulletins)) { + if (bulletins.length === otherBulletins.length) { + for (var i = 0; i < bulletins.length; i++) { + if (bulletins[i].id !== otherBulletins[i].id) { + return true; + } + } + } else { + return true; + } + } else if ($.isArray(bulletins) || $.isArray(otherBulletins)) { return true; } - } else if ($.isArray(bulletins) || $.isArray(otherBulletins)) { - return true; + return false; + }, + + /** + * Formats the specified bulletin list. + * + * @argument {array} bulletins The bulletins + * @return {array} The jQuery objects + */ + getFormattedBulletins: function (bulletins) { + var formattedBulletins = []; + $.each(bulletins, function (j, bulletin) { + // format the node address + var nodeAddress = ''; + if (nf.Common.isDefinedAndNotNull(bulletin.nodeAddress)) { + nodeAddress = '- ' + nf.Common.escapeHtml(bulletin.nodeAddress) + ' - '; + } + + // set the bulletin message (treat as text) + var bulletinMessage = $('
    ').css({
    +                    'white-space': 'pre-wrap'
    +                }).text(bulletin.message);
    +
    +                // create the bulletin message
    +                var formattedBulletin = $('
    ' + + nf.Common.escapeHtml(bulletin.timestamp) + ' ' + + nodeAddress + ' ' + + '' + nf.Common.escapeHtml(bulletin.level) + ' ' + + '
    ').append(bulletinMessage); + + formattedBulletins.push(formattedBulletin); + }); + return formattedBulletins; } - return false; - }, - - /** - * Formats the specified bulletin list. - * - * @argument {array} bulletins The bulletins - * @return {array} The jQuery objects - */ - getFormattedBulletins: function (bulletins) { - var formattedBulletins = []; - $.each(bulletins, function (j, bulletin) { - // format the node address - var nodeAddress = ''; - if (nf.Common.isDefinedAndNotNull(bulletin.nodeAddress)) { - nodeAddress = '- ' + nf.Common.escapeHtml(bulletin.nodeAddress) + ' - '; - } - - // set the bulletin message (treat as text) - var bulletinMessage = $('
    ').css({
    -                'white-space': 'pre-wrap'
    -            }).text(bulletin.message);
    -
    -            // create the bulletin message
    -            var formattedBulletin = $('
    ' + - nf.Common.escapeHtml(bulletin.timestamp) + ' ' + - nodeAddress + ' ' + - '' + nf.Common.escapeHtml(bulletin.level) + ' ' + - '
    ').append(bulletinMessage); - - formattedBulletins.push(formattedBulletin); - }); - return formattedBulletins; - } -}; + }; +}()); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js index 2b642eff50..cdb2e9ef9b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-storage.js @@ -20,7 +20,61 @@ nf.Storage = (function () { // Store items for two days before being eligible for removal. - var TWO_DAYS = 86400000 * 2; + var TWO_DAYS = nf.Common.MILLIS_PER_DAY * 2; + + /** + * Checks the expiration for the specified entry. + * + * @param {object} entry + * @returns {boolean} + */ + var checkExpiration = function (entry) { + if (nf.Common.isDefinedAndNotNull(entry.expires)) { + // get the expiration + var expires = new Date(entry.expires); + var now = new Date(); + + // return whether the expiration date has passed + return expires.valueOf() < now.valueOf(); + } else { + return false; + } + }; + + /** + * If the item at key is not expired, the value of field is returned. Otherwise, null. + * + * @param {string} key + * @param {string} field + * @return {object} the value + */ + var getEntryField = function (key, field) { + try { + // parse the entry + var entry = JSON.parse(localStorage.getItem(key)); + + // ensure the entry and item are present + if (nf.Common.isDefinedAndNotNull(entry)) { + + // if the entry is expired, drop it and return null + if (checkExpiration(entry)) { + nf.Storage.removeItem(key); + return null; + } + + // if the entry has the specified field return its value + if (nf.Common.isDefinedAndNotNull(entry[field])) { + return entry[field]; + } else { + return null; + } + } else { + return null; + } + } catch (e) { + return null; + } + }; return { /** @@ -34,16 +88,10 @@ nf.Storage = (function () { var key = localStorage.key(i); var entry = JSON.parse(localStorage.getItem(key)); - // get the expiration - var expires = new Date(entry.expires); - var now = new Date(); - - // if the expiration date has passed, remove it - if (expires.valueOf() < now.valueOf()) { - localStorage.removeItem(key); + if (checkExpiration(entry)) { + nf.Storage.removeItem(key); } } catch (e) { - // likely unable to parse the item } } }, @@ -51,12 +99,13 @@ nf.Storage = (function () { /** * Stores the specified item. * - * @param {type} key - * @param {type} item + * @param {string} key + * @param {object} item + * @param {integer} expires */ - setItem: function (key, item) { + setItem: function (key, item, expires) { // calculate the expiration - var expires = new Date().valueOf() + TWO_DAYS; + expires = nf.Common.isDefinedAndNotNull(expires) ? expires : new Date().valueOf() + TWO_DAYS; // create the entry var entry = { @@ -76,19 +125,18 @@ nf.Storage = (function () { * @param {type} key */ getItem: function (key) { - try { - // parse the entry - var entry = JSON.parse(localStorage.getItem(key)); - - // ensure the entry and item are present - if (nf.Common.isDefinedAndNotNull(entry) && nf.Common.isDefinedAndNotNull(entry.item)) { - return entry.item; - } else { - return null; - } - } catch (e) { - return null; - } + return getEntryField(key, 'item'); + }, + + /** + * Gets the expiration for the specified item. If the item does not exists our could + * not be parsed, returns null. + * + * @param {string} key + * @returns {integer} + */ + getItemExpiration: function (key) { + return getEntryField(key, 'expires'); }, /**