From 6b527cef6539a78113ae9c047266b5e48f045afd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 18 Feb 2021 10:48:58 +0100 Subject: [PATCH 1/6] basic log viewer --- scripts/logviewer/file.js | 36 +++++++++++ scripts/logviewer/html.js | 110 ++++++++++++++++++++++++++++++++++ scripts/logviewer/index.html | 113 +++++++++++++++++++++++++++++++++++ scripts/logviewer/main.js | 109 +++++++++++++++++++++++++++++++++ 4 files changed, 368 insertions(+) create mode 100644 scripts/logviewer/file.js create mode 100644 scripts/logviewer/html.js create mode 100644 scripts/logviewer/index.html create mode 100644 scripts/logviewer/main.js diff --git a/scripts/logviewer/file.js b/scripts/logviewer/file.js new file mode 100644 index 00000000..11f4ef5a --- /dev/null +++ b/scripts/logviewer/file.js @@ -0,0 +1,36 @@ + +export function openFile(mimeType = null) { + const input = document.createElement("input"); + input.setAttribute("type", "file"); + input.className = "hidden"; + if (mimeType) { + input.setAttribute("accept", mimeType); + } + const promise = new Promise((resolve, reject) => { + const checkFile = () => { + input.removeEventListener("change", checkFile, true); + const file = input.files[0]; + document.body.removeChild(input); + if (file) { + resolve(file); + } else { + reject(new Error("no file picked")); + } + } + input.addEventListener("change", checkFile, true); + }); + // IE11 needs the input to be attached to the document + document.body.appendChild(input); + input.click(); + return promise; +} + +export function readFileAsText(file) { + const reader = new FileReader(); + const promise = new Promise((resolve, reject) => { + reader.addEventListener("load", evt => resolve(evt.target.result)); + reader.addEventListener("error", evt => reject(evt.target.error)); + }); + reader.readAsText(file); + return promise; +} \ No newline at end of file diff --git a/scripts/logviewer/html.js b/scripts/logviewer/html.js new file mode 100644 index 00000000..a965a6ee --- /dev/null +++ b/scripts/logviewer/html.js @@ -0,0 +1,110 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// DOM helper functions + +export function isChildren(children) { + // children should be an not-object (that's the attributes), or a domnode, or an array + return typeof children !== "object" || !!children.nodeType || Array.isArray(children); +} + +export function classNames(obj, value) { + return Object.entries(obj).reduce((cn, [name, enabled]) => { + if (typeof enabled === "function") { + enabled = enabled(value); + } + if (enabled) { + return cn + (cn.length ? " " : "") + name; + } else { + return cn; + } + }, ""); +} + +export function setAttribute(el, name, value) { + if (name === "className") { + name = "class"; + } + if (value === false) { + el.removeAttribute(name); + } else { + if (value === true) { + value = name; + } + el.setAttribute(name, value); + } +} + +export function el(elementName, attributes, children) { + return elNS(HTML_NS, elementName, attributes, children); +} + +export function elNS(ns, elementName, attributes, children) { + if (attributes && isChildren(attributes)) { + children = attributes; + attributes = null; + } + + const e = document.createElementNS(ns, elementName); + + if (attributes) { + for (let [name, value] of Object.entries(attributes)) { + if (name === "className" && typeof value === "object" && value !== null) { + value = classNames(value); + } + setAttribute(e, name, value); + } + } + + if (children) { + if (!Array.isArray(children)) { + children = [children]; + } + for (let c of children) { + if (!c.nodeType) { + c = text(c); + } + e.appendChild(c); + } + } + return e; +} + +export function text(str) { + return document.createTextNode(str); +} + +export const HTML_NS = "http://www.w3.org/1999/xhtml"; +export const SVG_NS = "http://www.w3.org/2000/svg"; + +export const TAG_NAMES = { + [HTML_NS]: [ + "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", + "p", "strong", "em", "span", "img", "section", "main", "article", "aside", + "pre", "button", "time", "input", "textarea", "label", "form", "progress", "output"], + [SVG_NS]: ["svg", "circle"] +}; + +export const tag = {}; + + +for (const [ns, tags] of Object.entries(TAG_NAMES)) { + for (const tagName of tags) { + tag[tagName] = function(attributes, children) { + return elNS(ns, tagName, attributes, children); + } + } +} diff --git a/scripts/logviewer/index.html b/scripts/logviewer/index.html new file mode 100644 index 00000000..dbf54d70 --- /dev/null +++ b/scripts/logviewer/index.html @@ -0,0 +1,113 @@ + + + + + + + + +
+ + + + diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js new file mode 100644 index 00000000..b2b8a399 --- /dev/null +++ b/scripts/logviewer/main.js @@ -0,0 +1,109 @@ +import {tag as t} from "./html.js"; +import {openFile, readFileAsText} from "./file.js"; + +const main = document.querySelector("main"); + +let selectedItemNode; +let rootItem; + +document.querySelector("main").addEventListener("click", event => { + if (selectedItemNode) { + selectedItemNode.classList.remove("selected"); + selectedItemNode = null; + } + const itemNode = event.target.closest(".item"); + if (itemNode) { + selectedItemNode = itemNode; + selectedItemNode.classList.add("selected"); + const path = selectedItemNode.dataset.path; + let item = rootItem; + let parent; + if (path.length) { + const indices = path.split("/").map(i => parseInt(i, 10)); + for(const i of indices) { + parent = item; + item = itemChildren(item)[i]; + } + } + showItemDetails(item, parent); + } +}); + +function showItemDetails(item, parent) { + const aside = t.aside([ + t.h3(itemCaption(item)), + t.ul({class: "values"}, Object.entries(itemValues(item)).map(([key, value]) => { + return t.li([ + t.span(normalizeValueKey(key)), + t.span(value) + ]); + })) + ]); + document.querySelector("aside").replaceWith(aside); +} + +document.getElementById("openFile").addEventListener("click", loadFile); + +async function loadFile() { + const file = await openFile(); + const json = await readFileAsText(file); + const logs = JSON.parse(json); + rootItem = {c: logs.items}; + const fragment = logs.items.reduce((fragment, item, i, items) => { + const prevItem = i === 0 ? null : items[i - 1]; + fragment.appendChild(t.section([ + t.h2(prevItem ? `+ ${itemStart(item) - itemEnd(prevItem)} ms` : new Date(itemStart(item)).toString()), + t.div({className: "timeline"}, t.ol(itemToNode(item, [i]))) + ])); + return fragment; + }, document.createDocumentFragment()); + main.replaceChildren(fragment); +} + +function itemChildren(item) { return item.c; } +function itemStart(item) { return item.s; } +function itemEnd(item) { return item.s + item.d; } +function itemDuration(item) { return item.d; } +function itemValues(item) { return item.v; } +function itemLevel(item) { return item.l; } +function itemLabel(item) { return item.v?.l; } +function itemType(item) { return item.v?.t; } +function itemError(item) { return item.e; } +function itemCaption(item) { + if (itemType(item) === "network") { + return `${itemValues(item)?.method} ${itemValues(item)?.url}`; + } else if (itemLabel(item) && itemValues(item)?.id) { + return `${itemLabel(item)} ${itemValues(item).id}`; + } else { + return itemLabel(item) || itemType(item); + } +} +function normalizeValueKey(key) { + switch (key) { + case "t": return "type"; + case "l": return "label"; + default: return key; + } +} + +// returns the node and the total range (recursively) occupied by the node +function itemToNode(item, path) { + const className = { + item: true, + error: itemError(item), + [`type-${itemType(item)}`]: !!itemType(item), + [`level-${itemLevel(item)}`]: true, + }; + const li = t.li([ + t.div({className, "data-path": path.join("/")}, [ + t.span({class: "caption"}, itemCaption(item)), + t.span({class: "duration"}, `(${itemDuration(item)}ms)`), + ]) + ]); + if (itemChildren(item) && itemChildren(item).length) { + li.appendChild(t.ol(itemChildren(item).map((item, i) => { + return itemToNode(item, path.concat(i)); + }))); + } + return li; +} From 17c2fad4b41638352d8d72165f76cf2a65db65ba Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 18 Feb 2021 11:08:43 +0100 Subject: [PATCH 2/6] finish log viewer details panel --- scripts/logviewer/index.html | 22 ++++++++++++++++++---- scripts/logviewer/main.js | 14 +++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/scripts/logviewer/index.html b/scripts/logviewer/index.html index dbf54d70..aed67512 100644 --- a/scripts/logviewer/index.html +++ b/scripts/logviewer/index.html @@ -27,12 +27,17 @@ aside { grid-area: details; + padding: 8px; } aside h3 { word-wrap: anywhere; } + aside p { + margin: 2px 0; + } + aside .values li span { word-wrap: ; word-wrap: anywhere; @@ -41,17 +46,26 @@ aside .values { list-style: none; padding: 0; + border: 1px solid lightgray; } - aside .values span { - width: 50%; + aside .values span.key { + width: 30%; display: block; } + aside .values span.value { + width: 70%; + display: block; + padding-left: 10px; + } + aside .values li { display: flex; - border: ; - border-bottom: 1px solid lightgray; + } + + aside .values li:not(:first-child) { + border-top: 1px solid lightgray; } nav { diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js index b2b8a399..2a805787 100644 --- a/scripts/logviewer/main.js +++ b/scripts/logviewer/main.js @@ -6,7 +6,9 @@ const main = document.querySelector("main"); let selectedItemNode; let rootItem; -document.querySelector("main").addEventListener("click", event => { +const logLevels = [undefined, "All", "Debug", "Detail", "Info", "Warn", "Error", "Fatal", "Off"]; + +main.addEventListener("click", event => { if (selectedItemNode) { selectedItemNode.classList.remove("selected"); selectedItemNode = null; @@ -30,12 +32,18 @@ document.querySelector("main").addEventListener("click", event => { }); function showItemDetails(item, parent) { + const parentOffset = itemStart(parent) ? `${itemStart(item) - itemStart(parent)}ms` : "none"; const aside = t.aside([ t.h3(itemCaption(item)), + t.p([t.strong("Parent offset: "), parentOffset]), + t.p([t.strong("Log level: "), logLevels[itemLevel(item)]]), + t.p([t.strong("Error: "), itemError(item) ? `${itemError(item).name} ${itemError(item).stack}` : "none"]), + t.p([t.strong("Child count: "), itemChildren(item) ? `${itemChildren(item).length}` : "none"]), + t.p(t.strong("Values:")), t.ul({class: "values"}, Object.entries(itemValues(item)).map(([key, value]) => { return t.li([ - t.span(normalizeValueKey(key)), - t.span(value) + t.span({className: "key"}, normalizeValueKey(key)), + t.span({className: "value"}, value) ]); })) ]); From be1650defce4d13a14064c762d2cb0c967a27320 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 18 Feb 2021 11:26:27 +0100 Subject: [PATCH 3/6] add expand button to log items --- scripts/logviewer/index.html | 32 +++++++++++++++++++++++++++ scripts/logviewer/main.js | 43 ++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/scripts/logviewer/index.html b/scripts/logviewer/index.html index aed67512..2245e7a8 100644 --- a/scripts/logviewer/index.html +++ b/scripts/logviewer/index.html @@ -72,6 +72,31 @@ grid-area: nav; } + .timeline li:not(.expanded) > ol { + display: none; + } + + .timeline li > div { + display: flex; + } + + .timeline .toggleExpanded { + border: none; + background: none; + width: 24px; + height: 24px; + margin-right: 4px; + cursor: pointer; + } + + .timeline .toggleExpanded:before { + content: "▶"; + } + + .timeline li.expanded > div > .toggleExpanded:before { + content: "▼"; + } + .timeline ol { list-style: none; padding: 0 0 0 20px; @@ -86,6 +111,13 @@ padding: 2px; display: flex; margin: 1px; + flex: 1; + cursor: pointer; + } + + + .timeline div.item:not(.has-children) { + margin-left: calc(24px + 4px + 1px); } .timeline div.item .caption { diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js index 2a805787..5796e6f4 100644 --- a/scripts/logviewer/main.js +++ b/scripts/logviewer/main.js @@ -13,21 +13,26 @@ main.addEventListener("click", event => { selectedItemNode.classList.remove("selected"); selectedItemNode = null; } - const itemNode = event.target.closest(".item"); - if (itemNode) { - selectedItemNode = itemNode; - selectedItemNode.classList.add("selected"); - const path = selectedItemNode.dataset.path; - let item = rootItem; - let parent; - if (path.length) { - const indices = path.split("/").map(i => parseInt(i, 10)); - for(const i of indices) { - parent = item; - item = itemChildren(item)[i]; + if (event.target.classList.contains("toggleExpanded")) { + const li = event.target.parentElement.parentElement; + li.classList.toggle("expanded"); + } else { + const itemNode = event.target.closest(".item"); + if (itemNode) { + selectedItemNode = itemNode; + selectedItemNode.classList.add("selected"); + const path = selectedItemNode.dataset.path; + let item = rootItem; + let parent; + if (path.length) { + const indices = path.split("/").map(i => parseInt(i, 10)); + for(const i of indices) { + parent = item; + item = itemChildren(item)[i]; + } } + showItemDetails(item, parent); } - showItemDetails(item, parent); } }); @@ -96,16 +101,22 @@ function normalizeValueKey(key) { // returns the node and the total range (recursively) occupied by the node function itemToNode(item, path) { + const hasChildren = !!itemChildren(item)?.length; const className = { item: true, + "has-children": hasChildren, error: itemError(item), [`type-${itemType(item)}`]: !!itemType(item), [`level-${itemLevel(item)}`]: true, }; + const li = t.li([ - t.div({className, "data-path": path.join("/")}, [ - t.span({class: "caption"}, itemCaption(item)), - t.span({class: "duration"}, `(${itemDuration(item)}ms)`), + t.div([ + hasChildren ? t.button({className: "toggleExpanded"}) : "", + t.div({className, "data-path": path.join("/")}, [ + t.span({class: "caption"}, itemCaption(item)), + t.span({class: "duration"}, `(${itemDuration(item)}ms)`), + ]) ]) ]); if (itemChildren(item) && itemChildren(item).length) { From 19df43ca3ccbfcf8074e42e044be26da5bf67461 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 18 Feb 2021 11:54:21 +0100 Subject: [PATCH 4/6] more compact layout, a few more detail fields and expand recursively but --- scripts/logviewer/index.html | 7 +++++++ scripts/logviewer/main.js | 24 ++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/scripts/logviewer/index.html b/scripts/logviewer/index.html index 2245e7a8..e5f89cac 100644 --- a/scripts/logviewer/index.html +++ b/scripts/logviewer/index.html @@ -25,6 +25,11 @@ overflow-y: auto; } + main section h2 { + margin: 2px 14px; + font-size: 1rem; + } + aside { grid-area: details; padding: 8px; @@ -100,6 +105,7 @@ .timeline ol { list-style: none; padding: 0 0 0 20px; + margin: 0; } .timeline div.item { @@ -112,6 +118,7 @@ display: flex; margin: 1px; flex: 1; + min-width: 0; cursor: pointer; } diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js index 5796e6f4..13d4975d 100644 --- a/scripts/logviewer/main.js +++ b/scripts/logviewer/main.js @@ -31,18 +31,22 @@ main.addEventListener("click", event => { item = itemChildren(item)[i]; } } - showItemDetails(item, parent); + showItemDetails(item, parent, itemNode); } } }); -function showItemDetails(item, parent) { +function showItemDetails(item, parent, itemNode) { const parentOffset = itemStart(parent) ? `${itemStart(item) - itemStart(parent)}ms` : "none"; + const expandButton = t.button("Expand recursively"); + expandButton.addEventListener("click", () => expandResursively(itemNode.parentElement.parentElement)); const aside = t.aside([ t.h3(itemCaption(item)), - t.p([t.strong("Parent offset: "), parentOffset]), t.p([t.strong("Log level: "), logLevels[itemLevel(item)]]), t.p([t.strong("Error: "), itemError(item) ? `${itemError(item).name} ${itemError(item).stack}` : "none"]), + t.p([t.strong("Parent offset: "), parentOffset]), + t.p([t.strong("Start: "), new Date(itemStart(item)).toString()]), + t.p([t.strong("Duration: "), `${itemDuration(item)}ms`]), t.p([t.strong("Child count: "), itemChildren(item) ? `${itemChildren(item).length}` : "none"]), t.p(t.strong("Values:")), t.ul({class: "values"}, Object.entries(itemValues(item)).map(([key, value]) => { @@ -50,11 +54,23 @@ function showItemDetails(item, parent) { t.span({className: "key"}, normalizeValueKey(key)), t.span({className: "value"}, value) ]); - })) + })), + t.p(expandButton) ]); document.querySelector("aside").replaceWith(aside); } +function expandResursively(li) { + li.classList.add("expanded"); + const ol = li.querySelector("ol"); + if (ol) { + const len = ol.children.length; + for (let i = 0; i < len; i += 1) { + expandResursively(ol.children[i]); + } + } +} + document.getElementById("openFile").addEventListener("click", loadFile); async function loadFile() { From 560918e3738aaac4f5de52135d005e8c5f2e01e3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 18 Feb 2021 12:02:26 +0100 Subject: [PATCH 5/6] format times between root items better --- scripts/logviewer/index.html | 1 + scripts/logviewer/main.js | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/logviewer/index.html b/scripts/logviewer/index.html index e5f89cac..6533f1fa 100644 --- a/scripts/logviewer/index.html +++ b/scripts/logviewer/index.html @@ -23,6 +23,7 @@ min-width: 0; min-height: 0; overflow-y: auto; + padding: 8px; } main section h2 { diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js index 13d4975d..11ab7661 100644 --- a/scripts/logviewer/main.js +++ b/scripts/logviewer/main.js @@ -81,7 +81,7 @@ async function loadFile() { const fragment = logs.items.reduce((fragment, item, i, items) => { const prevItem = i === 0 ? null : items[i - 1]; fragment.appendChild(t.section([ - t.h2(prevItem ? `+ ${itemStart(item) - itemEnd(prevItem)} ms` : new Date(itemStart(item)).toString()), + t.h2(prevItem ? `+ ${formatTime(itemStart(item) - itemEnd(prevItem))}` : new Date(itemStart(item)).toString()), t.div({className: "timeline"}, t.ol(itemToNode(item, [i]))) ])); return fragment; @@ -89,6 +89,20 @@ async function loadFile() { main.replaceChildren(fragment); } +function formatTime(ms) { + if (ms < 1000) { + return `${ms}ms`; + } else if (ms < 1000 * 60) { + return `${(ms / 1000).toFixed(2)}s`; + } else if (ms < 1000 * 60 * 60) { + return `${(ms / (1000 * 60)).toFixed(2)}m`; + } else if (ms < 1000 * 60 * 60 * 24) { + return `${(ms / (1000 * 60 * 60)).toFixed(2)}h`; + } else { + return `${(ms / (1000 * 60 * 60 * 24)).toFixed(2)}d`; + } +} + function itemChildren(item) { return item.c; } function itemStart(item) { return item.s; } function itemEnd(item) { return item.s + item.d; } From 69feb40075da0537ef8f5400007532f93ad08509 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 18 Feb 2021 12:28:57 +0100 Subject: [PATCH 6/6] add copyright headers --- scripts/logviewer/file.js | 17 ++++++++++++++++- scripts/logviewer/main.js | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/logviewer/file.js b/scripts/logviewer/file.js index 11f4ef5a..64a8422b 100644 --- a/scripts/logviewer/file.js +++ b/scripts/logviewer/file.js @@ -1,3 +1,18 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ export function openFile(mimeType = null) { const input = document.createElement("input"); @@ -33,4 +48,4 @@ export function readFileAsText(file) { }); reader.readAsText(file); return promise; -} \ No newline at end of file +} diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js index 11ab7661..f22890fa 100644 --- a/scripts/logviewer/main.js +++ b/scripts/logviewer/main.js @@ -1,3 +1,19 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + import {tag as t} from "./html.js"; import {openFile, readFileAsText} from "./file.js";