diff --git a/scripts/logviewer/index.html b/scripts/logviewer/index.html index ef5f9f23..9b26cfd5 100644 --- a/scripts/logviewer/index.html +++ b/scripts/logviewer/index.html @@ -110,7 +110,7 @@ margin: 0; } - .timeline div.item { + .timeline .item { --hue: 100deg; --brightness: 80%; background-color: hsl(var(--hue), 60%, var(--brightness)); @@ -121,15 +121,16 @@ margin: 1px; flex: 1; min-width: 0; - cursor: pointer; + color: inherit; + text-decoration: none; } - .timeline div.item:not(.has-children) { + .timeline .item:not(.has-children) { margin-left: calc(24px + 4px + 1px); } - .timeline div.item .caption { + .timeline .item .caption { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; @@ -157,15 +158,15 @@ color: white; } - .timeline div.item.type-network { + .timeline .item.type-network { --hue: 30deg; } - .timeline div.item.type-navigation { + .timeline .item.type-navigation { --hue: 200deg; } - .timeline div.item.selected { + .timeline .item.selected { background-color: Highlight; border-color: Highlight; color: HighlightText; diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js index f06ee582..af5f562c 100644 --- a/scripts/logviewer/main.js +++ b/scripts/logviewer/main.js @@ -21,37 +21,55 @@ const main = document.querySelector("main"); let selectedItemNode; let rootItem; +let itemByRef; const logLevels = [undefined, "All", "Debug", "Detail", "Info", "Warn", "Error", "Fatal", "Off"]; main.addEventListener("click", event => { - if (selectedItemNode) { - selectedItemNode.classList.remove("selected"); - selectedItemNode = null; - } if (event.target.classList.contains("toggleExpanded")) { const li = event.target.parentElement.parentElement; li.classList.toggle("expanded"); } else { + // allow clicking any links other than .item in the timeline, like refs + if (event.target.tagName === "A" && !event.target.classList.contains("item")) { + return; + } 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, itemNode); + // we don't want scroll to jump when clicking + // so prevent default behaviour, and select and push to history manually + event.preventDefault(); + selectNode(itemNode); + history.pushState(null, null, `#${itemNode.id}`); } } }); +window.addEventListener("hashchange", () => { + const id = window.location.hash.substr(1); + const itemNode = document.getElementById(id); + if (itemNode && itemNode.closest("main")) { + selectNode(itemNode); + itemNode.scrollIntoView({behavior: "smooth", block: "nearest"}); + } +}); + +function selectNode(itemNode) { + if (selectedItemNode) { + selectedItemNode.classList.remove("selected"); + } + selectedItemNode = itemNode; + selectedItemNode.classList.add("selected"); + let item = rootItem; + let parent; + const indices = selectedItemNode.id.split("/").map(i => parseInt(i, 10)); + for(const i of indices) { + parent = item; + item = itemChildren(item)[i]; + } + showItemDetails(item, parent, selectedItemNode); +} + function stringifyItemValue(value) { if (typeof value === "object" && value !== null) { return JSON.stringify(value, undefined, 2); @@ -75,9 +93,20 @@ function showItemDetails(item, parent, itemNode) { t.p([t.strong("Forced finish: "), (itemForcedFinish(item) || false) + ""]), t.p(t.strong("Values:")), t.ul({class: "values"}, Object.entries(itemValues(item)).map(([key, value]) => { + let valueNode; + if (key === "ref") { + const refItem = itemByRef.get(value); + if (refItem) { + valueNode = t.a({href: `#${refItem.id}`}, itemCaption(refItem)); + } else { + valueNode = `unknown ref ${value}`; + } + } else { + valueNode = stringifyItemValue(value); + } return t.li([ t.span({className: "key"}, normalizeValueKey(key)), - t.span({className: "value"}, stringifyItemValue(value)) + t.span({className: "value"}, valueNode) ]); })), t.p(expandButton) @@ -117,6 +146,9 @@ async function loadFile() { const logs = JSON.parse(json); logs.items.sort((a, b) => itemStart(a) - itemStart(b)); rootItem = {c: logs.items}; + itemByRef = new Map(); + preprocessRecursively(rootItem, itemByRef, []); + const fragment = logs.items.reduce((fragment, item, i, items) => { const prevItem = i === 0 ? null : items[i - 1]; fragment.appendChild(t.section([ @@ -128,6 +160,21 @@ async function loadFile() { main.replaceChildren(fragment); } +function preprocessRecursively(item, refsMap, path) { + if (itemRefSource(item)) { + refsMap.set(itemRefSource(item), item); + } + if (itemChildren(item)) { + for (let i = 0; i < itemChildren(item).length; i += 1) { + // do it in advance for a child as we don't want to do it for the rootItem + const child = itemChildren(item)[i]; + const childPath = path.concat(i); + child.id = childPath.join("/"); + preprocessRecursively(child, refsMap, childPath); + } + } +} + function formatTime(ms) { if (ms < 1000) { return `${ms}ms`; @@ -152,6 +199,8 @@ function itemLabel(item) { return item.v?.l; } function itemType(item) { return item.v?.t; } function itemError(item) { return item.e; } function itemForcedFinish(item) { return item.f; } +function itemRef(item) { return item.v?.ref; } +function itemRefSource(item) { return item.v?.refId; } function itemShortErrorMessage(item) { if (itemError(item)) { const e = itemError(item); @@ -168,6 +217,13 @@ function itemCaption(item) { return `${itemLabel(item)} (${itemValues(item).status})`; } else if (itemLabel(item) && itemError(item)) { return `${itemLabel(item)} (${itemShortErrorMessage(item)})`; + } else if (itemRef(item)) { + const refItem = itemByRef.get(itemRef(item)); + if (refItem) { + return `ref "${itemCaption(refItem)}"` + } else { + return `unknown ref ${itemRef(item)}` + } } else { return itemLabel(item) || itemType(item); } @@ -181,7 +237,7 @@ function normalizeValueKey(key) { } // returns the node and the total range (recursively) occupied by the node -function itemToNode(item, path) { +function itemToNode(item) { const hasChildren = !!itemChildren(item)?.length; const className = { item: true, @@ -191,18 +247,29 @@ function itemToNode(item, path) { [`level-${itemLevel(item)}`]: true, }; + const id = item.id; + let captionNode; + if (itemRef(item)) { + const refItem = itemByRef.get(itemRef(item)); + if (refItem) { + captionNode = ["ref ", t.a({href: `#${refItem.id}`}, itemCaption(refItem))]; + } + } + if (!captionNode) { + captionNode = itemCaption(item); + } const li = t.li([ t.div([ hasChildren ? t.button({className: "toggleExpanded"}) : "", - t.div({className, "data-path": path.join("/")}, [ - t.span({class: "caption"}, itemCaption(item)), + t.a({className, id, href: `#${id}`}, [ + t.span({class: "caption"}, captionNode), 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)); + li.appendChild(t.ol(itemChildren(item).map(item => { + return itemToNode(item); }))); } return li;