From 147081c0db01cf0f8a150e20a75ff88eee0d9efe Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 29 Dec 2018 15:18:16 +0200 Subject: [PATCH] Add contextmenu in database explorer and implement deleting rows --- maubot/management/frontend/package.json | 1 + .../src/pages/dashboard/InstanceDatabase.js | 133 +++++++++++++++--- .../management/frontend/src/style/index.sass | 1 + .../frontend/src/style/lib/contextmenu.scss | 80 +++++++++++ .../src/style/pages/instance-database.sass | 5 +- maubot/management/frontend/yarn.lock | 12 +- 6 files changed, 206 insertions(+), 26 deletions(-) create mode 100644 maubot/management/frontend/src/style/lib/contextmenu.scss diff --git a/maubot/management/frontend/package.json b/maubot/management/frontend/package.json index e754eef..fb7e13a 100644 --- a/maubot/management/frontend/package.json +++ b/maubot/management/frontend/package.json @@ -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", diff --git a/maubot/management/frontend/src/pages/dashboard/InstanceDatabase.js b/maubot/management/frontend/src/pages/dashboard/InstanceDatabase.js index a1e9a06..472d6f0 100644 --- a/maubot/management/frontend/src/pages/dashboard/InstanceDatabase.js +++ b/maubot/management/frontend/src/pages/dashboard/InstanceDatabase.js @@ -15,6 +15,7 @@ // along with this program. If not, see . 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, - prevQuery: null, - rowCount: null, - insertedPrimaryKey: null, - error: null, - }) + this.setState({ loading: false }) + if (resetContent) { + this.setState({ + 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  (pk) + } else if (column.unique) { + return  (u) + } + 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 = () =>
- {this.state.header ? ( + {this.state.header ? <> {this.state.header.map(column => ( @@ -174,18 +257,24 @@ class InstanceDatabase extends Component { - {this.state.content.map((row, index) => ( - - {row.map((column, index) => ( - + {this.state.content.map((row, rowIndex) => ( + + {row.map((cell, colIndex) => ( + + {cell} + ))} ))}
- this.toggleSort(column)}> - {column} + this.toggleSort(column)} + title={this.getColumnType(column)}> + {column} + {this.getColumnInfo(column)} {this.getSortIcon(column)}
- {column} -
- ) : this.state.loading ? : null} + + Delete row + Edit cell + + : this.state.loading ? : null}
renderContent() { diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass index 9c1e193..d7b702d 100644 --- a/maubot/management/frontend/src/style/index.sass +++ b/maubot/management/frontend/src/style/index.sass @@ -14,6 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . @import lib/spinner +@import lib/contextmenu @import base/vars @import base/body diff --git a/maubot/management/frontend/src/style/lib/contextmenu.scss b/maubot/management/frontend/src/style/lib/contextmenu.scss new file mode 100644 index 0000000..5575fb4 --- /dev/null +++ b/maubot/management/frontend/src/style/lib/contextmenu.scss @@ -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; +} diff --git a/maubot/management/frontend/src/style/pages/instance-database.sass b/maubot/management/frontend/src/style/pages/instance-database.sass index 428dac9..50c75ef 100644 --- a/maubot/management/frontend/src/style/pages/instance-database.sass +++ b/maubot/management/frontend/src/style/pages/instance-database.sass @@ -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 diff --git a/maubot/management/frontend/yarn.lock b/maubot/management/frontend/yarn.lock index 60aec45..54df298 100644 --- a/maubot/management/frontend/yarn.lock +++ b/maubot/management/frontend/yarn.lock @@ -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"