NIFI-6346:

- Updating nfel to support referencing parameters.
This commit is contained in:
Matt Gilman 2019-06-04 15:04:42 -04:00 committed by Scott Aslan
parent 7228496801
commit d93ae47afc
3 changed files with 265 additions and 72 deletions

View File

@ -38,29 +38,66 @@
'use strict'; 'use strict';
/** /**
* Formats the specified arguments for the EL function tooltip. * Formats the specified function definition.
* *
* @param {type} args * @param details
* @returns {String} * @returns {jQuery|HTMLElement}
*/ */
var formatArguments = function(args) { var formatDetails = function (details) {
if ($.isEmptyObject(args)) { var detailsContainer = $('<div></div>');
return '<span class="unset">None</span>';
} else { // add the detail name
var formatted = '<div class="clear"></div><ul class="el-arguments">'; $('<div class="el-name el-section"></div>').text(details.name).appendTo(detailsContainer);
$.each(args, function(key, value) {
formatted += ( // add the detail description
'<li>' + $('<div class="el-section"></div>').text(details.description).appendTo(detailsContainer);
'<span class="el-argument-name">' + key + '</span> - ' +
value + // add the function arguments
'</li>' if (typeof details.args !== 'undefined') {
); var argumentsContainer = $('<div class="el-section"></div>').appendTo(detailsContainer);
}); $('<div class="el-header">Arguments</div>').appendTo(argumentsContainer);
formatted += '</ul>';
return formatted; if ($.isEmptyObject(details.args)) {
$('<span class="unset">None</span>').appendTo(argumentsContainer);
} else {
$('<div class="clear"></div>').appendTo(argumentsContainer);
// add the argument
var argumentContainer = $('<ul class="el-arguments"></ul>').appendTo(argumentsContainer);
$.each(details.args, function (key, value) {
var argName = $('<span class="el-argument-name"></span>').text(key);
var argDescription = $('<span></span>').text(value);
$('<li></li>').append(argName).append(' - ').append(argDescription).appendTo(argumentContainer);
});
}
} }
// add the function subject
if (typeof details.subject !== 'undefined') {
var subjectContainer = $('<div class="el-section"></div>').appendTo(detailsContainer);
$('<div class="el-header">Subject</div>').appendTo(subjectContainer);
$('<p></p>').text(details.subject).appendTo(subjectContainer);
$('<div class="clear"></div>').appendTo(subjectContainer);
}
// add the function return type
if (typeof details.returnType !== 'undefined') {
var returnTypeContainer = $('<div class="el-section"></div>').appendTo(detailsContainer);
$('<div class="el-header">Returns</div>').appendTo(returnTypeContainer);
$('<p></p>').text(details.returnType).appendTo(returnTypeContainer);
$('<div class="clear"></div>').appendTo(returnTypeContainer);
}
return detailsContainer;
}; };
var parameterKeyRegex = /^[a-zA-Z0-9-_. ]+/;
var parameters = [];
var parameterRegex = new RegExp('^$');
var parameterDetails = {};
var subjectlessFunctions = []; var subjectlessFunctions = [];
var functions = []; var functions = [];
@ -88,7 +125,7 @@
// Determine if this function supports running subjectless // Determine if this function supports running subjectless
if (subjectless.length) { if (subjectless.length) {
subjectlessFunctions.push(name); subjectlessFunctions.push(name);
subject = '<span class="unset">None</span>'; subject = 'None';
} }
// Determine if this function supports running with a subject // Determine if this function supports running with a subject
@ -105,26 +142,14 @@
args[argName.text()] = argDescription.text(); args[argName.text()] = argDescription.text();
}); });
// format the function tooltip // record the function details
functionDetails[name] = functionDetails[name] = {
'<div>' + name: name,
'<div class="el-name el-section">' + name + '</div>' + description: description,
'<div class="el-section">' + description + '</div>' + args: args,
'<div class="el-section">' + subject: subject,
'<div class="el-header">Arguments</div>' + returnType: returnType
formatArguments(args) + };
'</div>' +
'<div class="el-section">' +
'<div class="el-header">Subject</div>' +
'<p>' + subject + '</p>' +
'<div class="clear"></div>' +
'</div>' +
'<div class="el-section">' +
'<div class="el-header">Returns</div>' +
'<p>' + returnType + '</p>' +
'<div class="clear"></div>' +
'</div>' +
'</div>';
}); });
}).always(function() { }).always(function() {
// build the regex for all functions discovered // build the regex for all functions discovered
@ -139,37 +164,40 @@
var EXPRESSION = 'expression'; var EXPRESSION = 'expression';
var ARGUMENTS = 'arguments'; var ARGUMENTS = 'arguments';
var ARGUMENT = 'argument'; var ARGUMENT = 'argument';
var PARAMETER = 'parameter';
var INVALID = 'invalid'; var INVALID = 'invalid';
/** /**
* Handles dollars identifies on the stream. * Handles dollars identifies on the stream.
* *
* @param {object} stream The character stream * @param {string} startChar The start character
* @param {object} states The states * @param {string} context The context to transition to if we match on the specified start character
* @param {object} stream The character stream
* @param {object} states The states
*/ */
var handleDollar = function (stream, states) { var handleStart = function (startChar, context, stream, states) {
// determine the number of sequential dollars // determine the number of sequential start chars
var dollarCount = 0; var startCharCount = 0;
stream.eatWhile(function (ch) { stream.eatWhile(function (ch) {
if (ch === '$') { if (ch === startChar) {
dollarCount++; startCharCount++;
return true; return true;
} }
return false; return false;
}); });
// if there is an even number of consecutive dollars this expression is escaped // if there is an even number of consecutive start chars this expression is escaped
if (dollarCount % 2 === 0) { if (startCharCount % 2 === 0) {
// do not style an escaped expression // do not style an escaped expression
return null; return null;
} }
// if there was an odd number of consecutive dollars and there was more than 1 // if there was an odd number of consecutive start chars and there was more than 1
if (dollarCount > 1) { if (startCharCount > 1) {
// back up one char so we can process the start sequence next iteration // back up one char so we can process the start sequence next iteration
stream.backUp(1); stream.backUp(1);
// do not style the preceeding dollars // do not style the preceding start chars
return null; return null;
} }
@ -180,17 +208,16 @@
// new expression start // new expression start
states.push({ states.push({
context: EXPRESSION context: context
}); });
// consume any addition whitespace // consume any addition whitespace
stream.eatSpace(); stream.eatSpace();
return 'bracket'; return 'bracket';
} else {
// not a valid start sequence
return null;
} }
// not a valid start sequence
return null;
}; };
/** /**
@ -317,7 +344,7 @@
var renderer = function(element, self, data) { var renderer = function(element, self, data) {
var item = $('<div></div>').text(data.text); var item = $('<div></div>').text(data.text);
var li = $(element).qtip({ var li = $(element).qtip({
content: functionDetails[data.text], content: formatDetails(data.details),
style: { style: {
classes: 'nifi-tooltip nf-tooltip', classes: 'nifi-tooltip nf-tooltip',
tip: false, tip: false,
@ -346,6 +373,23 @@
return { return {
/**
* Sets the available parameters.
*
* @param parameterListing
*/
setParameters: function (parameterListing) {
parameters = [];
parameterDetails = {};
parameterListing.forEach(function (parameter) {
parameters.push(parameter.name);
parameterDetails[parameter.name] = parameter;
});
parameterRegex = new RegExp('^((' + parameters.join(')|(') + '))$');
},
/** /**
* Returns an object that provides syntax highlighting for NiFi expression language. * Returns an object that provides syntax highlighting for NiFi expression language.
*/ */
@ -410,8 +454,17 @@
// if we've hit some comments... will consume the remainder of the line // if we've hit some comments... will consume the remainder of the line
if (current === '#') { if (current === '#') {
stream.skipToEnd(); // consume the pound
return 'comment'; stream.next();
var afterPound = stream.peek();
if (afterPound !== '{') {
stream.skipToEnd();
return 'comment';
} else {
// unconsume the pound
stream.backUp(1);
}
} }
// get the current state // get the current state
@ -477,7 +530,7 @@
// nested expression // nested expression
// ----------------- // -----------------
var expressionDollarResult = handleDollar(stream, states); var expressionDollarResult = handleStart('$', EXPRESSION, stream, states);
// if we've found an embedded expression we need to... // if we've found an embedded expression we need to...
if (expressionDollarResult !== null) { if (expressionDollarResult !== null) {
@ -486,6 +539,21 @@
} }
return expressionDollarResult; return expressionDollarResult;
} else if (current === '#') {
// --------------------------
// nested parameter reference
// --------------------------
// handle the nested parameter reference
var parameterReferenceResult = handleStart('#', PARAMETER, stream, states);
// if we've found an embedded parameter reference we need to...
if (parameterReferenceResult !== null) {
// transition back to subject when this parameter reference completes
state.context = SUBJECT;
}
return parameterReferenceResult;
} else if (current === '}') { } else if (current === '}') {
// ----------------- // -----------------
// end of expression // end of expression
@ -737,7 +805,7 @@
// ----------------- // -----------------
// handle the nested expression // handle the nested expression
var argumentDollarResult = handleDollar(stream, states); var argumentDollarResult = handleStart('$', EXPRESSION, stream, states);
// if we've found an embedded expression we need to... // if we've found an embedded expression we need to...
if (argumentDollarResult !== null) { if (argumentDollarResult !== null) {
@ -746,6 +814,82 @@
} }
return argumentDollarResult; return argumentDollarResult;
} else if (current === '#') {
// --------------------------
// nested parameter reference
// --------------------------
// handle the nested parameter reference
var parameterReferenceResult = handleStart('#', PARAMETER, stream, states);
// if we've found an embedded parameter reference we need to...
if (parameterReferenceResult !== null) {
// transition back to arguments when this parameter reference completes
state.context = ARGUMENTS;
}
return parameterReferenceResult;
} else {
// ----------
// unexpected
// ----------
// consume and move along
stream.skipToEnd();
state.context = INVALID;
// unexpected...
return null;
}
}
// within a parameter reference
if (state.context === PARAMETER) {
// attempt to extract a parameter name
var parameterName = stream.match(parameterKeyRegex, false);
// if the result returned a match
if (parameterName !== null && parameterName.length === 1) {
// consume the entire token to ensure the whole function
// name is matched. this is an issue with functions like
// substring and substringAfter since 'substringA' would
// match the former and when we really want to autocomplete
// against the latter.
stream.match(parameterKeyRegex);
// see if this matches a known function and is followed by (
if (parameterRegex.test(parameterName)) {
// ------------------
// resolved parameter
// ------------------
// style for function
return 'builtin';
} else {
// --------------------
// unresolved parameter
// --------------------
// style for function
return 'string';
}
}
if (current === '}') {
// -----------------
// end of expression
// -----------------
// consume the close
stream.next();
// signifies the end of an parameter reference
if (typeof states.pop() === 'undefined') {
return null;
} else {
// style as expression
return 'bracket';
}
} else { } else {
// ---------- // ----------
// unexpected // unexpected
@ -762,7 +906,12 @@
// signifies the potential start of an expression // signifies the potential start of an expression
if (current === '$') { if (current === '$') {
return handleDollar(stream, states); return handleStart('$', EXPRESSION, stream, states);
}
// signifies the potential start of a parameter reference
if (current === '#') {
return handleStart('#', PARAMETER, stream, states);
} }
// signifies the end of an expression // signifies the end of an expression
@ -810,9 +959,15 @@
return context === EXPRESSION || context === SUBJECT_OR_FUNCTION; return context === EXPRESSION || context === SUBJECT_OR_FUNCTION;
}; };
// whether or not the current context is within a parameter reference
var isParameterReference = function (context) {
// attempting to match a function name or already successfully matched a function name
return context === PARAMETER;
};
// only support suggestion in certain cases // only support suggestion in certain cases
var context = state.context; var context = state.context;
if (!isSubjectlessFunction(context) && !isFunction(context)) { if (!isSubjectlessFunction(context) && !isFunction(context) && !isParameterReference(context)) {
return null; return null;
} }
@ -823,19 +978,29 @@
var trimmed = $.trim(value); var trimmed = $.trim(value);
// identify potential patterns and increment the start location appropriately // identify potential patterns and increment the start location appropriately
if (trimmed === '${' || trimmed === ':') { if (trimmed === '${' || trimmed === ':' || trimmed === '#{') {
includeAll = true; includeAll = true;
token.start += value.length; token.start += value.length;
} }
var getCompletions = function(functions) { var options = functions;
var useFunctionDetails = true;
if (isSubjectlessFunction(context)) {
options = subjectlessFunctions;
} else if (isParameterReference(context)) {
options = parameters;
useFunctionDetails = false;
}
var getCompletions = function(options) {
var found = []; var found = [];
$.each(functions, function (i, funct) { $.each(options, function (i, opt) {
if ($.inArray(funct, found) === -1) { if ($.inArray(opt, found) === -1) {
if (includeAll || funct.toLowerCase().indexOf(value) === 0) { if (includeAll || opt.toLowerCase().indexOf(value) === 0) {
found.push({ found.push({
text: funct, text: opt,
details: useFunctionDetails ? functionDetails[opt] : parameterDetails[opt],
render: renderer render: renderer
}); });
} }
@ -846,7 +1011,7 @@
}; };
// get the suggestions for the current context // get the suggestions for the current context
var completionList = getCompletions(isSubjectlessFunction(context) ? subjectlessFunctions : functions); var completionList = getCompletions(options);
completionList = completionList.sort(function (a, b) { completionList = completionList.sort(function (a, b) {
var aLower = a.text.toLowerCase(); var aLower = a.text.toLowerCase();
var bLower = b.text.toLowerCase(); var bLower = b.text.toLowerCase();

View File

@ -57,6 +57,8 @@
return parameterContainer; return parameterContainer;
}; };
var parameterKeyRegex = /^[a-zA-Z0-9-_. ]+/;
var parameters = []; var parameters = [];
var parameterRegex = new RegExp('^$'); var parameterRegex = new RegExp('^$');
@ -311,10 +313,8 @@
return null; return null;
} }
// within a function // within a parameter reference
if (state.context === PARAMETER) { if (state.context === PARAMETER) {
var parameterKeyRegex = /^[a-zA-Z0-9-_. ]+/;
// attempt to extract a parameter name // attempt to extract a parameter name
var parameterName = stream.match(parameterKeyRegex, false); var parameterName = stream.match(parameterKeyRegex, false);

View File

@ -1257,6 +1257,34 @@
columns: { columns: {
value: { value: {
editor: getNfEditor(function (propertyDescriptor) { editor: getNfEditor(function (propertyDescriptor) {
// set the available parameters
// TODO - obtain actual parameters and filter accordingly to sensitivity
nf.nfel.setParameters([
{
name: 'param 1',
sensitive: false,
description: 'this is the description for param 1',
value: 'value 1'
},
{
name: 'param 2',
sensitive: true,
description: 'this is the description for param 2',
value: 'value 2'
},
{
name: 'param 3',
sensitive: false,
value: 'value 3'
},
{
name: 'param 4',
sensitive: false,
description: 'this is the description for param 4',
value: 'value 4'
}
]);
return nf.nfel; return nf.nfel;
}) })
} }