node-red-contrib-monogoto-operator
Version:
Allows communicating with the Monogoto Operator API from node-red.
572 lines (481 loc) • 25.7 kB
HTML
<script type="text/x-red" data-template-name="monogoto-operator" id="monogoto-operator-template">
<style scoped>
.hiddenAttrs {display:none ;}
.visibleAttrs {display:flex ;}
.form-row > label {
align-self: center;
padding-left:10pt;
padding-right:10pt;
width:173pt;
}
div.main-container > .form-row {
display: flex;
white-space: nowrap;
white-space: pre;
flex-direction: row;
}
.form-row > label + input,
.form-row > label + select
{
flex-grow: 1;
width: 100%;
}
div.red-ui-typedInput-container {
width: 100% ;
}
</style>
</script>
<script type="text/x-red" data-help-name="monogoto-operator" id="monogoto-operator-help"></script>
<script type="text/javascript">
(()=>{
const areSetsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value));
const staticConfigParams = new Set(
[]
);
function getJsonPromise(url) {
return new Promise( function(resolve, reject) {
$.getJSON(url).done(resolve).error(reject);
});
}
function getExampleValueForSpecProps(schema) {
if(schema.type === 'object') {
const retVal = {};
for(const key in schema.properties) {
const val = schema.properties[key];
retVal[key] = getExampleValueForSpecProps(val);
}
return retVal;
} else if(schema.type === 'array') {
return [getExampleValueForSpecProps(schema.items)];
} else {
return schema.hasOwnProperty('example')? schema.example :
schema.type === 'string'? '' :
schema.type === 'number'? 0 :
schema.type === 'boolean'? false :
undefined;
}
}
getJsonPromise('./monogoto-operator/ui-choices')
.then(function ({choices, allParams}) {
const fieldsHtml = `
<div class="main-container">
<div class="form-row">
<label for="node-input-server">monogoto-operator server</label>
<input id="node-input-server" />
</div>
<hr/>
<div class="form-row">
<label for="node-input-api">API</label>
<select id="node-input-api">
${Object.keys(choices).map(key=>'<option value="'+key+'">'+key+'</option>').join('')}
</select>
</div>
<div class="form-row">
<label for="node-input-operation">Operation</label>
<select id="node-input-operation"></select>
</div>
${Object.keys(allParams).map(key=>staticConfigParams.has(key)?'':`
<div class="form-row param" id="param-${key}">
<label for="node-input-param-${key}">${key}</label>
<input type="text" id="node-input-param-${key}" class="param-input">
<input type="hidden" id="node-input-param-${key}-type">
</div>
`).join('')}
<div class="form-row" id="requestBody">
<label for="node-input-requestBody">requestBody</label>
<input type="text" id="node-input-requestBody" class="param-input">
<input type="hidden" id="node-input-requestBody-type">
</div>
`;
$('#monogoto-operator-template').append(document.createTextNode(fieldsHtml));
$('#monogoto-operator-template').append(document.createTextNode(`
<hr/>
<div class="form-row" title="Pass errors on 2nd output instead of catch node">
<label for="node-input-outErrors">Out Errors ➡️</label>
<input type="checkbox" id="node-input-outErrors">
</div>
<hr />
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Node Label</label>
<input type="text" id="node-input-node-label">
</div>
</div>
`));
const container = $('<div>');
container.append($('<p>API(s):</p>'));
const $apis = $('<ul></ul>');
for(const choice of Object.keys(choices)) {
const $api = $(`<li><h3>${choice}</h3></li>`);
const apiMethods = choices[choice];
const $apiMethods = $(`<ul></ul>`);
for(const op of Object.keys(apiMethods)) {
const opInfo = apiMethods[op];
const opId = `${choice}-${op}`;
const $opEl = $(`<li id="${opId}"></li>`);
$opEl.append($(`<h3>${op}</h3>`));
$opEl.append($(`<p>${opInfo.description}</p>`));
const $paramsEl = $(`<dl class="message-properties">`);
for(const paramName of Object.keys(opInfo.parameters)) {
const paramInfo = opInfo.parameters[paramName];
$paramsEl.append($(`
<dt ${paramInfo.required?'':'class="optional"'}>${paramName}</dt>
<dd>${paramInfo.description}</dd>
`));
}
// Request body documentation:
if(opInfo.requestBody && opInfo.requestBody.content && opInfo.requestBody.content["application/json"]) {
const schema = opInfo.requestBody.content["application/json"].schema;
if(!schema) return;
const bodyDoc = JSON.stringify(schema, null, 1);
$paramsEl.append($(`
<dt ${opInfo.requestBody.required?'':'class="optional"'}>requestBody</dt>
<dd>
<pre>${bodyDoc}</pre>
</dd>
`));
}
$opEl.append($paramsEl);
$apiMethods.append($opEl);
}
$apiMethods.appendTo($api);
$apis.append($api);
}
container.append($apis);
$('#monogoto-operator-help').html(container.html());
const defaultApiName = Object.keys(choices)[0];
const defaultOp = Object.keys(choices[defaultApiName])[0];
const defaults = {
"api": {value: defaultApiName},
"operation": {value: defaultOp},
"node-label": {value: undefined},
"requestBody-type": {},
"requestBody": {value:undefined, validate: RED.validators.typedInput('requestBody')},
server: {value:"", type:"monogoto-operator-server"},
outErrors: {value:false},
outputs: {value:1}
};
Object.keys(allParams).forEach(paramName=>{
defaults['param-'+CSS.escape(paramName)+'-type'] = {};
defaults['param-'+CSS.escape(paramName)] = {value:undefined, validate: RED.validators.typedInput(paramName)};
});
RED.nodes.registerType('monogoto-operator', {
category: "Monogoto"
,
color: "#8fcfb0"
,
icon: "monogoto-icon-op.png"
,
label: function () {
return this['node-label'] || this.operation;
},
defaults: defaults,
inputs:1,
outputs:1,
outputLabels: ["success","error"],
oneditsave: function() {
const isOutErrors = $('#node-input-outErrors').prop('checked');
this.outputs = isOutErrors? 2:1;
},
oneditprepare: function() {
const firstOpen = !this.hasOwnProperty('node-label');
const timeOfEditorOpen = new Date();
function eventTriggeredByUserInteraction() {
// After certain milli-seconds, we can be sure an event happened by user interaction.
return new Date()-timeOfEditorOpen > 500;
}
function shouldPutExampleValue() {
return eventTriggeredByUserInteraction() || firstOpen;
}
function focusOnChosenOp() {
try {
const chosenOp = $('#node-input-operation').val();
const chosenApi = $('#node-input-api').val();
const documentationId = `${chosenApi}-${chosenOp}`;
$('#'+documentationId).get(0).scrollIntoView();
} catch(e) {}
}
setTimeout(focusOnChosenOp, 310);
const typesForBodyParam = ['msg', 'global', 'flow', 'str', 'jsonata', 'json'];
$('#node-input-operation').on('change', function (element) {
const $chosenOp = $(element.target).val();
const $chosenApi = $('#node-input-api').val();
const opDef = choices[$chosenApi][$chosenOp];
const paramIds = Object.keys(opDef.parameters).map(item=>{
const selector = '#param-'+CSS.escape(item);
// Setting placeholder for field
const input = $(`${selector} > div > div > input`);
if(opDef.parameters[item].schema && opDef.parameters[item].schema.format) {
input.attr('placeholder', opDef.parameters[item].schema.format);
} else {
input.attr('placeholder', '');
}
return selector;
}).join(',');
$('.form-row.param').addClass('hiddenAttrs').removeClass('visibleAttrs');
$(paramIds).addClass('visibleAttrs').removeClass('hiddenAttrs');
// requestBody
const hasRequestBody = !!opDef.requestBody;
if(hasRequestBody) {
$('#requestBody.form-row').addClass('visibleAttrs').removeClass('hiddenAttrs');
if(shouldPutExampleValue()) {
const jsonContent = opDef.requestBody.content["application/json"];
if(jsonContent && jsonContent.schema) {
const props = jsonContent.schema;
const exampleJson = getExampleValueForSpecProps(props);
setTimeout(()=>$("#node-input-requestBody").typedInput('value', JSON.stringify(exampleJson)));
}
}
} else {
$('#requestBody.form-row').addClass('hiddenAttrs').removeClass('visibleAttrs');
}
focusOnChosenOp();
// Set typed input
const inputParamsForOp = $(paramIds).find('input.param-input');
inputParamsForOp.each((index, element)=>{
const paramName = element.id.split('node-input-param-')[1];
const paramInfo = opDef.parameters[paramName];
let types = paramInfo.in === 'body'? typesForBodyParam:
['msg', 'global', 'flow', 'jsonata', 'num', 'str'];
let defaultType;
if(paramInfo.schema && paramInfo.schema.enum) {
const EnumType = {
value:'enum',
label:'enum',
options: paramInfo.schema.enum
};
types = types.concat([EnumType]);
defaultType = 'enum';
} else {
defaultType = types[types.length-1];
}
const $el = $(element);
if(!element.types) { // If first time declaring typedInput on this field
$el.typedInput({
default: defaultType,
types: types,
typeField: $(`input#${CSS.escape(element.id)}-type`)
});
} else { // if not first type declaring typed input, check if types changed, if so, update typedInput options.
const oldTypesSet = new Set(element.types);
const newTypesSet = new Set(types);
if(!areSetsEqual(oldTypesSet, newTypesSet)) {
$el.typedInput('types', types);
$el.typedInput('type', defaultType);
}
}
element.types = types; // remembering previous types
// Setting initial example value
if(shouldPutExampleValue() && paramInfo.schema) {
const exampleValue = getExampleValueForSpecProps(paramInfo.schema);
setTimeout(()=>{
if(exampleValue || exampleValue === 0) {
$el.typedInput('value', exampleValue);
} else if($el.typedInput('type') === 'enum' && paramInfo.schema.enum && paramInfo.schema.enum.length) {
$el.typedInput('value', paramInfo.schema.enum[0]);
}
});
}
});
});
$('#node-input-api').on('change', function (element) {
const chosenApi = $(element.target).val();
const optionsHtml = `
${Object.keys(choices[chosenApi]).map(key=>'<option value="'+key+'">'+key+'</option>').join('')}
`;
$('#node-input-operation').html(optionsHtml);
$('#node-input-operation').trigger('change');
});
$('#node-input-api').trigger('change');
if(this.hasOwnProperty('operation')) {
setTimeout(() => {
$('#node-input-operation').val(this.operation);
$('#node-input-operation').trigger('change');
}, 0);
}
$('input#node-input-requestBody').typedInput({
default: typesForBodyParam[typesForBodyParam.length-1],
types: typesForBodyParam,
typeField: $(`input#node-input-requestBody-type`)
});
}
});
}, function (reason) {
console.error("failed to load monogoto-operator " + reason);
});
})();
</script>
<script type="text/javascript">
(()=>{
function findLogElements() {
return {
request: {
checkboxEnable: $('#log-request'),
optionsDiv: $('#log-request-options'),
url: $('#log-request-url'),
headers: $('#log-request-headers'),
body: $('#log-request-body'),
},
response: {
checkboxEnable: $('#log-response'),
optionsDiv: $('#log-response-options'),
headers: $('#log-response-headers'),
body: $('#log-response-body'),
}
};
}
RED.nodes.registerType('monogoto-operator-server',{
category: 'config',
defaults: {
name: {value: undefined},
host: {value: "console.monogoto.io", required:true},
"host-type": {value:"str"},
username: {value:undefined},
'username-type': {value:"str"},
password: {value:undefined},
'password-type': {value:"str"},
logging: {value:undefined},
loggingCfgFromEnvVar: {value:''}
},
label: function() {
return this.name || this.host;
},
oneditprepare: function() {
$('input#node-config-input-host').typedInput({
default: ['str'],
types: ['str', 'env'],
typeField: $(`input#node-config-input-host-type`)
});
$('input#node-config-input-username').typedInput({
default: ['str'],
types: ['str', 'env'],
typeField: $(`input#node-config-input-username-type`)
});
$('input#node-config-input-password').typedInput({
default: ['str'],
types: ['str', 'env'],
typeField: $(`input#node-config-input-password-type`)
});
const logElements = findLogElements();
logElements.request.checkboxEnable.change(function(){
logElements.request.optionsDiv[this.checked?'removeClass':'addClass']('hidden');
})
logElements.response.checkboxEnable.change(function(){
logElements.response.optionsDiv[this.checked?'removeClass':'addClass']('hidden');
})
logElements.request.checkboxEnable.prop('checked', this?.logging?.request != null);
logElements.request.url.prop('checked', this?.logging?.request?.url === true);
logElements.request.headers.prop('checked', this?.logging?.request?.headers === true);
logElements.request.body.prop('checked', this?.logging?.request?.body === true);
logElements.response.checkboxEnable.prop('checked', this?.logging?.response != null);
logElements.response.headers.prop('checked', this?.logging?.response?.headers === true);
logElements.response.body.prop('checked', this?.logging?.response?.body === true);
$('#node-logging-format').on('click',()=>{
alert('ENV VAR contents: json string with the following format:\r\n'+
JSON.stringify({
request: {
url:true,
body:true,
headers:true
},
response: {
body:true,
headers:true
}
},null,2));
});
// Refresh UI State
logElements.request.checkboxEnable.trigger('change')
logElements.response.checkboxEnable.trigger('change')
},
oneditsave: function() {
const logElements = findLogElements();
const enableReqLogging = logElements.request.checkboxEnable.prop('checked');
const enableResLogging = logElements.response.checkboxEnable.prop('checked');
if(!this.logging) this.logging = {};
if(enableReqLogging) {
this.logging.request = {
url: logElements.request.url.prop('checked'),
headers: logElements.request.headers.prop('checked'),
body: logElements.request.body.prop('checked'),
}
} else {
delete this.logging.request;
}
if(enableResLogging) {
this.logging.response = {
headers: logElements.response.headers.prop('checked'),
body: logElements.response.body.prop('checked'),
}
} else {
delete this.logging.response;
}
}
});
})()
</script>
<script type="text/x-red" data-template-name="monogoto-operator-server">
<style>
.hidden {
display:none
}
</style>
<div class="form-row">
<label for="node-config-input-host"><i class="fa fa-server"></i> Host</label>
<input type="text" id="node-config-input-host">
<input type="hidden" id="node-config-input-host-type">
</div>
<div class="form-row">
<label for="node-config-input-username"><i class="fa fa-user"></i> Username</label>
<input type="text" id="node-config-input-username">
<input type="hidden" id="node-config-input-username-type">
</div>
<div class="form-row">
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
<input type="password" id="node-config-input-password">
<input type="hidden" id="node-config-input-password-type">
</div>
<hr />
<h2>Logging</h4>
<div class="form-row">
<label for="log-request">Request</label>
<input type="checkbox" id="log-request">
</div>
<div id="log-request-options">
<div class="form-row">
<label for="log-request-url"> - URL</label>
<input type="checkbox" id="log-request-url">
</div>
<div class="form-row">
<label for="log-request-headers"> - Headers</label>
<input type="checkbox" id="log-request-headers">
</div>
<div class="form-row">
<label for="log-request-body"> - Body</label>
<input type="checkbox" id="log-request-body">
</div>
</div>
<div class="form-row">
<label for="log-response">Response</label>
<input type="checkbox" id="log-response">
</div>
<div id="log-response-options">
<div class="form-row">
<label for="log-response-headers"> - Headers</label>
<input type="checkbox" id="log-response-headers">
</div>
<div class="form-row">
<label for="log-response-body"> - Body</label>
<input type="checkbox" id="log-response-body">
</div>
</div>
<div class="form-row" style="display:flex;align-items:center">
<button id="node-logging-format">show format</button>
<label style="margin-left:5pt; width:90pt" for="node-config-input-loggingCfgFromEnvVar">Log config from env-var:</label>
<input style="margin-left:5pt; flex-grow:1" type="text" id="node-config-input-loggingCfgFromEnvVar">
</div>
<hr />
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name">
</div>
</script>