From: Benjamin Braatz Date: Tue, 23 Mar 2021 12:43:19 +0000 (+0100) Subject: Improve Debug web app. X-Git-Tag: v0.3.0~29 X-Git-Url: http://git.graph-it.com/?a=commitdiff_plain;h=25dc288ddc54376b414d9f3c43f346276dde5be5;p=graphit%2Fcontrolpi-wsserver.git Improve Debug web app. --- diff --git a/conf.json b/conf.json index f24a053..adf56e1 100644 --- a/conf.json +++ b/conf.json @@ -24,7 +24,7 @@ "to": { "target": "Waiter", "command": "wait", - "seconds": 5.0, + "seconds": 3.0, "id": "off delay" } }, @@ -32,6 +32,7 @@ "plugin": "Alias", "from": { "sender": { "const": "Waiter" }, + "event": { "const": "finished" }, "id": { "const": "off delay" } }, "to": { diff --git a/controlpi_plugins/web/Debug/controlpi-debug.css b/controlpi_plugins/web/Debug/controlpi-debug.css index 82ca93b..ac85cc1 100644 --- a/controlpi_plugins/web/Debug/controlpi-debug.css +++ b/controlpi_plugins/web/Debug/controlpi-debug.css @@ -1,170 +1,133 @@ +/* ************************************************** */ +/* Client container, clients and interface containers */ +/* ************************************************** */ .clientcontainer { + display: flex; + flex-direction: row; + flex-wrap: wrap; padding-right: 5px; padding-bottom: 5px; } - -.client { - display: inline-block; - vertical-align: top; - width: 600px; +.clientcontainer > * { margin-top: 5px; margin-left: 5px; - border: 2px solid black; - padding: 5px; - background-color: #bcdaf8; } +.client { + background-color: #bcdaf8; + border: 2px solid black; + padding-right: 5px; + padding-bottom: 5px; +} .client > header { display: flex; justify-content: space-between; } -.client > header > h2 { - vertical-align: top; - font-size: 1.2rem; - font-weight: bold; -} - -.client > header > h3 { - vertical-align: top; - font-size: 0.8rem; - font-weight: bold; -} - .interfacecontainer { - white-space: nowrap; + display: flex; + flex-direction: row; } - -.interfacecontainer > h3 { - display: inline-block; - vertical-align: top; - width: 20px; +.interfacecontainer > :first-child { margin-top: 5px; - font-weight: bold; + margin-left: 5px; } +/* ********************************************* */ +/* Containers for message templates and messages */ +/* ********************************************* */ +.templatecontainer, .lastcontainer { + display: flex; + flex-direction: row; +} .templatecontainer { - display: inline-block; - vertical-align: top; - width: 580px; - white-space: normal; + flex-wrap: wrap; } - .lastcontainer { - text-align: center; + justify-content: center; } - -.lastcontainer * { - text-align: left; +.templatecontainer > *, .lastcontainer > * { + margin-top: 5px; + margin-left: 5px; +} +.templatecontainer > *, .inputsizespan { + font-size: 0.8rem; } -.message { - display: inline-block; - vertical-align: top; +/* *********************** */ +/* JSON objects and arrays */ +/* *********************** */ +.object { + display: flex; + flex-direction: column; background-color: #79b5e7; border: 1px solid black; + padding-right: 2px; + padding-bottom: 2px; } - -.message.green { +.object.green { background-color: #85be71; } - -.message.red { +.object.red { background-color: #ee96a8; } - -.templatecontainer > .message { - font-size: 0.8rem; +.object > h2 { margin-top: 5px; margin-left: 5px; + margin-right: 3px; + margin-bottom: 3px; } - -.lastcontainer > .message { - margin-top: 5px; -} - -.message > span { - margin: 5px; - font-size: 1rem; -} - -.message > h4 { - margin: 2px; - font-size: 0.8rem; -} - -.message table { - border-spacing: 2px; +.object > h4 { + margin-top: 2px; + margin-left: 2px; } -.message .array { +.property { display: flex; flex-direction: row; - flex-wrap: wrap; } - -.message .array > * { - vertical-align: baseline; +.property > .key, .property > .value, .property > .object { + margin-top: 2px; + margin-left: 2px; } -.message .object { - display: flex; - flex-direction: column; - border: 1px solid black; - padding-top: 2px; - padding-left: 2px; - margin-bottom: 2px; -} - -.message .object > .property { +.array { display: flex; flex-direction: row; flex-wrap: wrap; } -.message .object > .property > .key { - margin-right: 2px; - margin-bottom: 2px; -} - -.message .object > .property > .value { - display: flex; - flex-direction: row; - flex-wrap: wrap; - margin-right: 2px; - margin-bottom: 2px; +/* TODO: Remove and use property-key-value structure: */ +.object table { + border-spacing: 2px; } -.message input { +/* ************************** */ +/* Form elements for commands */ +/* ************************** */ +input, select { background-color: #35bfd3; } - -.message input[readonly] { +input::selection { + background-color: #d69ae2; +} +input[readonly] { background-color: #79b5e7; } - -.message input:hover, -.message input:focus { +input:hover, input:focus, select:hover, select:focus { background-color: #afa7ec; } -.message input::selection { - background-color: #d69ae2; -} - -.message input[type="submit"] { +input[type="submit"] { border: 1px solid #0f4a53; border-radius: 12px; padding: 2px; } - -.message input[type="submit"]:hover, -.message input[type="submit"]:focus { +input[type="submit"]:hover, input[type="submit"]:focus { border: 1px solid #44405e; } -.message input[type="text"], -.message input[type="number"] { +input[type="text"], input[type="number"] { min-width: 5px; } @@ -175,12 +138,3 @@ input[type="number"]::-webkit-inner-spin-button { input[type="number"] { -moz-appearance: textfield; } - -.message select { - background-color: #35bfd3; -} - -.message select:hover, -.message select:focus { - background-color: #afa7ec; -} diff --git a/controlpi_plugins/web/Debug/controlpi-debug.js b/controlpi_plugins/web/Debug/controlpi-debug.js index 3cfe956..d7650d0 100644 --- a/controlpi_plugins/web/Debug/controlpi-debug.js +++ b/controlpi_plugins/web/Debug/controlpi-debug.js @@ -1,5 +1,75 @@ +// Open Websocket back to ControlPi we were loaded from: +const websocket = new WebSocket("ws://" + window.location.host) + +// When Websocket is ready request all clients from bus: +websocket.addEventListener('open', function (event) { + websocket.send(JSON.stringify({target: '', command: 'get clients'})) +}) + +// When receiving message from ControlPi through Websocket: +websocket.addEventListener('message', function (event) { + const message = JSON.parse(event.data) + if (message['sender'] == '') { + processBusMessage(message) + } else { + processClientMessage(message) + } +}) + +// Remove (if unregistered) or create (if not existent) elements for +// clients and update interface information: +function processBusMessage(message) { + if (message['event'] == 'unregistered') { + // On deregistration delete client element if it exists: + const clientElement = document.getElementById(message['client']) + if (clientElement != null) { + clientElement.remove() + } + } else { + // On registration or 'get clients' answer: + const clientElement = document.getElementById(message['client']) + if (clientElement == null) { + // Create element for client if not existent: + const main = document.getElementById('ControlPi Debug') + main.appendChild(createClient(message['client'], + message['plugin'])) + } + // Crate message elements for receives interface: + const receiveContainer = document.getElementById(message['client'] + ' Receives') + receiveContainer.innerHTML = '' + for (const template of message['receives']) { + if (template['command'] != null) { + receiveContainer.appendChild(createForCommand(template)) + } else { + receiveContainer.appendChild(createObject(template, 'template')) + } + } + // Create message elements for sends interface: + const sendContainer = document.getElementById(message['client'] + ' Sends') + sendContainer.innerHTML = '' + for (const template of message['sends']) { + sendContainer.appendChild(createObject(template, 'template')) + } + } +} + +// Create element for client (if not existent) +// and update last received message: +function processClientMessage(message) { + const clientElement = document.getElementById(message['sender']) + if (clientElement == null) { + // Create section for client if not existent: + const main = document.getElementById('ControlPi Debug') + main.appendChild(createClient(message.sender, '')) + } + // Update last received message: + const lastContainer = document.getElementById(message['sender'] + ' Last') + lastContainer.innerHTML = '' + lastContainer.appendChild(createObject(message, 'message')) +} + // Create section for client: -function createForClient(client, plugin) { +function createClient(client, plugin) { const section = document.createElement('section') section.setAttribute('id', client) section.setAttribute('class', 'client') @@ -7,9 +77,9 @@ function createForClient(client, plugin) { const headingH2 = document.createElement('h2') headingH2.appendChild(document.createTextNode(client)) heading.appendChild(headingH2) - const headingH3 = document.createElement('h3') - headingH3.appendChild(document.createTextNode(plugin)) - heading.appendChild(headingH3) + const headingH4 = document.createElement('h4') + headingH4.appendChild(document.createTextNode(plugin)) + heading.appendChild(headingH4) section.appendChild(heading) const receiveOuter = document.createElement('div') receiveOuter.setAttribute('class', 'interfacecontainer') @@ -38,60 +108,152 @@ function createForClient(client, plugin) { return section } -// Create div and table for template: -function createForTemplate(template) { - const div = document.createElement('div') - div.setAttribute('class', 'message') - if (Object.keys(template).length === 0) { - // Create span with '*' for empty templates: - const span = document.createElement('span') - const spanContent = document.createTextNode('*') - span.appendChild(spanContent) - div.appendChild(span) +// Create structure for arbirary JSON element: +function create(thing, parentType) { + if (typeof(thing) == 'object') { + if (Array.isArray(thing)) { + return createArray(thing, parentType) + } else if (parentType == 'template') { + if ('const' in thing) { + return create(thing['const']) + } + if ('type' in thing) { + if (thing['type'] == 'object') { + return createObject(thing, type) + } else { + const valueDiv = document.createElement('div') + valueDiv.setAttribute('class', 'value') + valueDiv.appendChild(document.createTextNode( + '<' + thing['type'] + '>')) + return valueDiv + } + } + } else { + type = parentType + if (parentType == 'message') { + type = '' + } + return createObject(thing, type) + } } else { - const table = document.createElement('table') - for (const key in template) { - // Append table row for key-value pair: - const tr = document.createElement('tr') - const keyTd = document.createElement('td') - const keyTdContent = document.createTextNode(key + ':') - keyTd.appendChild(keyTdContent) - tr.appendChild(keyTd) - const valueTd = document.createElement('td') - schema = template[key] - value = '' - if ('const' in schema) { - value = JSON.stringify(schema['const']) - } else { - value = JSON.stringify(schema) + const valueDiv = document.createElement('div') + valueDiv.setAttribute('class', 'value') + valueDiv.appendChild(document.createTextNode(JSON.stringify(thing))) + return valueDiv + } +} + +// Create structure for array: +function createArray(array, parentType) { + arrayDiv = document.createElement('div') + arrayDiv.setAttribute('class', 'array') + arrayDiv.appendChild(document.createTextNode('[')) + var counter = array.length + for (const item of array) { + arrayDiv.appendChild(create(item, parentType)) + if (--counter) { + arrayDiv.appendChild(document.createTextNode(', ')) + } + } + arrayDiv.appendChild(document.createTextNode(']')) +} + +// Create structure for object: +function createObject(object, type) { + const objectDiv = document.createElement('div') + objectDiv.setAttribute('class', 'object') + if (type == 'message') { + // Current (receive) time as heading: + const time = new Date().toLocaleTimeString() + const h4 = document.createElement('h4') + const h4Content = document.createTextNode(time) + h4.appendChild(h4Content) + objectDiv.appendChild(h4) + } + if (type == 'template') { + if (Object.keys(object).length === 0) { + // Create span with '*' for empty templates: + const wildcard = document.createElement('h2') + wildcard.appendChild(document.createTextNode('*')) + objectDiv.appendChild(wildcard) + } + } + for (const key in object) { + if (type == 'message') { + if (key == 'sender') { + // Ignore 'sender' for message + // (information redundantly present in client heading): + continue + } else if (key == 'state') { + // Set background according to state: + if (object[key] === true) { + objectDiv.classList.add('green') + } else if (object[key] === false) { + objectDiv.classList.add('red') + } } - const valueTdContent = document.createTextNode(value) - valueTd.appendChild(valueTdContent) - tr.appendChild(valueTd) - table.appendChild(tr) } - div.appendChild(table) + // Create property div: + const propertyDiv = document.createElement('div') + propertyDiv.setAttribute('class', 'property') + // Create key div and append to property div: + const keyDiv = document.createElement('div') + keyDiv.setAttribute('class', 'key') + keyDiv.appendChild(document.createTextNode(key + ':')) + propertyDiv.appendChild(keyDiv) + // Create value element and append to property div: + propertyDiv.appendChild(create(object[key], type)) + // Append property div to object div: + objectDiv.append(propertyDiv) } - return div + return objectDiv } -// Resize input fields to current input: -function resizeInput() { - const tmp = document.createElement('span') - // HACK: Should get this from CSS rather than hardcode it: - tmp.setAttribute('style', 'font-size: 0.8rem;') - var value = this.value - // HACK: 'number' fields for floats do not return trailing dot in - // value, so we add one unconditionally: - if (this.type == 'number' && this.step == 'any' && !value.includes('.')) { - value += '.' +// Create div, form and table for command template: +function createForCommand(template) { + const div = document.createElement('div') + div.setAttribute('class', 'object') + const form = document.createElement('form') + form.addEventListener("submit", function(e) { + // Convert names and values of all form elements to key-value pairs + // and send as JSON over websocket: + e.preventDefault(); + var data = {}; + for (const element of form.elements) { + if (element.type == 'text' || element.type == 'hidden') { + data[element.name] = element.value + } else if (element.type == 'number') { + data[element.name] = element.valueAsNumber + } else if (element.type == 'select-one') { + if (element.value == 'true') { + data[element.name] = true + } else if (element.value == 'false') { + data[element.name] = false + } else { + data[element.name] = element.value + } + } + } + websocket.send(JSON.stringify(data)) + }) + const table = document.createElement('table') + for (const key in template) { + // Append table row for key-value pair: + const tr = document.createElement('tr') + const keyTd = document.createElement('td') + const keyTdContent = document.createTextNode(key + ':') + keyTd.appendChild(keyTdContent) + tr.appendChild(keyTd) + const valueTd = document.createElement('td') + for (const element of inputsForKeyValue(key, template[key])) { + valueTd.appendChild(element) + } + tr.appendChild(valueTd) + table.appendChild(tr) } - const tmpContent = document.createTextNode(value) - tmp.appendChild(tmpContent) - document.body.appendChild(tmp); - const width = tmp.getBoundingClientRect().width - document.body.removeChild(tmp); - this.style.width = width + 'px' + form.appendChild(table) + div.appendChild(form) + return div } // Create form input (or select) for key-value pairs in receive templates: @@ -190,196 +352,21 @@ function inputsForKeyValue(key, schema) { return result } -// Create div, form and table for command template: -function createForCommand(template) { - const div = document.createElement('div') - div.setAttribute('class', 'message') - const form = document.createElement('form') - form.addEventListener("submit", function(e) { - // Convert names and values of all form elements to key-value pairs - // and send as JSON over websocket: - e.preventDefault(); - var data = {}; - for (const element of form.elements) { - if (element.type == 'text' || element.type == 'hidden') { - data[element.name] = element.value - } else if (element.type == 'number') { - data[element.name] = element.valueAsNumber - } else if (element.type == 'select-one') { - if (element.value == 'true') { - data[element.name] = true - } else if (element.value == 'false') { - data[element.name] = false - } else { - data[element.name] = element.value - } - } - } - websocket.send(JSON.stringify(data)) - }) - const table = document.createElement('table') - for (const key in template) { - // Append table row for key-value pair: - const tr = document.createElement('tr') - const keyTd = document.createElement('td') - const keyTdContent = document.createTextNode(key + ':') - keyTd.appendChild(keyTdContent) - tr.appendChild(keyTd) - const valueTd = document.createElement('td') - for (const element of inputsForKeyValue(key, template[key])) { - valueTd.appendChild(element) - } - tr.appendChild(valueTd) - table.appendChild(tr) - } - form.appendChild(table) - div.appendChild(form) - return div -} - -// Remove (if unregistered) or create (if not existent) elements for -// clients and update interface information: -function processBusMessage(message) { - if (message['event'] == 'unregistered') { - // On deregistration delete client element if it exists: - const clientElement = document.getElementById(message['client']) - if (clientElement != null) { - clientElement.remove() - } - } else { - // On registration or 'get clients' answer: - const clientElement = document.getElementById(message['client']) - if (clientElement == null) { - // Create element for client if not existent: - const main = document.getElementById('ControlPi Debug') - main.appendChild(createForClient(message['client'], - message['plugin'])) - } - // Crate message elements for receives interface: - const receiveContainer = document.getElementById(message['client'] + ' Receives') - receiveContainer.innerHTML = '' - for (const template of message['receives']) { - if (template['command'] != null) { - receiveContainer.appendChild(createForCommand(template)) - } else { - receiveContainer.appendChild(createForTemplate(template)) - } - } - // Create message elements for sends interface: - const sendContainer = document.getElementById(message['client'] + ' Sends') - sendContainer.innerHTML = '' - for (const template of message['sends']) { - sendContainer.appendChild(createForTemplate(template)) - } - } -} - -// Recursively create div structure for arrays, objects and primitive -// values: -function createForMessageValue(value) { - var result = '' - if (typeof(value) == 'object') { - if (Array.isArray(value)) { - result = document.createElement('div') - result.setAttribute('class', 'array') - var counter = value.length - for (const child of value) { - result.appendChild(createForMessageValue(child)) - if (--counter) { - result.appendChild(document.createTextNode(', ')) - } - } - } else { - result = document.createElement('div') - result.setAttribute('class', 'object') - for (const key in value) { - const property = document.createElement('div') - property.setAttribute('class', 'property') - const keyDiv = document.createElement('div') - keyDiv.setAttribute('class', 'key') - keyDiv.appendChild(document.createTextNode(key + ':')) - property.appendChild(keyDiv) - const valueDiv = document.createElement('div') - valueDiv.setAttribute('class', 'value') - valueDiv.appendChild(createForMessageValue(value[key])) - property.appendChild(valueDiv) - result.appendChild(property) - } - } - } else { - result = document.createTextNode(JSON.stringify(value)) - } - return result -} - -// Create div and table for message: -function createForMessage(message) { - const div = document.createElement('div') - div.setAttribute('class', 'message') - // Current (receive) time as heading: - const time = new Date().toLocaleTimeString() - const h4 = document.createElement('h4') - const h4Content = document.createTextNode(time) - h4.appendChild(h4Content) - div.appendChild(h4) - const table = document.createElement('table') - for (const key in message) { - if (key == 'sender') { - // Ignore 'sender' for last received messages - // (information redundantly present in client heading): - continue - } else if (key == 'state') { - // Set background according to state: - if (message[key] === true) { - div.classList.add('green') - } else if (message[key] === false) { - div.classList.add('red') - } - } - // Append table row for key-value pair: - const tr = document.createElement('tr') - const keyTd = document.createElement('td') - const keyTdContent = document.createTextNode(key + ':') - keyTd.appendChild(keyTdContent) - tr.appendChild(keyTd) - const valueTd = document.createElement('td') - valueTd.appendChild(createForMessageValue(message[key])) - tr.appendChild(valueTd) - table.appendChild(tr) - } - div.appendChild(table) - return div -} - -// Create element for client (if not existent) -// and update last received message: -function processClientMessage(message) { - const clientElement = document.getElementById(message['sender']) - if (clientElement == null) { - // Create section for client if not existent: - const main = document.getElementById('ControlPi Debug') - main.appendChild(createForClient(message.sender, '')) +// Resize input fields to current input: +function resizeInput() { + const tmp = document.createElement('span') + tmp.setAttribute('class', 'inputsizespan') + // Space + var value = this.value.replace(/ /g, '\u00a0') + // HACK: 'number' fields for floats do not return trailing dot in + // value, so we add one unconditionally: + if (this.type == 'number' && this.step == 'any' && !value.includes('.')) { + value += '.' } - // Update last received message: - const lastContainer = document.getElementById(message['sender'] + ' Last') - lastContainer.innerHTML = '' - lastContainer.appendChild(createForMessage(message)) + const tmpContent = document.createTextNode(value) + tmp.appendChild(tmpContent) + document.body.appendChild(tmp); + const width = tmp.getBoundingClientRect().width + document.body.removeChild(tmp); + this.style.width = width + 'px' } - -// Open Websocket back to ControlPi we were loaded from: -const websocket = new WebSocket("ws://" + window.location.host) - -// When Websocket is ready request all clients from bus: -websocket.addEventListener('open', function (event) { - websocket.send(JSON.stringify({target: '', command: 'get clients'})) -}) - -// When receiving message from ControlPi through Websocket: -websocket.addEventListener('message', function (event) { - const message = JSON.parse(event.data) - if (message['sender'] == '') { - processBusMessage(message) - } else { - processClientMessage(message) - } -}) diff --git a/controlpi_plugins/web/Debug/index.css b/controlpi_plugins/web/Debug/index.css index e5d2c50..e82d65e 100644 --- a/controlpi_plugins/web/Debug/index.css +++ b/controlpi_plugins/web/Debug/index.css @@ -52,7 +52,7 @@ body > header span.IT { color: #83a1b4; } -h1, h2, h3 { +h1, h2, h3, h4 { margin-top: 5px; margin-left: 5px; font-weight: bold; @@ -67,5 +67,9 @@ h2 { } h3 { - font-size: 1.2rem; + font-size: 1.0rem; +} + +h4 { + font-size: 0.8rem; } diff --git a/web/index.css b/web/index.css index e5d2c50..e82d65e 100644 --- a/web/index.css +++ b/web/index.css @@ -52,7 +52,7 @@ body > header span.IT { color: #83a1b4; } -h1, h2, h3 { +h1, h2, h3, h4 { margin-top: 5px; margin-left: 5px; font-weight: bold; @@ -67,5 +67,9 @@ h2 { } h3 { - font-size: 1.2rem; + font-size: 1.0rem; +} + +h4 { + font-size: 0.8rem; }