mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
commit
e9ce87ed9b
51
scripts/logviewer/file.js
Normal file
51
scripts/logviewer/file.js
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
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;
|
||||
}
|
110
scripts/logviewer/html.js
Normal file
110
scripts/logviewer/html.js
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
167
scripts/logviewer/index.html
Normal file
167
scripts/logviewer/index.html
Normal file
@ -0,0 +1,167 @@
|
||||
<!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;
|
||||
}
|
||||
|
||||
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;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
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 div.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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.timeline div.item:not(.has-children) {
|
||||
margin-left: calc(24px + 4px + 1px);
|
||||
}
|
||||
|
||||
.timeline div.item .caption {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.timeline div.item.level-3 {
|
||||
--brightness: 90%;
|
||||
}
|
||||
|
||||
.timeline div.item.level-6 {
|
||||
--hue: 0deg !important;
|
||||
}
|
||||
|
||||
.timeline div.item.type-network {
|
||||
--hue: 30deg;
|
||||
}
|
||||
|
||||
.timeline div.item.selected {
|
||||
background-color: Highlight;
|
||||
border-color: Highlight;
|
||||
color: HighlightText;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav><button id="openFile">Open log file</button></nav>
|
||||
<main></main>
|
||||
<aside></aside>
|
||||
<script type="module" src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
174
scripts/logviewer/main.js
Normal file
174
scripts/logviewer/main.js
Normal file
@ -0,0 +1,174 @@
|
||||
/*
|
||||
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;
|
||||
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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("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]) => {
|
||||
return t.li([
|
||||
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() {
|
||||
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 ? `+ ${formatTime(itemStart(item) - itemEnd(prevItem))}` : new Date(itemStart(item)).toString()),
|
||||
t.div({className: "timeline"}, t.ol(itemToNode(item, [i])))
|
||||
]));
|
||||
return fragment;
|
||||
}, document.createDocumentFragment());
|
||||
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; }
|
||||
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 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([
|
||||
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) {
|
||||
li.appendChild(t.ol(itemChildren(item).map((item, i) => {
|
||||
return itemToNode(item, path.concat(i));
|
||||
})));
|
||||
}
|
||||
return li;
|
||||
}
|
Loading…
Reference in New Issue
Block a user