UNPKG

node-red-contrib-ui-state-trail

Version:

A Node-RED dashboard ui node. Trail of state changes over time

599 lines (533 loc) 24.3 kB
<!-- MIT License Copyright (c) 2019 hotNipi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> <script type='text/html' data-template-name='ui_statetrail'> <div class='form-row' id='template-row-group'> <label for='node-input-group'><i class='fa fa-table'></i> Group</span></label> <input type='text' id='node-input-group'> </div> <div class='form-row' id='template-row-size'> <label><i class='fa fa-object-group'></i> Size</span></label> <input type='hidden' id='node-input-width'> <input type='hidden' id='node-input-height'> <button class='editor-button' id='node-input-size'></button> </div> </br> <div class='form-row' id='node-input-el-label'> <label for='node-input-label'><i class='icon-tag'></i> Label</label> <input type='text' id='node-input-label' placeholder='State trail'> </div> <div class="form-row" id="period-show"> <label for="node-input-periodLimit">Period</label> <input type="text" id="node-input-periodLimit" style="width:60px;"> <select id="node-input-periodLimitUnit" style="width:126px;"> <option value="1">seconds</option> <option value="60">minutes</option> <option value="3600">hours</option> </select> </div> <div class="form-row" id="time-label-show"> <label for="node-input-timeformat">Time Format</label> <input type="text" id="node-input-timeformat" style="width:100px;"> <label for='node-input-tickmarks' style='width:33px'>Ticks</label> <input type='number' id='node-input-tickmarks' min='2' max='25' value='4' size='1' style='width:47px' > </div> <div class='form-row' id='node-input-exact-time-ticks'> <label for='node-input-exactticks'><i class='icon-tag'></i> Exact Time Ticks</label> <input type="checkbox" id="node-input-exactticks" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:5px;margin-left:5px;">Marks the state changes with exact time ticks (ticks count will be ignored) </div> <div class="form-row" style="margin-bottom:0;"> <label><i class="fa fa-list"></i> States:</label> <span id="configError" style="color:#DD0000"><b>All Values must be unique.</b></span> </div> <div class="form-row node-input-rule-container-row"> <ol id="node-input-rule-container"></ol> </div> </br> <div class='form-row' id="node-input-legendchoice"> <label for='node-input-legend'><i class='icon-tag'></i> Legend</label> <select id='node-input-legend' style='width:200px !important'> <option value=0>No legend</option> <option value=1>Show all states</option> <option value=2>Show current states</option> <option value=3>Show latest state</option> </select> </div> <div class='form-row' id='node-input-persistance'> <label for='node-input-persist'><i class='icon-tag'></i> Data Storage</label> <input type="checkbox" id="node-input-persist" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:5px;margin-left:5px;">Use persistable storage if available </div> <div class='form-row' id='node-input-performance'> <label for='node-input-combine'><i class='icon-tag'></i> Performance</label> <input type="checkbox" checked id="node-input-combine" style="display:inline-block; width:auto; vertical-align:baseline; margin-right:5px;margin-left:5px;">Combine similar states </div> <div class='form-row' id='node-input-bl-label'> <label for='node-input-blanklabel'><i class='icon-tag'></i> Blank label</label> <input type='text' id='node-input-blanklabel' placeholder='Waiting data'> </div> <div class='form-row'> <label for='node-input-name'><i class='icon-tag'></i> Name</label> <input type='text' id='node-input-name' placeholder='Name'> </div> </script> <script type='text/javascript'> RED.nodes.registerType('ui_statetrail', { category: 'dashboard', color: 'rgb( 63, 173, 181)', configError: '', defaults: { group: {type: 'ui_group', required: true}, order: {value: 0}, width: { value: 0, validate: function (v) { var valid = true var width = v || 0; var currentGroup = $('#node-input-group').val() || this.group; var groupNode = RED.nodes.node(currentGroup); valid = !groupNode || +width <= +groupNode.width; $('#node-input-size').toggleClass('input-error', !valid); return valid; } }, height: {value: 0}, name: {value: ''}, label: {value: ''}, states: { value: [{ state: true, col: "#009933", t: "bool", label: '' }, { state: false, col: "#999999", t: "bool", label: '' }], validate: function (v) { var unique = new Set(v.map(function(o) {return o.state;})); return v.length == unique.size && v.length > 1; } }, periodLimit: { value: 1, validate: RED.validators.number(), required: true }, periodLimitUnit: {value: '3600',required: true}, timeformat: {value: 'HH:mm:ss'}, tickmarks: {val: 4, required: true}, exactticks: {val: false, required: true}, persist: {val: false, required: true}, legend: { value: 1, validate: RED.validators.number(), required: true }, combine: {val: true, required: true}, blanklabel: {value: ''} }, inputs: 1, outputs: 1, icon: 'statetrail.png', paletteLabel: 'state-trail', label: function () { return this.name || 'State-trail'; }, oneditprepare: function () { var lastRuleType = "bool" function getColor(idx) { var colors = ['#009933', '#999999', '#ff6666', '#009999', '#cccc00', '#ff33cc', '#cc6600', '#99ff66', '#660033' ] if (idx > colors.length - 1) { return colors[Math.floor(Math.random() * colors.length)] } return colors[idx] } function setLastType(t){ lastRuleType = t; } function defaultRule(idx) { return { state: lastRuleType == 'bool' ? true : "", col: getColor(idx), t:lastRuleType, label: '' } } $('#node-input-size').elementSizer({ width: '#node-input-width', height: '#node-input-height', group: '#node-input-group' }); if (this.tickmarks === undefined) { this.tickmarks = 4; $('#node-input-tickmarks').val('4'); } if (this.exactticks === undefined) { $('#node-input-exactticks').prop('checked', false); } if (this.legend === undefined) { this.legend = 1; $('#node-input-legend').val('1'); $('#node-input-legend').val('Show all states'); } if (this.blanklabel === undefined) { this.blanklabel = ''; $('#node-input-blanklabel').val(''); } if (this.persist === undefined) { $("#node-input-persist").prop('checked', false); } if (this.combine === undefined) { $("#node-input-keepall").prop('checked', true); } var oval = $("#node-input-timeformat").val(); if (!oval) { $("#node-input-timeformat").val("HH:mm:ss"); } var odef = 'HH:mm:ss'; if (oval === "HH:mm:ss") { odef = oval; } if (oval === "HH:mm") { odef = oval; } if (oval === "HH") { odef = oval; } if (oval === "mm:ss") { odef = oval; } if (oval === "mm") { odef = oval; } if (oval === "ss") { odef = oval; } var ohms = { value: "HH:mm:ss", label: "HH:mm:ss", hasValue: false }; var ohm = { value: "HH:mm", label: "HH:mm", hasValue: false }; var oh = { value: "HH", label: "HH", hasValue: false }; var oms = { value: "mm:ss", label: "mm:ss", hasValue: false }; var om = { value: "mm", label: "mm", hasValue: false }; var os = { value: "ss", label: "ss", hasValue: false }; $("#node-input-timeformat").typedInput({ default: odef, types: [ohms, ohm, oh, oms, om, os] }); $("#node-input-rule-container").editableList({ removable: true, header: $("<div>").css({ padding: "6px" }).append($.parseHTML(` <div style='width:120px; text-align:center; display: inline-grid'><b>State</b></div> <div style='width:80px; text-align:center; display: inline-grid'><b>Color</b></div> <div style='width:160px; text-align:center; display: inline-grid'><b>Label</b></div> `)), addItem: function (container, index, rule) { if (!rule.hasOwnProperty('state')) { rule = defaultRule(index) } //console.log(rule) container.css({ overflow: 'hidden', whiteSpace: 'nowrap' }); var row = $('<div/>').appendTo(container); var propField var colorField var labelField propField = $('<input/>', { class: "node-input-rule-state-value", style: "width:120px;", type: "text", value:rule.state }).appendTo(row).typedInput({types: ['bool', 'str', 'num'], default:rule.t}); colorField = $('<input/>', { value: rule.col, class: "node-input-rule-state-color", style: "width:80px; margin-left:8px;", type: "color" }).appendTo(row) labelField = $('<input/>', { class: "node-input-rule-state-label", style: "width:160px; margin-left:8px;", type: "text", value:rule.label, placeholder: 'optional' }).appendTo(row).typedInput({types: ['str']}); propField.typedInput().on('change', function (type, value) { validate() setLastType(value) }); validate() }, removeItem: function (data) { validate() } }); if (!this.states) { var rbt = { state: true, col: "#009933", t: 'bool', label: '' }; var rbf = { state: false, col: "#999999", t: 'bool', label: '' }; this.states = [rbt, rbf]; } for (var i = 0; i < this.states.length; i++) { var rule = this.states[i]; $("#node-input-rule-container").editableList('addItem', rule); } function validate(){ var states = $("#node-input-rule-container").editableList('items'); var node = this; node.states = []; var convert = function (rule) { if (rule.t === 'str') { return rule } if (rule.t === 'bool') { rule.state = rule.state == "true" ? true : false } if (rule.t == 'num') { rule.state = parseFloat(rule.state) } return rule } states.each(function (i) { var rule = $(this); var st = rule.find(".node-input-rule-state-value").typedInput('value') var tp = rule.find(".node-input-rule-state-value").typedInput('type') var co = rule.find(".node-input-rule-state-color").val(); var lb = rule.find(".node-input-rule-state-label").typedInput('value') var r = convert({ state: st, col: co, t: tp, label: lb }) node.states.push(r); }); var unique = new Set(node.states.map(function(o) {return o.state;})); if (node.states.length != unique.size) { $("#configError").text("Configured states must be unique") $("#configError").show(); $('.red-ui-editableList-border').toggleClass('input-error', true); } else if(node.states.length < 2){ $("#configError").text("Configure at least two states") $("#configError").show(); $('.red-ui-editableList-border').toggleClass('input-error', true); } else { // valid $("#configError").hide(); $('.red-ui-editableList-border').toggleClass('input-error', false); } } if(this.states){ validate(); } }, oneditsave: function () { $("#node-input-timeformat").val($("#node-input-timeformat").typedInput('type')); var states = $("#node-input-rule-container").editableList('items'); var node = this; node.states = []; var convert = function (rule) { if (rule.t === 'str') { return rule } if (rule.t === 'bool') { rule.state = rule.state == "true" ? true : false } if (rule.t == 'num') { rule.state = parseFloat(rule.state) } return rule } states.each(function (i) { var rule = $(this); var st = rule.find(".node-input-rule-state-value").typedInput('value') var tp = rule.find(".node-input-rule-state-value").typedInput('type') var co = rule.find(".node-input-rule-state-color").val(); var lb = rule.find(".node-input-rule-state-label").typedInput('value') var r = convert({ state: st, col: co, t: tp, label: lb }) node.states.push(r); }); // console.log(node.states) } }); </script> <script type='text/x-red' data-help-name='ui_statetrail'> <h3>Description</h3> <p>Node-RED dashboard widget. Gantt type chart to visualize state changes over time period.<p> </br> <h3>Configuration</h3> <h2>Label</h2> <p>To show the label, configure widget height to 2 units. <h2>Period</h2> <p>Time period. If configured to long period, keep input rate low. Too much data may harm performance significantly. <p>Configured period can be overrided by using <code>control.period</code> property. The value for period should be given in milliseconds. <p>For example to set period to 5 minutes, send <code>msg.control = {period:300000}</code> <h2>Time format and Ticks</h2> <p>Choose format of time and count of tick marks. If the "Exact Ticks" is selected, the state change will be marked with the exact time and the ticks count will be ignored. <h2>States</h2> <p>Configure at least 2 states. Type of state can be <code>string</code>, <code>number</code> or <code>boolean</code>. States can be configured with mixing the types. States <code>true</code> (boolean) and <code>"true"</code> (string) treated as different states. <p>Optionally you can configure label for each state. If configured, the legend shows the label instead of state. <h2>Blank label</h2> <p>Text to show when there is not yet enough data to display the chart. <h2>Legend</h2> <p>To show the legend, the widget height must be configured to 2 units. Legend can be configured for 3 modes. It even shows information for all the configured states or for states currently in timeline or only for latest state. <p> By clicking the legend on dashboard, you can toggle between names and summary. Summary shows aggregated time or percentage for each state. <h2>Combine similar states</h2> <p>By default, the node combines consecutive states if they have same value. You could turn this behavior off, if you wanted to present your data as it was provided. Doing so, the consecutive similar states will be splitted with thin lines, and all those states will be individually clickable. If splitting the consecutive similar states is not intentional or if you don't use click option, it is recommended to keep this option selected. For large amounts of data, combining the states helps to gain performance. </br> </br> <h3>Input</h3> <dl class="message-properties"> <dt>payload <span class="property-type">Boolean|String|Number|Object|Array</span> </dt> <dd> msg.payload should carry single value of one of configured states. <code>msg.payload = true</code> </dd> <br> <dt>reference <span class="property-type">String|Number|Object|Array</span> </dt> <dd> msg.reference can be any data related to the state. <code>msg.reference = "This is reference of the state." </code> </dd> <br> <dd> If you want to use the widget to show historical data, you need to pass in every state together with its timestamp. <code>msg.payload = {state:true,timestamp:1579362774639}</code> </dd> <br> <dd> Historical data can be also feed in within an array <code>msg.payload = [{state:true,timestamp:1579362774639},</code><code>{state:false,timestamp:1579362795665},</code><code>{state:true,timestamp:1579362895432}]</code> </dd> <br> <dd> Note, that feeding data in array will clear previous set of data! </dd> <br> <dd> To clear the the data, send an empty array <code>msg.payload = []</code> </dd> </dl> </br> <h2>Output</h2> <p>By clicking the chart bar, the widget sends message. Output msg contains clicked state in <code>msg.payload</code> and coordinates of click in <code>msg.event</code>. The order of values in <code>event.bbox</code> is <code>left bottom right top</code> <h2>Data Storage</h2> <p>After a full re-deploy, Node-RED restart or system reboot, the node will lose it's saved chart data, unless the user has selected the 'Data Storage' option (checkbox) AND enabled a persistent form of context storage in the Node-RED settings.js file. In that case, the node will attempt to restore the data as it was prior to the re-deploy, restart or reboot. <p>See the node-RED user guide to configure persistent storage - <a href="https://nodered.org/docs/user-guide/context#saving-context-data-to-the-file-system">https://nodered.org/docs/user-guide/context#saving-context-data-to-the-file-system</a> <br> <h3>Change the configuration at runtime</h3> <p>Some options of widget configuration can be overrided at runtime by using the <code>msg.control</code> property <h2>Change configured period</h2> Configured period can be overrided by using <code>control.period</code> property. The value for period should be given in milliseconds. For example to set period to 5 minutes, send <code> msg.control = {period:300000} </code> <h2>Change the states</h2> <p>All states can be overrided by using <code>control.states</code> property. Note that you can't adjust or change any of configured states individually. <p><b>All states must be unique!</b> <p>New states expected to be sent in <code>array</code> of <code>objects</code> <pre> var s = [ {state:1 ,col:"#009933", t:"num", label:'ONE'}, {state:'two', col:"#999999", t:"str", label:'TWO'}, {state:false, col:"#00FF99", t:"bool", label:'THREE'} ] msg.control = {states:s} </pre> Where each <code>object</code> must have all following properties <pre> state - (number,string,boolean) the state identifier col - (hex string) the color of the state label - (string) human-friendly name of the state (empty string for no label) t - (string) the type of the state. values for t can be: "num" - number "str" - string "bool" - boolean </pre> </script>