Rename complete -> device-dashboard, remove multiple-logins and dashboard

This commit is contained in:
Sergey Lyubka committed 2022-05-14 09:08:23 +01:00
1 parent c20e48179f
commit 443d0f0c6c
39 files changed
+440 -2169

No files matched your search

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Device Dashboard</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body></body>
<script type="module" src="main.js"></script>
</html>
+249
View File
@@ -0,0 +1,249 @@
'use strict';
import {Component, h, html, render, useEffect, useState} from './preact.min.js';
const Nav = props => html`
<div style="background: #333; padding: 0.5em; color: #fff;">
<div class="container d-flex">
<div style="flex: 1 1 auto; display: flex; align-items: center;">
<b>Your Product</b>
</div>
<div style="display: flex; align-items: center; flex: 0 0 auto; ">
<span>Logged in as:</span>
<span style="padding: 0 0.5em;"><img src="user.png" height="22" /></span>
<span>${props.user}</span>
<a class="btn" onclick=${props.logout}
style="margin-left: 1em; font-size: 0.8em; background: #8aa;">logout</a>
</div>
</div>
</div>`;
const Footer = props => html`
<div style="color: silver; margin-top: 2em; padding-top: 0.5em; border-top: 1px solid #ccc; ">
Copyright (c) Your Company
</div>
`;
const Hero = props => html`
<div style="margin-top: 1em; background: #eee; padding: 1em; border-radius: 0.5em; color: #777; ">
<h1 style="margin: 0.2em 0;">Interactive Device Dashboard</h1>
This device dashboard is developed with modern and compact Preact framework,
in order to fit on a very small devices. This is
a <a href="https://mongoose.ws/tutorials/http-server/">hybrid server</a> which
provides both static and dynamic content. Static files, like CSS/JS/HTML
or images, are compiled into the server binary. Dynamic content is
served via the REST API.
This dashboard shows values kept in server memory. Many clients can open
this page. The JS code that watches state changes, reconnects on network
failures. That means if server restarts, dashboard on all connected clients
refresh automatically.
NOTE: administrators can change settings values, whilst users cannot.
<p>
This UI uses the REST API implemented by the server, which you can examine
using <code>curl</code> command-line utility:
</p>
<div><code>curl localhost:8000/api/config/get</code> - get current device configuration</div>
<div><code>curl localhost:8000/api/config/set -d 'value1=7&value2=hello'</code> - set device configuration</div>
<div><code>curl localhost:8000/api/message/send -d 'msg=hello'</code> - send chat message</div>
<div><code>curl localhost:8000/api/watch</code> - get notifications on config changes or chat messages</div>
</div>`;
const ShowSettings = function(props) {
return html`
<div style="margin: 0 0.3em;">
<h3 style="background: #59d; color: #fff;padding: 0.4em;">Device configuration</h3>
<div style="margin: 0.5em 0;">
<span class="addon">value1:</span>
<input disabled type="text" class="smooth"
value=${(props.config || {}).value1 || ''} />
</div>
<div style="margin: 0.5em 0;">
<span class="addon">value2:</span>
<input disabled type="text" class="smooth"
value=${(props.config || {}).value2 || ''} />
</div>
<div class="msg">
Server's corresponding runtime configuration structure:
<pre>
struct config {
int value1;
char *value2;
} s_config;
</pre>
</div>
</div>`;
};
const ChangeSettings = function(props) {
const [value1, setValue1] = useState('');
const [value2, setValue2] = useState('');
useEffect(() => {
setValue1(props.config.value1);
setValue2(props.config.value2);
}, [props.config.value1, props.config.value2])
const update = (name, val) => fetch('/api/config/set', {
method: 'post',
body: `${name}=${encodeURIComponent(val)}`
}).catch(err => err);
const updatevalue1 = ev => update('value1', value1);
const updatevalue2 = ev => update('value2', value2);
return html`
<div style="margin: 0 0.3em;">
<h3 style="background: #c03434; color: #fff; padding: 0.4em;">
Change configuration</h3>
<div style="margin: 0.5em 0;">
<span class="addon">value1:</span>
<input type="text" value=${value1} onchange=${updatevalue1}
oninput=${ev => setValue1(ev.target.value)} />
<button class="btn" disabled=${!value1} onclick=${updatevalue1}
style="margin-left: 1em; background: #8aa;">Update</button>
</div>
<div style="margin: 0.5em 0;">
<span class="addon">value2:</span>
<input type="text" value=${value2} onchange=${updatevalue2}
oninput=${ev => setValue2(ev.target.value)} />
<button class="btn" disabled=${!value2} onclick=${updatevalue2}
style="margin-left: 1em; background: #8aa;">Update</button>
</div>
<div class="msg">
As soon as administrator updates configuration,
server iterates over all connected clients and sends update
notifications to all of them - so they update automatically.
</div>
</div>`;
};
const Login = function(props) {
const [user, setUser] = useState('');
const [pass, setPass] = useState('');
const login = ev =>
fetch(
'/api/login',
{headers: {Authorization: 'Basic ' + btoa(user + ':' + pass)}})
.then(r => r.json())
.then(r => r && props.login(r))
.catch(err => err);
return html`
<div class="rounded border" style="max-width: 480px; margin: 0 auto; margin-top: 5em; background: #eee; ">
<div style="padding: 2em; ">
<h1 style="color: #666;">Device Dashboard Login </h1>
<div style="margin: 0.5em 0;">
<input type='text' placeholder='Name' style="width: 100%;"
oninput=${ev => setUser(ev.target.value)} value=${user} />
</div>
<div style="margin: 0.5em 0;">
<input type="password" placeholder="Password" style="width: 100%;"
oninput=${ev => setPass(ev.target.value)} value=${pass}
onchange=${login} />
</div>
<div style="margin: 1em 0;">
<button class="btn" style="width: 100%; background: #8aa;"
disabled=${!user || !pass} onclick=${login}> Login </button>
</div>
<div style="color: #777; margin-top: 2em;">
Valid logins: admin:pass0, user1:pass1, user2:pass2
</div>
</div>
</div>`;
};
const Message = text => html`<div style="margin: 0.5em 0;">${text}</div>`;
const Chat = function(props) {
const [message, setMessage] = useState('');
const sendmessage = ev => fetch('/api/message/send', {
method: 'post',
body: `message=${encodeURIComponent(message)}`
}).then(r => setMessage(''));
const messages = props.messages.map(
text => html`<div style="margin: 0.5em 0;">${text}</div>`);
return html`
<div style="margin: 0 0.3em;">
<h3 style="background: #30c040; color: #fff; padding: 0.4em;">User chat</h3>
<div style="height: 10em; overflow: auto; padding: 0.5em; " class="border">
${messages}
</div>
<div style="margin: 0.5em 0;">
<input placeholder="type message..." style="width: 100%" value=${message}
onchange=${sendmessage}
oninput=${ev => setMessage(ev.target.value)} />
</div>
<div class="msg">
Chat demonsrates
real-time bidirectional data exchange between many clients and a server.
</div>
</div>`;
};
const App = function(props) {
const [messages, setMessages] = useState([]);
const [user, setUser] = useState('');
const [config, setConfig] = useState({});
const refresh = () =>
fetch('/api/config/get').then(r => r.json()).then(r => setConfig(r));
const login = function(u) {
document.cookie = `access_token=${u.token};path=/;max-age=3600`;
setUser(u.user);
refresh();
};
const logout = ev => {
document.cookie = `access_token=;path=/;max-age=0`;
setUser('');
};
const watch = function() {
var f = function(reader) {
return reader.read().then(function(result) {
var data = String.fromCharCode.apply(null, result.value);
var notification = JSON.parse(data);
if (notification.name == 'config') refresh();
if (notification.name == 'message') {
setMessages(m => m.concat([notification.data]))
}
// console.log(notification);
if (!result.done) return f(reader);
});
};
fetch('/api/watch')
.then(r => r.body.getReader())
.then(f)
.catch(e => setTimeout(watch, 1000));
};
useEffect(() => {
fetch('/api/login')
.then(r => r.json())
.then(r => login(r))
.catch(err => setUser(''));
refresh();
watch();
}, []);
if (!user) return html`<${Login} login=${login} />`;
const admin = user == 'admin';
const colsize = admin ? 'c4' : 'c6';
const cs = admin ? html`<${ChangeSettings} config=${config} />` : '';
return html`
<${Nav} user=${user} logout=${logout} />
<div class="container">
<${Hero} />
<div class="row">
<div class="col ${colsize}"><${ShowSettings} config=${config} /></div>
<div class="col ${colsize}">${cs}</div>
<div class="col ${colsize}"><${Chat} messages=${messages} /></div>
</div>
<${Footer} />
</div>`;
};
window.onload = () => render(h(App), document.body);
+1
View File
@@ -0,0 +1 @@
var e,n,_,t,o,r,u,l={},i=[],c=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function s(e,n){for(var _ in n)e[_]=n[_];return e}function f(e){var n=e.parentNode;n&&n.removeChild(e)}function a(n,_,t){var o,r,u,l={};for(u in _)"key"==u?o=_[u]:"ref"==u?r=_[u]:l[u]=_[u];if(arguments.length>2&&(l.children=arguments.length>3?e.call(arguments,2):t),"function"==typeof n&&null!=n.defaultProps)for(u in n.defaultProps)void 0===l[u]&&(l[u]=n.defaultProps[u]);return p(n,l,o,r,null)}function p(e,t,o,r,u){var l={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==u?++_:u};return null!=n.vnode&&n.vnode(l),l}function h(e){return e.children}function d(e,n){this.props=e,this.context=n}function v(e,n){if(null==n)return e.__?v(e.__,e.__.__k.indexOf(e)+1):null;for(var _;n<e.__k.length;n++)if(null!=(_=e.__k[n])&&null!=_.__e)return _.__e;return"function"==typeof e.type?v(e):null}function y(e){var n,_;if(null!=(e=e.__)&&null!=e.__c){for(e.__e=e.__c.base=null,n=0;n<e.__k.length;n++)if(null!=(_=e.__k[n])&&null!=_.__e){e.__e=e.__c.base=_.__e;break}return y(e)}}function m(e){(!e.__d&&(e.__d=!0)&&t.push(e)&&!g.__r++||r!==n.debounceRendering)&&((r=n.debounceRendering)||o)(g)}function g(){for(var e;g.__r=t.length;)e=t.sort(function(e,n){return e.__v.__b-n.__v.__b}),t=[],e.some(function(e){var n,_,t,o,r,u;e.__d&&(r=(o=(n=e).__v).__e,(u=n.__P)&&(_=[],(t=s({},o)).__v=o.__v+1,P(u,o,t,n.__n,void 0!==u.ownerSVGElement,null!=o.__h?[r]:null,_,null==r?v(o):r,o.__h),D(_,o),o.__e!=r&&y(o)))})}function k(e,n,_,t,o,r,u,c,s,f){var a,d,y,m,g,k,x,H=t&&t.__k||i,E=H.length;for(_.__k=[],a=0;a<n.length;a++)if(null!=(m=_.__k[a]=null==(m=n[a])||"boolean"==typeof m?null:"string"==typeof m||"number"==typeof m||"bigint"==typeof m?p(null,m,null,null,m):Array.isArray(m)?p(h,{children:m},null,null,null):m.__b>0?p(m.type,m.props,m.key,null,m.__v):m)){if(m.__=_,m.__b=_.__b+1,null===(y=H[a])||y&&m.key==y.key&&m.type===y.type)H[a]=void 0;else for(d=0;d<E;d++){if((y=H[d])&&m.key==y.key&&m.type===y.type){H[d]=void 0;break}y=null}P(e,m,y=y||l,o,r,u,c,s,f),g=m.__e,(d=m.ref)&&y.ref!=d&&(x||(x=[]),y.ref&&x.push(y.ref,null,m),x.push(d,m.__c||g,m)),null!=g?(null==k&&(k=g),"function"==typeof m.type&&null!=m.__k&&m.__k===y.__k?m.__d=s=b(m,s,e):s=C(e,m,y,H,g,s),f||"option"!==_.type?"function"==typeof _.type&&(_.__d=s):e.value=""):s&&y.__e==s&&s.parentNode!=e&&(s=v(y))}for(_.__e=k,a=E;a--;)null!=H[a]&&("function"==typeof _.type&&null!=H[a].__e&&H[a].__e==_.__d&&(_.__d=v(t,a+1)),U(H[a],H[a]));if(x)for(a=0;a<x.length;a++)T(x[a],x[++a],x[++a])}function b(e,n,_){var t,o;for(t=0;t<e.__k.length;t++)(o=e.__k[t])&&(o.__=e,n="function"==typeof o.type?b(o,n,_):C(_,o,o,e.__k,o.__e,n));return n}function C(e,n,_,t,o,r){var u,l,i;if(void 0!==n.__d)u=n.__d,n.__d=void 0;else if(null==_||o!=r||null==o.parentNode)e:if(null==r||r.parentNode!==e)e.appendChild(o),u=null;else{for(l=r,i=0;(l=l.nextSibling)&&i<t.length;i+=2)if(l==o)break e;e.insertBefore(o,r),u=r}return void 0!==u?u:o.nextSibling}function x(e,n,_){"-"===n[0]?e.setProperty(n,_):e[n]=null==_?"":"number"!=typeof _||c.test(n)?_:_+"px"}function H(e,n,_,t,o){var r;e:if("style"===n)if("string"==typeof _)e.style.cssText=_;else{if("string"==typeof t&&(e.style.cssText=t=""),t)for(n in t)_&&n in _||x(e.style,n,"");if(_)for(n in _)t&&_[n]===t[n]||x(e.style,n,_[n])}else if("o"===n[0]&&"n"===n[1])r=n!==(n=n.replace(/Capture$/,"")),n=n.toLowerCase()in e?n.toLowerCase().slice(2):n.slice(2),e.l||(e.l={}),e.l[n+r]=_,_?t||e.addEventListener(n,r?S:E,r):e.removeEventListener(n,r?S:E,r);else if("dangerouslySetInnerHTML"!==n){if(o)n=n.replace(/xlink[H:h]/,"h").replace(/sName$/,"s");else if("href"!==n&&"list"!==n&&"form"!==n&&"tabIndex"!==n&&"download"!==n&&n in e)try{e[n]=null==_?"":_;break e}catch(e){}"function"==typeof _||(null!=_&&(!1!==_||"a"===n[0]&&"r"===n[1])?e.setAttribute(n,_):e.removeAttribute(n))}}function E(e){this.l[e.type+!1](n.event?n.event(e):e)}function S(e){this.l[e.type+!0](n.event?n.event(e):e)}function P(e,_,t,o,r,u,l,i,c){var f,a,p,v,y,m,g,b,C,x,H,E=_.type;if(void 0!==_.constructor)return null;null!=t.__h&&(c=t.__h,i=_.__e=t.__e,_.__h=null,u=[i]),(f=n.__b)&&f(_);try{e:if("function"==typeof E){if(b=_.props,C=(f=E.contextType)&&o[f.__c],x=f?C?C.props.value:f.__:o,t.__c?g=(a=_.__c=t.__c).__=a.__E:("prototype"in E&&E.prototype.render?_.__c=a=new E(b,x):(_.__c=a=new d(b,x),a.constructor=E,a.render=A),C&&C.sub(a),a.props=b,a.state||(a.state={}),a.context=x,a.__n=o,p=a.__d=!0,a.__h=[]),null==a.__s&&(a.__s=a.state),null!=E.getDerivedStateFromProps&&(a.__s==a.state&&(a.__s=s({},a.__s)),s(a.__s,E.getDerivedStateFromProps(b,a.__s))),v=a.props,y=a.state,p)null==E.getDerivedStateFromProps&&null!=a.componentWillMount&&a.componentWillMount(),null!=a.componentDidMount&&a.__h.push(a.componentDidMount);else{if(null==E.getDerivedStateFromProps&&b!==v&&null!=a.componentWillReceiveProps&&a.componentWillReceiveProps(b,x),!a.__e&&null!=a.shouldComponentUpdate&&!1===a.shouldCompLine truncated
@@ -0,0 +1,36 @@
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; font: 16px sans-serif; }
select, input, label::before, textarea { outline: none; box-shadow:none !important; border: 1px solid #ccc !important; }
code, pre { color: #373; font-family: monospace; font-weight: bolder; font-size: smaller; background: #ddd; padding: 0.1em 0.3em; border-radius: 0.2em; }
textarea, input, .addon { font-size: 15px; border: 1px solid #ccc; padding: 0.5em; }
a, a:visited, a:active { color: #55f; }
.addon { background: #eee; }
.btn {
background: #ccc; border-radius: 0.3em; border: 0; color: #fff; cursor: pointer;
display: inline-block; padding: 0.6em 2em; font-weight: bolder;
}
.btn[disabled] { opacity: 0.5; cursor: auto;}
.smooth { transition: all .2s; }
.container { margin: 0 20px; width: auto; }
.d-flex { display: flex; }
.d-none { display: none; }
.border { border: 1px solid #ddd; }
.rounded { border-radius: 0.5em; }
.msg { background: #def; border-left: 5px solid #59d; padding: 1em; margin: 1em 0; }
.row { margin: 1% 0; overflow: auto; }
.col { float: left; }
.table, .c12 { width: 100%; }
.c11 { width: 91.66%; }
.c10 { width: 83.33%; }
.c9 { width: 75%; }
.c8 { width: 66.66%; }
.c7 { width: 58.33%; }
.c6 { width: 50%; }
.c5 { width: 41.66%; }
.c4 { width: 33.33%; }
.c3 { width: 25%; }
.c2 { width: 16.66%; }
.c1 { width: 8.33%; }
@media (min-width: 1310px) { .container { margin: auto; width: 1270px; } }
@media (max-width: 870px) { .row .col { width: 100%; } }
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB