Improved log viewer

This commit is contained in:
osmarks 2018-10-06 11:28:04 +01:00
parent d2a7ef7585
commit 8a18b5f543
1 changed files with 149 additions and 108 deletions

View File

@ -2,14 +2,19 @@
<style> <style>
.messages li { .messages li {
list-style-type: none; list-style-type: none;
font-family: monospace;
} }
.internal { .internal {
color: gray; color: gray;
} }
.error {
color: red;
}
.past { .past {
color: slateblue; color: darkblue;
} }
.user { .user {
@ -24,6 +29,16 @@
content: "< "; content: "< ";
} }
.channel {
font-weight: bold;
padding-right: 0.3em;
}
.timestamp {
float: right;
font-style: italic;
}
#app input { #app input {
width: 100%; width: 100%;
} }
@ -32,154 +47,180 @@
<script src="https://unpkg.com/@hyperapp/html@1.1.1/dist/hyperappHtml.js"></script> <script src="https://unpkg.com/@hyperapp/html@1.1.1/dist/hyperappHtml.js"></script>
<script src="https://unpkg.com/moment@2.22.2/min/moment.min.js"></script> <script src="https://unpkg.com/moment@2.22.2/min/moment.min.js"></script>
<script> <script>
const h = hyperappHtml; // From the ijk package - https://github.com/lukejacksonn/ijk
const push = (xs, x) => xs.concat([x]); const isString = x => typeof x === 'string'
const isArray = Array.isArray
const arrayPush = Array.prototype.push
const isObject = x => typeof x === 'object' && !isArray(x)
const clean = (arr, n) => (
n && arrayPush.apply(arr, isString(n[0]) ? [n] : n), arr
)
const child = (n, cb) =>
n != null ? (isArray(n) ? n.reduce(clean, []).map(cb) : [n + '']) : []
const ijk = (x, y, z) => {
const transform = node =>
isString(node)
? node
: isObject(node[1])
? {
[x]: node[0],
[y]: node[1],
[z]: child(node[2], transform),
}
: transform([node[0], {}, node[1]])
return transform
}
const h = hyperappHtml
const push = (xs, x) => xs.concat([x])
const state = { const state = {
messages: [], messages: [],
websocket: null, websocket: null,
URL: (window.location.href + "connect").replace("http", "ws"), URL: (window.location.href + "connect").replace("http", "ws"),
channel: "default" channel: "default"
}; }
let windowVisible = true; let windowVisible = true
let doNotify = false; let doNotify = false
window.onfocus = () => { windowVisible = true; doNotify = false; }; window.onfocus = () => { windowVisible = true; doNotify = false }
window.onblur = () => { windowVisible = false; }; window.onblur = () => { windowVisible = false }
const blinkTime = 1000; const blinkTime = 1000
// Blink title a bit by adding then removing ***. // Blink title a bit by adding then removing ***.
setInterval(() => { setInterval(() => {
if (doNotify && !windowVisible) { if (doNotify && !windowVisible) {
let title = document.title; let title = document.title
document.title = "*** " + title; document.title = "*** " + title
setTimeout(() => { setTimeout(() => {
document.title = title; document.title = title
}, blinkTime); }, blinkTime)
} }
}, blinkTime * 2); }, blinkTime * 2)
const notify = () => { doNotify = !windowVisible; }; // do not notify if window is visible const notify = () => { doNotify = !windowVisible } // do not start notification if window is visible
const stringifyMessage = m => {
let out = "";
if (m.time) {
out += moment(m.time).format("hh:mm:ss") + ": ";
}
if (m.channel) {
out += m.channel + ": ";
}
if (m.message) {
const msg = m.message;
if (typeof msg === "string") {
out += msg;
} else {
out += JSON.stringify(msg);
}
}
return out;
}
const actions = { const actions = {
connect: () => (state, actions) => { connect: () => (state, actions) => {
console.log("CONNECT", state.URL); console.log("CONN", state.URL)
if (state.websocket != null && state.websocket.close) state.websocket.close(); if (state.websocket && "close" in state.websocket) { state.websocket.close() }
const ws = new WebSocket(state.URL);
ws.addEventListener("message", ev => {
actions.handleMessage(ev.data);
});
ws.addEventListener("close", ev => actions.message(["internal", "Connection closed."])); const ws = new WebSocket(state.URL)
ws.addEventListener("open", ev => {
actions.message(["internal", "Connected."]); ws.addEventListener("message", data => {
actions.sendJSON({ try {
type: "open", actions.handleMessage(JSON.parse(data.data))
channel: "*" // wildcard } catch(e) {
}); console.warn(e)
actions.addMessage(["error", e.toString()])
}
})
ws.addEventListener("close", ce => actions.addMessage([ "internal", "Connection closed: code " + ce.code ]))
ws.addEventListener("open", () => {
actions.sendJSON({ actions.sendJSON({
type: "log" type: "log"
}); })
}); actions.sendJSON({
type: "open",
channel: "*"
})
})
return {websocket: ws} return { websocket: ws }
}, },
handleMessage: data => (state, actions) => { handleMessage: message => (state, actions) => {
const message = JSON.parse(data);
const type = message.type;
console.log("RECV", message) console.log("RECV", message)
const type = message.type;
if (type === "message") { if (type === "message") {
actions.normalMessage(["remote", message]) actions.addMessage([ "remote", message ])
} else if (type === "result" && message["for"] === "log") { } else if (type === "result") {
const pastMessages = message.log.reverse(); // Messages are sent to us newest-first if (message.for === "log") {
pastMessages.forEach(m => { message.log.forEach(x => actions.addMessage([ "remote past", x ]))
actions.normalMessage(["remote past", m]); } else if (message.for === "message") {
}); actions.addMessage([ "user", message ])
} else if (type === "error") { }
console.warn(message);
} }
}, },
message: value => state => ({messages: push(state.messages, value)}), urlInput: ev => (state, actions) => {
normalMessage: value => (state, actions) => actions.message([value[0], stringifyMessage(value[1])]), if (ev.keyCode === 13) { // enter
sendJSON: value => state => { actions.connect()
console.log("SEND", value); }
if (state.websocket !== null && state.websocket.readyState === 1) { return { URL: ev.target.value }
state.websocket.send(JSON.stringify(value)); },
channelInput: ev => (state, actions) => {
return { channel: ev.target.value }
},
addMessage: m => state => ({ messages: push(state.messages, m) }),
messageInput: ev => (state, actions) => {
if (ev.keyCode === 13) { // enter
actions.sendMessage()
ev.target.value = ""
}
return { message: ev.target.value }
},
sendJSON: x => (state, actions) => {
if (state.websocket.readyState === 1) { // socket is open
console.log("SEND", x)
state.websocket.send(JSON.stringify(x))
} else { } else {
actions.message(["internal", "Not connected."]); actions.addMessage(["error", "Open connection before sending messages."])
} }
}, },
sendMessage: ([channel, message]) => (state, actions) => { sendMessage: () => (state, actions) => {
const channel = state.channel
const message = state.message
actions.sendJSON({ actions.sendJSON({
type: "message", type: "message",
channel, channel,
message message
}); })
},
msgInput: event => (state, actions) => {
if (event.keyCode == 13) { // enter key
let val = event.target.value;
const channel = state.channel;
actions.sendMessage([channel, val]);
actions.normalMessage(["user", {
channel,
message: val
}]);
event.target.value = "";
}
},
channelInput: event => (state, actions) => {
return { channel: event.target.value };
},
urlInput: event => (state, actions) => {
const val = event.target.value;
if (event.keyCode == 13) { // enter key
actions.connect();
}
return { URL: val };
} }
}; }
const cls = x => ({ class: x }); const cls = x => ({ class: x })
const scrollDown = () => { const scrollDown = () => {
const scrollEl = document.scrollingElement; const scrollEl = document.scrollingElement
scrollEl.scrollTop = scrollEl.scrollHeight; scrollEl.scrollTop = scrollEl.scrollHeight
}; }
const viewMessage = m => h.li(cls(m[0]), m[1]); const viewMessage = m => {
const classes = m[0]
const data = m[1]
var children
const view = (state, actions) => h.div([ if (typeof data === "string") { children = data }
h.div([ else {
h.input({ onkeyup: actions.urlInput, placeholder: "URL", value: state.URL }) children = []
]), if (data.channel) {
h.ul({class: "messages", onupdate: (element, old) => scrollDown()}, state.messages.map(viewMessage)), children.push([ "span", cls("channel"), data.channel ])
h.input({ onkeyup: actions.channelInput, placeholder: "Channel", value: state.channel }), }
h.input({ onkeyup: actions.msgInput, placeholder: "Message" }), if (data.message) {
]); children.push([ "span", cls("message"), data.message ])
}
if (data.time) {
children.push([ "span", cls("timestamp"), moment(data.time).format("hh:mm:ss") ])
}
}
const main = hyperapp.app(state, actions, view, document.getElementById("app")); return [ "li", cls(classes), children ]
main.connect(); }
const view = (state, actions) => ijk("nodeName", "attributes", "children")(
[ "div", [
[ "input", { onkeyup: actions.urlInput, placeholder: "URL", value: state.URL } ],
[ "ul", { class: "messages", onupdate: (element, old) => scrollDown() }, state.messages.map(viewMessage) ],
[ "input", { onkeyup: actions.channelInput, placeholder: "Channel", value: state.channel } ],
[ "input", { onkeyup: actions.messageInput, placeholder: "Message" } ] // unfortunately, setting the value from the one in the state appears to cause problems when other stuff is going on
]])
const main = hyperapp.app(state, actions, view, document.getElementById("app"))
main.connect()
</script> </script>