mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-29 12:41:03 +00:00
Refactor device-dashboard
This commit is contained in:
1 parent
380d6ac5c2
commit
49a9bb5a8b
60 files changed
+7752
-5228
No files matched your search
@@ -0,0 +1 @@
|
||||
var n,t,e,_,r,o,i,u,l,c={},a=[],s=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function f(n,t){for(var e in t)n[e]=t[e];return n}function p(n){var t=n.parentNode;t&&t.removeChild(n)}function h(t,e,_){var r,o,i,u={};for(i in e)"key"==i?r=e[i]:"ref"==i?o=e[i]:u[i]=e[i];if(arguments.length>2&&(u.children=arguments.length>3?n.call(arguments,2):_),"function"==typeof t&&null!=t.defaultProps)for(i in t.defaultProps)void 0===u[i]&&(u[i]=t.defaultProps[i]);return d(t,u,r,o,null)}function d(n,_,r,o,i){var u={type:n,props:_,key:r,ref:o,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==i?++e:i};return null==i&&null!=t.vnode&&t.vnode(u),u}function v(){return{current:null}}function m(n){return n.children}function y(n,t){this.props=n,this.context=t}function g(n,t){if(null==t)return n.__?g(n.__,n.__.__k.indexOf(n)+1):null;for(var e;t<n.__k.length;t++)if(null!=(e=n.__k[t])&&null!=e.__e)return e.__e;return"function"==typeof n.type?g(n):null}function b(n){var t,e;if(null!=(n=n.__)&&null!=n.__c){for(n.__e=n.__c.base=null,t=0;t<n.__k.length;t++)if(null!=(e=n.__k[t])&&null!=e.__e){n.__e=n.__c.base=e.__e;break}return b(n)}}function k(n){(!n.__d&&(n.__d=!0)&&r.push(n)&&!C.__r++||o!==t.debounceRendering)&&((o=t.debounceRendering)||i)(C)}function C(){var n,t,e,_,o,i,l,c;for(r.sort(u);n=r.shift();)n.__d&&(t=r.length,_=void 0,o=void 0,l=(i=(e=n).__v).__e,(c=e.__P)&&(_=[],(o=f({},i)).__v=i.__v+1,T(c,i,o,e.__n,void 0!==c.ownerSVGElement,null!=i.__h?[l]:null,_,null==l?g(i):l,i.__h),R(_,i),i.__e!=l&&b(i)),r.length>t&&r.sort(u));C.__r=0}function x(n,t,e,_,r,o,i,u,l,s){var f,p,h,v,y,b,k,C=_&&_.__k||a,x=C.length;for(e.__k=[],f=0;f<t.length;f++)if(null!=(v=e.__k[f]=null==(v=t[f])||"boolean"==typeof v||"function"==typeof v?null:"string"==typeof v||"number"==typeof v||"bigint"==typeof v?d(null,v,null,null,v):Array.isArray(v)?d(m,{children:v},null,null,null):v.__b>0?d(v.type,v.props,v.key,v.ref?v.ref:null,v.__v):v)){if(v.__=e,v.__b=e.__b+1,null===(h=C[f])||h&&v.key==h.key&&v.type===h.type)C[f]=void 0;else for(p=0;p<x;p++){if((h=C[p])&&v.key==h.key&&v.type===h.type){C[p]=void 0;break}h=null}T(n,v,h=h||c,r,o,i,u,l,s),y=v.__e,(p=v.ref)&&h.ref!=p&&(k||(k=[]),h.ref&&k.push(h.ref,null,v),k.push(p,v.__c||y,v)),null!=y?(null==b&&(b=y),"function"==typeof v.type&&v.__k===h.__k?v.__d=l=E(v,l,n):l=U(n,v,h,C,y,l),"function"==typeof e.type&&(e.__d=l)):l&&h.__e==l&&l.parentNode!=n&&(l=g(h))}for(e.__e=b,f=x;f--;)null!=C[f]&&("function"==typeof e.type&&null!=C[f].__e&&C[f].__e==e.__d&&(e.__d=A(_).nextSibling),W(C[f],C[f]));if(k)for(f=0;f<k.length;f++)M(k[f],k[++f],k[++f])}function E(n,t,e){for(var _,r=n.__k,o=0;r&&o<r.length;o++)(_=r[o])&&(_.__=n,t="function"==typeof _.type?E(_,t,e):U(e,_,_,r,_.__e,t));return t}function H(n,t){return t=t||[],null==n||"boolean"==typeof n||(Array.isArray(n)?n.some((function(n){H(n,t)})):t.push(n)),t}function U(n,t,e,_,r,o){var i,u,l;if(void 0!==t.__d)i=t.__d,t.__d=void 0;else if(null==e||r!=o||null==r.parentNode)n:if(null==o||o.parentNode!==n)n.appendChild(r),i=null;else{for(u=o,l=0;(u=u.nextSibling)&&l<_.length;l+=1)if(u==r)break n;n.insertBefore(r,o),i=o}return void 0!==i?i:r.nextSibling}function A(n){var t,e,_;if(null==n.type||"string"==typeof n.type)return n.__e;if(n.__k)for(t=n.__k.length-1;t>=0;t--)if((e=n.__k[t])&&(_=A(e)))return _;return null}function P(n,t,e,_,r){var o;for(o in e)"children"===o||"key"===o||o in t||N(n,o,null,e[o],_);for(o in t)r&&"function"!=typeof t[o]||"children"===o||"key"===o||"value"===o||"checked"===o||e[o]===t[o]||N(n,o,t[o],e[o],_)}function S(n,t,e){"-"===t[0]?n.setProperty(t,null==e?"":e):n[t]=null==e?"":"number"!=typeof e||s.test(t)?e:e+"px"}function N(n,t,e,_,r){var o;n:if("style"===t)if("string"==typeof e)n.style.cssText=e;else{if("string"==typeof _&&(n.style.cssText=_=""),_)for(t in _)e&&t in e||S(n.style,t,"");if(e)for(t in e)_&&e[t]===_[t]||S(n.style,t,e[t])}else if("o"===t[0]&&"n"===t[1])o=t!==(t=t.replace(/Capture$/,"")),t=t.toLowerCase()in n?t.toLowerCase().slice(2):t.slice(2),n.l||(n.l={}),n.l[t+o]=e,e?_||n.addEventListener(t,o?D:w,o):n.removeEventListener(t,o?D:w,o);else if("dangerouslySetInnerHTML"!==t){if(r)t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==t&&"height"!==t&&"href"!==t&&"list"!==t&&"form"!==t&&"tabIndex"!==t&&"download"!==t&&t in n)try{n[t]=null==e?"":e;break n}catch(n){}"function"==typeof e||(null==e||!1===e&&"-"!==t[4]?n.removeAttribute(t):n.setAttribute(t,e))}}function w(n){return this.l[n.type+!1](t.event?t.event(n):n)}function D(n){return this.l[n.type+!0](t.event?t.event(n):n)}function T(n,e,_,r,o,i,u,l,c){var a,s,p,h,d,v,g,b,k,C,E,H,U,A,P,S=e.type;if(void 0!==e.constructor)return null;null!=_.__h&&(c=_.__h,l=e.__e=_.__e,e.__h=null,i=[l]),(a=t.__b)&&a(e);try{n:if("function"==typeof S){if(b=e.props,k=(a=S.contextType)&&r[a.__c],C=a?k?k.props.value:a.__:r,_.__c?g=(s=e.__c=_.__c).__=s.__E:("prototype"in S&&S.prototype.render?e.__c=s=new S(b,C):(e.__c=s=new y(b,C),s.constructor=S,s.rLine truncated
|
||||
@@ -0,0 +1,202 @@
|
||||
'use strict';
|
||||
import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js';
|
||||
|
||||
export const Icons = {
|
||||
heart: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"></path></svg>`,
|
||||
settings: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>`,
|
||||
desktop: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" /> </svg>`,
|
||||
bell: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5" /> </svg>`,
|
||||
refresh: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /> </svg> `,
|
||||
bars4: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5" /> </svg>`,
|
||||
bars3: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> </svg>`,
|
||||
logout: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12.75 15l3-3m0 0l-3-3m3 3h-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
|
||||
save: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>`,
|
||||
email: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /> </svg>`,
|
||||
expand: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" /> </svg>`,
|
||||
shrink: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" /> </svg>`,
|
||||
ok: props => html`<svg class=${props.class} fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
|
||||
fail: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>`,
|
||||
upload: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" /> </svg> `,
|
||||
download: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /> </svg> `,
|
||||
bolt: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>`,
|
||||
home: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> </svg> `,
|
||||
link: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" /> </svg> `,
|
||||
shield: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /> </svg> `,
|
||||
barsdown: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0l-3.75-3.75M17.25 21L21 17.25" /> </svg> `,
|
||||
arrowdown: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75" /> </svg> `,
|
||||
arrowup: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" /> </svg>`,
|
||||
warn: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> </svg>`,
|
||||
info: props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /> </svg>`,
|
||||
};
|
||||
|
||||
export const tipColors = {
|
||||
green: 'bg-green-100 text-green-900',
|
||||
yellow: 'bg-yellow-100 text-yellow-900',
|
||||
red: 'bg-red-100 text-red-900',
|
||||
};
|
||||
|
||||
export function Button({title, onclick, disabled, cls, icon, ref, colors, hovercolor, disabledcolor}) {
|
||||
const [spin, setSpin] = useState(false);
|
||||
const cb = function(ev) {
|
||||
const res = onclick ? onclick() : null;
|
||||
if (res && typeof (res.catch) === 'function') {
|
||||
setSpin(true);
|
||||
res.catch(() => false).then(() => setSpin(false));
|
||||
}
|
||||
};
|
||||
if (!colors) colors = 'bg-blue-600 hover:bg-blue-500 disabled:bg-blue-400';
|
||||
return html`
|
||||
<button type="button" class="inline-flex justify-center items-center gap-1 rounded px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm ${colors} ${cls}"
|
||||
ref=${ref} onclick=${cb} disabled=${disabled || spin} >
|
||||
${title}
|
||||
<${spin ? Icons.refresh : icon} class="w-4 ${spin ? 'animate-spin' : ''}" />
|
||||
<//>`
|
||||
};
|
||||
|
||||
export function Notification({ok, text, close}) {
|
||||
const closebtn = useRef(null);
|
||||
const from = 'translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2';
|
||||
const to = 'translate-y-0 opacity-100 sm:translate-x-0';
|
||||
const [tr, setTr] = useState(from);
|
||||
useEffect(function() {
|
||||
setTr(to);
|
||||
setTimeout(ev => closebtn && closebtn.current.click && closebtn.current.click(), 1500);
|
||||
}, []);
|
||||
const onclose = ev => { setTr(from); setTimeout(close, 300); };
|
||||
return html`
|
||||
<div aria-live="assertive" class="z-10 pointer-events-none absolute inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
|
||||
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
|
||||
<div class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transform ease-out duration-300 transition ${tr}">
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<${ok ? Icons.ok : Icons.failed} class="h-6 w-6 ${ok ? 'text-green-400' : 'text-red-400'}" />
|
||||
<//>
|
||||
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||
<p class="text-sm font-medium text-gray-900">${text}</p>
|
||||
<p class="hidden mt-1 text-sm text-gray-500">Anyone with a link can now view this file.</p>
|
||||
<//>
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<button type="button" ref=${closebtn} onclick=${onclose} class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
|
||||
<//>
|
||||
<//>
|
||||
<//>
|
||||
<//>
|
||||
<//>
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
export function Login({loginFn, logoIcon, title, tipText}) {
|
||||
const [user, setUser] = useState('');
|
||||
const [pass, setPass] = useState('');
|
||||
const onsubmit = function(ev) {
|
||||
const authhdr = 'Basic ' + btoa(user + ':' + pass);
|
||||
const headers = {Authorization: authhdr};
|
||||
return fetch('api/login', {headers}).then(loginFn).finally(r => setPass(''));
|
||||
};
|
||||
return html`
|
||||
<div class="h-full flex items-center justify-center bg-slate-200">
|
||||
<div class="border rounded bg-white w-96 p-5">
|
||||
<div class="my-5 py-2 flex items-center justify-center gap-x-4">
|
||||
<${logoIcon} class="h-12 stroke-cyan-600 stroke-1" />
|
||||
<h1 class="font-bold text-xl">${title || 'Login'}<//>
|
||||
<//>
|
||||
<div class="my-3">
|
||||
<label class="block text-sm mb-1 dark:text-white">Username</label>
|
||||
<input type="text" autocomplete="current-user" required
|
||||
class="font-normal bg-white rounded border border-gray-300 w-full
|
||||
flex-1 py-0.5 px-2 text-gray-900 placeholder:text-gray-400
|
||||
focus:outline-none sm:text-sm sm:leading-6 disabled:cursor-not-allowed
|
||||
disabled:bg-gray-100 disabled:text-gray-500"
|
||||
oninput=${ev => setUser(ev.target.value)} value=${user} />
|
||||
<//>
|
||||
<div class="my-3">
|
||||
<label class="block text-sm mb-1 dark:text-white">Password</label>
|
||||
<input type="password" autocomplete="current-password" required
|
||||
class="font-normal bg-white rounded border border-gray-300 w-full flex-1 py-0.5 px-2 text-gray-900 placeholder:text-gray-400 focus:outline-none sm:text-sm sm:leading-6 disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500"
|
||||
oninput=${ev => setPass(ev.target.value)}
|
||||
value=${pass} onchange=${onsubmit} />
|
||||
<//>
|
||||
<div class="mt-7">
|
||||
<${Button} title="Sign In" icon=${Icons.logout} onclick=${onsubmit} cls="flex w-full justify-center" />
|
||||
<//>
|
||||
<div class="mt-5 text-slate-400 text-xs">${tipText}<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
|
||||
export function Colored({icon, text, colors}) {
|
||||
return html`
|
||||
<span class="inline-flex items-center gap-1.5 py-0.5 px-2 rounded-full ${colors || 'bg-slate-100 text-slate-900'}">
|
||||
${icon && html`<${icon} class="w-5 h-5" />`}
|
||||
<span class="inline-block text-xs font-medium">${text}<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
export function Stat({title, text, tipText, tipIcon, tipColors}) {
|
||||
return html`
|
||||
<div class="flex flex-col bg-white border shadow-sm rounded-xl dark:bg-slate-900 dark:border-gray-800">
|
||||
<div class="p-4 md:p-5">
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p class="text-xs uppercase tracking-wide text-gray-500"> ${title} </p>
|
||||
<//>
|
||||
<div class="mt-1 flex items-center gap-x-2">
|
||||
<h3 class="text-xl sm:text-2xl font-medium text-gray-800 dark:text-gray-200">
|
||||
${text}
|
||||
<//>
|
||||
<span class="flex items-center">
|
||||
<${Colored} text=${tipText} icon=${tipIcon} colors=${tipColors} />
|
||||
<//>
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
export function TextValue({value, setfn, disabled, placeholder, type, addonRight, addonLeft, attr}) {
|
||||
const f = type == 'number' ? x => setfn(parseInt(x)) : setfn;
|
||||
return html`
|
||||
<div class="flex w-full items-center rounded border shadow-sm">
|
||||
${ addonLeft && html`<span class="inline-flex font-normal truncate py-1 border-r bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs">${addonLeft}</>` }
|
||||
<input type=${type || 'text'} disabled=${disabled}
|
||||
oninput=${ev => f(ev.target.value)} ...${attr}
|
||||
class="font-normal text-sm rounded w-full flex-1 py-0.5 px-2 text-gray-700 placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 disabled:text-gray-500" placeholder=${placeholder} value=${value} />
|
||||
${ addonRight && html`<span class="inline-flex font-normal truncate py-1 border-l bg-slate-100 items-center border-gray-300 px-2 text-gray-500 text-xs">${addonRight}</>` }
|
||||
<//>`;
|
||||
};
|
||||
|
||||
export function SelectValue({value, setfn, options, disabled}) {
|
||||
const toInt = x => x == parseInt(x) ? parseInt(x) : x;
|
||||
const onchange = ev => setfn(toInt(ev.target.value));
|
||||
return html`
|
||||
<select onchange=${onchange} class="w-full rounded font-normal border py-0.5 px-1 text-gray-600 focus:outline-none text-sm disabled:cursor-not-allowed" disabled=${disabled}>
|
||||
${options.map(v => html`<option value=${v[0]} selected=${v[0] == value}>${v[1]}<//>`) }
|
||||
<//>`;
|
||||
};
|
||||
|
||||
export function SwitchValue({value, setfn}) {
|
||||
const onclick = ev => setfn(!value);
|
||||
const bg = !!value ? 'bg-blue-600' : 'bg-gray-200';
|
||||
const tr = !!value ? 'translate-x-5' : 'translate-x-0';
|
||||
return html`
|
||||
<button type="button" onclick=${onclick} class="${bg} inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-0 ring-0" role="switch" aria-checked=${!!value}>
|
||||
<span aria-hidden="true" class="${tr} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 focus:ring-0 transition duration-200 ease-in-out"></span>
|
||||
</button>`;
|
||||
};
|
||||
|
||||
export function Setting(props) {
|
||||
return html`
|
||||
<div class=${props.cls || 'grid grid-cols-2 gap-2 my-1'}>
|
||||
<label class="flex items-center text-sm text-gray-700 mr-2 font-medium">${props.title}<//>
|
||||
<div class="flex items-center">
|
||||
${props.type == 'switch' ? h(SwitchValue, props) :
|
||||
props.type == 'select' ? h(SelectValue, props) :
|
||||
h(TextValue, props) }
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.History=n():t.History=n()}(this,function(){return function(t){function n(o){if(e[o])return e[o].exports;var r=e[o]={exports:{},id:o,loaded:!1};return t[o].call(r.exports,r,r.exports,n),r.loaded=!0,r.exports}var e={};return n.m=t,n.c=e,n.p="",n(0)}([function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0,n.createPath=n.parsePath=n.locationsAreEqual=n.createLocation=n.createMemoryHistory=n.createHashHistory=n.createBrowserHistory=void 0;var r=e(2);Object.defineProperty(n,"createLocation",{enumerable:!0,get:function(){return r.createLocation}}),Object.defineProperty(n,"locationsAreEqual",{enumerable:!0,get:function(){return r.locationsAreEqual}});var i=e(1);Object.defineProperty(n,"parsePath",{enumerable:!0,get:function(){return i.parsePath}}),Object.defineProperty(n,"createPath",{enumerable:!0,get:function(){return i.createPath}});var a=e(7),c=o(a),u=e(8),s=o(u),f=e(9),l=o(f);n.createBrowserHistory=c.default,n.createHashHistory=s.default,n.createMemoryHistory=l.default},function(t,n){"use strict";n.__esModule=!0;n.addLeadingSlash=function(t){return"/"===t.charAt(0)?t:"/"+t},n.stripLeadingSlash=function(t){return"/"===t.charAt(0)?t.substr(1):t},n.stripPrefix=function(t,n){return 0===t.indexOf(n)?t.substr(n.length):t},n.stripTrailingSlash=function(t){return"/"===t.charAt(t.length-1)?t.slice(0,-1):t},n.parsePath=function(t){var n=t||"/",e="",o="",r=n.indexOf("#");r!==-1&&(o=n.substr(r),n=n.substr(0,r));var i=n.indexOf("?");return i!==-1&&(e=n.substr(i),n=n.substr(0,i)),n=decodeURI(n),{pathname:n,search:"?"===e?"":e,hash:"#"===o?"":o}},n.createPath=function(t){var n=t.pathname,e=t.search,o=t.hash,r=encodeURI(n||"/");return e&&"?"!==e&&(r+="?"===e.charAt(0)?e:"?"+e),o&&"#"!==o&&(r+="#"===o.charAt(0)?o:"#"+o),r}},function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0,n.locationsAreEqual=n.createLocation=void 0;var r=Object.assign||function(t){for(var n=1;n<arguments.length;n++){var e=arguments[n];for(var o in e)Object.prototype.hasOwnProperty.call(e,o)&&(t[o]=e[o])}return t},i=e(10),a=o(i),c=e(11),u=o(c),s=e(1);n.createLocation=function(t,n,e,o){var i=void 0;return"string"==typeof t?(i=(0,s.parsePath)(t),i.state=n):(i=r({},t),void 0===i.pathname&&(i.pathname=""),i.search?"?"!==i.search.charAt(0)&&(i.search="?"+i.search):i.search="",i.hash?"#"!==i.hash.charAt(0)&&(i.hash="#"+i.hash):i.hash="",void 0!==n&&void 0===i.state&&(i.state=n)),i.key=e,o&&(i.pathname?"/"!==i.pathname.charAt(0)&&(i.pathname=(0,a.default)(i.pathname,o.pathname)):i.pathname=o.pathname),i},n.locationsAreEqual=function(t,n){return t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash&&t.key===n.key&&(0,u.default)(t.state,n.state)}},function(t,n,e){"use strict";var o=function(){};t.exports=o},function(t,n,e){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}n.__esModule=!0;var r=e(3),i=(o(r),function(){var t=null,n=function(n){return t=n,function(){t===n&&(t=null)}},e=function(n,e,o,r){if(null!=t){var i="function"==typeof t?t(n,e):t;"string"==typeof i?"function"==typeof o?o(i,r):r(!0):r(i!==!1)}else r(!0)},o=[],r=function(t){var n=!0,e=function(){n&&t.apply(void 0,arguments)};return o.push(e),function(){n=!1,o=o.filter(function(t){return t!==e})}},i=function(){for(var t=arguments.length,n=Array(t),e=0;e<t;e++)n[e]=arguments[e];o.forEach(function(t){return t.apply(void 0,n)})};return{setPrompt:n,confirmTransitionTo:e,appendListener:r,notifyListeners:i}});n.default=i},function(t,n){"use strict";n.__esModule=!0;n.canUseDOM=!("undefined"==typeof window||!window.document||!window.document.createElement),n.addEventListener=function(t,n,e){return t.addEventListener?t.addEventListener(n,e,!1):t.attachEvent("on"+n,e)},n.removeEventListener=function(t,n,e){return t.removeEventListener?t.removeEventListener(n,e,!1):t.detachEvent("on"+n,e)},n.getConfirmation=function(t,n){return n(window.confirm(t))},n.supportsHistory=function(){var t=window.navigator.userAgent;return(t.indexOf("Android 2.")===-1&&t.indexOf("Android 4.0")===-1||t.indexOf("Mobile Safari")===-1||t.indexOf("Chrome")!==-1||t.indexOf("Windows Phone")!==-1)&&(window.history&&"pushState"in window.history)},n.supportsPopStateOnHashChange=function(){return window.navigator.userAgent.indexOf("Trident")===-1},n.supportsGoWithoutReloadUsingHash=function(){return window.navigator.userAgent.indexOf("Firefox")===-1},n.isExtraneousPopstateEvent=function(t){return void 0===t.state&&navigator.userAgent.indexOf("CriOS")===-1}},function(t,n,e){"use strict";var o=function(t,n,e,o,r,i,a,c){if(!t){var u;if(void 0===n)u=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var s=[e,o,r,i,a,c],f=0;u=new Error(n.replace(/%s/g,function(){return s[f++]})),u.name="InLine truncated
|
||||
@@ -1,12 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="h-full bg-white">
|
||||
<head>
|
||||
<title>Device Dashboard</title>
|
||||
<title></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" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor'> <path stroke-linecap='round' stroke-linejoin='round' d='M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0' /> </svg>" />
|
||||
<link href="main.css" rel="stylesheet" />
|
||||
<link href="https://rsms.me/inter/inter.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body></body>
|
||||
<body class="h-full"></body>
|
||||
<script src="history.min.js"></script>
|
||||
<script type="module" src="main.js"></script>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter var,Helvetica,sans-serif;font-feature-settings:"cv11","ss01";font-variation-settings:"opsz" 32}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.right-auto{right:auto}.top-0{top:0}.z-10{z-index:10}.z-\[48\]{z-index:48}.z-\[60\]{z-index:60}.col-span-2{grid-column:span 2/span 2}.m-4{margin:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-1{margin-bottom:.25rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-3{margin-top:.75rem}.mt-5{margin-top:1.25rem}.mt-7{margin-top:1.75rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-0{width:0}.w-11{width:2.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-96{width:24rem}.w-full{width:100%}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow-0{flex-grow:0}.basis-\[30px\]{flex-basis:30px}.-translate-x-full{--tw-translate-x:-100%}.-translate-x-full,.translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translatLine truncated
|
||||
@@ -1,385 +1,252 @@
|
||||
'use strict';
|
||||
import {Component, h, html, render, useEffect, useState, useRef} from './preact.min.js';
|
||||
import { h, render, useState, useEffect, useRef, html, Router } from './bundle.js';
|
||||
import { Icons, Login, Setting, Button, Stat, tipColors, Colored, Notification } from './components.js';
|
||||
|
||||
var devaddr = "address:port";
|
||||
const Logo = props => html`<svg class=${props.class} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.87 12.85"><defs><style>.ll-cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:0.5px;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="ll-cls-1" d="M12.62,1.82V8.91A1.58,1.58,0,0,1,11,10.48H4a1.44,1.44,0,0,1-1-.37A.69.69,0,0,1,2.84,10l-.1-.12a.81.81,0,0,1-.15-.48V5.57a.87.87,0,0,1,.86-.86H4.73V7.28a.86.86,0,0,0,.86.85H9.42a.85.85,0,0,0,.85-.85V3.45A.86.86,0,0,0,10.13,3,.76.76,0,0,0,10,2.84a.29.29,0,0,0-.12-.1,1.49,1.49,0,0,0-1-.37H2.39V1.82A1.57,1.57,0,0,1,4,.25H11A1.57,1.57,0,0,1,12.62,1.82Z"/><path class="ll-cls-1" d="M10.48,10.48V11A1.58,1.58,0,0,1,8.9,12.6H1.82A1.57,1.57,0,0,1,.25,11V3.94A1.57,1.57,0,0,1,1.82,2.37H8.9a1.49,1.49,0,0,1,1,.37l.12.1a.76.76,0,0,1,.11.14.86.86,0,0,1,.14.47V7.28a.85.85,0,0,1-.85.85H8.13V5.57a.86.86,0,0,0-.85-.86H3.45a.87.87,0,0,0-.86.86V9.4a.81.81,0,0,0,.15.48l.1.12a.69.69,0,0,0,.13.11,1.44,1.44,0,0,0,1,.37Z"/></g></g></svg>`;
|
||||
|
||||
const MaxMetricsDataPoints = 50;
|
||||
|
||||
// This simple publish/subscribe is used to pass notifications that were
|
||||
// received from the server, to all child components of the app.
|
||||
var PubSub = (function() {
|
||||
var handlers = {}, id = 0;
|
||||
return {
|
||||
subscribe: function(fn) {
|
||||
handlers[id++] = fn;
|
||||
},
|
||||
unsubscribe: function(id) {
|
||||
delete handlers[id];
|
||||
},
|
||||
publish: function(data) {
|
||||
for (var k in handlers) handlers[k](data);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
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 Hero = props => html`
|
||||
<div class="section">
|
||||
<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>
|
||||
|
||||
<p>
|
||||
This device dashboard is developed using the modern and compact Preact framework,
|
||||
in order to fit on 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.
|
||||
|
||||
This UI uses the REST API implemented by the device, which you can examine
|
||||
using <code>curl</code> command-line utility:
|
||||
</p>
|
||||
|
||||
<div><code>curl -u admin:pass0 ${devaddr}/api/config/get</code> </div>
|
||||
<div><code>curl -u admin:pass0 ${devaddr}/api/config/set -d 'pub=mg/topic'</code> </div>
|
||||
<div><code>curl -u admin:pass0 ${devaddr}/api/message/send -d 'message=hello'</code> </div>
|
||||
|
||||
<p>
|
||||
The device can send notifications to this dashboard at anytime. Notifications
|
||||
are sent over WebSocket at URI <code>/api/watch</code> as JSON strings: <code>{"name": "..", "data": ...}</code>
|
||||
<div>Try <code>wscat --auth user1:pass1 --connect ws://${devaddr}/api/watch</code></div>
|
||||
</p>
|
||||
</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);
|
||||
function Header({logout, user, setShowSidebar, showSidebar}) {
|
||||
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>`;
|
||||
<div class="bg-white sticky top-0 z-[48] xw-full border-b py-2 ${showSidebar && 'pl-72'} transition-all duration-300 transform">
|
||||
<div class="px-2 w-full py-0 my-0 flex items-center">
|
||||
<button type="button" onclick=${ev => setShowSidebar(v => !v)} class="text-slate-400">
|
||||
<${Icons.bars3} class="h-6" />
|
||||
<//>
|
||||
<div class="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
|
||||
<div class="relative flex flex-1"><//>
|
||||
<div class="flex items-center gap-x-4 lg:gap-x-6">
|
||||
<span class="text-sm text-slate-400">logged in as: ${user}<//>
|
||||
<div class="hidden lg:block lg:h-4 lg:w-px lg:bg-gray-200" aria-hidden="true"><//>
|
||||
<${Button} title="Logout" icon=${Icons.logout} onclick=${logout} />
|
||||
<//>
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
|
||||
const Configuration = function(props) {
|
||||
const [url, setUrl] = useState(props.config.url || '');
|
||||
const [pub, setPub] = useState(props.config.pub || '');
|
||||
const [sub, setSub] = useState(props.config.sub || '');
|
||||
|
||||
useEffect(() => {
|
||||
setUrl(props.config.url);
|
||||
setPub(props.config.pub);
|
||||
setSub(props.config.sub);
|
||||
}, [props.config]);
|
||||
|
||||
const update = (name, val) => fetch('api/config/set', {
|
||||
method: 'post',
|
||||
body: `${name}=${encodeURIComponent(val)}`
|
||||
}).catch(err => err);
|
||||
const updateurl = ev => update('url', url);
|
||||
const updatepub = ev => update('pub', pub);
|
||||
const updatesub = ev => update('sub', sub);
|
||||
|
||||
// console.log(props, [url, pub, sub]);
|
||||
return html`
|
||||
<div class="section">
|
||||
<h3 style="background: #c03434; color: #fff; padding: 0.4em;">
|
||||
Device Configuration</h3>
|
||||
<div style="margin: 0.5em 0; display: flex;">
|
||||
<span class="addon nowrap">MQTT server:</span>
|
||||
<input type="text" style="flex: 1 100%;"
|
||||
value=${url} onchange=${updateurl}
|
||||
oninput=${ev => setUrl(ev.target.value)} />
|
||||
<button class="btn" disabled=${!url} onclick=${updateurl}
|
||||
style="margin-left: 1em; background: #8aa;">Update</button>
|
||||
</div>
|
||||
<div style="margin: 0.5em 0; display: flex; ">
|
||||
<span class="addon nowrap">Subscribe topic:</span>
|
||||
<input type="text" style="flex: 1 100%;"
|
||||
value=${sub} onchange=${updatesub}
|
||||
oninput=${ev => setSub(ev.target.value)} />
|
||||
<button class="btn" disabled=${!sub} onclick=${updatesub}
|
||||
style="margin-left: 1em; background: #8aa;">Update</button>
|
||||
</div>
|
||||
<div style="margin: 0.5em 0; display: flex;">
|
||||
<span class="addon nowrap">Publish topic:</span>
|
||||
<input type="text" style="flex: 1 100%;"
|
||||
value=${pub} onchange=${updatepub}
|
||||
oninput=${ev => setPub(ev.target.value)} />
|
||||
<button class="btn" disabled=${!pub} onclick=${updatepub}
|
||||
style="margin-left: 1em; background: #8aa;">Update</button>
|
||||
</div>
|
||||
function Sidebar({url, show}) {
|
||||
const NavLink = ({title, icon, href, url}) => html`
|
||||
<div>
|
||||
You can use <a href="http://www.hivemq.com/demos/websocket-client/">
|
||||
HiveMQ Websocket web client</a> to send messages to this console.
|
||||
</div>
|
||||
<div class="msg">
|
||||
The device keeps a persistent connection to the configured MQTT server.
|
||||
Changes to this configuration are propagated to all dashboards: try
|
||||
changing them in this dashboard and observe changes in other opened
|
||||
dashboards.
|
||||
</div><div class="msg">
|
||||
Note: administrators can see this section and can change device
|
||||
configuration, whilst users cannot.
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
|
||||
const Message = m => html`<div style="margin: 0.5em 0;">
|
||||
<span class="qos">qos: ${m.message.qos} </span>
|
||||
<span class="topic">topic: ${m.message.topic} </span>
|
||||
<span class="data">data: ${m.message.data}</span>
|
||||
</div>`;
|
||||
|
||||
const Messages = function(props) {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [txt, setTxt] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const id = PubSub.subscribe(function(msg) {
|
||||
if (msg.name == 'message') setMessages(x => x.concat([msg.data]));
|
||||
});
|
||||
return PubSub.unsubscribe(id);
|
||||
}, []);
|
||||
|
||||
const sendmessage = ev => fetch('api/message/send', {
|
||||
method: 'post',
|
||||
body: `message=${encodeURIComponent(txt)}`
|
||||
}).then(r => setTxt(''));
|
||||
const routing = "connected" in props.config;
|
||||
const connstatus = !routing ? 'This device has no MQTT functionality' : props.config.connected ? 'connected' : 'disconnected';
|
||||
<a href="#${href}" class="${href == url ? 'bg-slate-50 text-blue-600 group' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50 group'} flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
|
||||
<${icon} class="w-6 h-6"/>
|
||||
${title}
|
||||
<///>
|
||||
<//>`;
|
||||
return html`
|
||||
<div class="section">
|
||||
<h3 style="background: #30c040; color: #fff; padding: 0.4em;">MQTT messages</h3>
|
||||
<div>
|
||||
MQTT server status: <b>${connstatus}</b>
|
||||
</div>
|
||||
<div style="height: 10em; overflow: auto; padding: 0.5em; " class="border">
|
||||
${messages.map(message => h(Message, {message}))}
|
||||
</div>
|
||||
<div style="margin: 0.5em 0; display: flex">
|
||||
<span class="addon nowrap">Publish message:</span>
|
||||
<input placeholder="type and press enter..." style="flex: 1 100%;"
|
||||
value=${txt} onchange=${sendmessage} disabled=${!routing}
|
||||
oninput=${ev => setTxt(ev.target.value)} />
|
||||
</div>
|
||||
<div class="msg">
|
||||
The message gets passed to the device via REST. Then the device sends it to
|
||||
the MQTT server over MQTT. All MQTT messages on a subscribed topic
|
||||
received by the device, are propagated to this dashboard via /api/watch.
|
||||
</div>
|
||||
</div>`;
|
||||
<div class="bg-violet-100 hs-overlay hs-overlay-open:translate-x-0
|
||||
-translate-x-full transition-all duration-300 transform
|
||||
fixed top-0 left-0 bottom-0 z-[60] w-72 bg-white border-r
|
||||
border-gray-200 overflow-y-auto scrollbar-y
|
||||
${show && 'translate-x-0'} right-auto bottom-0">
|
||||
<div class="flex flex-col m-4 gap-y-6">
|
||||
<div class="flex h-10 shrink-0 items-center gap-x-4 font-bold text-xl text-slate-500">
|
||||
<${Logo} class="h-full"/> Your Brand
|
||||
<//>
|
||||
<div class="flex flex-1 flex-col">
|
||||
<${NavLink} title="Dashboard" icon=${Icons.home} href="/" url=${url} />
|
||||
<${NavLink} title="Settings" icon=${Icons.settings} href="/settings" url=${url} />
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
// Expected arguments:
|
||||
// data: timeseries, e.g. [ [1654361352, 19], [1654361353, 18], ... ]
|
||||
// width, height, yticks, xticks, ymin, ymax, xmin, xmax
|
||||
const SVG = function(props) {
|
||||
// w
|
||||
// +---------------------+
|
||||
// | h1 |
|
||||
// | +-----------+ |
|
||||
// | | | | h
|
||||
// | w1 | | w2 |
|
||||
// | +-----------+ |
|
||||
// | h2 |
|
||||
// +---------------------+
|
||||
//
|
||||
let w = props.width, h = props.height, w1 = 30, w2 = 0, h1 = 8, h2 = 18;
|
||||
let yticks = props.yticks || 4, xticks = props.xticks || 5;
|
||||
let data = props.data || [];
|
||||
let ymin = props.ymin || 0;
|
||||
let ymax = props.ymax || Math.max.apply(null, data.map(p => p[1]));
|
||||
let xmin = props.xmin || Math.min.apply(null, data.map(p => p[0]));
|
||||
let xmax = props.xmax || Math.max.apply(null, data.map(p => p[0]));
|
||||
function Events({}) {
|
||||
const [events, setEvents] = useState([]);
|
||||
const refresh = () => fetch('api/events/get').then(r => r.json()).then(r => setEvents(r)).catch(e => console.log(e));
|
||||
useEffect(refresh, []);
|
||||
|
||||
// Y-axis tick lines and labels
|
||||
let yta = (new Array(yticks + 1)).fill(0).map((_, i) => i); // indices
|
||||
let yti = i => h - h2 - (h - h1 - h2) * i / yticks; // index's Y
|
||||
let ytv = i => (ymax - ymin) * i / yticks;
|
||||
let ytl = y => html`<line x1=${w1} y1=${y} x2=${w} y2=${y} class="tick"/>`;
|
||||
let ytt = (y, v) => html`<text x=0 y=${y + 5} class="label">${v}</text>`;
|
||||
|
||||
// X-axis tick lines and labels
|
||||
let datefmt = unix => (new Date(unix * 1000)).toISOString().substr(14, 5);
|
||||
let xta = (new Array(xticks + 1)).fill(0).map((_, i) => i); // indices
|
||||
let xti = i => w1 + (w - w1 - w2) * i / xticks; // index's X
|
||||
let xtv = i => datefmt(xmin + (xmax - xmin) * i / xticks);
|
||||
let xtl = x => html`<path d="M ${x},${h1} L ${x},${h - h2}" class="tick"/>`;
|
||||
let xtt = (x, v) =>
|
||||
html`<text x=${x - 15} y=${h - 2} class="label">${v}</text>`;
|
||||
|
||||
// Transform data points array into coordinate
|
||||
let dx = v => w1 + (v - xmin) / ((xmax - xmin) || 1) * (w - w1 - w2);
|
||||
let dy = v => h - h2 - (v - ymin) / ((ymax - ymin) || 1) * (h - h1 - h2);
|
||||
let dd = data.map(p => [Math.round(dx(p[0])), Math.round(dy(p[1]))]);
|
||||
let ddl = dd.length;
|
||||
// And plot the data as <path> element
|
||||
let begin0 = ddl ? `M ${dd[0][0]},${dd[0][1]}` : `M 0,0`;
|
||||
let begin = `M ${w1},${h - h2}`; // Initial point
|
||||
let end = ddl ? `L ${dd[ddl - 1][0]},${h - h2}` : `L ${w1},${h - h2}`;
|
||||
let series = ddl ? dd.map(p => `L ${p[0]} ${p[1]}`) : [];
|
||||
const Th = props => html`<th scope="col" class="sticky top-0 z-10 border-b border-slate-300 bg-white bg-opacity-75 py-1.5 px-4 text-left text-sm font-semibold text-slate-900 backdrop-blur backdrop-filter">${props.title}</th>`;
|
||||
const Td = props => html`<td class="whitespace-nowrap border-b border-slate-200 py-2 px-4 pr-3 text-sm text-slate-900">${props.text}</td>`;
|
||||
const Prio = ({prio}) => {
|
||||
const text = ['high', 'medium', 'low'][prio];
|
||||
const colors = [tipColors.red, tipColors.yellow, tipColors.green][prio];
|
||||
return html`<${Colored} colors=${colors} text=${text} />`;
|
||||
};
|
||||
const Event = ({e}) => html`
|
||||
<tr>
|
||||
<${Td} text=${['power', 'hardware', 'tier3', 'tier4'][e.type]} />
|
||||
<${Td} text=${html`<${Prio} prio=${e.prio}/>`} />
|
||||
<${Td} text=${e.time || '1970-01-01'} />
|
||||
<${Td} text=${e.text} />
|
||||
<//>`;
|
||||
//console.log(events);
|
||||
|
||||
return html`
|
||||
<svg viewBox="0 0 ${w} ${h}">
|
||||
<style>
|
||||
.axis { stroke: #aaa; fill: none; }
|
||||
.label { stroke: #aaa; font-size: 13px; }
|
||||
.tick { stroke: #ccc; fill: none; stroke-dasharray: 5; }
|
||||
.seriesbg { stroke: none; fill: rgba(200,225,255, 0.25)}
|
||||
.series { stroke: #25a; fill: none; }
|
||||
</style>
|
||||
${yta.map(i => ytl(yti(i)))}
|
||||
${yta.map(i => ytt(yti(i), ytv(i)))}
|
||||
${xta.map(i => xtl(xti(i)))}
|
||||
${data.length ? xta.map(i => xtt(xti(i), xtv(i))) : ''}
|
||||
<path d="${begin} ${series.join(' ')} ${end}" class="seriesbg" />
|
||||
<path d="${begin0} ${series.join(' ')}" class="series" />
|
||||
</svg>`;
|
||||
<div class="my-4 h-64 divide-y divide-gray-200 rounded bg-white overflow-auto">
|
||||
<div class="font-light uppercase flex items-center text-slate-600 px-4 py-2">
|
||||
Event Log
|
||||
<//>
|
||||
<div class="">
|
||||
<table class="">
|
||||
<thead>
|
||||
<tr>
|
||||
<${Th} title="Type" />
|
||||
<${Th} title="Prio" />
|
||||
<${Th} title="Time" />
|
||||
<${Th} title="Description" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${events.map(e => h(Event, {e}))}
|
||||
</tbody>
|
||||
</table>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
|
||||
const Chart = function(props) {
|
||||
const [data, setData] = useState([]);
|
||||
useEffect(() => {
|
||||
const id = PubSub.subscribe(function(msg) {
|
||||
if (msg.name != 'metrics') return;
|
||||
setData(x => x.concat([msg.data]).splice(-MaxMetricsDataPoints));
|
||||
});
|
||||
return PubSub.unsubscribe(id);
|
||||
}, []);
|
||||
|
||||
let xmax = 0, missing = MaxMetricsDataPoints - data.length;
|
||||
if (missing > 0) xmax = Math.round(Date.now() / 1000) + missing;
|
||||
function Chart({data}) {
|
||||
const n = data.length /* entries */, w = 20 /* entry width */, ls = 15/* left space */;
|
||||
const h = 100 /* graph height */, yticks = 5 /* Y axis ticks */, bs = 10 /* bottom space */;
|
||||
const ymax = 25;
|
||||
const yt = i => (h - bs) / yticks * (i + 1);
|
||||
const bh = p => (h - bs) * p / 100; // Bar height
|
||||
const by = p => (h - bs) - bh(p);
|
||||
const range = (start, size, step) => Array.from({length: size}, (_, i) => i * (step || 1) + start);
|
||||
// console.log(ds);
|
||||
return html`
|
||||
<div class="section">
|
||||
<h3 style="background: #ec3; color: #fff; padding: 0.4em;">Data Chart</h3>
|
||||
<div style="overflow: auto; padding: 0.5em;" class="">
|
||||
<${SVG} height=240 width=600 ymin=0 ymax=20 xmax=${xmax} data=${data} />
|
||||
</div>
|
||||
<div class="msg">
|
||||
This chart plots live sensor data, sent by the device via /api/watch.
|
||||
</div>
|
||||
</div>`;
|
||||
<div class="my-4 divide-y divide-gray-200 overflow-auto rounded bg-white">
|
||||
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
|
||||
Temperature, last 24h
|
||||
<//>
|
||||
<div class="relative">
|
||||
<svg class="bg-yellow-x50 w-full p-4" viewBox="0 0 ${n*w+ls} ${h}">
|
||||
${range(0, yticks).map(i => html`
|
||||
<line x1=0 y1=${yt(i)} x2=${ls+n*w} y2=${yt(i)} stroke-width=0.3 class="stroke-slate-300" stroke-dasharray="1,1" />
|
||||
<text x=0 y=${yt(i)-2} class="text-[6px] fill-slate-400">${ymax-ymax/yticks*(i+1)}<//>
|
||||
`)}
|
||||
${range(0, n).map(x => html`
|
||||
<rect x=${ls+x*w} y=${by(data[x]*100/ymax)} width=12 height=${bh(data[x]*100/ymax)} rx=2 class="fill-cyan-500" />
|
||||
<text x=${ls+x*w} y=100 class="text-[6px] fill-slate-400">${x*2}:00<//>
|
||||
`)}
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
const App = function(props) {
|
||||
function DeveloperNote({text}) {
|
||||
return html`
|
||||
<div class="flex p-4 gap-2">
|
||||
<${Icons.info} class="self-start basis-[30px] grow-0 shrink-0 text-green-600" />
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mt-1">Developer Note<//>
|
||||
${text.split('.').map(v => html` <p class="my-2 text-slate-500">${v}<//>`)}
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
function Main({}) {
|
||||
const [stats, setStats] = useState(null);
|
||||
const refresh = () => fetch('api/stats/get').then(r => r.json()).then(r => setStats(r));
|
||||
useEffect(refresh, []);
|
||||
if (!stats) return '';
|
||||
return html`
|
||||
<div class="p-2">
|
||||
<div class="p-4 sm:p-2 mx-auto grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<${Stat} title="Temperature" text="${stats.temperature} °C" tipText="good" tipIcon=${Icons.ok} tipColors=${tipColors.green} />
|
||||
<${Stat} title="Humidity" text="${stats.humidity} %" tipText="warn" tipIcon=${Icons.warn} tipColors=${tipColors.yellow} />
|
||||
<div class="bg-white col-span-2 border rounded-md shadow-lg" role="alert">
|
||||
<${DeveloperNote} text="Stats data is received from the Mongoose backend" />
|
||||
<//>
|
||||
<//>
|
||||
<div class="p-4 sm:p-2 mx-auto grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<${Events} />
|
||||
|
||||
<div class="my-4 hx-24 bg-white border rounded-md shadow-lg" role="alert">
|
||||
<${DeveloperNote}
|
||||
text="Events data is also received from the backend,
|
||||
via the /api/events/get API call, which returns an array of objects each
|
||||
representing an event. Events table is scrollable,
|
||||
Table header is sticky" />
|
||||
<//>
|
||||
|
||||
<${Chart} data=${stats.points} />
|
||||
|
||||
<div class="my-4 hx-24 bg-white border rounded-md shadow-lg" role="alert">
|
||||
<${DeveloperNote}
|
||||
text="This chart is an SVG image, generated on the fly from the
|
||||
data returned by the /api/stats/get API call" />
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
function Settings({}) {
|
||||
const [settings, setSettings] = useState(null);
|
||||
const [saveResult, setSaveResult] = useState(null);
|
||||
const refresh = () => fetch('api/settings/get')
|
||||
.then(r => r.json())
|
||||
.then(r => setSettings(r));
|
||||
useEffect(refresh, []);
|
||||
|
||||
const mksetfn = k => (v => setSettings(x => Object.assign({}, x, {[k]: v})));
|
||||
const onsave = ev => fetch('api/settings/set', {
|
||||
method: 'post', body: JSON.stringify(settings)
|
||||
}).then(r => r.json())
|
||||
.then(r => setSaveResult(r))
|
||||
.then(refresh);
|
||||
|
||||
if (!settings) return '';
|
||||
const logOptions = [[0, 'Disable'], [1, 'Error'], [2, 'Info'], [3, 'Debug']];
|
||||
return html`
|
||||
<div class="m-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
|
||||
<div class="py-1 divide-y border rounded bg-white flex flex-col">
|
||||
<div class="font-light uppercase flex items-center text-gray-600 px-4 py-2">
|
||||
Device Settings
|
||||
<//>
|
||||
<div class="py-2 px-5 flex-1 flex flex-col relative">
|
||||
${saveResult && html`<${Notification} ok=${saveResult.status}
|
||||
text=${saveResult.message} close=${() => setSaveResult(null)} />`}
|
||||
|
||||
<${Setting} title="Enable Logs" value=${settings.log_enabled} setfn=${mksetfn('log_enabled')} type="switch" />
|
||||
<${Setting} title="Log Level" value=${settings.log_level} setfn=${mksetfn('log_level')} type="select" addonLeft="0-3" disabled=${!settings.log_enabled} options=${logOptions}/>
|
||||
<${Setting} title="Brightness" value=${settings.brightness} setfn=${mksetfn('brightness')} type="number" addonRight="%" />
|
||||
<${Setting} title="Device Name" value=${settings.device_name} setfn=${mksetfn('device_name')} type="" />
|
||||
<div class="mb-1 mt-3 flex place-content-end"><${Button} icon=${Icons.save} onclick=${onsave} title="Save Settings" /><//>
|
||||
<//>
|
||||
<//>
|
||||
|
||||
<div class="bg-white border rounded-md text-ellipsis overflow-auto" role="alert">
|
||||
<${DeveloperNote}
|
||||
text="A variety of controls are pre-defined to ease the development:
|
||||
toggle button, dropdown select, input field with left and right
|
||||
addons. Device settings are received by calling
|
||||
/api/settings/get API call, which returns settings JSON object.
|
||||
Clicking on a save button calls /api/settings/set
|
||||
API call" />
|
||||
<//>
|
||||
|
||||
<//>`;
|
||||
};
|
||||
|
||||
const App = function({}) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [url, setUrl] = useState('/');
|
||||
const [user, setUser] = useState('');
|
||||
const [config, setConfig] = useState({});
|
||||
const [showSidebar, setShowSidebar] = useState(true);
|
||||
|
||||
const getconfig = () =>
|
||||
fetch('api/config/get', {headers: {Authorization: ''}})
|
||||
.then(r => r.json())
|
||||
.then(r => setConfig(r))
|
||||
.catch(err => console.log(err));
|
||||
const logout = () => fetch('api/logout').then(r => setUser(''));
|
||||
const login = r => !r.ok ? setLoading(false) && setUser(null) : r.json()
|
||||
.then(r => setUser(r.user))
|
||||
.finally(r => setLoading(false));
|
||||
|
||||
// Watch for notifications. As soon as a notification arrives, pass it on
|
||||
// to all subscribed components
|
||||
const watch = function() {
|
||||
var l = window.location, proto = l.protocol.replace('http', 'ws');
|
||||
var tid, wsURI = proto + '//' + l.host + l.pathname + 'api/watch'
|
||||
var reconnect = function() {
|
||||
var ws = new WebSocket(wsURI);
|
||||
// ws.onopen = () => console.log('ws connected');
|
||||
ws.onmessage = function(ev) {
|
||||
try {
|
||||
var msg = JSON.parse(ev.data);
|
||||
PubSub.publish(msg);
|
||||
// if (msg.name != 'metrics') console.log('ws->', msg);
|
||||
} catch (e) {
|
||||
console.log('Invalid ws frame:', ev.data); // eslint-disable-line
|
||||
}
|
||||
};
|
||||
ws.onclose = function() {
|
||||
clearTimeout(tid);
|
||||
tid = setTimeout(reconnect, 1000);
|
||||
console.log('ws disconnected');
|
||||
};
|
||||
};
|
||||
reconnect();
|
||||
};
|
||||
useEffect(() => fetch('api/login').then(login), []);
|
||||
|
||||
const login = function(u) {
|
||||
document.cookie = `access_token=${u.token}; Secure, HttpOnly; SameSite=Lax; path=/; max-age=3600`;
|
||||
setUser(u.user);
|
||||
if (location.search.substring(1) == 'nows') {
|
||||
// If query string is ?nows, then do not connect to websocket. For debug.
|
||||
} else {
|
||||
watch(); // Connect to websocket, receive constant graph updates
|
||||
}
|
||||
return getconfig();
|
||||
};
|
||||
|
||||
const logout = ev => {
|
||||
document.cookie = `access_token=; Secure, HttpOnly; SameSite=Lax; path=/; max-age=0`;
|
||||
setUser('');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Called once at init time
|
||||
PubSub.subscribe(msg => msg.name == 'config' && getconfig());
|
||||
fetch('api/login', {headers: {Authorization: ''}})
|
||||
.then(r => r.json())
|
||||
.then(r => login(r))
|
||||
.catch(err => setUser(''));
|
||||
}, []);
|
||||
|
||||
if (!user) return html`<${Login} login=${login} />`;
|
||||
if (loading) return ''; // Show blank page on initial load
|
||||
if (!user) return html`<${Login} loginFn=${login} logoIcon=${Logo}
|
||||
title="Device Dashboard Login"
|
||||
tipText="To login, use: admin/admin, user1/user1, user2/user2" />`; // If not logged in, show login screen
|
||||
|
||||
return html`
|
||||
<${Nav} user=${user} logout=${logout} />
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col col-6"><${Hero} /></div>
|
||||
<div class="col col-6"><${Chart} /></div>
|
||||
<div class="col col-6">
|
||||
${user == 'admin' && h(Configuration, {config})}
|
||||
</div>
|
||||
<div class="col col-6"><${Messages} config=${config} /></div>
|
||||
</div>
|
||||
</div>`;
|
||||
<div class="min-h-screen bg-slate-100">
|
||||
<${Sidebar} url=${url} show=${showSidebar} />
|
||||
<${Header} logout=${logout} user=${user} showSidebar=${showSidebar} setShowSidebar=${setShowSidebar} />
|
||||
<div class="${showSidebar && 'pl-72'} transition-all duration-300 transform">
|
||||
<${Router} onChange=${ev => setUrl(ev.url)} history=${History.createHashHistory()} >
|
||||
<${Main} default=${true} />
|
||||
<${Settings} path="settings" />
|
||||
<//>
|
||||
<//>
|
||||
<//>`;
|
||||
};
|
||||
|
||||
window.onload = () => render(h(App), document.body);
|
||||
@@ -1 +0,0 @@
|
||||
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
|
||||
@@ -1,43 +0,0 @@
|
||||
* { 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; min-width: 9em;}
|
||||
.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; }
|
||||
.nowrap { white-space: nowrap; }
|
||||
.msg { background: #def; border-left: 5px solid #59d; padding: 0.5em; font-size: 90%; margin: 1em 0; }
|
||||
.section { margin: 0 1em; }
|
||||
.topic, .data, .qos { padding: 0.2em 0.5em; border-radius: 0.4em; margin-right: 0.5em; }
|
||||
.qos { background: #efa; }
|
||||
.topic { background: #fea; }
|
||||
.data { background: #aef; }
|
||||
|
||||
/* Grid */
|
||||
.row { display: flex; flex-wrap: wrap; }
|
||||
.col { margin: 0; padding: 0; overflow: auto; }
|
||||
.col-12 { width: 100%; }
|
||||
.col-11 { width: 91.66%; }
|
||||
.col-10 { width: 83.33%; }
|
||||
.col-9 { width: 75%; }
|
||||
.col-8 { width: 66.66%; }
|
||||
.col-7 { width: 58.33%; }
|
||||
.col-6 { width: 50%; }
|
||||
.col-5 { width: 41.66%; }
|
||||
.col-4 { width: 33.33%; }
|
||||
.col-3 { width: 25%; }
|
||||
.col-2 { width: 16.66%; }
|
||||
.col-1 { width: 8.33%; }
|
||||
@media (min-width: 1310px) { .container { margin: auto; width: 1270px; } }
|
||||
@media (max-width: 920px) { .row .col { width: 100%; } }
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
Reference in new issue
Block a user