That's looking much better Steve, thanks for that.
Updated version of my uibuilder flow:
[{"id":"db553021.e64ac","type":"uibuilder","z":"63281c77.40a064","name":"uib cron","topic":"","url":"cron","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"showfolder":false,"x":170,"y":2920,"wires":[["478c4836.d0fa98","b28661ee.2f57b"],[]]},{"id":"755458a1.0e2c28","type":"function","z":"63281c77.40a064","name":"describe cmd","func":"msg.payload = {\n \"command\": \"describe\",\n \"expression\": msg.payload\n}\nreturn msg;","outputs":1,"noerr":0,"x":200,"y":3220,"wires":[["29d47dae.6be632"]]},{"id":"29d47dae.6be632","type":"cronplus","z":"63281c77.40a064","name":"","outputField":"payload","timeZone":"","options":[],"x":390,"y":3220,"wires":[["5487012f.84a5e"]]},{"id":"478c4836.d0fa98","type":"switch","z":"63281c77.40a064","name":"request type","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"describe","vt":"str"},{"t":"eq","v":"addJob","vt":"str"},{"t":"eq","v":"refresh","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":4,"x":350,"y":2920,"wires":[["a768cfd6.d5677","dcc724ce.033568"],["ce00103f.456d5","70525ed3.3878c"],["ce00103f.456d5","70525ed3.3878c"],[]],"outputLabels":["describe","add","refresh","other"]},{"id":"2dfd1241.3fee8e","type":"link in","z":"63281c77.40a064","name":"describe-cron","links":["a768cfd6.d5677"],"x":75,"y":3220,"wires":[["755458a1.0e2c28"]]},{"id":"a768cfd6.d5677","type":"link out","z":"63281c77.40a064","name":"uib-cron-describe-out","links":["2dfd1241.3fee8e"],"x":515,"y":2880,"wires":[]},{"id":"5487012f.84a5e","type":"link out","z":"63281c77.40a064","name":"cron-describe-out","links":["6f114bd6.d008d4"],"x":545,"y":3220,"wires":[]},{"id":"6f114bd6.d008d4","type":"link in","z":"63281c77.40a064","name":"uib-cron-in","links":["5487012f.84a5e","dee69c83.8a411","a9a1ff52.62be1"],"x":75,"y":2920,"wires":[["db553021.e64ac"]]},{"id":"e51df78.11b3408","type":"comment","z":"63281c77.40a064","name":"==== ---- ==== ---- uibuilder cron-plus example ---- ==== ---- ====","info":"","x":290,"y":2820,"wires":[]},{"id":"ce00103f.456d5","type":"link out","z":"63281c77.40a064","name":"uib-cron-add-out","links":["56cfdd0a.d0ee94"],"x":515,"y":2920,"wires":[]},{"id":"30c625f2.1c648a","type":"cronplus","z":"63281c77.40a064","name":"","outputField":"payload","timeZone":"","options":[],"x":180,"y":3060,"wires":[["591fda51.4b4e84","293616a5.b66aca"]]},{"id":"56cfdd0a.d0ee94","type":"link in","z":"63281c77.40a064","name":"cron-input","links":["ce00103f.456d5"],"x":75,"y":3060,"wires":[["30c625f2.1c648a"]]},{"id":"591fda51.4b4e84","type":"switch","z":"63281c77.40a064","name":"1=Cron Event 2=List Results ","property":"cronplus.triggerTimestamp","propertyType":"msg","rules":[{"t":"nnull"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":400,"y":3060,"wires":[["b2ed28f2.18e348"],["40f2cd30.5e41a4","b2a4c86.5f63e38"]]},{"id":"f6abacae.6147d","type":"debug","z":"63281c77.40a064","name":"DO SOMETHING USEFULL","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":900,"y":3020,"wires":[]},{"id":"40f2cd30.5e41a4","type":"function","z":"63281c77.40a064","name":"Make listJobs table","func":"msg.originalTopic = msg.topic\nmsg.topic = 'listJobs'\n\nif(!msg.payload || !msg.payload.result || !msg.payload.result.length){\n msg.payload = [];\n return msg;\n}\n\nvar result = [];\n\nmsg.payload.result.forEach(function(job){\n node.warn(job)\n result.push({\n name: job.config.name,\n expression: job.config.expression,\n payload: job.config.payload,\n description: job.status.description,\n nextRun: job.status.nextDescription,\n nextDate: job.status.nextDate,\n state: job.status.isRunning,\n })\n})\n\nmsg.payload = result\n\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":3080,"wires":[["a9a1ff52.62be1"]]},{"id":"b2ed28f2.18e348","type":"change","z":"63281c77.40a064","name":"trigger","rules":[{"t":"set","p":"originalTopic","pt":"msg","to":"topic","tot":"msg"},{"t":"set","p":"topic","pt":"msg","to":"trigger","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":3040,"wires":[["f6abacae.6147d","a9a1ff52.62be1"]]},{"id":"a9a1ff52.62be1","type":"link out","z":"63281c77.40a064","name":"cron-trigger-out","links":["6f114bd6.d008d4"],"x":795,"y":3060,"wires":[]},{"id":"81955350.e8a99","type":"catch","z":"63281c77.40a064","name":"catch uib-cron+","scope":["db553021.e64ac","755458a1.0e2c28","29d47dae.6be632","478c4836.d0fa98","2dfd1241.3fee8e","a768cfd6.d5677","2482e719.803068","58877c4f.ce50f4","bfda6f87.ffbb1","7c319937.5dc158","5487012f.84a5e","6f114bd6.d008d4","c2da6d69.e515","3f74672c.cf13c8","e51df78.11b3408","ce00103f.456d5","30c625f2.1c648a","56cfdd0a.d0ee94","591fda51.4b4e84","f6abacae.6147d","40f2cd30.5e41a4","b2ed28f2.18e348","a9a1ff52.62be1"],"uncaught":false,"x":620,"y":2820,"wires":[["e2580d45.3036c"]]},{"id":"e2580d45.3036c","type":"debug","z":"63281c77.40a064","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":790,"y":2820,"wires":[]},{"id":"b28661ee.2f57b","type":"debug","z":"63281c77.40a064","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":330,"y":2860,"wires":[]},{"id":"70525ed3.3878c","type":"debug","z":"63281c77.40a064","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":610,"y":2920,"wires":[]},{"id":"dcc724ce.033568","type":"debug","z":"63281c77.40a064","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":610,"y":2880,"wires":[]},{"id":"293616a5.b66aca","type":"debug","z":"63281c77.40a064","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":330,"y":3020,"wires":[]},{"id":"b2a4c86.5f63e38","type":"debug","z":"63281c77.40a064","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":610,"y":3100,"wires":[]}]
index.html
<!doctype html><html lang="en"><head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
<title>uibuilder cron-plus example</title>
<meta name="description" content="Node-RED UI Builder - cron-plus example">
<link rel="icon" href="./images/node-blue.ico">
<link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" />
<link rel="stylesheet" href="./index.css" media="all">
</head><body>
<script type="text/x-template" id="cron-input-template">
<div>
<b-form inline>
<b-form-input
id="input-name"
v-model="inputName"
type="text"
required autofocus
placeholder="Name for this job"
></b-form-input>
<b-form-input
id="input-expression"
v-model="inputExpression"
type="text"
required
placeholder="CRON expression"
></b-form-input>
<b-form-input
id="input-payload"
v-model="inputPayload"
type="text"
required
placeholder="Output payload"
></b-form-input>
<b-button @click="addJob">Add</b-button>
<b-button @click="refresh">Refresh</b-button>
</b-form>
<b-card>
Expression Description ... <br>
<b>{{expressionDescription}}</b>
<h5>Next</h5>
<div>Date: {{nextDate}}</div>
<div>Description: {{nextDescription}}</div>
</b-card>
<!-- <b-card><pre>{{testOutput}}</pre></b-card> -->
</div>
</script>
<script type="text/x-template" id="cron-schedules-template">
<div>
<b-table-lite :items="schedules" small hover striped caption-top head-variant="dark">
<template v-slot:table-caption>Current Schedules</template>
</b-table-lite>
<div v-html="lastTrigger"></div>
</div>
</script>
<script type="text/x-template" id="cron-help-template">
<div>
<h5 v-b-toggle.collapse-1 tabindex="0" role="button" class="pointer">
⇵
Help
</h5>
<b-collapse id="collapse-1" class="mt-2">
<b-card>
<pre>{{cronExpression}}</pre>
<ul>
<li>* = All values</li>
<li>, = List of expressions, e.g. 4,5,15.</li>
<li>/ = Increments of a range. e.g. */3 (every 3 min/hours/etc).</li>
<li>- = Range, e.g. 20019-2024.</li>
<li>L = Last day of month, or with day of week, last of those days of the month. e.g. WedL, last Wed of the month.</li>
<li>W = Nearest weekday (Mon-Fri) of given day of month.</li>
<li># = Nth of month. e.g. Tue#3 is the 3rd Tue of the month.</li>
</ul>
<b-table-lite :items="cronEgs" small hover striped caption-top head-variant="dark">
<template v-slot:table-caption>Example CRON Expressions</template>
</b-table-lite>
</b-card>
</b-collapse>
</div>
</script>
<div id="app" v-cloak>
<b-container id="app_container">
<h1>
UIbuilder - Cron-Plus front-end
</h1>
<p>
This is a <a href="https://github.com/TotallyInformation/node-red-contrib-uibuilder">uibuilder</a> example of allowing end-users to build cron schedules with the help of node-red-contrib-cron-plus.
</p>
<cron-input></cron-input>
<cron-schedules></cron-schedules>
<cron-help></cron-help>
</b-container>
</div>
<script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
<script src="../uibuilder/vendor/vue/dist/vue.min.js"></script> <!-- dev version with component compiler -->
<script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>
<script src="./uibuilderfe.min.js"></script> <!-- //prod version -->
<script src="./index.js"></script>
</body></html>
index.js
/* jshint browser: true, esversion: 5, asi: true */
/*globals Vue, uibuilder */
// @ts-nocheck
/*
Copyright (c) 2019 Julian Knight (Totally Information)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict'
/** @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods */
// Global Template Components
Vue.component('cron-input', {
props: [],
template: '#cron-input-template',
data: function() { return {
inputName: '',
inputPayload: '',
inputExpression: '* * * * *', // If you update this, change the description too
expressionDescription: 'Every minute',
nextDate: '',
nextDescription: '',
testOutput: '',
}},
computed: {
// inputDescription: function() {
// console.log(this.inputName + '/' + this.inputExpression + '/' + this.inputPayload)
// return this.inputName + '/' + this.inputExpression + '/' + this.inputPayload
// },
},
methods: {
addJob: function() {
console.log('addJob', this.inputName, this.inputExpression, this.inputPayload)
uibuilder.send({
'topic': 'addJob',
'payload': {
'name': this.inputName,
'expression': this.inputExpression,
'payload': this.inputPayload,
'command': 'add',
'type': 'str',
},
})
},
refresh: function() {
console.log('refresh', this.inputName, this.inputExpression, this.inputPayload)
uibuilder.send({
'topic': 'refresh',
'payload': {
'command': 'list-all',
},
})
},
},
watch: {
/** Whenever inputExpression changes, ask cron-plus for the description */
inputExpression: function() {
//console.log(this.inputExpression)
uibuilder.send({
'topic': 'describe',
'payload': this.inputExpression
})
},
},
mounted: function() {
var that = this
uibuilder.onChange('msg', function(msg){
//console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)
if ( msg.topic === 'describe' ) {
that.expressionDescription = msg.payload.result.description
that.nextDate = msg.payload.result.nextDate
that.nextDescription = msg.payload.result.nextDescription
console.log('DESCRIBE:', msg.payload)
} else if ( msg.topic === 'test' ) {
console.log('test', msg.payload)
that.testOutput = msg.payload
}
})
},
})
Vue.component('cron-schedules', {
props: [],
template: '#cron-schedules-template',
data: function() { return {
schedules: [],
lastTrigger: '',
}},
computed: {
},
methods: {
},
mounted: function() {
var that = this
uibuilder.onChange('msg', function(msg){
if ( msg.topic === 'trigger' ) {
console.log('trigger', msg)
var triggerTs = new Date(msg.cronplus.triggerTimestamp)
that.lastTrigger = `Payload <b>${msg.payload}</b> triggered at <i>${triggerTs.toLocaleString()}</i> by job <i>${msg.cronplus.config.name}</i>`
} else if ( msg.topic === 'listJobs' ) {
console.log('listJobs', msg)
that.schedules = msg.payload
}
})
},
})
Vue.component('cron-help', {
props: [],
template: '#cron-help-template',
data: function() { return {
cronEgs: [
{'expression': '* * * * * *', 'description': 'Every Second',},
{'expression': '0 * * * * *', 'description': 'Every minute',},
{'expression': '0 */10 * * * *', 'description': 'Every 10 minutes',},
{'expression': '0 */20 1 * * *', 'description': 'Every 20 minutes, between 01:00 AM and 01:59 AM',},
{'expression': '0 15,30,45 * * * *', 'description': 'At 15, 30, and 45 minutes past the hour',},
{'expression': '0 0 12 * * *', 'description': 'Every day at noon - 12pm',},
{'expression': '0 0 2 29 FEB * 2020-2040', 'description': 'At 02:00 AM, on day 29 of the month, only in February, every 4 years, 2020 through 2040',},
{'expression': '0 0 7 * * MON#1 *', 'description': 'At 07:00 AM, on the first Monday of the month',},
{'expression': '0 0 12 * JAN,FEB,MAR,APR *', 'description': 'Every day at noon in January, February, March and April',},
{'expression': '* * 1W * *', 'description': 'Every minute, on the first weekday of the month',},
{'expression': '* * * * Tue#', 'description': 'Every minute, on the third Tuesday of the month',},
{'expression': '0 12 * * ', 'description': 'At 12:00 PM, on the last Monday of the month',},
],
cronExpression: `
* * * * * * * Field Allowed values Special symbols
| | | | | | | ----------------- --------------- ---------------
\--|--|--|--|--|--|-> Second (optional) 0-59 * / , -
\--|--|--|--|--|-> Minute 0-59 * / , -
\--|--|--|--|-> Hour 0-23 * / , -
\--|--|--|-> Day of Month 1-31 * / , - L W
\--|--|-> Month 1-12 or JAN-DEC * / , -
\--|-> Day of Week 0-7 or SUN-SAT * / , - L #
\-> Year (optional) 1970-2099 * / , -
`,
}},
computed: {
},
methods: {
},
})
// eslint-disable-next-line no-unused-vars
var app1 = new Vue({
el: '#app',
data: {
}, // --- End of data --- //
computed: {
}, // --- End of computed --- //
methods: {
}, // --- End of methods --- //
// Available hooks: init,mounted,updated,destroyed
mounted: function(){
//console.debug('[indexjs:Vue.mounted] app mounted - setting up uibuilder watchers')
/** **REQUIRED** Start uibuilder comms with Node-RED @since v2.0.0-dev3
* Pass the namespace and ioPath variables if hosting page is not in the instance root folder
* e.g. If you get continual `uibuilderfe:ioSetup: SOCKET CONNECT ERROR` error messages.
* e.g. uibuilder.start('/nr/uib', '/nr/uibuilder/vendor/socket.io') // change to use your paths/names
*/
uibuilder.start()
var vueApp = this
uibuilder.onChange('msg', function(msg){
//console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)
vueApp.msgRecvd = msg
})
} // --- End of mounted hook --- //
}) // --- End of app1 --- //
// EOF
Bedtime!