From 57bebd16ff7e89bfe5e9089ac4c0a21733675f22 Mon Sep 17 00:00:00 2001 From: Marin Ivanov Date: Wed, 21 Aug 2024 00:04:40 +0300 Subject: config and setting fields --- app.js | 160 ++++++++++++++++++++++++++++++++---------------------- cgi-bin/getconfig | 8 +-- config.js | 44 +++++++++++++++ index.html | 1 + ka.js | 16 ++++-- style.css | 26 ++++++--- 6 files changed, 173 insertions(+), 82 deletions(-) create mode 100644 config.js diff --git a/app.js b/app.js index ddbb66b..a74104c 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,7 @@ (() => { - let isLoading = 0; + state = {}; + isLoading = 0; + const loading = (val) => { isLoading = val; loader.$cls(isLoading ? "" : "hidden"); @@ -7,44 +9,56 @@ loader.$click(() => loading(0)); let error = null; - function errClose() { - error = null; - app.reload(); - } - let data = {}; - function onerror(err) { - error = err; - console.error(err); + setError = (err) => { + error = String(err); app.reload(); } - function parsedata(r) { - return Object.fromEntries(r.split("\n").map(x=>x.trim()).filter(x=>x).map(x=>x.split('='))); + function errClose() { } - function cmd(url, body) { - return fetch(url, {method:body?'POST':'GET', body}) - .then(r => { - if (!r.ok){ - throw new Error(r.statusText); - } - return r.text(); - }) - .then(r => parsedata(r)); - }; - function loadconfig() { + window.onerror = (message, source, lineNumber, colno, err) => { + setError(message); + }; + window.onunhandledrejection = (event) => { + setError(event.reason); + }; + let parseini = (data) => Object.fromEntries(data.split("\n").map(x=>x.trim()).filter(x=>x).map(x=>x.split("="))); + let buildini = (data, prefix="") => entries(data).map(([k,v])=>`${prefix}${k}=${v}\n`).join(); + let resptext = r => { + if (!r.ok){ + throw new Error(r.statusText); + } + return r.text(); + }; + getconfig = () => fetch("/cgi-bin/getconfig").then(resptext).then(x => parseini(x)); + cmd = (body) => fetch("/cgi-bin/cmd",{method:"POST",body}).then(resptext); + setconfig = (data, prefix) => { loading(1); - cmd("/cgi-bin/getconfig") + cmd(buildini(data, prefix)+"save\n") + .finally(() => loading(0)) + }; + loadconfig = () => { + loading(1); + getconfig("/cgi-bin/getconfig") .then(x => { - data = x; + state = {...state, ...x}; app.reload(); }) - .catch(onerror) - .then(() => loading(0)) - } - function saveconfig(groups) { - cmd("/cgi-bin/setconfig", data) - .catch(onerror) - .then(loadconfig) + .finally(() => loading(0)); } + reboot = (data, prefix) => { + var ok = 0; + loading(1); + cmd("reboot\n") + .then(() => { + ok = 1; + }) + .finally(() => { + setTimeout(() => { + loading(0); + app.reload(); + }, ok?10000:0); + }) + }; function CErr(text) { var el = div( @@ -72,52 +86,64 @@ return routes[r](); } } + + function CSettingInput(f) { + var inp = input("text") + .$attr("id","form__"+f.id) + .$attr("name",f.id) + .$attr("required",!!f.required) + .$value(state[f.id]??null) + .$change2state(state, f.id); + if (f.pattern) { + inp.$attr("pattern", f.pattern??null); + } + if (f.invalidmsg) { + inp.oninvalid = () => inp.setCustomValidity(f.invalidmsg); + } + inp.$attr("placeholder", f.hint??""); + return tr( + td(labelfor(inp.id, f.title)).$cls("title"), + td(inp), + ); + } + + function CSettings(section) { + let s = config.sections[section]; + let form_ = form( + table(...s.fields.map(x => CSettingInput(x))).$cls("settings"), + input("submit").$value("Save") + ); + form_.onsubmit = (e) => {e.preventDefault(); setconfig({});} + return div( + h1(s.title), + form_, + ) + } + const navbar = CNav([ ["#", "Home"], - ["#/network", "Network"], - ["#/config", "Configuration"], - ["#/tools", "Tools"], + ...entries(config.sections).map(([id, x]) => [`#/${id}`, x.title]), ]); const router = CRouter({ "/": () => div( - h2("Lorem Ipsum"), - p("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "), - p("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."), - ), - "/network": () => div( - h1("Network settings"), + h1("Information"), table( - tr( - td("IP"), - td(input("text").$value(data["cfg.ip"])), - ), - tr( - td("MAC"), - td( - input("text").$value(data["cfg.mac"]) - .$change2state(data, "cfg.mac") - ), - ) - ).$cls("network"), - input("button").$value("Save").$click(saveconfig) + tr(td("Device:").$cls("title"), td(config.device)), + tr(td("Software Version:").$cls("title"), td(config.swVersion)), + tr(td("Hardware Version:").$cls("title"), td(config.hwVersion)), + ), + h1("Controls"), + p( + button("Reboot").$click(reboot), + ).$cls("center"), ), - "/config": () => { - return div(); - }, - "/waiting": () => { - loading(1); - setTimeout(() => { - loading(0); - }, 3000); - return div(""); - }, + ...Object.fromEntries(entries(config.sections).map(([id, _]) => [`/${id}`, () => CSettings(id)])), "": () => div( h1("Not Found"), p("The requested page is not available.") ), }); - - mount(app, (h) => { + CApp = (h) => { loading(isLoading); return [ div(error && CErr(String(error))), @@ -126,6 +152,8 @@ section(router(h)) ), ]; - }); + }; + + mount(app, CApp); loadconfig(); })() diff --git a/cgi-bin/getconfig b/cgi-bin/getconfig index 21128a9..51de5b1 100644 --- a/cgi-bin/getconfig +++ b/cgi-bin/getconfig @@ -1,6 +1,6 @@ -cfg.ip=127.0.0.1 -cfg.mac=11:22:33:44:55:66 - -cfg.gateway=0.0.0.0 +macaddr=11:22:33:44:55:66 +ipaddr=127.0.0.1 +netmask=255.255.255.0 +gateway=0.0.0.0 diff --git a/config.js b/config.js new file mode 100644 index 0000000..814371e --- /dev/null +++ b/config.js @@ -0,0 +1,44 @@ +config = { + device: "Device Name", + swVersion: "1.0", + hwVersion: "1.0", + sections: { + network: { + title: "Network", + fields: [ + { + id: "macaddr", + type: "mac", + title: "MAC address", + pattern: "[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}", + invalidmsg: "Enter MAC address. E.g. 00:11:22:33:44:55", + required: true, + }, + { + id: "ipaddr", + type: "ip", + title: "IP address", + pattern: "\\d+.\\d+.\\d+.\\d+", + hint: "192.168.4.2", + invalidmsg: "Enter IP address. E.g. 192.168.4.2", + required: true, + }, + { + id: "netmask", + type: "netmask", + title: "Netmask", + pattern: "\\d+.\\d+.\\d+.\\d+", + hint: "255.255.255.0", + invalidmsg: "Enter network mask. E.g. 255.255.255.0", + required: true, + }, + { + id: "gateway", + type: "ip", + title: "Gateway", + pattern: "\\d+.\\d+.\\d+.\\d+", + }, + ], + }, + }, +} diff --git a/index.html b/index.html index 6f1cfb8..3cc012b 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,7 @@ + diff --git a/ka.js b/ka.js index ea75b2a..d2ae4bc 100644 --- a/ka.js +++ b/ka.js @@ -1,5 +1,6 @@ (()=>{ doc = document; + entries = Object.entries; var ep = Element.prototype; ep.attr = function(name){ @@ -10,7 +11,7 @@ return this; }; ep.$attrs = function(attrs) { - Object.entries(attrs).forEach(([attr, value]) => this.$attr(attr, value)); + entries(attrs).forEach(([attr, value]) => this.$attr(attr, value)); return this; } ep.$cls = function(cls) { @@ -26,14 +27,19 @@ } const getvalue = x => x.value; ep.$change2state = function(state, field, valgetter=getvalue) { - this.onchange = () => { - state[field] = valgetter(this); + this.oninput = () => { + this.setCustomValidity(''); + if (this.validity.valid) { + state[field] = valgetter(this); + } else { + this.reportValidity(); + } }; return this; } tag = (name, attrs, ...children) => { const el = doc.createElement(name); - el.$attrs(attrs||{}); + attrs && el.$attrs(attrs); for (const child of children.filter(x=>x)) { el.appendChild((typeof(child) === 'string') ? doc.createTextNode(child) : child); } @@ -43,7 +49,7 @@ labelfor = (for_, ...children) => tag("label", {"for":for_}, ...children); img = (src) => tag("img", {src}); input = (type) => tag("input", {type}); - const TRIVIAL = "main,section,nav,h1,h2,h3,p,,div,span,select,table,tr,td"; + const TRIVIAL = "main,section,nav,h1,h2,h3,p,b,div,span,form,select,button,table,tr,td"; for (let name of TRIVIAL.split(",")) { window[name] = (...children) => tag(name, null, ...children); } diff --git a/style.css b/style.css index 334a765..c17bb9e 100644 --- a/style.css +++ b/style.css @@ -4,7 +4,9 @@ body { } h1, h2, h3 { - margin: 0; + margin: 0 0 0.5em 0; + padding: 0.5em 0 0.5em 0; + border-bottom: 1px solid #ccc; } .container { @@ -19,7 +21,7 @@ header { padding: 1em; } section { - padding: 0.5em; + padding: 0 1em 0 1em; } main { display: flex; @@ -34,7 +36,7 @@ section > div { nav { display: inline-block; - /*background-color: #f1f1f1;*/ + background-color: #f1f1f1; width: 250px; } @@ -73,19 +75,26 @@ nav a:hover:not(.active) { color: white; } -input[type=button] { +input[type=submit], button { + display: inline-block; background-color: #07a2a0; - width: 200px; + width: 175px; height: 40px; - display: block; border:0; color:#fff; } -input[type=button]:active { +input[type=submit]:active, button:active { background-color: #079290; } +.settings { + margin: 0 0 1em 0; +} +table td.title { + min-width: 150px; +} + .err { color: #fff; background-color: #f00; @@ -157,3 +166,6 @@ input[type=button]:active { } } +.center { + text-align: center; +} -- cgit v1.2.3