Add contextmenu in database explorer and implement deleting rows
This commit is contained in:
parent
46186452dc
commit
147081c0db
@ -6,6 +6,7 @@
|
||||
"node-sass": "^4.9.4",
|
||||
"react": "^16.6.0",
|
||||
"react-ace": "^6.2.0",
|
||||
"react-contextmenu": "^2.10.0",
|
||||
"react-dom": "^16.6.0",
|
||||
"react-json-tree": "^0.11.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import React, { Component } from "react"
|
||||
import { NavLink, Link, withRouter } from "react-router-dom"
|
||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from "react-contextmenu"
|
||||
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"
|
||||
@ -87,7 +88,7 @@ class InstanceDatabase extends Component {
|
||||
return order
|
||||
}
|
||||
|
||||
buildSQLQuery(table = this.state.selectedTable) {
|
||||
buildSQLQuery(table = this.state.selectedTable, resetContent = true) {
|
||||
let query = `SELECT * FROM ${table}`
|
||||
|
||||
if (this.order.size > 0) {
|
||||
@ -97,24 +98,25 @@ class InstanceDatabase extends Component {
|
||||
}
|
||||
|
||||
query += " LIMIT 100"
|
||||
this.setState({ query }, this.reloadContent)
|
||||
this.setState({ query }, () => this.reloadContent(resetContent))
|
||||
}
|
||||
|
||||
reloadContent = async () => {
|
||||
reloadContent = async (resetContent = true) => {
|
||||
this.setState({ loading: true })
|
||||
const res = await api.queryInstanceDatabase(this.props.instanceID, this.state.query)
|
||||
this.setState({ loading: false })
|
||||
if (resetContent) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
prevQuery: null,
|
||||
rowCount: null,
|
||||
insertedPrimaryKey: null,
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
if (!res.ok) {
|
||||
this.setState({
|
||||
error: res.error,
|
||||
})
|
||||
this.buildSQLQuery()
|
||||
} else if (res.rows) {
|
||||
this.setState({
|
||||
header: res.columns,
|
||||
@ -126,7 +128,7 @@ class InstanceDatabase extends Component {
|
||||
rowCount: res.rowcount,
|
||||
insertedPrimaryKey: res.insertedPrimaryKey,
|
||||
})
|
||||
this.buildSQLQuery()
|
||||
this.buildSQLQuery(this.state.selectedTable, false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,15 +160,96 @@ class InstanceDatabase extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getColumnInfo(columnName) {
|
||||
const table = this.state.tables.get(this.state.selectedTable)
|
||||
if (!table) {
|
||||
return null
|
||||
}
|
||||
const column = table.columns.get(columnName)
|
||||
if (!column) {
|
||||
return null
|
||||
}
|
||||
if (column.primary) {
|
||||
return <span className="meta"> (pk)</span>
|
||||
} else if (column.unique) {
|
||||
return <span className="meta"> (u)</span>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
getColumnType(columnName) {
|
||||
const table = this.state.tables.get(this.state.selectedTable)
|
||||
if (!table) {
|
||||
return null
|
||||
}
|
||||
const column = table.columns.get(columnName)
|
||||
if (!column) {
|
||||
return null
|
||||
}
|
||||
return column.type
|
||||
}
|
||||
|
||||
deleteRow = async (_, data) => {
|
||||
const values = this.state.content[data.row]
|
||||
const keys = this.state.header
|
||||
const condition = []
|
||||
for (const [index, key] of Object.entries(keys)) {
|
||||
const val = values[index]
|
||||
condition.push(`${key}='${this.sqlEscape(val.toString())}'`)
|
||||
}
|
||||
const query = `DELETE FROM ${this.state.selectedTable} WHERE ${condition.join(" AND ")}`
|
||||
const res = await api.queryInstanceDatabase(this.props.instanceID, query)
|
||||
this.setState({
|
||||
prevQuery: `DELETE FROM ${this.state.selectedTable} ...`,
|
||||
rowCount: res.rowcount,
|
||||
})
|
||||
await this.reloadContent(false)
|
||||
}
|
||||
|
||||
editCell = async (evt, data) => {
|
||||
console.log("Edit", data)
|
||||
}
|
||||
|
||||
collectContextMeta = props => ({
|
||||
row: props.row,
|
||||
col: props.col,
|
||||
})
|
||||
|
||||
sqlEscape = str => str.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, char => {
|
||||
switch (char) {
|
||||
case "\0":
|
||||
return "\\0"
|
||||
case "\x08":
|
||||
return "\\b"
|
||||
case "\x09":
|
||||
return "\\t"
|
||||
case "\x1a":
|
||||
return "\\z"
|
||||
case "\n":
|
||||
return "\\n"
|
||||
case "\r":
|
||||
return "\\r"
|
||||
case "\"":
|
||||
case "'":
|
||||
case "\\":
|
||||
case "%":
|
||||
return "\\" + char
|
||||
default:
|
||||
return char
|
||||
}
|
||||
})
|
||||
|
||||
renderTable = () => <div className="table">
|
||||
{this.state.header ? (
|
||||
{this.state.header ? <>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{this.state.header.map(column => (
|
||||
<td key={column}>
|
||||
<span onClick={() => this.toggleSort(column)}>
|
||||
{column}
|
||||
<span onClick={() => this.toggleSort(column)}
|
||||
title={this.getColumnType(column)}>
|
||||
<strong>{column}</strong>
|
||||
{this.getColumnInfo(column)}
|
||||
{this.getSortIcon(column)}
|
||||
</span>
|
||||
</td>
|
||||
@ -174,18 +257,24 @@ class InstanceDatabase extends Component {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.content.map((row, index) => (
|
||||
<tr key={index}>
|
||||
{row.map((column, index) => (
|
||||
<td key={index}>
|
||||
{column}
|
||||
</td>
|
||||
{this.state.content.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell, colIndex) => (
|
||||
<ContextMenuTrigger key={colIndex} id="database_table_menu"
|
||||
renderTag="td" row={rowIndex} col={colIndex}
|
||||
collect={this.collectContextMeta}>
|
||||
{cell}
|
||||
</ContextMenuTrigger>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : this.state.loading ? <Spinner/> : null}
|
||||
<ContextMenu id="database_table_menu">
|
||||
<MenuItem onClick={this.deleteRow}>Delete row</MenuItem>
|
||||
<MenuItem disabled onClick={this.editCell}>Edit cell</MenuItem>
|
||||
</ContextMenu>
|
||||
</> : this.state.loading ? <Spinner/> : null}
|
||||
</div>
|
||||
|
||||
renderContent() {
|
||||
|
@ -14,6 +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 lib/spinner
|
||||
@import lib/contextmenu
|
||||
|
||||
@import base/vars
|
||||
@import base/body
|
||||
|
80
maubot/management/frontend/src/style/lib/contextmenu.scss
Normal file
80
maubot/management/frontend/src/style/lib/contextmenu.scss
Normal file
@ -0,0 +1,80 @@
|
||||
.react-contextmenu {
|
||||
background-color: #fff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid rgba(0, 0, 0, .15);
|
||||
border-radius: .25rem;
|
||||
color: #373a3c;
|
||||
font-size: 16px;
|
||||
margin: 2px 0 0;
|
||||
min-width: 160px;
|
||||
outline: none;
|
||||
opacity: 0;
|
||||
padding: 5px 0;
|
||||
pointer-events: none;
|
||||
text-align: left;
|
||||
transition: opacity 250ms ease !important;
|
||||
}
|
||||
|
||||
.react-contextmenu.react-contextmenu--visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.react-contextmenu-item {
|
||||
background: 0 0;
|
||||
border: 0;
|
||||
color: #373a3c;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
padding: 3px 20px;
|
||||
text-align: inherit;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.react-contextmenu-item.react-contextmenu-item--active,
|
||||
.react-contextmenu-item.react-contextmenu-item--selected {
|
||||
color: #fff;
|
||||
background-color: #20a0ff;
|
||||
border-color: #20a0ff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.react-contextmenu-item.react-contextmenu-item--disabled,
|
||||
.react-contextmenu-item.react-contextmenu-item--disabled:hover {
|
||||
background-color: transparent;
|
||||
border-color: rgba(0, 0, 0, .15);
|
||||
color: #878a8c;
|
||||
}
|
||||
|
||||
.react-contextmenu-item--divider {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .15);
|
||||
cursor: inherit;
|
||||
margin-bottom: 3px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.react-contextmenu-item--divider:hover {
|
||||
background-color: transparent;
|
||||
border-color: rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.react-contextmenu-item.react-contextmenu-submenu {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item {
|
||||
}
|
||||
|
||||
.react-contextmenu-item.react-contextmenu-submenu > .react-contextmenu-item:after {
|
||||
content: "▶";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
.example-multiple-targets::after {
|
||||
content: attr(data-count);
|
||||
display: block;
|
||||
}
|
@ -80,6 +80,9 @@
|
||||
span.query
|
||||
font-family: "Fira Code", monospace
|
||||
|
||||
p
|
||||
margin: 0
|
||||
|
||||
> div.table
|
||||
overflow-x: auto
|
||||
overflow-y: hidden
|
||||
@ -90,8 +93,6 @@
|
||||
box-sizing: border-box
|
||||
|
||||
> thead
|
||||
font-weight: bold
|
||||
|
||||
> tr > td > span
|
||||
align-items: center
|
||||
justify-items: center
|
||||
|
@ -4606,7 +4606,7 @@ gonzales-pe-sl@^4.2.3:
|
||||
dependencies:
|
||||
minimist "1.1.x"
|
||||
|
||||
"gonzales-pe-sl@github:srowhani/gonzales-pe#dev":
|
||||
gonzales-pe-sl@srowhani/gonzales-pe#dev:
|
||||
version "4.2.3"
|
||||
resolved "https://codeload.github.com/srowhani/gonzales-pe/tar.gz/3b052416074edc280f7d04bbe40b2e410693c4a3"
|
||||
dependencies:
|
||||
@ -8579,7 +8579,7 @@ rc@^1.2.7:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-ace@^6.3.2:
|
||||
react-ace@^6.2.0:
|
||||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-6.3.2.tgz#4fc75edce17d79c3169791dc184744950aca4794"
|
||||
integrity sha512-eSk0fWvrBe2oqYIYX0njLddLG5H0hemWv5VVoQi5yDSPTjGlSSnzFwdgPyfuwRe8mSARZuRdprPQa5p61hKirw==
|
||||
@ -8611,6 +8611,14 @@ react-base16-styling@^0.5.1:
|
||||
lodash.flow "^3.3.0"
|
||||
pure-color "^1.2.0"
|
||||
|
||||
react-contextmenu@^2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/react-contextmenu/-/react-contextmenu-2.10.0.tgz#3a5338a552964db85c300072f719bc1f6b969838"
|
||||
integrity sha512-neiZGpfxfYFjqbcIExi69qruqhB7l0LKEguHDXeizgyTGbJHTwbq1GplXCHIafUAkbGZH8FfD9PBeUcSRG78+Q==
|
||||
dependencies:
|
||||
classnames "^2.2.5"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-dev-utils@^6.0.5:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-6.1.1.tgz#a07e3e8923c4609d9f27e5af5207e3ca20724895"
|
||||
|
Loading…
Reference in New Issue
Block a user