mirror of
https://github.com/maubot/maubot
synced 2025-08-29 08:50:38 +00:00
256 lines
8.9 KiB
JavaScript
256 lines
8.9 KiB
JavaScript
// 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 } from "react"
|
|
import { Route, Switch, Link, withRouter } from "react-router-dom"
|
|
import api from "../../api"
|
|
import { ReactComponent as Plus } from "../../res/plus.svg"
|
|
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) {
|
|
super(props)
|
|
this.state = {
|
|
instances: {},
|
|
clients: {},
|
|
plugins: {},
|
|
sidebarOpen: false,
|
|
modalOpen: false,
|
|
logFocus: null,
|
|
logLines: [],
|
|
}
|
|
this.logModal = {
|
|
open: () => undefined,
|
|
isOpen: () => false,
|
|
}
|
|
window.maubot = this
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.props.location !== prevProps.location) {
|
|
this.setState({ sidebarOpen: false })
|
|
}
|
|
}
|
|
|
|
async componentWillMount() {
|
|
const [instanceList, clientList, pluginList] = await Promise.all([
|
|
api.getInstances(), api.getClients(), api.getPlugins(),
|
|
api.updateDebugOpenFileEnabled()])
|
|
const instances = {}
|
|
if (api.getFeatures().instance) {
|
|
for (const instance of instanceList) {
|
|
instances[instance.id] = instance
|
|
}
|
|
}
|
|
const clients = {}
|
|
if (api.getFeatures().client) {
|
|
for (const client of clientList) {
|
|
clients[client.id] = client
|
|
}
|
|
}
|
|
const plugins = {}
|
|
if (api.getFeatures().plugin) {
|
|
for (const plugin of pluginList) {
|
|
plugins[plugin.id] = plugin
|
|
}
|
|
}
|
|
this.setState({ instances, clients, plugins })
|
|
|
|
await this.enableLogs()
|
|
}
|
|
|
|
async enableLogs() {
|
|
if (!api.getFeatures().log) {
|
|
return
|
|
}
|
|
|
|
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)}`
|
|
}
|
|
}
|
|
|
|
logs.onHistory = history => {
|
|
for (const data of history) {
|
|
processEntry(data)
|
|
}
|
|
this.setState({
|
|
logLines: history,
|
|
})
|
|
}
|
|
logs.onLog = data => {
|
|
processEntry(data)
|
|
this.setState({
|
|
logLines: this.state.logLines.concat(data),
|
|
})
|
|
}
|
|
}
|
|
|
|
renderList(field, type) {
|
|
return this.state[field] && Object.values(this.state[field]).map(entry =>
|
|
React.createElement(type, { key: entry.id, entry }))
|
|
}
|
|
|
|
delete(stateField, id) {
|
|
const data = Object.assign({}, this.state[stateField])
|
|
delete data[id]
|
|
this.setState({ [stateField]: data })
|
|
}
|
|
|
|
add(stateField, entry, oldID = undefined) {
|
|
const data = Object.assign({}, this.state[stateField])
|
|
if (oldID && oldID !== entry.id) {
|
|
delete data[oldID]
|
|
}
|
|
data[entry.id] = entry
|
|
this.setState({ [stateField]: data })
|
|
}
|
|
|
|
renderView(field, type, id) {
|
|
const typeName = field.slice(0, -1)
|
|
if (!api.getFeatures()[typeName]) {
|
|
return this.renderDisabled(typeName)
|
|
}
|
|
const entry = this.state[field][id]
|
|
if (!entry) {
|
|
return this.renderNotFound(typeName)
|
|
}
|
|
return React.createElement(type, {
|
|
entry,
|
|
onDelete: () => this.delete(field, id),
|
|
onChange: newEntry => this.add(field, newEntry, id),
|
|
openLog: this.openLog,
|
|
ctx: this.state,
|
|
})
|
|
}
|
|
|
|
openLog = filter => {
|
|
this.setState({
|
|
logFocus: typeof filter === "string" ? filter : null,
|
|
})
|
|
this.logModal.open()
|
|
}
|
|
|
|
renderNotFound = (thing = "path") => (
|
|
<div className="not-found">
|
|
Oops! I'm afraid that {thing} couldn't be found.
|
|
</div>
|
|
)
|
|
|
|
renderDisabled = (thing = "path") => (
|
|
<div className="not-found">
|
|
The {thing} API has been disabled in the maubot config.
|
|
</div>
|
|
)
|
|
|
|
renderMain() {
|
|
return <div className={`dashboard ${this.state.sidebarOpen ? "sidebar-open" : ""}`}>
|
|
<Link to="/" className="title">
|
|
<img src="favicon.png" alt=""/>
|
|
Maubot Manager
|
|
</Link>
|
|
<div className="user">
|
|
<span>{localStorage.username}</span>
|
|
</div>
|
|
|
|
<nav className="sidebar">
|
|
<div className="buttons">
|
|
{api.getFeatures().log && <button className="open-log" onClick={this.openLog}>
|
|
<span>View logs</span>
|
|
</button>}
|
|
</div>
|
|
{api.getFeatures().instance && <div className="instances list">
|
|
<div className="title">
|
|
<h2>Instances</h2>
|
|
<Link to="/new/instance"><Plus/></Link>
|
|
</div>
|
|
{this.renderList("instances", Instance.ListEntry)}
|
|
</div>}
|
|
{api.getFeatures().client && <div className="clients list">
|
|
<div className="title">
|
|
<h2>Clients</h2>
|
|
<Link to="/new/client"><Plus/></Link>
|
|
</div>
|
|
{this.renderList("clients", Client.ListEntry)}
|
|
</div>}
|
|
{api.getFeatures().plugin && <div className="plugins list">
|
|
<div className="title">
|
|
<h2>Plugins</h2>
|
|
{api.getFeatures().plugin_upload && <Link to="/new/plugin"><Plus/></Link>}
|
|
</div>
|
|
{this.renderList("plugins", Plugin.ListEntry)}
|
|
</div>}
|
|
</nav>
|
|
|
|
<div className="topbar"
|
|
onClick={evt => this.setState({ sidebarOpen: !this.state.sidebarOpen })}>
|
|
<div className={`hamburger ${this.state.sidebarOpen ? "active" : ""}`}>
|
|
<span/><span/><span/>
|
|
</div>
|
|
</div>
|
|
|
|
<main className="view">
|
|
<Switch>
|
|
<Route path="/" exact render={() => <Home openLog={this.openLog}/>}/>
|
|
<Route path="/new/instance" render={() =>
|
|
<Instance onChange={newEntry => this.add("instances", newEntry)}
|
|
entry={{}} ctx={this.state}/>}/>
|
|
<Route path="/new/client" render={() =>
|
|
<Client entry={{}} onChange={newEntry =>
|
|
this.add("clients", newEntry)}/>}/>
|
|
<Route path="/new/plugin" render={() =>
|
|
<Plugin entry={{}} onChange={newEntry =>
|
|
this.add("plugins", newEntry)}/>}/>
|
|
<Route path="/instance/:id" render={({ match }) =>
|
|
this.renderView("instances", Instance, match.params.id)}/>
|
|
<Route path="/client/:id" render={({ match }) =>
|
|
this.renderView("clients", Client, match.params.id)}/>
|
|
<Route path="/plugin/:id" render={({ match }) =>
|
|
this.renderView("plugins", Plugin, match.params.id)}/>
|
|
<Route render={() => this.renderNotFound()}/>
|
|
</Switch>
|
|
</main>
|
|
</div>
|
|
}
|
|
|
|
renderModal() {
|
|
return <Modal ref={ref => this.logModal = ref}>
|
|
<Log lines={this.state.logLines} focus={this.state.logFocus}/>
|
|
</Modal>
|
|
}
|
|
|
|
render() {
|
|
return <>
|
|
{this.renderMain()}
|
|
{this.renderModal()}
|
|
</>
|
|
}
|
|
}
|
|
|
|
export default withRouter(Dashboard)
|