import strformat, jsconsole, jsffi, karax/[karax, kdom, karaxdsl, vdom], chronicles_tail/jsplugins # Make sure that the Karax instance in the plugin is the same one # as the Karax instance in the enclosing chronicle-tail page. kxi = getKarax() type EventsTable = ref object of VComponent proc renderNetworkEvents(page: VComponent): VNode = result = buildHtml: table: tr: th: text "Time" th: text "Nodes" const columnWidth = 320 timestampsHeight = 50 eventsMargin = 10 var eventsTable = newComponent(EventsTable, renderNetworkEvents) protocolMessages = newJsAssoc[cstring, JsAssoc[cstring, cstring]]() pendingEvents = newSeq[TailEvent]() freedColumns = newSeq[int]() columnBottoms = newSeq[int]() peerToColumnTable = newJsAssoc[cstring, int]() lastTimestampBottom = timestampsHeight proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.} proc getMsgName(protocol: cstring, msgId: int): cstring = protocolMessages[protocol][cast[cstring](msgId)] proc renderEvent(ev: TailEvent): cstring = var res = newStringOfCap(1024) let eventType = ev.msg res.add &"""
""" template addField(class, value) = res.add "
" res.addEscaped $value res.add "
" if eventType.startsWith(cstring("peer_")): addField "peer", ev.peer addField "port", ev.port else: addField "msgName", getMsgName(ev.protocol, ev.msgId) res.addAsHtml ev.data res.add """
""" return cstring(res) proc selectColumn(ev: TailEvent): int = let key = cast[cstring](ev.port)# & ev.peer kout ev.msg, key if ev.msg in [cstring"peer_accepted", "peer_connected"]: if freedColumns.len > 0: result = freedColumns.pop() else: result = columnBottoms.len columnBottoms.add(timestampsHeight) peerToColumnTable[key] = result elif ev.msg == cstring("peer_disconnected"): result = peerToColumnTable[key] discard jsDelete peerToColumnTable[key] freedColumns.add result else: result = peerToColumnTable[key] template pixels(n: int): cstring = cast[cstring](n) & "px" proc addEvent(ev: TailEvent) = var row = document.createElement("tr") timeElem = document.createElement("td") eventElem = document.createElement("td") eventsTable = eventsTable.dom eventsCount = eventsTable.children.len lastEventRow = eventsTable.children[eventsCount - 1] row.class = if eventsCount mod 2 == 0: "even" else: "odd" # Hide the element initially, so we can safely measure its size. # It has to be added to the DOM before it can be measured. row.style.visibility = "hidden" row.appendChild(timeElem) row.appendChild(eventElem) timeElem.innerHtml = ev.ts timeElem.class = "time" eventElem.innerHTML = renderEvent(ev) eventsTable.appendChild(row) let rowHeight = row.offsetHeight let eventColumn = selectColumn(ev) let timestampOffset = max(lastTimestampBottom, columnBottoms[eventColumn]) let prevTimestampOffset = lastTimestampBottom - timestampsHeight lastTimestampBottom = timestampOffset + timestampsHeight columnBottoms[eventColumn] += rowHeight + eventsMargin # Make sure the event data is in the right column and that it # can overflow past the row height: eventElem.style.paddingLeft = pixels(eventColumn * columnWidth) # Position the row in its right place and show it: lastEventRow.style.height = pixels(timestampOffset - prevTimestampOffset) row.style.top = pixels(timestampOffset) row.style.visibility = "" proc networkSectionContent: VNode = result = buildHtml(tdiv(id = "network")): text "Network section" eventsTable proc tailEventFilter(ev: TailEvent): bool = if ev.topics != "p2pdump": return false if ev.msg == "p2p_protocols": protocolMessages = cast[type(protocolMessages)](ev.data) else: if eventsTable.dom == nil: pendingEvents.add ev else: addEvent ev return true proc addPending = if eventsTable.dom != nil and pendingEvents.len > 0: defer: pendingEvents.setLen(0) for ev in pendingEvents: addEvent ev let interval = window.setInterval(addPending, 1000) proc addStyles(styles: cstring) = var s = document.createElement("style") s.appendChild document.createTextNode(styles) document.head.appendChild(s) once: addStyles cstring""" #network > table { position: relative; } #network .event { border: 1px solid blue; } #network .event table { width: 100%; } #network > table > tr { position: absolute; display: flex; flex-direction: row; border-left: 1px solid red; } #network .time { width: 160px; } #network .event { width: 320px; } """ addSection("Network", networkSectionContent) addEventFilter(tailEventFilter) kxi.redraw()