From 32b60fa0ff501dbd779a5d2f976268feb2f3d8ac Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 29 Nov 2018 00:58:20 +0200 Subject: [PATCH] Overhaul log viewer * Move viewer to separate modal to allow using more horizontal space * Load log history (up to 2048 lines) * Add colors and use table styling for log viewer * Add links to log entries that open mentioned instance/client or line in code --- maubot/__main__.py | 7 +- maubot/instance.py | 4 +- maubot/management/api/__init__.py | 3 +- maubot/management/api/dev_open.py | 64 +++++++++ maubot/management/api/log.py | 47 ++++--- maubot/management/frontend/package.json | 1 + maubot/management/frontend/src/api.js | 24 +++- .../src/pages/dashboard/BaseMainView.js | 5 +- .../frontend/src/pages/dashboard/Client.js | 2 +- .../frontend/src/pages/dashboard/Home.js | 5 +- .../frontend/src/pages/dashboard/Instance.js | 2 +- .../frontend/src/pages/dashboard/Log.js | 124 ++++++++++++++++-- .../frontend/src/pages/dashboard/Modal.js | 51 +++++++ .../frontend/src/pages/dashboard/Plugin.js | 2 +- .../frontend/src/pages/dashboard/index.js | 71 +++++++--- .../management/frontend/src/style/index.sass | 2 + .../frontend/src/style/pages/dashboard.sass | 21 +-- .../frontend/src/style/pages/log.sass | 80 +++++++++++ .../frontend/src/style/pages/modal.sass | 71 ++++++++++ maubot/management/frontend/yarn.lock | 41 +++++- 20 files changed, 545 insertions(+), 82 deletions(-) create mode 100644 maubot/management/api/dev_open.py create mode 100644 maubot/management/frontend/src/pages/dashboard/Modal.js create mode 100644 maubot/management/frontend/src/style/pages/log.sass create mode 100644 maubot/management/frontend/src/style/pages/modal.sass diff --git a/maubot/__main__.py b/maubot/__main__.py index 95118a7..4d6d525 100644 --- a/maubot/__main__.py +++ b/maubot/__main__.py @@ -26,7 +26,7 @@ from .server import MaubotServer from .client import Client, init as init_client_class from .loader.zip import init as init_zip_loader from .instance import init as init_plugin_instance_class -from .management.api import init as init_management_api, stop as stop_management_api +from .management.api import init as init_mgmt_api, stop as stop_mgmt_api, init_log_listener from .__meta__ import __version__ parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.", @@ -43,6 +43,7 @@ config.load() config.update() logging.config.dictConfig(copy.deepcopy(config["logging"])) +init_log_listener() log = logging.getLogger("maubot.init") log.info(f"Initializing maubot {__version__}") @@ -52,7 +53,7 @@ init_zip_loader(config) db_session = init_db(config) clients = init_client_class(db_session, loop) plugins = init_plugin_instance_class(db_session, config, loop) -management_api = init_management_api(config, loop) +management_api = init_mgmt_api(config, loop) server = MaubotServer(config, loop) server.app.add_subapp(config["server.base_path"], management_api) @@ -88,7 +89,7 @@ except KeyboardInterrupt: loop=loop)) db_session.commit() log.debug("Closing websockets") - loop.run_until_complete(stop_management_api()) + loop.run_until_complete(stop_mgmt_api()) log.debug("Stopping server") try: loop.run_until_complete(asyncio.wait_for(server.stop(), 5, loop=loop)) diff --git a/maubot/instance.py b/maubot/instance.py index c797466..494ac4b 100644 --- a/maubot/instance.py +++ b/maubot/instance.py @@ -31,7 +31,7 @@ from .client import Client from .loader import PluginLoader from .plugin_base import Plugin -log = logging.getLogger("maubot.plugin") +log = logging.getLogger("maubot.instance") yaml = YAML() yaml.indent(4) @@ -54,7 +54,7 @@ class PluginInstance: def __init__(self, db_instance: DBPlugin): self.db_instance = db_instance - self.log = logging.getLogger(f"maubot.plugin.{self.id}") + self.log = log.getChild(self.id) self.config = None self.started = False self.loader = None diff --git a/maubot/management/api/__init__.py b/maubot/management/api/__init__.py index 70f6e4b..760299e 100644 --- a/maubot/management/api/__init__.py +++ b/maubot/management/api/__init__.py @@ -23,7 +23,8 @@ from .auth import web as _ from .plugin import web as _ from .instance import web as _ from .client import web as _ -from .log import stop_all as stop_log_sockets +from .dev_open import web as _ +from .log import stop_all as stop_log_sockets, init as init_log_listener def init(cfg: Config, loop: AbstractEventLoop) -> web.Application: diff --git a/maubot/management/api/dev_open.py b/maubot/management/api/dev_open.py new file mode 100644 index 0000000..1447edb --- /dev/null +++ b/maubot/management/api/dev_open.py @@ -0,0 +1,64 @@ +# maubot - A plugin-based Matrix bot system. +# Copyright (C) 2018 Tulir Asokan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from string import Template +from subprocess import run +import re + +from ruamel.yaml import YAML +from aiohttp import web + +from .base import routes + +enabled = False + + +@routes.get("/debug/open") +async def check_enabled(_: web.Request) -> web.Response: + return web.json_response({ + "enabled": enabled, + }) + + +try: + yaml = YAML() + + with open(".dev-open-cfg.yaml", "r") as file: + cfg = yaml.load(file) + editor_command = Template(cfg["editor"]) + pathmap = [(re.compile(item["find"]), item["replace"]) for item in cfg["pathmap"]] + + + @routes.post("/debug/open") + async def open_file(request: web.Request) -> web.Response: + data = await request.json() + try: + path = data["path"] + for find, replace in pathmap: + path = find.sub(replace, path) + cmd = editor_command.substitute(path=path, line=data["line"]) + except (KeyError, ValueError): + return web.Response(status=400) + res = run(cmd, shell=True) + return web.json_response({ + "return": res.returncode, + "stdout": res.stdout, + "stderr": res.stderr + }) + + + enabled = True +except Exception: + pass diff --git a/maubot/management/api/log.py b/maubot/management/api/log.py index 572bf87..93fe41d 100644 --- a/maubot/management/api/log.py +++ b/maubot/management/api/log.py @@ -13,13 +13,15 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from typing import Deque, List from datetime import datetime +from collections import deque import logging import asyncio from aiohttp import web -from .base import routes, get_loop +from .base import routes, get_loop, get_config from .auth import is_valid_token BUILTIN_ATTRS = {"args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName", @@ -29,13 +31,19 @@ BUILTIN_ATTRS = {"args", "asctime", "created", "exc_info", "exc_text", "filename INCLUDE_ATTRS = {"filename", "funcName", "levelname", "levelno", "lineno", "module", "name", "pathname"} EXCLUDE_ATTRS = BUILTIN_ATTRS - INCLUDE_ATTRS +MAX_LINES = 2048 -class WebSocketHandler(logging.Handler): - def __init__(self, ws, level=logging.NOTSET) -> None: +class LogCollector(logging.Handler): + lines: Deque[dict] + formatter: logging.Formatter + listeners: List[web.WebSocketResponse] + + def __init__(self, level=logging.NOTSET) -> None: super().__init__(level) - self.ws = ws + self.lines = deque(maxlen=MAX_LINES) self.formatter = logging.Formatter() + self.listeners = [] def emit(self, record: logging.LogRecord) -> None: try: @@ -51,9 +59,9 @@ class WebSocketHandler(logging.Handler): for name, value in record.__dict__.items() if name not in EXCLUDE_ATTRS } - content["id"] = record.relativeCreated + content["id"] = str(record.relativeCreated) content["msg"] = record.getMessage() - content["time"] = datetime.utcnow() + content["time"] = datetime.fromtimestamp(record.created) if record.exc_info: content["exc_info"] = self.formatter.formatException(record.exc_info) @@ -61,22 +69,29 @@ class WebSocketHandler(logging.Handler): for name, value in content.items(): if isinstance(value, datetime): content[name] = value.astimezone().isoformat() - - asyncio.ensure_future(self.send(content), loop=get_loop()) + asyncio.ensure_future(self.send(content)) + self.lines.append(content) async def send(self, record: dict) -> None: - try: - await self.ws.send_json(record) - except Exception as e: - print("Log sending error:", e) + for ws in self.listeners: + try: + await ws.send_json(record) + except Exception as e: + print("Log sending error:", e) +handler = LogCollector() log_root = logging.getLogger("maubot") log = logging.getLogger("maubot.server.websocket") sockets = [] +def init() -> None: + log_root.addHandler(handler) + + async def stop_all() -> None: + log_root.removeHandler(handler) for socket in sockets: try: await socket.close(code=1012) @@ -90,7 +105,6 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: await ws.prepare(request) sockets.append(ws) log.debug(f"Connection from {request.remote} opened") - handler = WebSocketHandler(ws) authenticated = False async def close_if_not_authenticated(): @@ -106,11 +120,12 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: if msg.type != web.WSMsgType.TEXT: continue if is_valid_token(msg.data): + await ws.send_json({"auth_success": True}) + await ws.send_json({"history": list(handler.lines)}) if not authenticated: log.debug(f"Connection from {request.remote} authenticated") - log_root.addHandler(handler) + handler.listeners.append(ws) authenticated = True - await ws.send_json({"auth_success": True}) elif not authenticated: await ws.send_json({"auth_success": False}) except Exception: @@ -118,7 +133,7 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse: await ws.close() except Exception: pass - log_root.removeHandler(handler) + handler.listeners.remove(ws) log.debug(f"Connection from {request.remote} closed") sockets.remove(ws) return ws diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index 29e19e0..e754eef 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -7,6 +7,7 @@ "react": "^16.6.0", "react-ace": "^6.2.0", "react-dom": "^16.6.0", + "react-json-tree": "^0.11.0", "react-router-dom": "^4.3.1", "react-scripts": "2.0.5", "react-select": "^2.1.1" diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js index 0992233..49465af 100644 --- a/maubot/management/frontend/src/api.js +++ b/maubot/management/frontend/src/api.js @@ -82,7 +82,8 @@ export async function openLogSocket() { socket: null, connected: false, authenticated: false, - onLog: data => {}, + onLog: data => undefined, + onHistory: history => undefined, fails: -1, } const openHandler = () => { @@ -100,9 +101,9 @@ export async function openLogSocket() { } else { console.info("Websocket connection authentication failed") } + } else if (data.history) { + wrapper.onHistory(data.history) } else { - data.time = new Date(data.time) - console.log("SERVLOG", data) wrapper.onLog(data) } } @@ -131,6 +132,21 @@ export async function openLogSocket() { return wrapper } +let _debugOpenFileEnabled = undefined +export const debugOpenFileEnabled = () => _debugOpenFileEnabled +export const updateDebugOpenFileEnabled = async () => { + const resp = await defaultGet("/debug/open") + _debugOpenFileEnabled = resp["enabled"] || false +} +export async function debugOpenFile(path, line) { + const resp = await fetch(`${BASE_PATH}/debug/open`, { + headers: getHeaders(), + body: JSON.stringify({ path, line }), + method: "POST", + }) + return await resp.json() +} + export const getInstances = () => defaultGet("/instances") export const getInstance = id => defaultGet(`/instance/${id}`) export const putInstance = (instance, id) => defaultPut("instance", instance, id) @@ -179,7 +195,7 @@ export const deleteClient = id => defaultDelete("client", id) export default { BASE_PATH, - login, ping, openLogSocket, + login, ping, openLogSocket, debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled, getInstances, getInstance, putInstance, deleteInstance, getPlugins, getPlugin, uploadPlugin, deletePlugin, getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, diff --git a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js index de4d4e4..2ad72b1 100644 --- a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js +++ b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js @@ -1,6 +1,5 @@ import React, { Component } from "react" import { Link } from "react-router-dom" -import Log from "./Log" class BaseMainView extends Component { constructor(props) { @@ -65,7 +64,9 @@ class BaseMainView extends Component { ) - renderLog = () => !this.isNew && + renderLogButton = (filter) => !this.isNew &&
+ +
} export default BaseMainView diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 0f2b3e7..e520c26 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -196,6 +196,7 @@ class Client extends BaseMainView { {this.state.saving ? : (this.isNew ? "Create" : "Save")} + {this.renderLogButton(this.state.id)}
{this.state.error}
@@ -209,7 +210,6 @@ class Client extends BaseMainView { {this.renderInstances()} - {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/Home.js b/maubot/management/frontend/src/pages/dashboard/Home.js index f1a4fad..8dc22b9 100644 --- a/maubot/management/frontend/src/pages/dashboard/Home.js +++ b/maubot/management/frontend/src/pages/dashboard/Home.js @@ -14,7 +14,6 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import React, { Component } from "react" -import Log from "./Log" class Home extends Component { render() { @@ -22,7 +21,9 @@ class Home extends Component {
See sidebar to get started
- +
+ +
} } diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index 37596de..5ea2d2b 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -167,8 +167,8 @@ class Instance extends BaseMainView { {this.state.saving ? : (this.isNew ? "Create" : "Save")} + {this.renderLogButton(`instance.${this.state.id}`)}
{this.state.error}
- {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/Log.js b/maubot/management/frontend/src/pages/dashboard/Log.js index 55b5b86..7e710f9 100644 --- a/maubot/management/frontend/src/pages/dashboard/Log.js +++ b/maubot/management/frontend/src/pages/dashboard/Log.js @@ -13,20 +13,116 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react" +import React, { PureComponent } from "react" +import { Link } from "react-router-dom" +import JSONTree from "react-json-tree" +import api from "../../api" +import Modal from "./Modal" -const Log = ({ lines, showName = true }) =>
- {lines.map(data => <> -
- {data.time.toLocaleTimeString()} - {data.levelname} - {showName && {data.name}} - {data.msg} -
- {data.exc_info &&
- {data.exc_info.replace(/\\n/g, "\n")} -
} - )} -
+class LogEntry extends PureComponent { + static contextType = Modal.Context + + renderName() { + const line = this.props.line + if (line.nameLink) { + const modal = this.context + return ( + + {line.name} + + ) + } + return line.name + } + + renderContent() { + if (this.props.line.matrix_http_request) { + const req = this.props.line.matrix_http_request + + return <> + {req.method} {req.path} +
+ {Object.entries(req.content).length > 0 + && } +
+ + } + return this.props.line.msg + } + + renderTime() { + return this.props.line.time.toLocaleTimeString("en-GB") + } + + renderLevelName() { + return this.props.line.levelname + } + + get unfocused() { + return this.props.focus && this.props.line.name !== this.props.focus + ? "unfocused" + : "" + } + + renderRow(content) { + return ( +
+ {this.renderTime()} + {this.renderLevelName()} + {this.renderName()} + {content} +
+ ) + } + + renderExceptionInfo() { + if (!api.debugOpenFileEnabled()) { + return this.props.line.exc_info + } + const fileLinks = [] + let str = this.props.line.exc_info.replace( + /File "(.+)", line ([0-9]+), in (.+)/g, + (_, file, line, method) => { + fileLinks.push( + { + api.debugOpenFile(file, line) + return false + }}>File "{file}", line {line}, in {method}, + ) + return "||EDGE||" + }) + fileLinks.reverse() + + const result = [] + let key = 0 + for (const part of str.split("||EDGE||")) { + result.push( + {part} + {fileLinks.pop()} + ) + } + return result + } + + render() { + return <> + {this.renderRow(this.renderContent())} + {this.props.line.exc_info && this.renderRow(this.renderExceptionInfo())} + + } +} + +class Log extends PureComponent { + render() { + return ( +
+
+ {this.props.lines.map(data => )} +
+
+ ) + } +} export default Log diff --git a/maubot/management/frontend/src/pages/dashboard/Modal.js b/maubot/management/frontend/src/pages/dashboard/Modal.js new file mode 100644 index 0000000..9bfc48e --- /dev/null +++ b/maubot/management/frontend/src/pages/dashboard/Modal.js @@ -0,0 +1,51 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React, { Component, createContext } from "react" + +const rem = 16 + +class Modal extends Component { + static Context = createContext(null) + + constructor(props) { + super(props) + this.state = { + open: false, + } + this.wrapper = { clientWidth: 9001 } + } + + open = () => this.setState({ open: true }) + close = () => this.setState({ open: false }) + + render() { + return this.state.open && ( +
this.wrapper = ref} + onClick={() => this.wrapper.clientWidth > 45 * rem && this.close()}> +
evt.stopPropagation()}> + +
+ + {this.props.children} + +
+
+
+ ) + } +} + +export default Modal diff --git a/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot/management/frontend/src/pages/dashboard/Plugin.js index b29ec2d..e3ea809 100644 --- a/maubot/management/frontend/src/pages/dashboard/Plugin.js +++ b/maubot/management/frontend/src/pages/dashboard/Plugin.js @@ -93,9 +93,9 @@ class Plugin extends BaseMainView { {this.state.deleting ? : "Delete"} } + {this.renderLogButton("loader.zip")}
{this.state.error}
{this.renderInstances()} - {this.renderLog()} } } diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index ec4ad7d..c042fe8 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -21,6 +21,8 @@ import Instance from "./Instance" import Client from "./Client" import Plugin from "./Plugin" import Home from "./Home" +import Log from "./Log" +import Modal from "./Modal" class Dashboard extends Component { constructor(props) { @@ -30,9 +32,14 @@ class Dashboard extends Component { clients: {}, plugins: {}, sidebarOpen: false, + modalOpen: false, + logFocus: "", } this.logLines = [] this.logMap = {} + this.logModal = { + open: () => undefined, + } window.maubot = this } @@ -44,7 +51,8 @@ class Dashboard extends Component { async componentWillMount() { const [instanceList, clientList, pluginList] = await Promise.all([ - api.getInstances(), api.getClients(), api.getPlugins()]) + api.getInstances(), api.getClients(), api.getPlugins(), + api.updateDebugOpenFileEnabled()]) const instances = {} for (const instance of instanceList) { instances[instance.id] = instance @@ -60,10 +68,32 @@ class Dashboard extends Component { this.setState({ instances, clients, plugins }) const logs = await api.openLogSocket() + + const processEntry = (entry) => { + entry.time = new Date(entry.time) + if (entry.name.startsWith("maubot.")) { + entry.name = entry.name.substr("maubot.".length) + } + if (entry.name.startsWith("client.")) { + entry.name = entry.name.substr("client.".length) + entry.nameLink = `/client/${entry.name}` + } else if (entry.name.startsWith("instance.")) { + entry.nameLink = `/instance/${entry.name.substr("instance.".length)}` + } + (this.logMap[entry.name] || (this.logMap[entry.name] = [])).push(entry) + } + + logs.onHistory = history => { + for (const data of history) { + processEntry(data) + } + this.logLines = history + this.setState({ logFocus: this.state.logFocus }) + } logs.onLog = data => { + processEntry(data) this.logLines.push(data) - ;(this.logMap[data.name] || (this.logMap[data.name] = [])).push(data) - this.setState({}) + this.setState({ logFocus: this.state.logFocus }) } } @@ -87,28 +117,22 @@ class Dashboard extends Component { this.setState({ [stateField]: data }) } - getLog(field, id) { - if (field === "clients") { - return this.logMap[`maubot.client.${id}`] - } else if (field === "instances") { - return this.logMap[`maubot.plugin.${id}`] - } else if (field === "plugins") { - return this.logMap["maubot.loader.zip"] - } - } - renderView(field, type, id) { const entry = this.state[field][id] if (!entry) { return this.renderNotFound(field.slice(0, -1)) } - console.log(`maubot.${field.slice(0, -1)}.${id}`) return React.createElement(type, { entry, onDelete: () => this.delete(field, id), onChange: newEntry => this.add(field, newEntry, id), + openLog: filter => { + this.setState({ + logFocus: filter, + }) + this.logModal.open() + }, ctx: this.state, - log: this.getLog(field, id) || [], }) } @@ -118,7 +142,7 @@ class Dashboard extends Component { ) - render() { + renderMain() { return
@@ -161,7 +185,7 @@ class Dashboard extends Component {
- }/> + }/> this.add("instances", newEntry)} ctx={this.state}/>}/> @@ -180,6 +204,19 @@ class Dashboard extends Component {
} + + renderModal() { + return this.logModal = ref}> + + + } + + render() { + return <> + {this.renderMain()} + {this.renderModal()} + + } } export default withRouter(Dashboard) diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index b496d8b..9c1e193 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -27,3 +27,5 @@ @import pages/login @import pages/dashboard +@import pages/modal +@import pages/log diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index 5568854..d04cbba 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -89,29 +89,16 @@ margin-top: 5rem font-size: 1.5rem - div.log - text-align: left - font-size: 12px - max-height: 20rem - font-family: "Fira Code", monospace - overflow: auto - - > div.row - white-space: pre - - > span.level:before - content: " [" - > span.logger:before - content: "@" - > span.text:before - content: "] " - div.buttons +button-group display: flex margin: 1rem .5rem width: calc(100% - 1rem) + button.open-log + +button + +main-color-button + div.error +notification($error) margin: 1rem .5rem diff --git a/maubot/management/frontend/src/style/pages/log.sass b/maubot/management/frontend/src/style/pages/log.sass new file mode 100644 index 0000000..8b75dfe --- /dev/null +++ b/maubot/management/frontend/src/style/pages/log.sass @@ -0,0 +1,80 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +div.log + height: 100% + width: 100% + overflow: auto + + > div.lines + text-align: left + font-size: 12px + max-height: 100% + min-width: 100% + font-family: "Fira Code", monospace + display: table + + > div.row + display: table-row + white-space: pre + + &.debug + background-color: $background + + &:nth-child(odd) + background-color: $background-dark + + &.info + background-color: #AAFAFA + + &:nth-child(odd) + background-color: #66FAFA + + &.warning, &.warn + background-color: #FABB77 + + &:nth-child(odd) + background-color: #FAAA55 + + &.error + background-color: #FAAAAA + + &:nth-child(odd) + background-color: #FA9999 + + &.fatal + background-color: #CC44CC + + &:nth-child(odd) + background-color: #AA44AA + + &.unfocused + opacity: .25 + + > span + padding: .125rem .25rem + display: table-cell + + a + color: inherit + text-decoration: none + + &:hover + text-decoration: underline + + > span.text + > div.content > * + background-color: inherit !important + margin: 0 !important diff --git a/maubot/management/frontend/src/style/pages/modal.sass b/maubot/management/frontend/src/style/pages/modal.sass new file mode 100644 index 0000000..876e563 --- /dev/null +++ b/maubot/management/frontend/src/style/pages/modal.sass @@ -0,0 +1,71 @@ +// maubot - A plugin-based Matrix bot system. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +div.modal-wrapper-wrapper + z-index: 9001 + position: fixed + top: 0 + bottom: 0 + left: 0 + right: 0 + background-color: rgba(0, 0, 0, 0.5) + + --modal-margin: 2.5rem + --button-height: 0rem + + @media screen and (max-width: 45rem) + --modal-margin: 1rem + --button-height: 2.5rem + + @media screen and (max-width: 35rem) + --modal-margin: 0rem + --button-height: 3rem + + button.close + +button + + display: none + + width: 100% + height: var(--button-height) + border-radius: .25rem .25rem 0 0 + + @media screen and (max-width: 45rem) + display: block + @media screen and (max-width: 35rem) + border-radius: 0 + + div.modal-wrapper + width: calc(100% - 2 * var(--modal-margin)) + height: calc(100% - 2 * var(--modal-margin) - var(--button-height)) + margin: var(--modal-margin) + border-radius: .25rem + + @media screen and (max-width: 35rem) + border-radius: 0 + + div.modal + padding: 1rem + height: 100% + width: 100% + background-color: $background + box-sizing: border-box + border-radius: .25rem + + @media screen and (max-width: 45rem) + border-radius: 0 0 .25rem .25rem + @media screen and (max-width: 35rem) + border-radius: 0 + padding: .5rem diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock index a0489ab..f72ec5e 100644 --- a/maubot/management/frontend/yarn.lock +++ b/maubot/management/frontend/yarn.lock @@ -1770,7 +1770,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.6.1: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= @@ -1829,6 +1829,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base16@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70" + integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA= + base64-js@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" @@ -6303,11 +6308,21 @@ lodash.clonedeep@^4.3.2: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.curry@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170" + integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA= + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= +lodash.flow@^3.3.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" + integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -8366,6 +8381,11 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= +pure-color@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e" + integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4= + q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" @@ -8476,6 +8496,16 @@ react-app-polyfill@^0.1.3: raf "3.4.0" whatwg-fetch "3.0.0" +react-base16-styling@^0.5.1: + version "0.5.3" + resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.5.3.tgz#3858f24e9c4dd8cbd3f702f3f74d581ca2917269" + integrity sha1-OFjyTpxN2MvT9wLz901YHKKRcmk= + dependencies: + base16 "^1.0.0" + lodash.curry "^4.0.1" + lodash.flow "^3.3.0" + pure-color "^1.2.0" + react-dev-utils@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.5.tgz#6ef34d0a416dc1c97ac20025031ea1f0d819b21d" @@ -8526,6 +8556,15 @@ react-input-autosize@^2.2.1: dependencies: prop-types "^15.5.8" +react-json-tree@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/react-json-tree/-/react-json-tree-0.11.0.tgz#f5b17e83329a9c76ae38be5c04fda3a7fd684a35" + integrity sha1-9bF+gzKanHauOL5cBP2jp/1oSjU= + dependencies: + babel-runtime "^6.6.1" + prop-types "^15.5.8" + react-base16-styling "^0.5.1" + react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"