WIP towards the tails GUI plugins
* Tup automated builds with hot-code reloading * Most the Plugin API fleshed out. The work-in-progress P2P traffic visualition plugin is not committed yet.
This commit is contained in:
parent
8b4e8e174b
commit
3b06528906
|
@ -1,5 +1,8 @@
|
||||||
import
|
import
|
||||||
karax/vdom
|
jsffi, karax/[karax, vdom]
|
||||||
|
|
||||||
|
export
|
||||||
|
karax, vdom
|
||||||
|
|
||||||
type
|
type
|
||||||
SectionRenderer* = proc(): VNode
|
SectionRenderer* = proc(): VNode
|
||||||
|
@ -8,12 +11,68 @@ type
|
||||||
title*: cstring
|
title*: cstring
|
||||||
content*: SectionRenderer
|
content*: SectionRenderer
|
||||||
|
|
||||||
|
TailEvent* = object of js
|
||||||
|
msg*: cstring
|
||||||
|
level*: cstring
|
||||||
|
ts*: cstring
|
||||||
|
protocol*: cstring
|
||||||
|
msgId*: int
|
||||||
|
peer*: cstring
|
||||||
|
port*: int
|
||||||
|
data*: js
|
||||||
|
topics*: cstring
|
||||||
|
|
||||||
|
TailEventFilter* = proc(e: TailEvent): bool
|
||||||
|
|
||||||
when defined(createChroniclesTail):
|
when defined(createChroniclesTail):
|
||||||
|
proc getKarax*: KaraxInstance {.exportc.} = kxi
|
||||||
|
|
||||||
var
|
var
|
||||||
sections* = newSeq[Section](0)
|
sections* = newSeq[Section](0)
|
||||||
|
filters* = newSeq[TailEventFilter]()
|
||||||
|
|
||||||
proc addSection*(title: cstring, content: SectionRenderer) {.exportc.} =
|
proc addSection*(title: cstring, content: SectionRenderer) {.exportc.} =
|
||||||
sections.add Section(title: title, content: content)
|
sections.add Section(title: title, content: content)
|
||||||
else:
|
redrawSync()
|
||||||
proc addSection*(title: cstring, content: SectionRenderer) {.importc.}
|
|
||||||
|
proc addEventFilter*(f: TailEventFilter) {.exportc.} =
|
||||||
|
filters.add f
|
||||||
|
|
||||||
|
proc addEscaped*(result: var string, s: string) {.exportc.} =
|
||||||
|
## same as ``result.add(escape(s))``, but more efficient.
|
||||||
|
for c in items(s):
|
||||||
|
case c
|
||||||
|
of '<': result.add("<")
|
||||||
|
of '>': result.add(">")
|
||||||
|
of '&': result.add("&")
|
||||||
|
of '"': result.add(""")
|
||||||
|
of '\'': result.add("'")
|
||||||
|
of '/': result.add("/")
|
||||||
|
else: result.add(c)
|
||||||
|
|
||||||
|
proc addAsHtml*(result: var string, obj: js) {.exportc.} =
|
||||||
|
if jsTypeOf(obj) == "object":
|
||||||
|
result.add "<table>"
|
||||||
|
for key, value in obj:
|
||||||
|
result.add """<tr><td class = "key">"""
|
||||||
|
result.addEscaped $key
|
||||||
|
result.add """</td><td class = "value">"""
|
||||||
|
result.addAsHtml value
|
||||||
|
result.add "</td></tr>"
|
||||||
|
result.add "</table>"
|
||||||
|
else:
|
||||||
|
result.add cast[cstring](obj.toString())
|
||||||
|
|
||||||
|
proc asHtml*(obj: js): string {.exportc.} =
|
||||||
|
result = newStringOfCap(128)
|
||||||
|
result.addAsHtml(obj)
|
||||||
|
|
||||||
|
else:
|
||||||
|
proc addEscaped*(result: var string, s: string) {.importc.}
|
||||||
|
proc addAsHtml*(result: var string, obj: js) {.importc.}
|
||||||
|
proc asHtml*(obj: js): string {.importc.}
|
||||||
|
|
||||||
|
proc getKarax*(): KaraxInstance {.importc.}
|
||||||
|
proc addSection*(title: cstring, content: SectionRenderer) {.importc.}
|
||||||
|
proc addEventFilter*(f: TailEventFilter) {.importc.}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
: karax_app.nim |> nim --hotCodeReloading:on -d:createChroniclesTail -o:%o js %f |> %B.js
|
: karax_app.nim |> nim --hotCodeReloading:on -d:createChroniclesTail -o:%o js %f |> %B.js
|
||||||
|
: eth_p2p_plugin.nim |> nim --hotCodeReloading:on -o:%o js %f |> %B.js
|
||||||
|
|
|
@ -8,7 +8,10 @@
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="ROOT" />
|
<div>
|
||||||
|
<div id="ROOT"></div>
|
||||||
|
</div>
|
||||||
<script src="karax_app.js"></script>
|
<script src="karax_app.js"></script>
|
||||||
|
<script src="eth_p2p_plugin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,7 +3,7 @@ include
|
||||||
|
|
||||||
import
|
import
|
||||||
jsconsole, jscore, jsffi, jswebsockets,
|
jsconsole, jscore, jsffi, jswebsockets,
|
||||||
tail_jsplugins
|
chronicles_tail/jsplugins
|
||||||
|
|
||||||
type
|
type
|
||||||
ChroniclesPrompt = object
|
ChroniclesPrompt = object
|
||||||
|
@ -11,13 +11,11 @@ type
|
||||||
suggestions: seq[string]
|
suggestions: seq[string]
|
||||||
activeSuggestionIdx: int
|
activeSuggestionIdx: int
|
||||||
|
|
||||||
LogEvent = js
|
|
||||||
|
|
||||||
var
|
var
|
||||||
chroniclesCredentials* {.importc, nodecl.}: js
|
chroniclesCredentials* {.importc, nodecl.}: js
|
||||||
chroniclesPrompt: ChroniclesPrompt
|
chroniclesPrompt: ChroniclesPrompt
|
||||||
activeSectionIdx = 0
|
activeSectionIdx = 0
|
||||||
logEvents = newSeq[LogEvent]()
|
logEvents = newSeq[TailEvent]()
|
||||||
|
|
||||||
proc activeOrInactive(idx, currentActiveIdx: int): cstring =
|
proc activeOrInactive(idx, currentActiveIdx: int): cstring =
|
||||||
if idx == currentActiveIdx: "active"
|
if idx == currentActiveIdx: "active"
|
||||||
|
@ -48,14 +46,12 @@ proc logSectionContent: VNode =
|
||||||
|
|
||||||
tbody:
|
tbody:
|
||||||
for event in logEvents:
|
for event in logEvents:
|
||||||
let level = cast[cstring](event.level)
|
let level = event.level
|
||||||
tr(class = level):
|
tr(class = level):
|
||||||
td(class = "level"): text level
|
td(class = "level"): text level
|
||||||
td(class = "ts") : text cast[cstring](event.ts)
|
td(class = "ts") : text event.ts
|
||||||
td(class = "msg") : text cast[cstring](event.msg)
|
td(class = "msg") : text event.msg
|
||||||
td(class = "props"): text cast[cstring](event.x)
|
td(class = "props"): text event.data.asHtml
|
||||||
|
|
||||||
addSection("Log", logSectionContent)
|
|
||||||
|
|
||||||
var chroniclesSocket = newWebSocket(cast[cstring](chroniclesCredentials.url))
|
var chroniclesSocket = newWebSocket(cast[cstring](chroniclesCredentials.url))
|
||||||
|
|
||||||
|
@ -63,8 +59,13 @@ chroniclesSocket.onOpen = proc (e: jswebsockets.Event) =
|
||||||
chroniclesSocket.send(cast[cstring](chroniclesCredentials.accessToken))
|
chroniclesSocket.send(cast[cstring](chroniclesCredentials.accessToken))
|
||||||
|
|
||||||
chroniclesSocket.onMessage = proc (e: MessageEvent) =
|
chroniclesSocket.onMessage = proc (e: MessageEvent) =
|
||||||
var msg = JSON.parse(e.data).js
|
var msg = cast[TailEvent](JSON.parse(e.data))
|
||||||
if msg.level == jsundefined:
|
|
||||||
|
for f in filters:
|
||||||
|
if f(msg):
|
||||||
|
return
|
||||||
|
|
||||||
|
if msg.level.toJs == jsundefined:
|
||||||
console.log msg
|
console.log msg
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -100,4 +101,5 @@ proc pageContent(): VNode =
|
||||||
activeSection()
|
activeSection()
|
||||||
|
|
||||||
setRenderer pageContent
|
setRenderer pageContent
|
||||||
|
addSection "Log", logSectionContent
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
import
|
import
|
||||||
macros, json, random, times, strutils,
|
macros, json, random, times, strutils, os,
|
||||||
asynchttpserver, asyncnet, asyncdispatch, websocket,
|
asynchttpserver, asyncnet, asyncdispatch, websocket,
|
||||||
chronicles_tail/configuration
|
chronicles_tail/configuration
|
||||||
|
|
||||||
const
|
const
|
||||||
indexFile = staticRead "karax_app.html"
|
indexFile = staticRead "karax_app.html"
|
||||||
faviconBytes = staticRead "favicon.png"
|
faviconBytes = staticRead "favicon.png"
|
||||||
styles = staticRead "styles.css"
|
|
||||||
|
template fileContents(file: string): string =
|
||||||
|
when defined(debug):
|
||||||
|
readFile("webui" / file)
|
||||||
|
else:
|
||||||
|
const contents = staticRead(file)
|
||||||
|
contents
|
||||||
|
|
||||||
|
|
||||||
type
|
type
|
||||||
WebuiServer* = ref object
|
WebuiServer* = ref object
|
||||||
|
@ -45,7 +52,7 @@ proc serve*(s: WebuiServer): Future[void] =
|
||||||
|
|
||||||
var nextLineToSend = 0
|
var nextLineToSend = 0
|
||||||
while nextLineToSend < s.logLines.len:
|
while nextLineToSend < s.logLines.len:
|
||||||
await ws.sendText(s.logLines[nextLineToSend])
|
await ws.sendText(s.logLines[nextLineToSend], maskingKey = "")
|
||||||
inc nextLineToSend
|
inc nextLineToSend
|
||||||
|
|
||||||
s.clients.add ws
|
s.clients.add ws
|
||||||
|
@ -54,9 +61,13 @@ proc serve*(s: WebuiServer): Future[void] =
|
||||||
await req.respond(Http200, indexFile)
|
await req.respond(Http200, indexFile)
|
||||||
|
|
||||||
of "/styles.css":
|
of "/styles.css":
|
||||||
await req.respond(Http200, styles,
|
await req.respond(Http200, fileContents("styles.css"),
|
||||||
newHttpHeaders({"Content-Type": "text/css"}))
|
newHttpHeaders({"Content-Type": "text/css"}))
|
||||||
|
|
||||||
|
of "/eth_p2p_plugin.js":
|
||||||
|
await req.respond(Http200, readFile("webui/eth_p2p_plugin.js"),
|
||||||
|
newHttpHeaders({"Content-Type": "application/javascript"}))
|
||||||
|
|
||||||
of "/karax_app.js":
|
of "/karax_app.js":
|
||||||
var scripts = newStringOfCap(16000)
|
var scripts = newStringOfCap(16000)
|
||||||
|
|
||||||
|
@ -69,8 +80,8 @@ proc serve*(s: WebuiServer): Future[void] =
|
||||||
|
|
||||||
scripts.add compileJs("karax_app.nim", "-d:createChroniclesTail")
|
scripts.add compileJs("karax_app.nim", "-d:createChroniclesTail")
|
||||||
|
|
||||||
for plugin in s.plugins:
|
# for plugin in s.plugins:
|
||||||
scripts.add plugin
|
# scripts.add plugin
|
||||||
|
|
||||||
await req.respond(Http200, scripts,
|
await req.respond(Http200, scripts,
|
||||||
newHttpHeaders({"Content-Type": "application/javascript"}))
|
newHttpHeaders({"Content-Type": "application/javascript"}))
|
||||||
|
|
|
@ -71,15 +71,16 @@ table {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: white;
|
color: white;
|
||||||
flex: 0;
|
flex: 0 auto;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header h1 {
|
#header h1 {
|
||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
order: 3;
|
order: 3;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
flex: 1;
|
flex: 1 auto;
|
||||||
font-size: 13px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content, #header {
|
#content, #header {
|
||||||
|
@ -87,7 +88,7 @@ table {
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
flex: 1;
|
flex: 1 auto;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -136,7 +137,7 @@ table {
|
||||||
#log_section {
|
#log_section {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#log_section .events {
|
#log_section .events {
|
||||||
|
@ -144,7 +145,7 @@ table {
|
||||||
border: 2px solid #e8e8e8;
|
border: 2px solid #e8e8e8;
|
||||||
padding-top: 1.875em;
|
padding-top: 1.875em;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1;
|
flex: 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue