forked from mystiq/hydrogen-web
move logviewer to own package
This commit is contained in:
parent
1d900b5184
commit
f6ea7803f2
7 changed files with 7 additions and 829 deletions
|
@ -26,6 +26,7 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
|
"homepage": "https://github.com/vector-im/hydrogen-web/#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@matrixdotorg/structured-logviewer": "^0.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
"@typescript-eslint/eslint-plugin": "^4.29.2",
|
||||||
"@typescript-eslint/parser": "^4.29.2",
|
"@typescript-eslint/parser": "^4.29.2",
|
||||||
"acorn": "^8.6.0",
|
"acorn": "^8.6.0",
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style type="text/css">
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin: 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: "nav nav" "items details";
|
|
||||||
grid-template-columns: 1fr 400px;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
grid-area: items;
|
|
||||||
min-width: 0;
|
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main section h2 {
|
|
||||||
margin: 2px 14px;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside .values {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
border: 1px solid lightgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside .values span.key {
|
|
||||||
width: 30%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside .values span.value {
|
|
||||||
width: 70%;
|
|
||||||
display: block;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside .values li {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
aside .values li:not(:first-child) {
|
|
||||||
border-top: 1px solid lightgray;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
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;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item {
|
|
||||||
--hue: 100deg;
|
|
||||||
--brightness: 80%;
|
|
||||||
background-color: hsl(var(--hue), 60%, var(--brightness));
|
|
||||||
border: 1px solid hsl(var(--hue), 60%, calc(var(--brightness) - 40%));
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 2px;
|
|
||||||
display: flex;
|
|
||||||
margin: 1px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.timeline .item:not(.has-children) {
|
|
||||||
margin-left: calc(24px + 4px + 1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item .caption {
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.level-3 {
|
|
||||||
--brightness: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.level-2 {
|
|
||||||
--brightness: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.level-5 {
|
|
||||||
--brightness: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.level-6, .timeline .item.level-7 {
|
|
||||||
--hue: 0deg !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.level-7 {
|
|
||||||
--brightness: 50%;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.type-network {
|
|
||||||
--hue: 30deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.type-call {
|
|
||||||
--hue: 50deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.type-navigation {
|
|
||||||
--hue: 200deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.selected {
|
|
||||||
background-color: Highlight;
|
|
||||||
border-color: Highlight;
|
|
||||||
color: HighlightText;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline .item.highlighted {
|
|
||||||
background-color: fuchsia;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#highlight {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav form {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#filename {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav>
|
|
||||||
<div>
|
|
||||||
<button id="openFile">Open log file</button>
|
|
||||||
<button id="collapseAll">Collapse all</button>
|
|
||||||
<button id="hideCollapsed">Hide collapsed root items</button>
|
|
||||||
<button id="hideHighlightedSiblings" title="Hide collapsed siblings of highlighted">Hide non-highlighted</button>
|
|
||||||
<button id="showAll">Show all</button>
|
|
||||||
<form id="highlightForm">
|
|
||||||
<input type="text" id="highlight" name="highlight" placeholder="Highlight a search term" autocomplete="on">
|
|
||||||
<output id="highlightMatches"></output>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<h2 id="filename"></h2>
|
|
||||||
</nav>
|
|
||||||
<main></main>
|
|
||||||
<aside></aside>
|
|
||||||
<script type="module" src="main.js"></script>
|
|
||||||
<script type="module">
|
|
||||||
import {loadBlob} from "./main.js";
|
|
||||||
window.addEventListener("message", event => {
|
|
||||||
console.log("message", event);
|
|
||||||
if (event.data.type === "open") {
|
|
||||||
document.getElementById("filename").innerText = "Loading logs from other tab...";
|
|
||||||
loadBlob(event.data.logs).then(() => {
|
|
||||||
document.getElementById("filename").innerText = "Logs from other tab at " + new Date();
|
|
||||||
});
|
|
||||||
} else if (event.data.type === "ping") {
|
|
||||||
document.getElementById("filename").innerText = "Waiting for logs from other tab...";
|
|
||||||
event.source.postMessage({type: "pong"});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -143,7 +143,7 @@ export class SettingsView extends TemplateView {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openLogs(vm) {
|
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);
|
const win = window.open(logviewerUrl);
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
let shouldSendPings = true;
|
let shouldSendPings = true;
|
||||||
|
|
|
@ -56,6 +56,11 @@
|
||||||
version "3.2.3"
|
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"
|
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":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||||
|
|
Loading…
Reference in a new issue