Wip
This commit is contained in:
parent
a76c093d44
commit
c255b58335
15 changed files with 658 additions and 115 deletions
|
|
@ -25,3 +25,10 @@
|
|||
font-size: 0.8em;
|
||||
align-self: center;
|
||||
}
|
||||
#login .auth-failed {
|
||||
margin-top: 32px;
|
||||
color: #a00;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,60 @@ body {
|
|||
margin: 0px;
|
||||
padding: 0px;
|
||||
font-family: 'Liberation Mono', monospace;
|
||||
font-size: 16pt;
|
||||
font-size: 14pt;
|
||||
background-color: #494949;
|
||||
}
|
||||
h1 {
|
||||
color: #ecbf00;
|
||||
}
|
||||
#app {
|
||||
padding: 32px;
|
||||
color: #fff;
|
||||
}
|
||||
.crumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 16px;
|
||||
background: #ecbf00;
|
||||
color: #000;
|
||||
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.crumbs .crumb {
|
||||
margin-right: 8px;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.crumbs .crumb:after {
|
||||
content: ">";
|
||||
margin-left: 8px;
|
||||
color: #a08100;
|
||||
}
|
||||
.crumbs .crumb:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.crumbs .crumb:last-child:after {
|
||||
content: '';
|
||||
margin-left: 0px;
|
||||
}
|
||||
.child-nodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 16px 16px 0px 16px;
|
||||
background-color: #3c3c3c;
|
||||
box-shadow: 0px 0px 16px -4px rgba(0, 0, 0, 0.55) inset;
|
||||
}
|
||||
.child-nodes .child-node {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: #292929;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.node-name {
|
||||
padding: 16px;
|
||||
}
|
||||
.node-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
"@preact/signals-core": "/js/{{ .VERSION }}/lib/signals/signals-core.mjs",
|
||||
"preact/signals": "/js/{{ .VERSION }}/lib/signals/signals.mjs",
|
||||
"htm": "/js/{{ .VERSION }}/lib/htm/htm.mjs",
|
||||
"session": "/js/{{ .VERSION }}/session.mjs"
|
||||
"session": "/js/{{ .VERSION }}/session.mjs",
|
||||
"node": "/js/{{ .VERSION }}/node.mjs"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import 'preact/debug'
|
||||
import 'preact/devtools'
|
||||
//import { signal } from 'preact/signals'
|
||||
import { signal } from 'preact/signals'
|
||||
import { h, Component, render, createRef } from 'preact'
|
||||
import htm from 'htm'
|
||||
import { Session } from 'session'
|
||||
import { NodeUI } from 'node'
|
||||
const html = htm.bind(h)
|
||||
|
||||
class App extends Component {
|
||||
|
|
@ -17,18 +17,19 @@ class App extends Component {
|
|||
|
||||
this.session = new Session(this)
|
||||
this.session.initialize()
|
||||
this.login = createRef()
|
||||
this.nodeUI = createRef()
|
||||
}//}}}
|
||||
render() {//{{{
|
||||
if(!this.session.initialized) {
|
||||
return false;
|
||||
//return html`<div>Validating session</div>`
|
||||
return html`<div>Validating session</div>`
|
||||
}
|
||||
|
||||
if(!this.session.authenticated()) {
|
||||
return html`<${Login} />`
|
||||
return html`<${Login} ref=${this.login} />`
|
||||
}
|
||||
|
||||
return html`Welcome`;
|
||||
return html`<${NodeUI} app=${this} ref=${this.nodeUI} />`
|
||||
}//}}}
|
||||
|
||||
wsLoop() {//{{{
|
||||
|
|
@ -40,7 +41,7 @@ class App extends Component {
|
|||
}, 1000)
|
||||
}//}}}
|
||||
wsConnect() {//{{{
|
||||
this.websocket = new WebSocket(`wss://notes.ahall.se/ws`)
|
||||
this.websocket = new WebSocket(`ws://192.168.11.60:1371/ws`)
|
||||
this.websocket.onopen = evt=>this.wsOpen(evt)
|
||||
this.websocket.onmessage = evt=>this.wsMessage(evt)
|
||||
this.websocket.onerror = evt=>this.wsError(evt)
|
||||
|
|
@ -76,8 +77,8 @@ class App extends Component {
|
|||
return
|
||||
}
|
||||
|
||||
if(app !== undefined && app.hasOwnProperty('error')) {
|
||||
alert(app.error)
|
||||
if(app !== undefined && app.hasOwnProperty('Error')) {
|
||||
alert(app.Error)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +92,6 @@ class App extends Component {
|
|||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
console.log(this.session)
|
||||
if(this.session.UUID !== '')
|
||||
headers['X-Session-Id'] = this.session.UUID
|
||||
|
||||
|
|
@ -108,8 +108,9 @@ class App extends Component {
|
|||
})
|
||||
.then(json=>{
|
||||
// An application level error occured
|
||||
if(!json.OK)
|
||||
if(!json.OK) {
|
||||
return reject({app: json})
|
||||
}
|
||||
return resolve(json)
|
||||
})
|
||||
.catch(err=>reject({comm: err}))
|
||||
|
|
@ -126,13 +127,28 @@ class App extends Component {
|
|||
}
|
||||
|
||||
class Login extends Component {
|
||||
render() {//{{{
|
||||
constructor() {//{{{
|
||||
super()
|
||||
this.authentication_failed = signal(false)
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
}//}}}
|
||||
render({}, { username, password }) {//{{{
|
||||
console.log('login render')
|
||||
|
||||
let authentication_failed = html``;
|
||||
if(this.authentication_failed.value)
|
||||
authentication_failed = html`<div class="auth-failed">Authentication failed</div>`;
|
||||
|
||||
return html`
|
||||
<div id="login">
|
||||
<h1>Notes</h1>
|
||||
<input id="username" type="text" placeholder="Username" onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<input id="password" type="password" placeholder="Password" onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<input id="username" type="text" placeholder="Username" value=${username} oninput=${evt=>this.setState({ username: evt.target.value})} onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<input id="password" type="password" placeholder="Password" value=${password} oninput=${evt=>this.setState({ password: evt.target.value})} onkeydown=${evt=>{ if(evt.code == 'Enter') this.login() }} />
|
||||
<button onclick=${()=>this.login()}>Login</button>
|
||||
${authentication_failed}
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
|
|
@ -149,6 +165,27 @@ class Login extends Component {
|
|||
|
||||
// Init{{{
|
||||
//let urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
/*
|
||||
async function debug(type, context, data) {
|
||||
await fetch("https://msg.kon-it.se/log", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
systemId: 12,
|
||||
type,
|
||||
context,
|
||||
data: JSON.stringify(data, null, 4),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
window.onerror = (event, source, lineno, colon, error) => {
|
||||
debug('Notes', 'error', {
|
||||
event, source, lineno, colon, error
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
window._app = createRef()
|
||||
window._resourceModels = []
|
||||
render(html`<${App} ref=${window._app} />`, document.getElementById('app'))
|
||||
|
|
|
|||
89
static/js/node.mjs
Normal file
89
static/js/node.mjs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { h, Component } from 'preact'
|
||||
import htm from 'htm'
|
||||
import { signal } from 'preact/signals'
|
||||
const html = htm.bind(h)
|
||||
|
||||
export class NodeUI extends Component {
|
||||
constructor() {//{{{
|
||||
super()
|
||||
this.node = signal(null)
|
||||
window.addEventListener('popstate', evt=>{
|
||||
if(evt.state && evt.state.hasOwnProperty('nodeID'))
|
||||
this.goToNode(evt.state.nodeID, true)
|
||||
else
|
||||
this.goToNode(0, true)
|
||||
})
|
||||
}//}}}
|
||||
render() {//{{{
|
||||
if(this.node.value === null)
|
||||
return
|
||||
|
||||
let node = this.node.value
|
||||
|
||||
let crumbs = [
|
||||
html`<div class="crumb" onclick=${()=>this.goToNode(0)}>Start</div>`
|
||||
]
|
||||
|
||||
crumbs = crumbs.concat(node.Crumbs.slice(1).map(node=>
|
||||
html`<div class="crumb" onclick=${()=>this.goToNode(node.ID)}>${node.Name}</div>`
|
||||
).reverse())
|
||||
|
||||
let children = node.Children.sort((a,b)=>{
|
||||
if(a.Name.toLowerCase() > b.Name.toLowerCase()) return 1;
|
||||
if(a.Name.toLowerCase() < b.Name.toLowerCase()) return -1;
|
||||
return 0
|
||||
}).map(child=>html`
|
||||
<div class="child-node" onclick=${()=>this.goToNode(child.ID)}>${child.Name}</div>
|
||||
`)
|
||||
|
||||
return html`
|
||||
${node.ID > 0 ? html`<div class="crumbs">${crumbs}</crumbs>` : html``}
|
||||
<div class="node-name">${node.Name}</div>
|
||||
${children.length > 0 ? html`<div class="child-nodes">${children}</div>` : html``}
|
||||
<div class="node-content">${node.Content}</div>
|
||||
`
|
||||
}//}}}
|
||||
componentDidMount() {//{{{
|
||||
let root = new Node(this.props.app, 0)
|
||||
root.retrieve(node=>{
|
||||
this.node.value = node
|
||||
})
|
||||
}//}}}
|
||||
goToNode(nodeID, dontPush) {//{{{
|
||||
if(!dontPush)
|
||||
history.pushState({ nodeID }, '', `#${nodeID}`)
|
||||
let node = new Node(this.props.app, nodeID)
|
||||
node.retrieve(node=>{
|
||||
this.node.value = node
|
||||
})
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class Node {
|
||||
constructor(app, nodeID) {//{{{
|
||||
this.app = app
|
||||
this.ID = nodeID
|
||||
this.ParentID = 0
|
||||
this.UserID = 0
|
||||
this.Name = ''
|
||||
this.Content = ''
|
||||
this.Children = []
|
||||
this.Crumbs = []
|
||||
}//}}}
|
||||
|
||||
retrieve(callback) {//{{{
|
||||
this.app.request('/node/retrieve', { ID: this.ID })
|
||||
.then(res=>{
|
||||
this.ParentID = res.Node.ParentID
|
||||
this.UserID = res.Node.UserID
|
||||
this.Name = res.Node.Name
|
||||
this.Content = res.Node.Content
|
||||
this.Children = res.Node.Children
|
||||
this.Crumbs = res.Node.Crumbs
|
||||
callback(this)
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}//}}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
@ -9,37 +9,61 @@ export class Session {
|
|||
initialize() {//{{{
|
||||
// Retrieving the stored session UUID, if any.
|
||||
// If one found, validate with server.
|
||||
let uuid = window.localStorage.getItem("session.UUID")
|
||||
|
||||
|
||||
// If the browser doesn't know anything about a session,
|
||||
// a call to /session/create is necessary to retrieve a session UUID.
|
||||
let uuid= window.localStorage.getItem("session.UUID")
|
||||
if(uuid === null) {
|
||||
this.create()
|
||||
} else {
|
||||
this.UUID = uuid
|
||||
this.app.request('/session/retrieve', {})
|
||||
.then(res=>{
|
||||
this.UserID = res.Session.UserID
|
||||
return
|
||||
}
|
||||
|
||||
// When loading the page, the web app needs to know session information
|
||||
// such as user ID and possibly some state to render properly.
|
||||
//
|
||||
// A call to /session/retrieve with a session UUID validates that the
|
||||
// session is still valid and returns all session information.
|
||||
this.UUID = uuid
|
||||
this.app.request('/session/retrieve', {})
|
||||
.then(res=>{
|
||||
if(res.Valid) {
|
||||
// Session exists on server.
|
||||
// Not necessarily authenticated.
|
||||
this.UserID = res.Session.UserID // could be 0
|
||||
this.initialized = true
|
||||
this.app.forceUpdate()
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}
|
||||
} else {
|
||||
// Session has probably expired. A new is required.
|
||||
this.create()
|
||||
}
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}//}}}
|
||||
create() {//{{{
|
||||
this.app.request('/session/create', {})
|
||||
.then(res=>{
|
||||
this.UUID = res.Session.UUID
|
||||
window.localStorage.setItem('session.UUID', this.UUID)
|
||||
this.initialized = true
|
||||
this.app.forceUpdate()
|
||||
})
|
||||
.catch(this.responseError)
|
||||
}//}}}
|
||||
authenticate(username, password) {//{{{
|
||||
this.app.login.current.authentication_failed.value = false
|
||||
|
||||
this.app.request('/session/authenticate', {
|
||||
username,
|
||||
password,
|
||||
})
|
||||
.then(res=>{
|
||||
this.UserID = res.Session.UserID
|
||||
this.app.forceUpdate()
|
||||
if(res.Authenticated) {
|
||||
this.UserID = res.Session.UserID
|
||||
this.app.forceUpdate()
|
||||
} else {
|
||||
this.app.login.current.authentication_failed.value = true
|
||||
}
|
||||
})
|
||||
.catch(this.app.responseError)
|
||||
}//}}}
|
||||
|
|
|
|||
|
|
@ -30,4 +30,12 @@
|
|||
font-size: 0.8em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.auth-failed {
|
||||
margin-top: 32px;
|
||||
color: #a00;
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ html, body {
|
|||
margin: 0px;
|
||||
padding: 0px;
|
||||
font-family: 'Liberation Mono', monospace;
|
||||
font-size: 16pt;
|
||||
font-size: @fontsize;
|
||||
|
||||
background-color: @background;
|
||||
}
|
||||
|
|
@ -14,6 +14,63 @@ h1 {
|
|||
}
|
||||
|
||||
#app {
|
||||
padding: 32px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 16px;
|
||||
background: @accent_1;
|
||||
color: #000;
|
||||
box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.4);
|
||||
|
||||
.crumb {
|
||||
margin-right: 8px;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.crumb:after {
|
||||
content: ">";
|
||||
margin-left: 8px;
|
||||
color: darken(@accent_1, 15%);
|
||||
}
|
||||
|
||||
.crumb:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.crumb:last-child:after {
|
||||
content: '';
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.child-nodes {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 16px 16px 0px 16px;
|
||||
background-color: darken(@background, 5%);
|
||||
box-shadow: 0px 0px 16px -4px rgba(0,0,0,0.55) inset;
|
||||
|
||||
.child-node {
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: darken(@background, 12.5%);
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
white-space: nowrap;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.node-name {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
padding: 32px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
@fontsize: 14pt;
|
||||
@background: #494949;
|
||||
@accent_1: #ecbf00;
|
||||
@accent_2: #abc837;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue