From 9dad72fc607e0f218122c635499ef27fd2c75226 Mon Sep 17 00:00:00 2001 From: Benjamin Braatz Date: Sun, 7 Mar 2021 03:23:49 +0100 Subject: [PATCH] Add debug Web app --- web/controlpi-debug.css | 126 +++++++++++++++++ web/controlpi-debug.js | 302 ++++++++++++++++++++++++++++++++++++++++ web/index.css | 61 ++++++++ web/index.html | 31 +++++ 4 files changed, 520 insertions(+) create mode 100644 web/controlpi-debug.css create mode 100644 web/controlpi-debug.js create mode 100644 web/index.css create mode 100644 web/index.html diff --git a/web/controlpi-debug.css b/web/controlpi-debug.css new file mode 100644 index 0000000..22c072d --- /dev/null +++ b/web/controlpi-debug.css @@ -0,0 +1,126 @@ +.clientcontainer { + padding-right: 5px; + padding-bottom: 5px; +} + +.client { + display: inline-block; + vertical-align: top; + width: 300px; + margin-top: 5px; + margin-left: 5px; + border: 2px solid black; + padding: 5px; + background-color: #bcdaf8; +} + +.client h2 { + font-size: 1.2rem; + font-weight: bold; +} + +.templatecontainer { +} + +.lastcontainer { + text-align: center; +} + +.lastcontainer * { + text-align: left; +} + +.templatecontainer h3 { + display: inline-block; + margin-top: 5px; + font-weight: bold; +} + +.message { + display: inline-block; + vertical-align: top; + background-color: #79b5e7; + border: 1px solid black; +} + +.message.green { + background-color: #85be71; +} + +.message.red { + background-color: #ee96a8; +} + +.templatecontainer .message { + font-size: 0.8rem; + margin-top: 5px; + margin-left: 5px; +} + +.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; +} + +.message input { + background-color: #35bfd3; +} + +.message input[readonly] { + background-color: #79b5e7; +} + +.message input:hover, +.message input:focus { + background-color: #afa7ec; +} + +.message input::selection { + background-color: #d69ae2; +} + +.message input[type="submit"] { + border: 1px solid #0f4a53; + border-radius: 12px; + padding: 2px; +} + +.message input[type="submit"]:hover, +.message input[type="submit"]:focus { + border: 1px solid #44405e; +} + +.message input[type="text"], +.message input[type="number"] { + min-width: 5px; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; +} +input[type="number"] { + -moz-appearance: textfield; +} + +.message select { + background-color: #35bfd3; +} + +.message select:hover, +.message select:focus { + background-color: #afa7ec; +} diff --git a/web/controlpi-debug.js b/web/controlpi-debug.js new file mode 100644 index 0000000..e9e39dd --- /dev/null +++ b/web/controlpi-debug.js @@ -0,0 +1,302 @@ +// 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'})) +}) + +// Create section for client: +function createForClient(client) { + const section = document.createElement('section') + section.setAttribute('id', client) + section.setAttribute('class', 'client') + section.innerHTML = ` +

${client}

+
+

=>

+
+
+

<=

+
+
+
+ ` + return section +} + +// Create div and table for template or message: +function createForMessage(message, isMessage) { + const div = document.createElement('div') + div.setAttribute('class', 'message') + if (isMessage) { + // 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) + } + if (Object.keys(message).length === 0) { + // Create span with '*' for empty templates: + const span = document.createElement('span') + const spanContent = document.createTextNode('*') + span.appendChild(spanContent) + div.appendChild(span) + } else { + const table = document.createElement('table') + for (const key in message) { + if (isMessage && key == 'sender') { + // Ignore 'sender' for messages (redundant in client heading): + continue + } + // 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') + value = JSON.stringify(message[key]) + if (value.startsWith('""')) { + // Remove quotes if class type instead of literal value: + value = value.replace(/^"|"$/g, '') + } + const valueTdContent = document.createTextNode(value) + valueTd.appendChild(valueTdContent) + tr.appendChild(valueTd) + table.appendChild(tr) + } + div.appendChild(table) + } + return div +} + +// Resize input fields to current input: +function resizeInput() { + const tmp = document.createElement('span') + // Hack: Should get this from CSS class rather than hardcode it: + tmp.setAttribute('style', 'font-size: 0.8rem;') + const tmpContent = document.createTextNode(this.value) + tmp.appendChild(tmpContent) + document.body.appendChild(tmp); + const width = tmp.getBoundingClientRect().width + document.body.removeChild(tmp); + this.style.width = width + 'px' +} + +// 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') + value = template[key] + // Create submit button for command itself: + if (key == 'command') { + const hidden = document.createElement('input') + hidden.setAttribute('type', 'hidden') + hidden.setAttribute('name', key) + hidden.setAttribute('value', value) + valueTd.appendChild(hidden) + const submit = document.createElement('input') + submit.setAttribute('type', 'submit') + submit.setAttribute('value', value) + valueTd.appendChild(submit) + // Create appropriate input fields for '': + } else if (value == '') { + const openquote = document.createTextNode('"') + valueTd.appendChild(openquote) + const input = document.createElement('input') + input.setAttribute('type', 'text') + input.setAttribute('name', key) + input.setAttribute('value', '') + input.setAttribute('required', '') + input.addEventListener('input', resizeInput) + resizeInput.call(input) + valueTd.appendChild(input) + const closequote = document.createTextNode('"') + valueTd.appendChild(closequote) + } else if (value == '') { + const input = document.createElement('input') + input.setAttribute('type', 'number') + input.setAttribute('step', '1') + input.setAttribute('name', key) + input.setAttribute('value', '') + input.setAttribute('required', '') + input.addEventListener('input', resizeInput) + resizeInput.call(input) + valueTd.appendChild(input) + } else if (value == '') { + const input = document.createElement('input') + input.setAttribute('type', 'number') + input.setAttribute('step', 'any') + input.setAttribute('name', key) + input.setAttribute('value', '') + input.setAttribute('required', '') + input.addEventListener('input', resizeInput) + resizeInput.call(input) + valueTd.appendChild(input) + } else if (value == '') { + const select = document.createElement('select') + select.setAttribute('name', key) + const optionTrue = document.createElement('option') + optionTrue.setAttribute('value', true) + const optionTrueContent = document.createTextNode('true') + optionTrue.appendChild(optionTrueContent) + select.appendChild(optionTrue) + const optionFalse = document.createElement('option') + optionFalse.setAttribute('value', false) + const optionFalseContent = document.createTextNode('false') + optionFalse.appendChild(optionFalseContent) + select.appendChild(optionFalse) + valueTd.appendChild(select) + // Create readonly input fields for literals: + } else if (typeof(value) == 'string') { + const openquote = document.createTextNode('"') + valueTd.appendChild(openquote) + const input = document.createElement('input') + input.setAttribute('type', 'text') + input.setAttribute('name', key) + input.setAttribute('value', value) + input.setAttribute('readonly', '') + input.addEventListener('input', resizeInput) + resizeInput.call(input) + valueTd.appendChild(input) + const closequote = document.createTextNode('"') + valueTd.appendChild(closequote) + } else if (typeof(value) == 'number') { + const input = document.createElement('input') + input.setAttribute('type', 'number') + input.setAttribute('name', key) + input.setAttribute('value', value) + input.setAttribute('readonly', '') + input.addEventListener('input', resizeInput) + resizeInput.call(input) + valueTd.appendChild(input) + } else if (typeof(value) == 'boolean') { + const select = document.createElement('select') + select.setAttribute('name', key) + const optionTrue = document.createElement('option') + optionTrue.setAttribute('value', 'true') + if (value) { + optionTrue.setAttribute('selected', '') + } else { + optionTrue.setAttribute('disabled', '') + } + const optionTrueContent = document.createTextNode('true') + optionTrue.appendChild(optionTrueContent) + select.appendChild(optionTrue) + const optionFalse = document.createElement('option') + optionFalse.setAttribute('value', 'false') + if (value) { + optionTrue.setAttribute('disabled', '') + } else { + optionTrue.setAttribute('selected', '') + } + const optionFalseContent = document.createTextNode('false') + optionFalse.appendChild(optionFalseContent) + select.appendChild(optionFalse) + valueTd.appendChild(select) + } + tr.appendChild(valueTd) + table.appendChild(tr) + } + form.appendChild(table) + div.appendChild(form) + return div +} + +// When receiving message from ControlPi through Websocket: +websocket.addEventListener('message', function (event) { + const message = JSON.parse(event.data) + if (message.sender == '') { + // When message is from bus: + if (message.event == 'unregistered') { + // On deregistration delete section if it exists: + const clientElement = document.getElementById(message.client) + if (clientElement != null) { + clientElement.remove() + } + } else { + // On registration or initial answers for all clients: + const clientElement = document.getElementById(message.client) + if (clientElement == null) { + // Create section for client if not existent: + const main = document.getElementById('ControlPi Debug') + main.appendChild(createForClient(message.client)) + } + const receiveContainer = document.getElementById(message.client + ' Receives') + receiveContainer.innerHTML = '

=>

' + for (const template of message.receives) { + if (template.command != null) { + const templateElement = createForCommand(template) + receiveContainer.appendChild(templateElement) + } else { + const templateElement = createForMessage(template) + receiveContainer.appendChild(templateElement) + } + } + const sendContainer = document.getElementById(message.client + ' Sends') + sendContainer.innerHTML = '

<=

' + for (const template of message.sends) { + const templateElement = createForMessage(template) + sendContainer.appendChild(templateElement) + } + } + } else { + // When message is from client: + 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)) + } + // Update last received message: + const lastContainer = document.getElementById(message.sender + ' Last') + lastContainer.innerHTML = '' + const messageElement = createForMessage(message, true) + lastContainer.appendChild(messageElement) + if (message.state != null) { + // If this is some kind of state set background + // green or red depending on it: + if (message.state) { + messageElement.classList.remove('red') + messageElement.classList.add('green') + } else { + messageElement.classList.remove('green') + messageElement.classList.add('red') + } + } + } +}) diff --git a/web/index.css b/web/index.css new file mode 100644 index 0000000..e33bc74 --- /dev/null +++ b/web/index.css @@ -0,0 +1,61 @@ +@import url('https://fonts.googleapis.com/css?family=Roboto'); +@import url('https://fonts.googleapis.com/css?family=Michroma'); + +* { + vertical-align: baseline; + margin: 0; + outline: 0; + border: 0 none; + padding: 0; + font-family: 'Roboto', sans-serif; + font-weight: inherit; + font-style: inherit; + font-size: inherit; +} + +html { + font-size: 16px; +} + +body { + background-color: white; + color: black; +} + +header { + padding: 10px; + background-color: #2c343b; +} + +header * { + vertical-align: middle; +} + +header svg { + height: 40px; +} + +header span { + font-family: "Michroma", sans-serif; + font-weight: bold; + font-size: 20px; + text-transform: uppercase; +} + +header span.Graph { + margin-left: 10px; + color: white; +} + +header span.IT { + margin-right: 10px; + color: #83a1b4; +} + +h1 { + margin-top: 5px; + margin-left: 5px; + margin-right: 5px; + font-size: 1.4rem; + font-weight: bold; +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f6d6ff5 --- /dev/null +++ b/web/index.html @@ -0,0 +1,31 @@ + + + + + + ControlPi Debug + + + + + + +
+ + + + + + + + + + + + Graph-IT +
+

ControlPi Debug

+
+
+ + -- 2.34.1