NIFI-6506 - Add ability to convert properties to parameters

* open existing add param dialog from property table
* convert prop to param, update param context
* keep add parameter dialog open until update of the context completes when converting property to param
* Updating parameter context while converting property to parameter.
* Disable the Apply button while updating
* When Cancel is pressed and there is an update in progress, issue the DELETE request to cancel the update.
* Show some status in the Add Parameter dialog when parameter context is updating following a property conversion.
* enforce character restrictions on property names.
* Add convert property to parameter to controller service config.
* Properly set sensitive setting when converting sensitive properties
* Allow converting of properties from controller services configuration into parameters
* Refactor addNewParameter method to be able to share common parts with convertPropertyToParameter
This commit is contained in:
Rob Fellows 2019-08-13 14:36:13 -04:00 committed by Scott Aslan
parent 78d9f13ded
commit eddc5624d8
6 changed files with 423 additions and 132 deletions

View File

@ -117,6 +117,10 @@
<div class="clear"></div>
</div>
</div>
<div id="parameter-context-updating-status">
<div class='parameter-context-step ajax-loading'></div>
<div class='status-message'>Updating parameter context</div>
</div>
</div>
<div id="referencing-components-template" class="referencing-components-template hidden clear">
<div class="setting">

View File

@ -384,3 +384,22 @@ div.parameter-context-step {
background-color: transparent;
float: right;
}
#parameter-context-updating-status {
display: none;
position: absolute;
left: 12px;
bottom: 0;
height: 32px;
}
#parameter-context-updating-status.show-status {
display: flex;
flex-direction: row;
align-items: center;
}
#parameter-context-updating-status .status-message {
white-space: nowrap;
margin-left: 6px;
}

View File

@ -30,7 +30,9 @@
'nf.Client',
'nf.ErrorHandler',
'nf.ProcessGroupConfiguration',
'nf.Settings'],
'nf.Settings',
'nf.ParameterContexts',
'lodash-core'],
function ($,
Slick,
nfCommon,
@ -40,7 +42,9 @@
nfClient,
nfErrorHandler,
nfProcessGroupConfiguration,
nfSettings) {
nfSettings,
nfParameterContexts,
_) {
factory($,
Slick,
nfCommon,
@ -50,7 +54,9 @@
nfClient,
nfErrorHandler,
nfProcessGroupConfiguration,
nfSettings);
nfSettings,
nfParameterContexts,
_);
});
} else if (typeof exports === 'object' && typeof module === 'object') {
factory(require('jquery'),
@ -62,7 +68,9 @@
require('nf.Client'),
require('nf.ErrorHandler'),
require('nf.ProcessGroupConfiguration'),
require('nf.Settings'));
require('nf.Settings'),
recuire('nf.ParameterContexts'),
require('lodash-core'));
} else {
factory(root.$,
root.Slick,
@ -73,7 +81,9 @@
root.nf.Client,
root.nf.ErrorHandler,
root.nf.ProcessGroupConfiguration,
root.nf.Settings);
root.nf.Settings,
root.nf.ParameterContexts,
root._);
}
}(this, function ($,
Slick,
@ -84,7 +94,9 @@
nfClient,
nfErrorHandler,
nfProcessGroupConfiguration,
nfSettings) {
nfSettings,
nfParameterContexts,
_) {
var groupId = null;
var COMBO_MIN_WIDTH = 212;
@ -1350,9 +1362,22 @@
});
}
// allow user defined properties to be removed
if (options.readOnly !== true && dataContext.type === 'userDefined') {
markup += '<div title="Delete" class="delete-property pointer fa fa-trash"></div>';
if (options.readOnly !== true) {
var canModifyParamContexts = nfCommon.canModifyParameterContexts();
var paramContextIsSet = false;
if (_.isFunction(options.getParameterContextId)) {
paramContextIsSet = !_.isNil(options.getParameterContextId(groupId));
}
var referencesParam = referencesParameter(dataContext.value);
if (canModifyParamContexts && paramContextIsSet && !referencesParam && !identifiesControllerService) {
markup += '<div title="Convert to parameter" class="convert-to-parameter pointer fa fa-level-up"></div>';
}
// allow user defined properties to be removed
if (dataContext.type === 'userDefined') {
markup += '<div title="Delete" class="delete-property pointer fa fa-trash"></div>';
}
}
return markup;
@ -1545,6 +1570,25 @@
});
}
}
} else if (target.hasClass('convert-to-parameter')) {
var parameterContextId;
if (_.isFunction(options.getParameterContextId)) {
parameterContextId = options.getParameterContextId(groupId);
}
if (options.readOnly !== true && !_.isNil(parameterContextId)) {
var descriptors = table.data('descriptors');
var propertyDescriptor = descriptors[property.displayName];
nfParameterContexts.convertPropertyToParameter(property, propertyDescriptor, parameterContextId)
.done(function (parameter) {
// set the property value to the reference the parameter that was created
propertyData.updateItem(property.id, $.extend(property, {
previousValue: property.value,
value: '#{' + parameter.name + '}'
}));
});
}
}
}
});

