Finish basic database viewing

This commit is contained in:
Tulir Asokan 2018-12-28 19:19:58 +02:00
parent dc22f35d08
commit 4cc605df76
7 changed files with 1337 additions and 1117 deletions

View File

@ -19,7 +19,7 @@ from asyncio import AbstractEventLoop
from ...config import Config
from .base import routes, set_config, set_loop
from .middleware import auth, error
from . import auth, plugin, instance, database, client, client_proxy, client_auth, dev_open
from . import auth, plugin, instance, instance_database, client, client_proxy, client_auth, dev_open
from .log import stop_all as stop_log_sockets, init as init_log_listener

View File

@ -16,7 +16,7 @@
from typing import TYPE_CHECKING
from aiohttp import web
from sqlalchemy import Table, Column
from sqlalchemy import Table, Column, asc, desc
from ...instance import PluginInstance
from .base import routes
@ -59,3 +59,19 @@ async def get_table(request: web.Request) -> web.Response:
elif not instance.inst_db:
return resp.plugin_has_no_database
tables = instance.get_db_tables()
try:
table = tables[request.match_info.get("table", "")]
except KeyError:
return resp.table_not_found
db = instance.inst_db
try:
order = [tuple(order.split(":")) for order in request.query.getall("order")]
order = [(asc if sort == "asc" else desc)(table.columns[column])
if sort else table.columns[column]
for column, sort in order]
except KeyError:
order = []
limit = int(request.query.get("limit", 100))
query = table.select().order_by(*order).limit(limit)
data = [[*row] for row in db.execute(query)]
return web.json_response(data)

View File

@ -159,6 +159,13 @@ class _Response:
"errcode": "plugin_has_no_database",
})
@property
def table_not_found(self) -> web.Response:
return web.json_response({
"error": "Given table not found in plugin database",
"errcode": "table_not_found",
})
@property
def method_not_allowed(self) -> web.Response:
return web.json_response({

View File

@ -154,6 +154,8 @@ export const putInstance = (instance, id) => defaultPut("instance", instance, id
export const deleteInstance = id => defaultDelete("instance", id)
export const getInstanceDatabase = id => defaultGet(`/instance/${id}/database`)
export const getInstanceDatabaseTable = (id, table, query = []) =>
defaultGet(`/instance/${id}/database/${table}?${query.join("&")}`)
export const getPlugins = () => defaultGet("/plugins")
export const getPlugin = id => defaultGet(`/plugin/${id}`)
@ -205,7 +207,7 @@ export default {
BASE_PATH,
login, ping, openLogSocket, debugOpenFile, debugOpenFileEnabled, updateDebugOpenFileEnabled,
getInstances, getInstance, putInstance, deleteInstance,
getInstanceDatabase,
getInstanceDatabase, getInstanceDatabaseTable,
getPlugins, getPlugin, uploadPlugin, deletePlugin,
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
}

View File

@ -14,7 +14,7 @@
// 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 { NavLink, Link, Route, withRouter } 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"
@ -26,53 +26,130 @@ class InstanceDatabase extends Component {
super(props)
this.state = {
tables: null,
sortBy: null,
tableContent: null,
}
this.sortBy = []
}
async componentWillMount() {
const tables = new Map(Object.entries(await api.getInstanceDatabase(this.props.instanceID)))
for (const table of tables.values()) {
for (const [name, table] of tables) {
table.name = name
table.columns = new Map(Object.entries(table.columns))
for (const column of table.columns.values()) {
column.sort = "desc"
for (const [columnName, column] of table.columns) {
column.name = columnName
column.sort = null
}
}
this.setState({ tables })
this.checkLocationTable()
}
toggleSort(column) {
column.sort = column.sort === "desc" ? "asc" : "desc"
this.forceUpdate()
componentDidUpdate(prevProps) {
if (this.props.location !== prevProps.location) {
this.sortBy = []
this.setState({ tableContent: null })
this.checkLocationTable()
}
}
checkLocationTable() {
const prefix = `/instance/${this.props.instanceID}/database/`
if (this.props.location.pathname.startsWith(prefix)) {
const table = this.props.location.pathname.substr(prefix.length)
this.reloadContent(table)
}
}
getSortQuery(table) {
const sort = []
for (const column of this.sortBy) {
sort.push(`order=${column.name}:${column.sort}`)
}
return sort
}
async reloadContent(name) {
const table = this.state.tables.get(name)
const query = this.getSortQuery(table)
query.push("limit=100")
this.setState({
tableContent: await api.getInstanceDatabaseTable(
this.props.instanceID, table.name, query),
})
}
toggleSort(tableName, column) {
const index = this.sortBy.indexOf(column)
if (index >= 0) {
this.sortBy.splice(index, 1)
}
switch (column.sort) {
default:
column.sort = "desc"
this.sortBy.unshift(column)
break
case "desc":
column.sort = "asc"
this.sortBy.unshift(column)
break
case "asc":
column.sort = null
break
}
this.forceUpdate()
this.reloadContent(tableName)
}
renderTableHead = table => <thead>
<tr>
{Array.from(table.columns.entries()).map(([name, column]) => (
<td key={name}>
<span onClick={() => this.toggleSort(table.name, column)}>
{name}
{column.sort === "desc" ?
<OrderDesc/> :
column.sort === "asc"
? <OrderAsc/>
: null}
</span>
</td>
))}
</tr>
</thead>
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>
{this.state.tableContent ? (
<table>
{this.renderTableHead(table)}
<tbody>
{this.state.tableContent.map(row => (
<tr key={row}>
{row.map((column, index) => (
<td key={index}>
{column}
</td>
))}
</tr>
))}
</tr>
</thead>
<tbody>
</tbody>
</table>
</tbody>
</table>
) : <>
<table>
{this.renderTableHead(table)}
</table>
<Spinner/>
</>}
</div>
}
renderContent() {
return <>
<div className="tables">
{Object.keys(this.state.tables).map(key => (
{Array.from(this.state.tables.keys()).map(key => (
<NavLink key={key} to={`/instance/${this.props.instanceID}/database/${key}`}>
{key}
</NavLink>
@ -98,4 +175,4 @@ class InstanceDatabase extends Component {
}
}
export default InstanceDatabase
export default withRouter(InstanceDatabase)

View File

@ -62,6 +62,9 @@
background-color: $primary
> div.table
overflow-x: auto
overflow-y: hidden
table
font-family: "Fira Code", monospace
width: 100%

File diff suppressed because it is too large Load Diff