diff --git a/package.json b/package.json index b1704e43..28b726e2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "homepage": "https://github.com/vector-im/hydrogen-web/#readme", "devDependencies": { + "@matrixdotorg/structured-logviewer": "^0.0.1", "@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/parser": "^4.29.2", "acorn": "^8.6.0", diff --git a/scripts/logviewer/file.js b/scripts/logviewer/file.js deleted file mode 100644 index 64a8422b..00000000 --- a/scripts/logviewer/file.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -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"); - 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; -} diff --git a/scripts/logviewer/html.js b/scripts/logviewer/html.js deleted file mode 100644 index a965a6ee..00000000 --- a/scripts/logviewer/html.js +++ /dev/null @@ -1,110 +0,0 @@ -/* -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 deleted file mode 100644 index 109cf8d1..00000000 --- a/scripts/logviewer/index.html +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - - - -
- - - - - diff --git a/scripts/logviewer/main.js b/scripts/logviewer/main.js deleted file mode 100644 index e552a094..00000000 --- a/scripts/logviewer/main.js +++ /dev/null @@ -1,430 +0,0 @@ -/* -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"; - -const main = document.querySelector("main"); - -let selectedItemNode; -let rootItem; -let itemByRef; -let itemsRefFrom; - -const logLevels = [undefined, "All", "Debug", "Detail", "Info", "Warn", "Error", "Fatal", "Off"]; - -main.addEventListener("click", event => { - 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) { - // 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")) { - ensureParentsExpanded(itemNode); - 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 ensureParentsExpanded(itemNode) { - let li = itemNode.parentElement.parentElement; - while (li.tagName === "LI") { - li.classList.add("expanded"); - li = li.parentElement.parentElement; - } -} - -function stringifyItemValue(value) { - if (typeof value === "object" && value !== null) { - return JSON.stringify(value, undefined, 2); - } else { - return value + ""; - } -} - -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 start = itemStart(item); - const aside = t.aside([ - t.h3(itemCaption(item)), - 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(start).toString(), ` (${start})`]), - t.p([t.strong("Duration: "), `${itemDuration(item)}ms`]), - t.p([t.strong("Child count: "), itemChildren(item) ? `${itemChildren(item).length}` : "none"]), - 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 if (key === "refId") { - const refSources = itemsRefFrom.get(value) ?? []; - valueNode = t.div([t.p([`${value}`, t.br(),`Found these references:`]),t.ul(refSources.map(item => { - return t.li(t.a({href: `#${item.id}`}, itemCaption(item))); - }))]); - } else { - valueNode = stringifyItemValue(value); - } - return t.li([ - t.span({className: "key"}, normalizeValueKey(key)), - t.span({className: "value"}, valueNode) - ]); - })), - 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); - -function getRootItemHeader(prevItem, item) { - if (prevItem) { - const diff = itemStart(item) - itemEnd(prevItem); - if (diff >= 0) { - return `+ ${formatTime(diff)}`; - } else { - const overlap = -diff; - if (overlap >= itemDuration(item)) { - return `ran entirely in parallel with`; - } else { - return `ran ${formatTime(-diff)} in parallel with`; - } - } - } else { - return new Date(itemStart(item)).toString(); - } -} - -async function loadFile() { - const file = await openFile(); - document.getElementById("filename").innerText = file.name; - await loadBlob(file); -} - -export async function loadBlob(blob) { - const json = await readFileAsText(blob); - const logs = JSON.parse(json); - logs.items.sort((a, b) => itemStart(a) - itemStart(b)); - rootItem = {c: logs.items}; - itemByRef = new Map(); - itemsRefFrom = new Map(); - preprocessRecursively(rootItem, null, itemByRef, itemsRefFrom, []); - - const fragment = logs.items.reduce((fragment, item, i, items) => { - const prevItem = i === 0 ? null : items[i - 1]; - fragment.appendChild(t.section([ - t.h2(getRootItemHeader(prevItem, item)), - t.div({className: "timeline"}, t.ol(itemToNode(item, [i]))) - ])); - return fragment; - }, document.createDocumentFragment()); - main.replaceChildren(fragment); - main.scrollTop = main.scrollHeight; -} - -// TODO: make this use processRecursively -function preprocessRecursively(item, parentElement, refsMap, refsFromMap, path) { - item.s = (parentElement?.s || 0) + item.s; - if (itemRefSource(item)) { - refsMap.set(itemRefSource(item), item); - } - if (itemRef(item)) { - let refs = refsFromMap.get(itemRef(item)); - if (!refs) { - refs = []; - refsFromMap.set(itemRef(item), refs); - } - refs.push(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, item, refsMap, refsFromMap, childPath); - } - } -} - -const MS_IN_SEC = 1000; -const MS_IN_MIN = MS_IN_SEC * 60; -const MS_IN_HOUR = MS_IN_MIN * 60; -const MS_IN_DAY = MS_IN_HOUR * 24; -function formatTime(ms) { - let str = ""; - if (ms > MS_IN_DAY) { - const days = Math.floor(ms / MS_IN_DAY); - ms -= days * MS_IN_DAY; - str += `${days}d`; - } - if (ms > MS_IN_HOUR) { - const hours = Math.floor(ms / MS_IN_HOUR); - ms -= hours * MS_IN_HOUR; - str += `${hours}h`; - } - if (ms > MS_IN_MIN) { - const mins = Math.floor(ms / MS_IN_MIN); - ms -= mins * MS_IN_MIN; - str += `${mins}m`; - } - if (ms > MS_IN_SEC) { - const secs = ms / MS_IN_SEC; - str += `${secs.toFixed(2)}s`; - } else if (ms > 0 || !str.length) { - str += `${ms}ms`; - } - return str; -} - -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 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); - return e.name || e.stack.substr(0, e.stack.indexOf("\n")); - } -} - -function itemCaption(item) { - if (itemLabel(item) && itemError(item)) { - return `${itemLabel(item)} (${itemShortErrorMessage(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 if (itemLabel(item) && itemValues(item)?.status) { - return `${itemLabel(item)} (${itemValues(item).status})`; - } else if (itemLabel(item) && itemValues(item)?.type) { - return `${itemLabel(item)} (${itemValues(item)?.type})`; - } 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); - } -} -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) { - 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 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.a({className, id, href: `#${id}`}, [ - t.span({class: "caption"}, captionNode), - t.span({class: "duration"}, `(${formatTime(itemDuration(item))})`), - ]) - ]) - ]); - if (itemChildren(item) && itemChildren(item).length) { - li.appendChild(t.ol(itemChildren(item).map(item => { - return itemToNode(item); - }))); - } - return li; -} - -const highlightForm = document.getElementById("highlightForm"); - -highlightForm.addEventListener("submit", evt => { - evt.preventDefault(); - const matchesOutput = document.getElementById("highlightMatches"); - const query = document.getElementById("highlight").value; - if (query) { - matchesOutput.innerText = "Searching…"; - let matches = 0; - processRecursively(rootItem, item => { - let domNode = document.getElementById(item.id); - if (itemMatchesFilter(item, query)) { - matches += 1; - domNode.classList.add("highlighted"); - domNode = domNode.parentElement; - while (domNode.nodeName !== "SECTION") { - if (domNode.nodeName === "LI") { - domNode.classList.add("expanded"); - } - domNode = domNode.parentElement; - } - } else { - domNode.classList.remove("highlighted"); - } - }); - matchesOutput.innerText = `${matches} matches`; - } else { - for (const node of document.querySelectorAll(".highlighted")) { - node.classList.remove("highlighted"); - } - matchesOutput.innerText = ""; - } -}); - -function itemMatchesFilter(item, query) { - if (itemError(item)) { - if (valueMatchesQuery(itemError(item), query)) { - return true; - } - } - return valueMatchesQuery(itemValues(item), query); -} - -function valueMatchesQuery(value, query) { - if (typeof value === "string") { - return value.includes(query); - } else if (typeof value === "object" && value !== null) { - for (const key in value) { - if (value.hasOwnProperty(key) && valueMatchesQuery(value[key], query)) { - return true; - } - } - } else if (typeof value === "number") { - return value.toString().includes(query); - } - return false; -} - -function processRecursively(item, callback, parentItem) { - if (item.id) { - callback(item, parentItem); - } - 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]; - processRecursively(child, callback, item); - } - } -} - -document.getElementById("collapseAll").addEventListener("click", () => { - for (const node of document.querySelectorAll(".expanded")) { - node.classList.remove("expanded"); - } -}); -document.getElementById("hideCollapsed").addEventListener("click", () => { - for (const node of document.querySelectorAll("section > div.timeline > ol > li:not(.expanded)")) { - node.closest("section").classList.add("hidden"); - } -}); -document.getElementById("hideHighlightedSiblings").addEventListener("click", () => { - for (const node of document.querySelectorAll(".highlighted")) { - const list = node.closest("ol"); - const siblings = Array.from(list.querySelectorAll("li > div > a:not(.highlighted)")).map(n => n.closest("li")); - for (const sibling of siblings) { - if (!sibling.classList.contains("expanded")) { - sibling.classList.add("hidden"); - } - } - } -}); -document.getElementById("showAll").addEventListener("click", () => { - for (const node of document.querySelectorAll(".hidden")) { - node.classList.remove("hidden"); - } -}); diff --git a/src/platform/web/ui/session/settings/SettingsView.js b/src/platform/web/ui/session/settings/SettingsView.js index 09d3cb5a..ffc20f25 100644 --- a/src/platform/web/ui/session/settings/SettingsView.js +++ b/src/platform/web/ui/session/settings/SettingsView.js @@ -143,7 +143,7 @@ export class SettingsView extends TemplateView { } async function openLogs(vm) { - const logviewerUrl = (await import("../../../../../../scripts/logviewer/index.html?url")).default; + const logviewerUrl = (await import("@matrixdotorg/structured-logviewer/index.html?url")).default; const win = window.open(logviewerUrl); await new Promise((resolve, reject) => { let shouldSendPings = true; diff --git a/yarn.lock b/yarn.lock index 6405fe4d..cbce8b06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,6 +56,11 @@ version "3.2.3" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" +"@matrixdotorg/structured-logviewer@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@matrixdotorg/structured-logviewer/-/structured-logviewer-0.0.1.tgz#9c29470b552f874afbb1df16c6e8e9e0c55cbf59" + integrity sha512-IdPYxAFDEoEs2G1ImKCkCxFI3xF1DDctP3N9JOtHRvIPbPPdTT9DyNqKTewCb5zwjNB1mGBrnWyURnHDiOOL3w== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"