View File

@ -1875,6 +1875,16 @@
},
goToServiceDeferred: function () {
return goToServiceFromProperty(serviceTable);
},
getParameterContextId: function (groupId) {
// attempt to identify the parameter context id, conditional based on whether
// the user is configuring the current process group
if (_.isNil(groupId) || groupId === nfCanvasUtils.getGroupId()) {
return nfCanvasUtils.getParameterContextId();
} else {
var parentProcessGroup = nfCanvasUtils.getComponentByType('ProcessGroup').get(groupId);
return parentProcessGroup.parameterContextId;
}
}
});

View File

@ -36,9 +36,10 @@
'nf.PolicyManagement',
'nf.Processor',
'nf.ProcessGroup',
'nf.ProcessGroupConfiguration'],
function ($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfNgBridge, nfErrorHandler, nfFilteredDialogCommon, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration) {
return (nf.ParameterContexts = factory($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfNgBridge, nfErrorHandler, nfFilteredDialogCommon, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration));
'nf.ProcessGroupConfiguration',
'lodash-core'],
function ($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfNgBridge, nfErrorHandler, nfFilteredDialogCommon, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration, _) {
return (nf.ParameterContexts = factory($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfNgBridge, nfErrorHandler, nfFilteredDialogCommon, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration, _));
});
} else if (typeof exports === 'object' && typeof module === 'object') {
module.exports = (nf.ParameterContexts =
@ -59,7 +60,8 @@
require('nf.PolicyManagement'),
require('nf.Processor'),
require('nf.ProcessGroup'),
require('nf.ProcessGroupConfiguration')));
require('nf.ProcessGroupConfiguration'),
require('lodash-core')));
} else {
nf.ParameterContexts = factory(root.$,
root.Slick,
@ -78,9 +80,10 @@
root.nf.PolicyManagement,
root.nf.Processor,
root.nf.ProcessGroup,
root.nf.ProcessGroupConfiguration);
root.nf.ProcessGroupConfiguration,
root._);
}
}(this, function ($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfNgBridge, nfErrorHandler, nfFilteredDialogCommon, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration) {
}(this, function ($, Slick, d3, nfClient, nfDialog, nfStorage, nfCommon, nfCanvasUtils, nfNgBridge, nfErrorHandler, nfFilteredDialogCommon, nfShell, nfComponentState, nfComponentVersion, nfPolicyManagement, nfProcessor, nfProcessGroup, nfProcessGroupConfiguration, _) {
'use strict';
var config = {
@ -139,8 +142,10 @@
// defines a function for sorting
var comparer = function (a, b) {
if (a.permissions.canRead && b.permissions.canRead) {
var aString = nfCommon.isDefinedAndNotNull(a.component[sortDetails.columnId]) ? a.component[sortDetails.columnId] : '';
var bString = nfCommon.isDefinedAndNotNull(b.component[sortDetails.columnId]) ? b.component[sortDetails.columnId] : '';
// use _.get to try to access the piece of the object you want, but provide a default value it it is not there
var aString = _.get(a, 'component[' + sortDetails.columnId + ']', '');
var bString = _.get(b, 'component[' + sortDetails.columnId + ']', '');
return aString === bString ? 0 : aString > bString ? 1 : -1;
} else {
if (!a.permissions.canRead && !b.permissions.canRead) {
@ -170,8 +175,8 @@
// defines a function for sorting
var comparer = function (a, b) {
if (sortDetails.columnId === 'name') {
var aString = nfCommon.isDefinedAndNotNull(a[sortDetails.columnId]) ? a[sortDetails.columnId] : '';
var bString = nfCommon.isDefinedAndNotNull(b[sortDetails.columnId]) ? b[sortDetails.columnId] : '';
var aString = _.get(a, '[' + sortDetails.columnId + ']', '');
var bString = _.get(b, '[' + sortDetails.columnId + ']', '');
return aString === bString ? 0 : aString > bString ? 1 : -1;
}
};
@ -752,139 +757,128 @@
return a.component.name.localeCompare(b.component.name);
};
var parameterKeyRegex = /^[a-zA-Z0-9-_. ]+$/;
/**
* Adds a new parameter.
*/
var addNewParameter = function () {
var parameterName = $.trim($('#parameter-name').val());
var param = getParameterFromFieldValues();
// ensure the parameter name is specified
if (parameterName !== '' && parameterKeyRegex.test(parameterName)) {
var parameterGrid = $('#parameter-table').data('gridInstance');
var parameterData = parameterGrid.getData();
var parameterGrid = $('#parameter-table').data('gridInstance');
var parameterData = parameterGrid.getData();
// ensure the parameter name is unique
var matchingParameter = null;
$.each(parameterData.getItems(), function (_, item) {
if (parameterName === item.name) {
matchingParameter = item;
return false;
}
var isValid = validateParameter(param, parameterData.getItems());
if (isValid) {
var parameter = _.extend({}, param, {
id: _.defaultTo(param.id, parameterCount),
hidden: false,
type: 'Parameter',
previousValue: null,
previousDescription: null,
isEditable: true,
isModified: true,
hasValueChanged: false,
isNew: true,
});
var isSensitive = $('#parameter-dialog').find('input[name="sensitive"]:checked').val() === "sensitive" ? true : false;
var isChecked = $('#parameter-set-empty-string-field').hasClass('checkbox-checked');
if (matchingParameter === null) {
var parameter = {
id: parameterCount,
hidden: false,
type: 'Parameter',
sensitive: isSensitive,
name: parameterName,
description: $('#parameter-description-field').val(),
previousValue: null,
previousDescription: null,
isEditable: true,
isEmptyStringSet: isChecked,
isModified: true,
hasValueChanged: false,
isNew: true
};
var value = $('#parameter-value-field').val();
if (!nfCommon.isBlank(value)) {
parameter.value = value;
} else {
if (isChecked) {
parameter.value = '';
} else {
parameter.value = null;
}
}
if (_.isNil(param.id)) {
// add a row for the new parameter
parameterData.addItem(parameter);
// sort the data
parameterData.reSort();
// select the new parameter row
var row = parameterData.getRowById(parameterCount);
parameterGrid.setActiveCell(row, parameterGrid.getColumnIndex('value'));
parameterCount++;
} else {
// if this row is currently hidden, make sure the sensitivity is equivalent before we allow recreate
if (matchingParameter.hidden === true && matchingParameter.sensitive !== isSensitive) {
nfDialog.showOkDialog({
headerText: 'Parameter Exists',
dialogContent: 'A parameter with this name has been marked for deletion. Please apply this change to delete this parameter from the parameter context before recreating it with a different sensitivity.'
});
} else if (matchingParameter.hidden === true && matchingParameter.sensitive === isSensitive) {
var parameter = $.extend(matchingParameter, {
hidden: false,
sensitive: isSensitive,
previousValue: null,
description: $('#parameter-description-field').val(),
previousDescription: null,
isEditable: true,
isEmptyStringSet: isChecked,
isModified: true,
hasValueChanged: false,
isNew: true
});
var value = $('#parameter-value-field').val();
if (!nfCommon.isBlank(value)) {
parameter.value = value;
} else {
if (isChecked) {
parameter.value = '';
} else {
parameter.value = null;
}
}
parameterData.updateItem(matchingParameter.id, parameter);
// sort the data
parameterData.reSort();
// select the new parameter row
var row = parameterData.getRowById(matchingParameter.id);
parameterGrid.setActiveCell(row, parameterGrid.getColumnIndex('value'));
parameterCount++;
} else {
nfDialog.showOkDialog({
headerText: 'Parameter Exists',
dialogContent: 'A parameter with this name already exists.'
});
// select the existing properties row
var matchingRow = parameterData.getRowById(matchingParameter.id);
parameterGrid.setSelectedRows([matchingRow]);
parameterGrid.scrollRowIntoView(matchingRow);
}
parameterData.updateItem(param.id, parameter);
}
// sort the data
parameterData.reSort();
// select the new parameter row
var row = parameterData.getRowById(parameterCount);
parameterGrid.setActiveCell(row, parameterGrid.getColumnIndex('value'));
parameterCount++;
// close the new parameter dialog
$('#parameter-dialog').modal('hide');
// update the buttons to possibly trigger the disabled state
$('#parameter-context-dialog').modal('refreshButtons');
}
} else if (!parameterKeyRegex.test(parameterName)) {
nfDialog.showOkDialog({
headerText: 'Configuration Error',
dialogContent: 'This parameter appears to have an invalid character or characters. Only alpha-numeric characters (a-z, A-Z, 0-9), hyphens (-), underscores (_), periods (.), and spaces ( ) are accepted.'
});
} else {
// update the buttons to possibly trigger the disabled state
$('#parameter-context-dialog').modal('refreshButtons');
};
/**
* Builds a parameter object from the user-entered parameter inputs
*
* @return {{isEmptyStringSet: *, name: *, description: *, sensitive: *, value: *}}
*/
var getParameterFromFieldValues = function () {
var name = $.trim($('#parameter-name').val());
var value = $('#parameter-value-field').val();
var description = $('#parameter-description-field').val();
var isSensitive = $('#parameter-dialog').find('input[name="sensitive"]:checked').val() === 'sensitive' ? true : false;
var isEmptyStringSet = $('#parameter-set-empty-string-field').hasClass('checkbox-checked');
if (_.isEmpty(value)) {
value = isEmptyStringSet ? '' : null;
}
return {
name,
value,
description,
sensitive: isSensitive,
isEmptyStringSet
}
};
/**
* Checks the validity of a parameter
* @param parameter Parameter to validate
* @param existingParameters Existing parameters to verify there are no duplicates
* @return {boolean}
*/
var validateParameter = function (parameter, existingParameters) {
if (parameter.name === '') {
nfDialog.showOkDialog({
headerText: 'Configuration Error',
dialogContent: 'The name of the parameter must be specified.'
});
return false;
}
// make sure the parameter name does not use any unsupported characters
var parameterNameRegex = /^[a-zA-Z0-9-_. ]+$/;
if (!parameterNameRegex.test(parameter.name)) {
nfDialog.showOkDialog({
headerText: 'Configuration Error',
dialogContent: 'The name of the parameter appears to have an invalid character or characters. Only alpha-numeric characters (a-z, A-Z, 0-9), hyphens (-), underscores (_), periods (.), and spaces ( ) are accepted.'
});
return false;
}
// validate the parameter is not a duplicate
var matchingParameter = _.find(existingParameters, { name: parameter.name });
if (_.isNil(matchingParameter)) {
return true;
} else {
var matchingParamIsHidden = _.get(matchingParameter, 'hidden', false);
if (matchingParamIsHidden && matchingParameter.sensitive !== parameter.sensitive) {
nfDialog.showOkDialog({
headerText: 'Parameter Exists',
dialogContent: 'A parameter with this name has been marked for deletion. Please apply this change to delete this parameter from the parameter context before recreating it with a different sensitivity.'
});
} else if (matchingParamIsHidden && matchingParameter.sensitive === parameter.sensitive) {
// set the id of the parameter that was found. It could be used to update it.
parameter.id = matchingParameter.id;
return true;
} else {
nfDialog.showOkDialog({
headerText: 'Parameter Exists',
dialogContent: 'A parameter with this name already exists.'
});
}
return false;
}
};
@ -1856,6 +1850,50 @@
initParameterTable();
};
var openAddParameterDialog = function ({ onApply, onCancel }) {
$('#parameter-dialog')
.modal('setHeaderText', 'Add Parameter')
.modal('setButtonModel', [{
buttonText: 'Apply',
color: {
base: '#728E9B',
hover: '#004849',
text: '#ffffff'
},
disabled: function () {
var parameterName = $('#parameter-name').val();
var parameterValue = $('#parameter-value-field').val();
var isEmptyString = $('#parameter-dialog').find('.nf-checkbox').hasClass('checkbox-checked');
var isUpdatingParameterContext = $('#parameter-context-updating-status').hasClass('show-status');
if (isUpdatingParameterContext) {
return true;
}
if ((parameterName !== '' && parameterValue !== '') || (parameterName !== '' && isEmptyString)) {
return false;
}
return true;
},
handler: {
click: onApply
}
}, {
buttonText: 'Cancel',
color: {
base: '#E3E8EB',
hover: '#C7D2D7',
text: '#004849'
},
handler: {
click: onCancel
}
}])
.modal('show');
$('#parameter-dialog').modal('show');
};
/**
* Loads the parameter contexts.
*/
@ -2298,6 +2336,178 @@
var parameterContextData = parameterContextGrid.getData();
parameterContextData.deleteItem(parameterContextEntity.id);
}).fail(nfErrorHandler.handleAjaxError);
},
/**
* Converts a property in to a parameter and adds the parameter to the current parameter context.
* @param property property to convert
* @param propertyDescriptor property descriptor for the property bering converted
* @param parameterContextId id of the current parameter context
* @return Promise A Promise that resolves with the added parameter
*/
convertPropertyToParameter: function (property, propertyDescriptor, parameterContextId) {
return $.Deferred(function (deferred) {
if (_.isNil(parameterContextId)) {
nfDialog.showOkDialog({
headerText: 'Unable to Convert Property',
dialogContent: 'There is no parameter context set for the current process group.'
});
deferred.reject();
}
var getContext = $.ajax({
type: 'GET',
url: config.urls.parameterContexts + '/' + encodeURIComponent(parameterContextId),
dataType: 'json'
});
// get the parameter context, show the dialog
getContext
.done(function (parameterContextEntity) {
var showUpdateStatus = function (isOn) {
if (isOn) {
// show the status message
$('#parameter-context-updating-status').addClass('show-status');
// disable the apply button
$('#parameter-dialog').modal('refreshButtons');
} else {
$('#parameter-context-updating-status').removeClass('show-status');
// possibly re-enable the apply button
$('#parameter-dialog').modal('refreshButtons');
}
};
var requestId;
var updateTimeoutReference;
openAddParameterDialog({
onApply: function () {
showUpdateStatus(true);
var existingParameters = parameterContextEntity.component.parameters.map(function(p) { return p.parameter });
var parameter = getParameterFromFieldValues();
var isValid = validateParameter(parameter, existingParameters);
if (!isValid) {
// Do not resolve or reject here. Give the user the chance to fix the issue.
showUpdateStatus(false);
return;
}
// only adding a new parameter, just put the new one in the list for the update
parameterContextEntity.component.parameters = [{
parameter: {
name: parameter.name,
value: parameter.value,
sensitive: parameter.sensitive,
description: parameter.description
}}];
requestId = null;
// initiate the parameter context update with the new parameter
submitUpdateRequest(parameterContextEntity)
.done(function(response) {
var pollUpdateRequest = function (updateRequestEntity) {
var updateRequest = updateRequestEntity.request;
var errored = !_.isEmpty(updateRequest.failureReason);
// get the request id
requestId = updateRequest.requestId;
if (updateRequest.complete === true) {
showUpdateStatus(false);
if (errored) {
nfDialog.showOkDialog({
headerText: 'Parameter Context Update Error',
dialogContent: 'Unable to complete parameter context update request: ' + nfCommon.escapeHtml(updateRequest.failureReason)
});
// update failed, therefore converting failed. reject the promise for the caller
deferred.reject();
} else {
// resolve the promise for the caller if the update was successful
deferred.resolve(parameter);
}
// delete the update request
deleteUpdateRequest(parameterContextEntity.id, requestId);
// hide the param dialog
$('#parameter-dialog').modal('hide');
} else {
// wait to get an updated status
updateTimeoutReference = setTimeout(function () {
getUpdateRequest(parameterContextEntity.id, requestId)
.done(function (getResponse) {
pollUpdateRequest(getResponse);
})
.fail(function (e) {
if (!_.isNil(parameterContextEntity.id) && !_.isNil(requestId)) {
deleteUpdateRequest(parameterContextEntity.id, requestId);
}
deferred.reject(e)
});
}, 1000);
}
};
pollUpdateRequest(response);
})
.fail(function (e) {
deferred.reject(e)
});
},
onCancel: function() {
showUpdateStatus(false);
if (!_.isNil(parameterContextEntity.id) && !_.isNil(requestId)) {
deleteUpdateRequest(parameterContextEntity.id, requestId);
requestId = null;
if (!_.isNil(updateTimeoutReference)) {
clearTimeout(updateTimeoutReference);
}
}
// hide the dialog
$(this).modal('hide');
}
});
// set the values of the form from the passed in property
$('#parameter-name').val(property.displayName);
$('#parameter-name').prop('disabled', false);
$('#parameter-sensitive-radio-button').prop('disabled', true);
$('#parameter-not-sensitive-radio-button').prop('disabled', true);
if (property.value === '') {
$('#parameter-dialog').find('.nf-checkbox').removeClass('checkbox-unchecked').addClass('checkbox-checked');
} else {
$('#parameter-dialog').find('.nf-checkbox').removeClass('checkbox-checked').addClass('checkbox-unchecked');
}
if (nfCommon.isSensitiveProperty(propertyDescriptor)) {
$('#parameter-sensitive-radio-button').prop('checked', true);
$('#parameter-not-sensitive-radio-button').prop('checked', false);
} else {
$('#parameter-sensitive-radio-button').prop('checked', false);
$('#parameter-not-sensitive-radio-button').prop('checked', true);
$('#parameter-value-field').val(property.value);
}
$('#parameter-description-field').val(property.description);
// update the buttons to possibly trigger the disabled state
$('#parameter-dialog').modal('refreshButtons');
})
.fail(function(e) {
deferred.reject(e);
});
}).promise();
}
};

View File

@ -659,7 +659,11 @@
}
}).promise();
},
goToServiceDeferred: goToServiceFromProperty
goToServiceDeferred: goToServiceFromProperty,
getParameterContextId: function (groupId) {
// processors being configured must be in the current group
return nfCanvasUtils.getParameterContextId();
}
});
},