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
This commit is contained in:
parent
aac99b7ee4
commit
32b60fa0ff
20 changed files with 545 additions and 82 deletions
|
@ -26,7 +26,7 @@ from .server import MaubotServer
|
||||||
from .client import Client, init as init_client_class
|
from .client import Client, init as init_client_class
|
||||||
from .loader.zip import init as init_zip_loader
|
from .loader.zip import init as init_zip_loader
|
||||||
from .instance import init as init_plugin_instance_class
|
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__
|
from .__meta__ import __version__
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.",
|
parser = argparse.ArgumentParser(description="A plugin-based Matrix bot system.",
|
||||||
|
@ -43,6 +43,7 @@ config.load()
|
||||||
config.update()
|
config.update()
|
||||||
|
|
||||||
logging.config.dictConfig(copy.deepcopy(config["logging"]))
|
logging.config.dictConfig(copy.deepcopy(config["logging"]))
|
||||||
|
init_log_listener()
|
||||||
log = logging.getLogger("maubot.init")
|
log = logging.getLogger("maubot.init")
|
||||||
log.info(f"Initializing maubot {__version__}")
|
log.info(f"Initializing maubot {__version__}")
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ init_zip_loader(config)
|
||||||
db_session = init_db(config)
|
db_session = init_db(config)
|
||||||
clients = init_client_class(db_session, loop)
|
clients = init_client_class(db_session, loop)
|
||||||
plugins = init_plugin_instance_class(db_session, config, 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 = MaubotServer(config, loop)
|
||||||
server.app.add_subapp(config["server.base_path"], management_api)
|
server.app.add_subapp(config["server.base_path"], management_api)
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ except KeyboardInterrupt:
|
||||||
loop=loop))
|
loop=loop))
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
log.debug("Closing websockets")
|
log.debug("Closing websockets")
|
||||||
loop.run_until_complete(stop_management_api())
|
loop.run_until_complete(stop_mgmt_api())
|
||||||
log.debug("Stopping server")
|
log.debug("Stopping server")
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(asyncio.wait_for(server.stop(), 5, loop=loop))
|
loop.run_until_complete(asyncio.wait_for(server.stop(), 5, loop=loop))
|
||||||
|
|
|
@ -31,7 +31,7 @@ from .client import Client
|
||||||
from .loader import PluginLoader
|
from .loader import PluginLoader
|
||||||
from .plugin_base import Plugin
|
from .plugin_base import Plugin
|
||||||
|
|
||||||
log = logging.getLogger("maubot.plugin")
|
log = logging.getLogger("maubot.instance")
|
||||||
|
|
||||||
yaml = YAML()
|
yaml = YAML()
|
||||||
yaml.indent(4)
|
yaml.indent(4)
|
||||||
|
@ -54,7 +54,7 @@ class PluginInstance:
|
||||||
|
|
||||||
def __init__(self, db_instance: DBPlugin):
|
def __init__(self, db_instance: DBPlugin):
|
||||||
self.db_instance = db_instance
|
self.db_instance = db_instance
|
||||||
self.log = logging.getLogger(f"maubot.plugin.{self.id}")
|
self.log = log.getChild(self.id)
|
||||||
self.config = None
|
self.config = None
|
||||||
self.started = False
|
self.started = False
|
||||||
self.loader = None
|
self.loader = None
|
||||||
|
|
|
@ -23,7 +23,8 @@ from .auth import web as _
|
||||||
from .plugin import web as _
|
from .plugin import web as _
|
||||||
from .instance import web as _
|
from .instance import web as _
|
||||||
from .client 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:
|
def init(cfg: Config, loop: AbstractEventLoop) -> web.Application:
|
||||||
|
|
64
maubot/management/api/dev_open.py
Normal file
64
maubot/management/api/dev_open.py
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
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
|
|
@ -13,13 +13,15 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from typing import Deque, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from collections import deque
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .base import routes, get_loop
|
from .base import routes, get_loop, get_config
|
||||||
from .auth import is_valid_token
|
from .auth import is_valid_token
|
||||||
|
|
||||||
BUILTIN_ATTRS = {"args", "asctime", "created", "exc_info", "exc_text", "filename", "funcName",
|
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",
|
INCLUDE_ATTRS = {"filename", "funcName", "levelname", "levelno", "lineno", "module", "name",
|
||||||
"pathname"}
|
"pathname"}
|
||||||
EXCLUDE_ATTRS = BUILTIN_ATTRS - INCLUDE_ATTRS
|
EXCLUDE_ATTRS = BUILTIN_ATTRS - INCLUDE_ATTRS
|
||||||
|
MAX_LINES = 2048
|
||||||
|
|
||||||
|
|
||||||
class WebSocketHandler(logging.Handler):
|
class LogCollector(logging.Handler):
|
||||||
def __init__(self, ws, level=logging.NOTSET) -> None:
|
lines: Deque[dict]
|
||||||
|
formatter: logging.Formatter
|
||||||
|
listeners: List[web.WebSocketResponse]
|
||||||
|
|
||||||
|
def __init__(self, level=logging.NOTSET) -> None:
|
||||||
super().__init__(level)
|
super().__init__(level)
|
||||||
self.ws = ws
|
self.lines = deque(maxlen=MAX_LINES)
|
||||||
self.formatter = logging.Formatter()
|
self.formatter = logging.Formatter()
|
||||||
|
self.listeners = []
|
||||||
|
|
||||||
def emit(self, record: logging.LogRecord) -> None:
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
try:
|
try:
|
||||||
|
@ -51,9 +59,9 @@ class WebSocketHandler(logging.Handler):
|
||||||
for name, value in record.__dict__.items()
|
for name, value in record.__dict__.items()
|
||||||
if name not in EXCLUDE_ATTRS
|
if name not in EXCLUDE_ATTRS
|
||||||
}
|
}
|
||||||
content["id"] = record.relativeCreated
|
content["id"] = str(record.relativeCreated)
|
||||||
content["msg"] = record.getMessage()
|
content["msg"] = record.getMessage()
|
||||||
content["time"] = datetime.utcnow()
|
content["time"] = datetime.fromtimestamp(record.created)
|
||||||
|
|
||||||
if record.exc_info:
|
if record.exc_info:
|
||||||
content["exc_info"] = self.formatter.formatException(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():
|
for name, value in content.items():
|
||||||
if isinstance(value, datetime):
|
if isinstance(value, datetime):
|
||||||
content[name] = value.astimezone().isoformat()
|
content[name] = value.astimezone().isoformat()
|
||||||
|
asyncio.ensure_future(self.send(content))
|
||||||
asyncio.ensure_future(self.send(content), loop=get_loop())
|
self.lines.append(content)
|
||||||
|
|
||||||
async def send(self, record: dict) -> None:
|
async def send(self, record: dict) -> None:
|
||||||
|
for ws in self.listeners:
|
||||||
try:
|
try:
|
||||||
await self.ws.send_json(record)
|
await ws.send_json(record)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Log sending error:", e)
|
print("Log sending error:", e)
|
||||||
|
|
||||||
|
|
||||||
|
handler = LogCollector()
|
||||||
log_root = logging.getLogger("maubot")
|
log_root = logging.getLogger("maubot")
|
||||||
log = logging.getLogger("maubot.server.websocket")
|
log = logging.getLogger("maubot.server.websocket")
|
||||||
sockets = []
|
sockets = []
|
||||||
|
|
||||||
|
|
||||||
|
def init() -> None:
|
||||||
|
log_root.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
async def stop_all() -> None:
|
async def stop_all() -> None:
|
||||||
|
log_root.removeHandler(handler)
|
||||||
for socket in sockets:
|
for socket in sockets:
|
||||||
try:
|
try:
|
||||||
await socket.close(code=1012)
|
await socket.close(code=1012)
|
||||||
|
@ -90,7 +105,6 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse:
|
||||||
await ws.prepare(request)
|
await ws.prepare(request)
|
||||||
sockets.append(ws)
|
sockets.append(ws)
|
||||||
log.debug(f"Connection from {request.remote} opened")
|
log.debug(f"Connection from {request.remote} opened")
|
||||||
handler = WebSocketHandler(ws)
|
|
||||||
authenticated = False
|
authenticated = False
|
||||||
|
|
||||||
async def close_if_not_authenticated():
|
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:
|
if msg.type != web.WSMsgType.TEXT:
|
||||||
continue
|
continue
|
||||||
if is_valid_token(msg.data):
|
if is_valid_token(msg.data):
|
||||||
|
await ws.send_json({"auth_success": True})
|
||||||
|
await ws.send_json({"history": list(handler.lines)})
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
log.debug(f"Connection from {request.remote} authenticated")
|
log.debug(f"Connection from {request.remote} authenticated")
|
||||||
log_root.addHandler(handler)
|
handler.listeners.append(ws)
|
||||||
authenticated = True
|
authenticated = True
|
||||||
await ws.send_json({"auth_success": True})
|
|
||||||
elif not authenticated:
|
elif not authenticated:
|
||||||
await ws.send_json({"auth_success": False})
|
await ws.send_json({"auth_success": False})
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -118,7 +133,7 @@ async def log_websocket(request: web.Request) -> web.WebSocketResponse:
|
||||||
await ws.close()
|
await ws.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
log_root.removeHandler(handler)
|
handler.listeners.remove(ws)
|
||||||
log.debug(f"Connection from {request.remote} closed")
|
log.debug(f"Connection from {request.remote} closed")
|
||||||
sockets.remove(ws)
|
sockets.remove(ws)
|
||||||
return ws
|
return ws
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"react": "^16.6.0",
|
"react": "^16.6.0",
|
||||||
"react-ace": "^6.2.0",
|
"react-ace": "^6.2.0",
|
||||||
"react-dom": "^16.6.0",
|
"react-dom": "^16.6.0",
|
||||||
|
"react-json-tree": "^0.11.0",
|
||||||
"react-router-dom": "^4.3.1",
|
"react-router-dom": "^4.3.1",
|
||||||
"react-scripts": "2.0.5",
|
"react-scripts": "2.0.5",
|
||||||
"react-select": "^2.1.1"
|
"react-select": "^2.1.1"
|
||||||
|
|
|
@ -82,7 +82,8 @@ export async function openLogSocket() {
|
||||||
socket: null,
|
socket: null,
|
||||||
connected: false,
|
connected: false,
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
onLog: data => {},
|
onLog: data => undefined,
|
||||||
|
onHistory: history => undefined,
|
||||||
fails: -1,
|
fails: -1,
|
||||||
}
|
}
|
||||||
const openHandler = () => {
|
const openHandler = () => {
|
||||||
|
@ -100,9 +101,9 @@ export async function openLogSocket() {
|
||||||
} else {
|
} else {
|
||||||
console.info("Websocket connection authentication failed")
|
console.info("Websocket connection authentication failed")
|
||||||
}
|
}
|
||||||
|
} else if (data.history) {
|
||||||
|
wrapper.onHistory(data.history)
|
||||||
} else {
|
} else {
|
||||||
data.time = new Date(data.time)
|
|
||||||
console.log("SERVLOG", data)
|
|
||||||
wrapper.onLog(data)
|
wrapper.onLog(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,6 +132,21 @@ export async function openLogSocket() {
|
||||||
return wrapper
|
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 getInstances = () => defaultGet("/instances")
|
||||||
export const getInstance = id => defaultGet(`/instance/${id}`)
|
export const getInstance = id => defaultGet(`/instance/${id}`)
|
||||||
export const putInstance = (instance, id) => defaultPut("instance", instance, id)
|
export const putInstance = (instance, id) => defaultPut("instance", instance, id)
|
||||||
|
@ -179,7 +195,7 @@ export const deleteClient = id => defaultDelete("client", id)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
BASE_PATH,
|
BASE_PATH,
|
||||||
login, ping, openLogSocket,
|
login, ping, openLogSocket, debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled,
|
||||||
getInstances, getInstance, putInstance, deleteInstance,
|
getInstances, getInstance, putInstance, deleteInstance,
|
||||||
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
||||||
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
|
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { Component } from "react"
|
import React, { Component } from "react"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import Log from "./Log"
|
|
||||||
|
|
||||||
class BaseMainView extends Component {
|
class BaseMainView extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -65,7 +64,9 @@ class BaseMainView extends Component {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
renderLog = () => !this.isNew && <Log showName={false} lines={this.props.log}/>
|
renderLogButton = (filter) => !this.isNew && <div className="buttons">
|
||||||
|
<button className="open-log" onClick={() => this.props.openLog(filter)}>View logs</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseMainView
|
export default BaseMainView
|
||||||
|
|
|
@ -196,6 +196,7 @@ class Client extends BaseMainView {
|
||||||
{this.state.saving ? <Spinner/> : (this.isNew ? "Create" : "Save")}
|
{this.state.saving ? <Spinner/> : (this.isNew ? "Create" : "Save")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{this.renderLogButton(this.state.id)}
|
||||||
<div className="error">{this.state.error}</div>
|
<div className="error">{this.state.error}</div>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
@ -209,7 +210,6 @@ class Client extends BaseMainView {
|
||||||
{this.renderInstances()}
|
{this.renderInstances()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.renderLog()}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { Component } from "react"
|
import React, { Component } from "react"
|
||||||
import Log from "./Log"
|
|
||||||
|
|
||||||
class Home extends Component {
|
class Home extends Component {
|
||||||
render() {
|
render() {
|
||||||
|
@ -22,7 +21,9 @@ class Home extends Component {
|
||||||
<div className="home">
|
<div className="home">
|
||||||
See sidebar to get started
|
See sidebar to get started
|
||||||
</div>
|
</div>
|
||||||
<Log lines={this.props.log}/>
|
<div className="buttons">
|
||||||
|
<button className="open-log" onClick={this.props.openLog}>View logs</button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,8 +167,8 @@ class Instance extends BaseMainView {
|
||||||
{this.state.saving ? <Spinner/> : (this.isNew ? "Create" : "Save")}
|
{this.state.saving ? <Spinner/> : (this.isNew ? "Create" : "Save")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{this.renderLogButton(`instance.${this.state.id}`)}
|
||||||
<div className="error">{this.state.error}</div>
|
<div className="error">{this.state.error}</div>
|
||||||
{this.renderLog()}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,20 +13,116 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
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 }) => <div className="log">
|
class LogEntry extends PureComponent {
|
||||||
{lines.map(data => <>
|
static contextType = Modal.Context
|
||||||
<div className="row" key={data.id}>
|
|
||||||
<span className="time">{data.time.toLocaleTimeString()}</span>
|
renderName() {
|
||||||
<span className="level">{data.levelname}</span>
|
const line = this.props.line
|
||||||
{showName && <span className="logger">{data.name}</span>}
|
if (line.nameLink) {
|
||||||
<span className="text">{data.msg}</span>
|
const modal = this.context
|
||||||
|
return (
|
||||||
|
<Link to={line.nameLink} onClick={modal.close}>
|
||||||
|
{line.name}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return line.name
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
if (this.props.line.matrix_http_request) {
|
||||||
|
const req = this.props.line.matrix_http_request
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{req.method} {req.path}
|
||||||
|
<div className="content">
|
||||||
|
{Object.entries(req.content).length > 0
|
||||||
|
&& <JSONTree data={{ content: req.content }} hideRoot={true}/>}
|
||||||
</div>
|
</div>
|
||||||
{data.exc_info && <div className="row exception" key={data.id + "-exc"}>
|
</>
|
||||||
{data.exc_info.replace(/\\n/g, "\n")}
|
}
|
||||||
</div>}
|
return this.props.line.msg
|
||||||
</>)}
|
}
|
||||||
</div>
|
|
||||||
|
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 (
|
||||||
|
<div className={`row ${this.props.line.levelname.toLowerCase()} ${this.unfocused}`}>
|
||||||
|
<span className="time">{this.renderTime()}</span>
|
||||||
|
<span className="level">{this.renderLevelName()}</span>
|
||||||
|
<span className="logger">{this.renderName()}</span>
|
||||||
|
<span className="text">{content}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
<a href={"#/debugOpenFile"} onClick={() => {
|
||||||
|
api.debugOpenFile(file, line)
|
||||||
|
return false
|
||||||
|
}}>File "{file}", line {line}, in {method}</a>,
|
||||||
|
)
|
||||||
|
return "||EDGE||"
|
||||||
|
})
|
||||||
|
fileLinks.reverse()
|
||||||
|
|
||||||
|
const result = []
|
||||||
|
let key = 0
|
||||||
|
for (const part of str.split("||EDGE||")) {
|
||||||
|
result.push(<React.Fragment key={key++}>
|
||||||
|
{part}
|
||||||
|
{fileLinks.pop()}
|
||||||
|
</React.Fragment>)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <>
|
||||||
|
{this.renderRow(this.renderContent())}
|
||||||
|
{this.props.line.exc_info && this.renderRow(this.renderExceptionInfo())}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Log extends PureComponent {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="log">
|
||||||
|
<div className="lines">
|
||||||
|
{this.props.lines.map(data => <LogEntry key={data.id} line={data}
|
||||||
|
focus={this.props.focus}/>)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default Log
|
export default Log
|
||||||
|
|
51
maubot/management/frontend/src/pages/dashboard/Modal.js
Normal file
51
maubot/management/frontend/src/pages/dashboard/Modal.js
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
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 && (
|
||||||
|
<div className="modal-wrapper-wrapper" ref={ref => this.wrapper = ref}
|
||||||
|
onClick={() => this.wrapper.clientWidth > 45 * rem && this.close()}>
|
||||||
|
<div className="modal-wrapper" onClick={evt => evt.stopPropagation()}>
|
||||||
|
<button className="close" onClick={this.close}>Close</button>
|
||||||
|
<div className="modal">
|
||||||
|
<Modal.Context.Provider value={this}>
|
||||||
|
{this.props.children}
|
||||||
|
</Modal.Context.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal
|
|
@ -93,9 +93,9 @@ class Plugin extends BaseMainView {
|
||||||
{this.state.deleting ? <Spinner/> : "Delete"}
|
{this.state.deleting ? <Spinner/> : "Delete"}
|
||||||
</button>
|
</button>
|
||||||
</div>}
|
</div>}
|
||||||
|
{this.renderLogButton("loader.zip")}
|
||||||
<div className="error">{this.state.error}</div>
|
<div className="error">{this.state.error}</div>
|
||||||
{this.renderInstances()}
|
{this.renderInstances()}
|
||||||
{this.renderLog()}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import Instance from "./Instance"
|
||||||
import Client from "./Client"
|
import Client from "./Client"
|
||||||
import Plugin from "./Plugin"
|
import Plugin from "./Plugin"
|
||||||
import Home from "./Home"
|
import Home from "./Home"
|
||||||
|
import Log from "./Log"
|
||||||
|
import Modal from "./Modal"
|
||||||
|
|
||||||
class Dashboard extends Component {
|
class Dashboard extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -30,9 +32,14 @@ class Dashboard extends Component {
|
||||||
clients: {},
|
clients: {},
|
||||||
plugins: {},
|
plugins: {},
|
||||||
sidebarOpen: false,
|
sidebarOpen: false,
|
||||||
|
modalOpen: false,
|
||||||
|
logFocus: "",
|
||||||
}
|
}
|
||||||
this.logLines = []
|
this.logLines = []
|
||||||
this.logMap = {}
|
this.logMap = {}
|
||||||
|
this.logModal = {
|
||||||
|
open: () => undefined,
|
||||||
|
}
|
||||||
window.maubot = this
|
window.maubot = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +51,8 @@ class Dashboard extends Component {
|
||||||
|
|
||||||
async componentWillMount() {
|
async componentWillMount() {
|
||||||
const [instanceList, clientList, pluginList] = await Promise.all([
|
const [instanceList, clientList, pluginList] = await Promise.all([
|
||||||
api.getInstances(), api.getClients(), api.getPlugins()])
|
api.getInstances(), api.getClients(), api.getPlugins(),
|
||||||
|
api.updateDebugOpenFileEnabled()])
|
||||||
const instances = {}
|
const instances = {}
|
||||||
for (const instance of instanceList) {
|
for (const instance of instanceList) {
|
||||||
instances[instance.id] = instance
|
instances[instance.id] = instance
|
||||||
|
@ -60,10 +68,32 @@ class Dashboard extends Component {
|
||||||
this.setState({ instances, clients, plugins })
|
this.setState({ instances, clients, plugins })
|
||||||
|
|
||||||
const logs = await api.openLogSocket()
|
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 => {
|
logs.onLog = data => {
|
||||||
|
processEntry(data)
|
||||||
this.logLines.push(data)
|
this.logLines.push(data)
|
||||||
;(this.logMap[data.name] || (this.logMap[data.name] = [])).push(data)
|
this.setState({ logFocus: this.state.logFocus })
|
||||||
this.setState({})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,28 +117,22 @@ class Dashboard extends Component {
|
||||||
this.setState({ [stateField]: data })
|
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) {
|
renderView(field, type, id) {
|
||||||
const entry = this.state[field][id]
|
const entry = this.state[field][id]
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
return this.renderNotFound(field.slice(0, -1))
|
return this.renderNotFound(field.slice(0, -1))
|
||||||
}
|
}
|
||||||
console.log(`maubot.${field.slice(0, -1)}.${id}`)
|
|
||||||
return React.createElement(type, {
|
return React.createElement(type, {
|
||||||
entry,
|
entry,
|
||||||
onDelete: () => this.delete(field, id),
|
onDelete: () => this.delete(field, id),
|
||||||
onChange: newEntry => this.add(field, newEntry, id),
|
onChange: newEntry => this.add(field, newEntry, id),
|
||||||
|
openLog: filter => {
|
||||||
|
this.setState({
|
||||||
|
logFocus: filter,
|
||||||
|
})
|
||||||
|
this.logModal.open()
|
||||||
|
},
|
||||||
ctx: this.state,
|
ctx: this.state,
|
||||||
log: this.getLog(field, id) || [],
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +142,7 @@ class Dashboard extends Component {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
render() {
|
renderMain() {
|
||||||
return <div className={`dashboard ${this.state.sidebarOpen ? "sidebar-open" : ""}`}>
|
return <div className={`dashboard ${this.state.sidebarOpen ? "sidebar-open" : ""}`}>
|
||||||
<Link to="/" className="title">
|
<Link to="/" className="title">
|
||||||
<img src="/favicon.png" alt=""/>
|
<img src="/favicon.png" alt=""/>
|
||||||
|
@ -161,7 +185,7 @@ class Dashboard extends Component {
|
||||||
|
|
||||||
<main className="view">
|
<main className="view">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" exact render={() => <Home log={this.logLines}/>}/>
|
<Route path="/" exact render={() => <Home openLog={this.logModal.open}/>}/>
|
||||||
<Route path="/new/instance" render={() =>
|
<Route path="/new/instance" render={() =>
|
||||||
<Instance onChange={newEntry => this.add("instances", newEntry)}
|
<Instance onChange={newEntry => this.add("instances", newEntry)}
|
||||||
ctx={this.state}/>}/>
|
ctx={this.state}/>}/>
|
||||||
|
@ -180,6 +204,19 @@ class Dashboard extends Component {
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderModal() {
|
||||||
|
return <Modal ref={ref => this.logModal = ref}>
|
||||||
|
<Log lines={this.logLines} focus={this.state.logFocus}/>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <>
|
||||||
|
{this.renderMain()}
|
||||||
|
{this.renderModal()}
|
||||||
|
</>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Dashboard)
|
export default withRouter(Dashboard)
|
||||||
|
|
|
@ -27,3 +27,5 @@
|
||||||
|
|
||||||
@import pages/login
|
@import pages/login
|
||||||
@import pages/dashboard
|
@import pages/dashboard
|
||||||
|
@import pages/modal
|
||||||
|
@import pages/log
|
||||||
|
|
|
@ -89,29 +89,16 @@
|
||||||
margin-top: 5rem
|
margin-top: 5rem
|
||||||
font-size: 1.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
|
div.buttons
|
||||||
+button-group
|
+button-group
|
||||||
display: flex
|
display: flex
|
||||||
margin: 1rem .5rem
|
margin: 1rem .5rem
|
||||||
width: calc(100% - 1rem)
|
width: calc(100% - 1rem)
|
||||||
|
|
||||||
|
button.open-log
|
||||||
|
+button
|
||||||
|
+main-color-button
|
||||||
|
|
||||||
div.error
|
div.error
|
||||||
+notification($error)
|
+notification($error)
|
||||||
margin: 1rem .5rem
|
margin: 1rem .5rem
|
||||||
|
|
80
maubot/management/frontend/src/style/pages/log.sass
Normal file
80
maubot/management/frontend/src/style/pages/log.sass
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
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
|
71
maubot/management/frontend/src/style/pages/modal.sass
Normal file
71
maubot/management/frontend/src/style/pages/modal.sass
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
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
|
|
@ -1770,7 +1770,7 @@ babel-register@^6.26.0:
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^0.5.1"
|
||||||
source-map-support "^0.4.15"
|
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"
|
version "6.26.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
|
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
|
||||||
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
|
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"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
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:
|
base64-js@^1.0.2:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||||
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
|
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:
|
lodash.debounce@^4.0.8:
|
||||||
version "4.0.8"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
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:
|
lodash.get@^4.4.2:
|
||||||
version "4.4.2"
|
version "4.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
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"
|
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
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:
|
q@^1.1.2:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
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"
|
raf "3.4.0"
|
||||||
whatwg-fetch "3.0.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:
|
react-dev-utils@^6.0.5:
|
||||||
version "6.0.5"
|
version "6.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.0.5.tgz#6ef34d0a416dc1c97ac20025031ea1f0d819b21d"
|
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:
|
dependencies:
|
||||||
prop-types "^15.5.8"
|
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:
|
react-lifecycles-compat@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||||
|
|
Loading…
Reference in a new issue