Add beginning of database explorer
This commit is contained in:
parent
0a39c1365d
commit
dc22f35d08
@ -30,7 +30,7 @@ from mautrix.types import UserID
|
|||||||
from .db import DBPlugin
|
from .db import DBPlugin
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .client import Client
|
from .client import Client
|
||||||
from .loader import PluginLoader
|
from .loader import PluginLoader, ZippedPluginLoader
|
||||||
from .plugin_base import Plugin
|
from .plugin_base import Plugin
|
||||||
|
|
||||||
log = logging.getLogger("maubot.instance")
|
log = logging.getLogger("maubot.instance")
|
||||||
@ -52,6 +52,8 @@ class PluginInstance:
|
|||||||
plugin: Plugin
|
plugin: Plugin
|
||||||
config: BaseProxyConfig
|
config: BaseProxyConfig
|
||||||
base_cfg: RecursiveDict[CommentedMap]
|
base_cfg: RecursiveDict[CommentedMap]
|
||||||
|
inst_db: sql.engine.Engine
|
||||||
|
inst_db_tables: Dict[str, sql.Table]
|
||||||
started: bool
|
started: bool
|
||||||
|
|
||||||
def __init__(self, db_instance: DBPlugin):
|
def __init__(self, db_instance: DBPlugin):
|
||||||
@ -62,6 +64,8 @@ class PluginInstance:
|
|||||||
self.loader = None
|
self.loader = None
|
||||||
self.client = None
|
self.client = None
|
||||||
self.plugin = None
|
self.plugin = None
|
||||||
|
self.inst_db = None
|
||||||
|
self.inst_db_tables = None
|
||||||
self.base_cfg = None
|
self.base_cfg = None
|
||||||
self.cache[self.id] = self
|
self.cache[self.id] = self
|
||||||
|
|
||||||
@ -73,8 +77,16 @@ class PluginInstance:
|
|||||||
"started": self.started,
|
"started": self.started,
|
||||||
"primary_user": self.primary_user,
|
"primary_user": self.primary_user,
|
||||||
"config": self.db_instance.config,
|
"config": self.db_instance.config,
|
||||||
|
"database": self.inst_db is not None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_db_tables(self) -> Dict[str, sql.Table]:
|
||||||
|
if not self.inst_db_tables:
|
||||||
|
metadata = sql.MetaData()
|
||||||
|
metadata.reflect(self.inst_db)
|
||||||
|
self.inst_db_tables = metadata.tables
|
||||||
|
return self.inst_db_tables
|
||||||
|
|
||||||
def load(self) -> bool:
|
def load(self) -> bool:
|
||||||
if not self.loader:
|
if not self.loader:
|
||||||
try:
|
try:
|
||||||
@ -89,6 +101,9 @@ class PluginInstance:
|
|||||||
self.log.error(f"Failed to get client for user {self.primary_user}")
|
self.log.error(f"Failed to get client for user {self.primary_user}")
|
||||||
self.db_instance.enabled = False
|
self.db_instance.enabled = False
|
||||||
return False
|
return False
|
||||||
|
if self.loader.meta.database:
|
||||||
|
db_path = os.path.join(self.mb_config["plugin_directories.db"], self.id)
|
||||||
|
self.inst_db = sql.create_engine(f"sqlite:///{db_path}.db")
|
||||||
self.log.debug("Plugin instance dependencies loaded")
|
self.log.debug("Plugin instance dependencies loaded")
|
||||||
self.loader.references.add(self)
|
self.loader.references.add(self)
|
||||||
self.client.references.add(self)
|
self.client.references.add(self)
|
||||||
@ -105,7 +120,11 @@ class PluginInstance:
|
|||||||
pass
|
pass
|
||||||
self.db.delete(self.db_instance)
|
self.db.delete(self.db_instance)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
# TODO delete plugin db
|
if self.inst_db:
|
||||||
|
self.inst_db.dispose()
|
||||||
|
ZippedPluginLoader.trash(
|
||||||
|
os.path.join(self.mb_config["plugin_directories.db"], f"{self.id}.db"),
|
||||||
|
reason="deleted")
|
||||||
|
|
||||||
def load_config(self) -> CommentedMap:
|
def load_config(self) -> CommentedMap:
|
||||||
return yaml.load(self.db_instance.config)
|
return yaml.load(self.db_instance.config)
|
||||||
@ -135,12 +154,9 @@ class PluginInstance:
|
|||||||
except (FileNotFoundError, KeyError):
|
except (FileNotFoundError, KeyError):
|
||||||
self.base_cfg = None
|
self.base_cfg = None
|
||||||
self.config = config_class(self.load_config, lambda: self.base_cfg, self.save_config)
|
self.config = config_class(self.load_config, lambda: self.base_cfg, self.save_config)
|
||||||
db = None
|
|
||||||
if self.loader.meta.database:
|
|
||||||
db_path = os.path.join(self.mb_config["plugin_directories.db"], self.id)
|
|
||||||
db = sql.create_engine(f"sqlite:///{db_path}.db")
|
|
||||||
self.plugin = cls(client=self.client.client, loop=self.loop, http=self.client.http_client,
|
self.plugin = cls(client=self.client.client, loop=self.loop, http=self.client.http_client,
|
||||||
instance_id=self.id, log=self.log, config=self.config, database=db)
|
instance_id=self.id, log=self.log, config=self.config,
|
||||||
|
database=self.inst_db)
|
||||||
try:
|
try:
|
||||||
await self.plugin.start()
|
await self.plugin.start()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -148,6 +164,7 @@ class PluginInstance:
|
|||||||
self.db_instance.enabled = False
|
self.db_instance.enabled = False
|
||||||
return
|
return
|
||||||
self.started = True
|
self.started = True
|
||||||
|
self.inst_db_tables = None
|
||||||
self.log.info(f"Started instance of {self.loader.meta.id} v{self.loader.meta.version} "
|
self.log.info(f"Started instance of {self.loader.meta.id} v{self.loader.meta.version} "
|
||||||
f"with user {self.client.id}")
|
f"with user {self.client.id}")
|
||||||
|
|
||||||
@ -162,6 +179,7 @@ class PluginInstance:
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Failed to stop instance")
|
self.log.exception("Failed to stop instance")
|
||||||
self.plugin = None
|
self.plugin = None
|
||||||
|
self.inst_db_tables = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, instance_id: str, db_instance: Optional[DBPlugin] = None
|
def get(cls, instance_id: str, db_instance: Optional[DBPlugin] = None
|
||||||
|
@ -19,13 +19,7 @@ from asyncio import AbstractEventLoop
|
|||||||
from ...config import Config
|
from ...config import Config
|
||||||
from .base import routes, set_config, set_loop
|
from .base import routes, set_config, set_loop
|
||||||
from .middleware import auth, error
|
from .middleware import auth, error
|
||||||
from .auth import web as _
|
from . import auth, plugin, instance, database, client, client_proxy, client_auth, dev_open
|
||||||
from .plugin import web as _
|
|
||||||
from .instance import web as _
|
|
||||||
from .client import web as _
|
|
||||||
from .client_proxy import web as _
|
|
||||||
from .client_auth import web as _
|
|
||||||
from .dev_open import web as _
|
|
||||||
from .log import stop_all as stop_log_sockets, init as init_log_listener
|
from .log import stop_all as stop_log_sockets, init as init_log_listener
|
||||||
|
|
||||||
|
|
||||||
|
61
maubot/management/api/database.py
Normal file
61
maubot/management/api/database.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# 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 typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
from sqlalchemy import Table, Column
|
||||||
|
|
||||||
|
from ...instance import PluginInstance
|
||||||
|
from .base import routes
|
||||||
|
from .responses import resp
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/instance/{id}/database")
|
||||||
|
async def get_database(request: web.Request) -> web.Response:
|
||||||
|
instance_id = request.match_info.get("id", "")
|
||||||
|
instance = PluginInstance.get(instance_id, None)
|
||||||
|
if not instance:
|
||||||
|
return resp.instance_not_found
|
||||||
|
elif not instance.inst_db:
|
||||||
|
return resp.plugin_has_no_database
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
table: Table
|
||||||
|
column: Column
|
||||||
|
return web.json_response({
|
||||||
|
table.name: {
|
||||||
|
"columns": {
|
||||||
|
column.name: {
|
||||||
|
"type": str(column.type),
|
||||||
|
"unique": column.unique or False,
|
||||||
|
"default": column.default,
|
||||||
|
"nullable": column.nullable,
|
||||||
|
"primary": column.primary_key,
|
||||||
|
"autoincrement": column.autoincrement,
|
||||||
|
} for column in table.columns
|
||||||
|
},
|
||||||
|
} for table in instance.get_db_tables().values()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/instance/{id}/database/{table}")
|
||||||
|
async def get_table(request: web.Request) -> web.Response:
|
||||||
|
instance_id = request.match_info.get("id", "")
|
||||||
|
instance = PluginInstance.get(instance_id, None)
|
||||||
|
if not instance:
|
||||||
|
return resp.instance_not_found
|
||||||
|
elif not instance.inst_db:
|
||||||
|
return resp.plugin_has_no_database
|
||||||
|
tables = instance.get_db_tables()
|
@ -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/>.
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
|
@ -152,6 +152,13 @@ class _Response:
|
|||||||
"errcode": "server_not_found",
|
"errcode": "server_not_found",
|
||||||
}, status=HTTPStatus.NOT_FOUND)
|
}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin_has_no_database(self) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "Given plugin does not have a database",
|
||||||
|
"errcode": "plugin_has_no_database",
|
||||||
|
})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def method_not_allowed(self) -> web.Response:
|
def method_not_allowed(self) -> web.Response:
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
|
@ -153,6 +153,8 @@ 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)
|
||||||
export const deleteInstance = id => defaultDelete("instance", id)
|
export const deleteInstance = id => defaultDelete("instance", id)
|
||||||
|
|
||||||
|
export const getInstanceDatabase = id => defaultGet(`/instance/${id}/database`)
|
||||||
|
|
||||||
export const getPlugins = () => defaultGet("/plugins")
|
export const getPlugins = () => defaultGet("/plugins")
|
||||||
export const getPlugin = id => defaultGet(`/plugin/${id}`)
|
export const getPlugin = id => defaultGet(`/plugin/${id}`)
|
||||||
export const deletePlugin = id => defaultDelete("plugin", id)
|
export const deletePlugin = id => defaultDelete("plugin", id)
|
||||||
@ -203,6 +205,7 @@ export default {
|
|||||||
BASE_PATH,
|
BASE_PATH,
|
||||||
login, ping, openLogSocket, debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled,
|
login, ping, openLogSocket, debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled,
|
||||||
getInstances, getInstance, putInstance, deleteInstance,
|
getInstances, getInstance, putInstance, deleteInstance,
|
||||||
|
getInstanceDatabase,
|
||||||
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
||||||
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
|
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// 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 from "react"
|
||||||
import { NavLink, withRouter } from "react-router-dom"
|
import { Link, NavLink, Route, Switch, withRouter } from "react-router-dom"
|
||||||
import AceEditor from "react-ace"
|
import AceEditor from "react-ace"
|
||||||
import "brace/mode/yaml"
|
import "brace/mode/yaml"
|
||||||
import "brace/theme/github"
|
import "brace/theme/github"
|
||||||
@ -23,6 +23,7 @@ import PrefTable, { PrefInput, PrefSelect, PrefSwitch } from "../../components/P
|
|||||||
import api from "../../api"
|
import api from "../../api"
|
||||||
import Spinner from "../../components/Spinner"
|
import Spinner from "../../components/Spinner"
|
||||||
import BaseMainView from "./BaseMainView"
|
import BaseMainView from "./BaseMainView"
|
||||||
|
import InstanceDatabase from "./InstanceDatabase"
|
||||||
|
|
||||||
const InstanceListEntry = ({ entry }) => (
|
const InstanceListEntry = ({ entry }) => (
|
||||||
<NavLink className="instance entry" to={`/instance/${entry.id}`}>
|
<NavLink className="instance entry" to={`/instance/${entry.id}`}>
|
||||||
@ -137,47 +138,65 @@ class Instance extends BaseMainView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="instance">
|
return <Switch>
|
||||||
<PrefTable>
|
<Route path="/instance/:id/database" render={this.renderDatabase}/>
|
||||||
<PrefInput rowName="ID" type="text" name="id" value={this.state.id}
|
<Route render={this.renderMain}/>
|
||||||
placeholder="fancybotinstance" onChange={this.inputChange}
|
</Switch>
|
||||||
disabled={!this.isNew} fullWidth={true} className="id"/>
|
|
||||||
<PrefSwitch rowName="Enabled"
|
|
||||||
active={this.state.enabled} origActive={this.props.entry.enabled}
|
|
||||||
onToggle={enabled => this.setState({ enabled })}/>
|
|
||||||
<PrefSwitch rowName="Running"
|
|
||||||
active={this.state.started} origActive={this.props.entry.started}
|
|
||||||
onToggle={started => this.setState({ started })}/>
|
|
||||||
<PrefSelect rowName="Primary user" options={this.clientOptions}
|
|
||||||
isSearchable={false} value={this.selectedClientEntry}
|
|
||||||
origValue={this.props.entry.primary_user}
|
|
||||||
onChange={({ id }) => this.setState({ primary_user: id })}/>
|
|
||||||
<PrefSelect rowName="Type" options={this.typeOptions} isSearchable={false}
|
|
||||||
value={this.selectedPluginEntry} origValue={this.props.entry.type}
|
|
||||||
onChange={({ id }) => this.setState({ type: id })}/>
|
|
||||||
</PrefTable>
|
|
||||||
{!this.isNew &&
|
|
||||||
<AceEditor mode="yaml" theme="github" onChange={config => this.setState({ config })}
|
|
||||||
name="config" value={this.state.config}
|
|
||||||
editorProps={{
|
|
||||||
fontSize: "10pt",
|
|
||||||
$blockScrolling: true,
|
|
||||||
}}/>}
|
|
||||||
<div className="buttons">
|
|
||||||
{!this.isNew && (
|
|
||||||
<button className="delete" onClick={this.delete} disabled={this.loading}>
|
|
||||||
{this.state.deleting ? <Spinner/> : "Delete"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button className={`save ${this.isValid ? "" : "disabled-bg"}`}
|
|
||||||
onClick={this.save} disabled={this.loading || !this.isValid}>
|
|
||||||
{this.state.saving ? <Spinner/> : (this.isNew ? "Create" : "Save")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{this.renderLogButton(`instance.${this.state.id}`)}
|
|
||||||
<div className="error">{this.state.error}</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderDatabase = () => <InstanceDatabase instanceID={this.props.entry.id}/>
|
||||||
|
|
||||||
|
renderMain = () => <div className="instance">
|
||||||
|
<PrefTable>
|
||||||
|
<PrefInput rowName="ID" type="text" name="id" value={this.state.id}
|
||||||
|
placeholder="fancybotinstance" onChange={this.inputChange}
|
||||||
|
disabled={!this.isNew} fullWidth={true} className="id"/>
|
||||||
|
<PrefSwitch rowName="Enabled"
|
||||||
|
active={this.state.enabled} origActive={this.props.entry.enabled}
|
||||||
|
onToggle={enabled => this.setState({ enabled })}/>
|
||||||
|
<PrefSwitch rowName="Running"
|
||||||
|
active={this.state.started} origActive={this.props.entry.started}
|
||||||
|
onToggle={started => this.setState({ started })}/>
|
||||||
|
<PrefSelect rowName="Primary user" options={this.clientOptions}
|
||||||
|
isSearchable={false} value={this.selectedClientEntry}
|
||||||
|
origValue={this.props.entry.primary_user}
|
||||||
|
onChange={({ id }) => this.setState({ primary_user: id })}/>
|
||||||
|
<PrefSelect rowName="Type" options={this.typeOptions} isSearchable={false}
|
||||||
|
value={this.selectedPluginEntry} origValue={this.props.entry.type}
|
||||||
|
onChange={({ id }) => this.setState({ type: id })}/>
|
||||||
|
</PrefTable>
|
||||||
|
{!this.isNew &&
|
||||||
|
<AceEditor mode="yaml" theme="github" onChange={config => this.setState({ config })}
|
||||||
|
name="config" value={this.state.config}
|
||||||
|
editorProps={{
|
||||||
|
fontSize: "10pt",
|
||||||
|
$blockScrolling: true,
|
||||||
|
}}/>}
|
||||||
|
<div className="buttons">
|
||||||
|
{!this.isNew && (
|
||||||
|
<button className="delete" onClick={this.delete} disabled={this.loading}>
|
||||||
|
{this.state.deleting ? <Spinner/> : "Delete"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className={`save ${this.isValid ? "" : "disabled-bg"}`}
|
||||||
|
onClick={this.save} disabled={this.loading || !this.isValid}>
|
||||||
|
{this.state.saving ? <Spinner/> : (this.isNew ? "Create" : "Save")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{!this.isNew && <div className="buttons">
|
||||||
|
{this.props.entry.database && (
|
||||||
|
<Link className="button open-database"
|
||||||
|
to={`/instance/${this.state.id}/database`}>
|
||||||
|
View database
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<button className="open-log"
|
||||||
|
onClick={() => this.props.openLog(`instance.${this.state.id}`)}>
|
||||||
|
View logs
|
||||||
|
</button>
|
||||||
|
</div>}
|
||||||
|
<div className="error">{this.state.error}</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Instance)
|
export default withRouter(Instance)
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
// 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 { NavLink, Link, Route } from "react-router-dom"
|
||||||
|
import { ReactComponent as ChevronLeft } from "../../res/chevron-left.svg"
|
||||||
|
import { ReactComponent as OrderDesc } from "../../res/sort-down.svg"
|
||||||
|
import { ReactComponent as OrderAsc } from "../../res/sort-up.svg"
|
||||||
|
import api from "../../api"
|
||||||
|
import Spinner from "../../components/Spinner"
|
||||||
|
|
||||||
|
class InstanceDatabase extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
tables: null,
|
||||||
|
sortBy: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentWillMount() {
|
||||||
|
const tables = new Map(Object.entries(await api.getInstanceDatabase(this.props.instanceID)))
|
||||||
|
for (const table of tables.values()) {
|
||||||
|
table.columns = new Map(Object.entries(table.columns))
|
||||||
|
for (const column of table.columns.values()) {
|
||||||
|
column.sort = "desc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({ tables })
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSort(column) {
|
||||||
|
column.sort = column.sort === "desc" ? "asc" : "desc"
|
||||||
|
this.forceUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTable = ({ match }) => {
|
||||||
|
const table = this.state.tables.get(match.params.table)
|
||||||
|
console.log(table)
|
||||||
|
return <div className="table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{Array.from(table.columns.entries()).map(([name, column]) => (
|
||||||
|
<td key={name}>
|
||||||
|
<span onClick={() => this.toggleSort(column)}>
|
||||||
|
{name}
|
||||||
|
{column.sort === "desc" ? <OrderDesc/> : <OrderAsc/>}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
return <>
|
||||||
|
<div className="tables">
|
||||||
|
{Object.keys(this.state.tables).map(key => (
|
||||||
|
<NavLink key={key} to={`/instance/${this.props.instanceID}/database/${key}`}>
|
||||||
|
{key}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Route path={`/instance/${this.props.instanceID}/database/:table`}
|
||||||
|
render={this.renderTable}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className="instance-database">
|
||||||
|
<div className="topbar">
|
||||||
|
<Link className="topbar" to={`/instance/${this.props.instanceID}`}>
|
||||||
|
<ChevronLeft/>
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{this.state.tables
|
||||||
|
? this.renderContent()
|
||||||
|
: <Spinner className="maubot-loading"/>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstanceDatabase
|
@ -184,9 +184,9 @@ class Dashboard extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="topbar">
|
<div className="topbar"
|
||||||
<div className={`hamburger ${this.state.sidebarOpen ? "active" : ""}`}
|
onClick={evt => this.setState({ sidebarOpen: !this.state.sidebarOpen })}>
|
||||||
onClick={evt => this.setState({ sidebarOpen: !this.state.sidebarOpen })}>
|
<div className={`hamburger ${this.state.sidebarOpen ? "active" : ""}`}>
|
||||||
<span/><span/><span/>
|
<span/><span/><span/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
1
maubot/management/frontend/src/res/chevron-left.svg
Normal file
1
maubot/management/frontend/src/res/chevron-left.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 183 B |
1
maubot/management/frontend/src/res/sort-down.svg
Normal file
1
maubot/management/frontend/src/res/sort-down.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 152 B |
1
maubot/management/frontend/src/res/sort-up.svg
Normal file
1
maubot/management/frontend/src/res/sort-up.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 14l5-5 5 5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 152 B |
@ -62,13 +62,13 @@
|
|||||||
> button, > .button
|
> button, > .button
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
&:first-of-type
|
&:first-child
|
||||||
margin-right: .5rem
|
margin-right: .5rem
|
||||||
|
|
||||||
&:last-of-type
|
&:last-child
|
||||||
margin-left: .5rem
|
margin-left: .5rem
|
||||||
|
|
||||||
&:first-of-type:last-of-type
|
&:first-child:last-child
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
||||||
=vertical-button-group()
|
=vertical-button-group()
|
||||||
|
@ -76,13 +76,14 @@
|
|||||||
|
|
||||||
@import client/index
|
@import client/index
|
||||||
@import instance
|
@import instance
|
||||||
|
@import instance-database
|
||||||
@import plugin
|
@import plugin
|
||||||
|
|
||||||
> div
|
> div
|
||||||
margin: 2rem 4rem
|
margin: 2rem 4rem
|
||||||
|
|
||||||
@media screen and (max-width: 50rem)
|
@media screen and (max-width: 50rem)
|
||||||
margin: 2rem 1rem
|
margin: 1rem
|
||||||
|
|
||||||
> div.not-found, > div.home
|
> div.not-found, > div.home
|
||||||
text-align: center
|
text-align: center
|
||||||
@ -95,10 +96,13 @@
|
|||||||
margin: 1rem .5rem
|
margin: 1rem .5rem
|
||||||
width: calc(100% - 1rem)
|
width: calc(100% - 1rem)
|
||||||
|
|
||||||
button.open-log
|
button.open-log, a.open-database
|
||||||
+button
|
+button
|
||||||
+main-color-button
|
+main-color-button
|
||||||
|
|
||||||
|
a.open-database
|
||||||
|
+link-button
|
||||||
|
|
||||||
div.error
|
div.error
|
||||||
+notification($error)
|
+notification($error)
|
||||||
margin: 1rem .5rem
|
margin: 1rem .5rem
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
// 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.instance-database
|
||||||
|
margin: 0
|
||||||
|
|
||||||
|
> div.topbar
|
||||||
|
background-color: $primary-light
|
||||||
|
|
||||||
|
display: flex
|
||||||
|
justify-items: center
|
||||||
|
align-items: center
|
||||||
|
|
||||||
|
> a
|
||||||
|
display: flex
|
||||||
|
justify-items: center
|
||||||
|
align-items: center
|
||||||
|
text-decoration: none
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
height: 2.5rem
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
> *:not(.topbar)
|
||||||
|
margin: 2rem 4rem
|
||||||
|
|
||||||
|
@media screen and (max-width: 50rem)
|
||||||
|
margin: 1rem
|
||||||
|
|
||||||
|
> div.tables
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
> a
|
||||||
|
+link-button
|
||||||
|
color: black
|
||||||
|
flex: 1
|
||||||
|
|
||||||
|
border-bottom: 2px solid $primary
|
||||||
|
|
||||||
|
padding: .25rem
|
||||||
|
margin: .25rem
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: $primary-light
|
||||||
|
border-bottom: 2px solid $primary-dark
|
||||||
|
|
||||||
|
&.active
|
||||||
|
background-color: $primary
|
||||||
|
|
||||||
|
> div.table
|
||||||
|
table
|
||||||
|
font-family: "Fira Code", monospace
|
||||||
|
width: 100%
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
> thead
|
||||||
|
font-weight: bold
|
||||||
|
|
||||||
|
> tr > td > span
|
||||||
|
align-items: center
|
||||||
|
justify-items: center
|
||||||
|
display: flex
|
||||||
|
cursor: pointer
|
||||||
|
user-select: none
|
@ -14,7 +14,7 @@
|
|||||||
// 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/>.
|
||||||
|
|
||||||
.topbar
|
> .topbar
|
||||||
background-color: $primary
|
background-color: $primary
|
||||||
|
|
||||||
display: flex
|
display: flex
|
||||||
@ -28,7 +28,10 @@
|
|||||||
// Hamburger menu based on "Pure CSS Hamburger fold-out menu" codepen by Erik Terwan (MIT license)
|
// Hamburger menu based on "Pure CSS Hamburger fold-out menu" codepen by Erik Terwan (MIT license)
|
||||||
// https://codepen.io/erikterwan/pen/EVzeRP
|
// https://codepen.io/erikterwan/pen/EVzeRP
|
||||||
|
|
||||||
.hamburger
|
> .topbar
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
> .topbar > .hamburger
|
||||||
display: block
|
display: block
|
||||||
user-select: none
|
user-select: none
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
@ -42,6 +45,7 @@
|
|||||||
|
|
||||||
background: white
|
background: white
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
|
user-select: none
|
||||||
|
|
||||||
z-index: 1
|
z-index: 1
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user