Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.

Commit 2c9d490

Browse files
committed
webui: Add basic updating of tables to database page for live databases
This adds basic update functionality to the database page of live databases. By double clicking a cell a text editor opens and a new value can be typed in. Copy and paste of single cell values works as well. For this feature to work a list of the primary key columns must be provided by the server. This list is also used in the code for non-live databases to be able to distinguish rows. So ideally the memcache should be cleared when rolling out these changes or else the added information is not provided for cached pages.
1 parent 24ef62a commit 2c9d490

File tree

10 files changed

+255
-56
lines changed

10 files changed

+255
-56
lines changed

api/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func columnsHandler(w http.ResponseWriter, r *http.Request) {
187187
}
188188
} else {
189189
// Send the columns request to our AMQP backend
190-
cols, err = com.LiveColumns(liveNode, loggedInUser, dbOwner, dbName, table)
190+
cols, _, err = com.LiveColumns(liveNode, loggedInUser, dbOwner, dbName, table)
191191
if err != nil {
192192
jsonErr(w, err.Error(), http.StatusBadRequest)
193193
log.Println(err)

common/live.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
"sort"
1111
"time"
1212

13-
amqp "github.com/rabbitmq/amqp091-go"
1413
sqlite "github.com/gwenn/gosqlite"
14+
amqp "github.com/rabbitmq/amqp091-go"
1515
)
1616

1717
const (
@@ -79,7 +79,7 @@ func ConnectMQ() (channel *amqp.Channel, err error) {
7979
}
8080

8181
// LiveColumns requests the AMQP backend to return a list of all columns of the given table
82-
func LiveColumns(liveNode, loggedInUser, dbOwner, dbName, table string) (columns []sqlite.Column, err error) {
82+
func LiveColumns(liveNode, loggedInUser, dbOwner, dbName, table string) (columns []sqlite.Column, pk []string, err error) {
8383
var rawResponse []byte
8484
rawResponse, err = MQRequest(AmqpChan, liveNode, "columns", loggedInUser, dbOwner, dbName, table)
8585
if err != nil {
@@ -101,6 +101,7 @@ func LiveColumns(liveNode, loggedInUser, dbOwner, dbName, table string) (columns
101101
return
102102
}
103103
columns = resp.Columns
104+
pk = resp.PkColumns
104105
return
105106
}
106107

common/live_types.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import (
88

99
// LiveDBColumnsResponse holds the fields used for receiving column list responses from our AMQP backend
1010
type LiveDBColumnsResponse struct {
11-
Node string `json:"node"`
12-
Columns []sqlite.Column `json:"columns"`
13-
Error string `json:"error"`
14-
ErrCode AMQPErrorCode `json:"error_code"`
11+
Node string `json:"node"`
12+
Columns []sqlite.Column `json:"columns"`
13+
PkColumns []string `json:"pkColuns"`
14+
Error string `json:"error"`
15+
ErrCode AMQPErrorCode `json:"error_code"`
1516
}
1617

1718
// LiveDBErrorResponse holds just the node name and any error message used in responses by our AMQP backend

common/sqlite.go

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -720,8 +720,22 @@ func ReadSQLiteDBCols(sdb *sqlite.Conn, dbTable, sortCol, sortDir string, ignore
720720
}
721721
}
722722

723+
// Get information on the primary key of the table
724+
pk, _, _, err := GetPrimaryKeyAndOtherColumns(sdb, "main", dbTable)
725+
if err != nil {
726+
log.Printf("error retrieving primary key columns: %s\n", err.Error())
727+
return SQLiteRecordSet{}, err
728+
}
729+
730+
// If there is no primary key the rowid column serves as an implicit primary key. In this case
731+
// also get the rowid column
732+
dbQuery := "SELECT "
733+
if len(pk) == 1 && pk[0] == "rowid" {
734+
dbQuery += "rowid,"
735+
}
736+
723737
// Construct the main SQL query
724-
dbQuery := sqlite.Mprintf(`SELECT * FROM "%w"`, dbTable)
738+
dbQuery += sqlite.Mprintf(`* FROM "%w"`, dbTable)
725739

726740
// If a sort column was given, include it
727741
if sortCol != "" {
@@ -761,6 +775,7 @@ func ReadSQLiteDBCols(sdb *sqlite.Conn, dbTable, sortCol, sortDir string, ignore
761775
dataRows.RowCount = tmpCount
762776

763777
// Fill out other data fields
778+
dataRows.PrimaryKeyColumns = pk
764779
dataRows.Tablename = dbTable
765780
dataRows.SortCol = sortCol
766781
dataRows.SortDir = sortDir
@@ -972,7 +987,7 @@ func SQLiteExecuteQueryLive(baseDir, dbOwner, dbName, loggedInUser, query string
972987
}
973988

974989
// SQLiteGetColumnsLive is used by our AMQP backend nodes to retrieve the list of columns from a SQLite database
975-
func SQLiteGetColumnsLive(baseDir, dbOwner, dbName, table string) (columns []sqlite.Column, err error, errCode AMQPErrorCode) {
990+
func SQLiteGetColumnsLive(baseDir, dbOwner, dbName, table string) (columns []sqlite.Column, pk []string, err error, errCode AMQPErrorCode) {
976991
// Open the database on the local node
977992
var sdb *sqlite.Conn
978993
sdb, err = OpenSQLiteDatabaseLive(baseDir, dbOwner, dbName)
@@ -1001,6 +1016,13 @@ func SQLiteGetColumnsLive(baseDir, dbOwner, dbName, table string) (columns []sql
10011016

10021017
// Retrieve the list of columns for the table
10031018
columns, err = sdb.Columns("", table)
1019+
1020+
// Retrieve a list of primary key columns
1021+
pk, _, _, err = GetPrimaryKeyAndOtherColumns(sdb, "main", table)
1022+
if err != nil {
1023+
return
1024+
}
1025+
10041026
return
10051027
}
10061028

@@ -1656,17 +1678,34 @@ func GetPrimaryKeyAndOtherColumns(sdb *sqlite.Conn, schema, table string) (pks [
16561678
} else {
16571679
// Implicit primary key
16581680

1659-
implicitPk = true
1681+
// Views do not even have have implicit keys
1682+
vws, err := Views(sdb)
1683+
if err != nil {
1684+
log.Printf("Error retrieving views: %s\n", err)
1685+
return nil, false, nil, err
1686+
}
16601687

1661-
if !hasColumnRowid {
1662-
pks = append(pks, "rowid")
1663-
} else if !hasColumn_Rowid_ {
1664-
pks = append(pks, "_rowid_")
1665-
} else if !hasColumnOid {
1666-
pks = append(pks, "oid")
1667-
} else {
1668-
log.Printf("Unreachable rowid column in GetPrimaryKey()\n")
1669-
return nil, false, nil, errors.New("Unreachable rowid column")
1688+
isView := false
1689+
for _, vw := range vws {
1690+
if vw == table {
1691+
isView = true
1692+
break
1693+
}
1694+
}
1695+
1696+
if isView == false {
1697+
implicitPk = true
1698+
1699+
if !hasColumnRowid {
1700+
pks = append(pks, "rowid")
1701+
} else if !hasColumn_Rowid_ {
1702+
pks = append(pks, "_rowid_")
1703+
} else if !hasColumnOid {
1704+
pks = append(pks, "oid")
1705+
} else {
1706+
log.Printf("Unreachable rowid column in GetPrimaryKey()\n")
1707+
return nil, false, nil, errors.New("Unreachable rowid column")
1708+
}
16701709
}
16711710
}
16721711

common/types.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,16 @@ type SQLiteDBinfo struct {
385385
}
386386

387387
type SQLiteRecordSet struct {
388-
ColCount int
389-
ColNames []string
390-
Offset int
391-
Records []DataRow
392-
RowCount int
393-
SortCol string
394-
SortDir string
395-
Tablename string
396-
TotalRows int
388+
ColCount int
389+
ColNames []string
390+
Offset int
391+
PrimaryKeyColumns []string `json:"primaryKeyColumns,omitempty"`
392+
Records []DataRow
393+
RowCount int
394+
SortCol string
395+
SortDir string
396+
Tablename string
397+
TotalRows int
397398
}
398399

399400
type StatusUpdateEntry struct {

live/main.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,9 @@ func main() {
174174
continue
175175

176176
case "columns":
177-
var columns []sqlite.Column
178-
var errCode com.AMQPErrorCode
179-
columns, err, errCode = com.SQLiteGetColumnsLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, fmt.Sprintf("%s", req.Data))
177+
columns, pk, err, errCode := com.SQLiteGetColumnsLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, fmt.Sprintf("%s", req.Data))
180178
if err != nil {
181-
resp := com.LiveDBColumnsResponse{Node: com.Conf.Live.Nodename, Columns: []sqlite.Column{}, Error: err.Error(), ErrCode: errCode}
179+
resp := com.LiveDBColumnsResponse{Node: com.Conf.Live.Nodename, Columns: []sqlite.Column{}, PkColumns: nil, Error: err.Error(), ErrCode: errCode}
182180
err = com.MQResponse("COLUMNS", msg, ch, com.Conf.Live.Nodename, resp)
183181
if err != nil {
184182
log.Printf("Error: occurred on '%s' in MQResponse() while constructing an AMQP error message response: '%s'", com.Conf.Live.Nodename, err)
@@ -191,7 +189,7 @@ func main() {
191189
}
192190

193191
// Return the columns list to the caller
194-
resp := com.LiveDBColumnsResponse{Node: com.Conf.Live.Nodename, Columns: columns, Error: "", ErrCode: com.AMQPNoError}
192+
resp := com.LiveDBColumnsResponse{Node: com.Conf.Live.Nodename, Columns: columns, PkColumns: pk, Error: "", ErrCode: com.AMQPNoError}
195193
err = com.MQResponse("COLUMNS", msg, ch, com.Conf.Live.Nodename, resp)
196194
if err != nil {
197195
log.Printf("Error: occurred on '%s' in MQResponse() while constructing the AMQP columns list response: '%s'", com.Conf.Live.Nodename, err)

webui/jsx/database-view.js

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const React = require("react");
22
const ReactDOM = require("react-dom");
33

4-
import DataGrid from "react-data-grid";
4+
import DataGrid, {textEditor} from "react-data-grid";
55
import "react-data-grid/lib/styles.css";
66
import Select from "react-dropdown-select";
77

@@ -229,6 +229,7 @@ export default function DatabaseView() {
229229
const [maxRows, setMaxRows] = React.useState(meta.maxRows);
230230
const [rowCount, setRowCount] = React.useState(0);
231231
const [sortColumns, setSortColumns] = React.useState([]);
232+
const [primaryKeyColumns, setPrimaryKeyColumns] = React.useState([]);
232233

233234
// Retrieves the branch being viewed
234235
function changeBranch(newbranch) {
@@ -248,13 +249,18 @@ export default function DatabaseView() {
248249
fetch("/x/table/" + meta.owner + "/" + meta.database + "?commit=" + meta.commitID + "&table=" + newTable + "&sort=" + (newSortCol ? newSortCol : "") + "&dir=" + (newSortDir ? newSortDir : "") + "&offset=" + newOffset)
249250
.then((response) => response.json())
250251
.then(function (data) {
252+
// Get primary key columns. They are an optional property
253+
let pk = Object.hasOwn(data, "primaryKeyColumns") ? data.primaryKeyColumns : [];
254+
251255
// Convert data to format required by grid view
252256
// TODO Just deliver the data in the right format to begin with
253257
let cols = [];
254258
data.ColNames.forEach(function(c) {
255259
// Remove the rowid column if it was added by the server
256260
if (c !== "rowid") {
257261
// Add the column
262+
// The editing feature is enable if this is a live database and if there is
263+
// a primary key here (which excludes views here)
258264
cols.push({
259265
key: c,
260266
name: c,
@@ -265,6 +271,7 @@ export default function DatabaseView() {
265271
return props.row[c];
266272
}
267273
},
274+
editor: meta.isLive && pk.length ? textEditor : null
268275
});
269276
}
270277
});
@@ -287,6 +294,60 @@ export default function DatabaseView() {
287294
setOffset(data.Offset);
288295
setRowCount(data.RowCount);
289296
setSortColumns([{columnKey: data.SortCol, direction: data.SortDir}]);
297+
setPrimaryKeyColumns(pk);
298+
});
299+
}
300+
301+
// This function returns the primary key of a row when it is selected in the data grid.
302+
// This can be used for addressing a row
303+
function rowKeyGetter(row) {
304+
let key = {};
305+
primaryKeyColumns.forEach(function(p) {
306+
key[p] = row[p];
307+
});
308+
return JSON.stringify(key);
309+
}
310+
311+
// This function is called when the user tries to edit a row
312+
function updateRowData(rows, data) {
313+
// Get name of updated column
314+
let column = data.column.key;
315+
316+
// Iterate over the indexes to the changed rows
317+
let updateData = [];
318+
data.indexes.forEach(function(i) {
319+
// Get old value
320+
let oldValue = records[i];
321+
322+
// Get new value
323+
let newValues = {};
324+
newValues[column] = rows[i][column];
325+
326+
updateData.push({
327+
key: JSON.parse(rowKeyGetter(oldValue)),
328+
values: newValues,
329+
});
330+
});
331+
332+
// Send data to server
333+
fetch("/x/updatedata/" + meta.owner + "/" + meta.database, {
334+
method: "post",
335+
headers: {
336+
"Content-Type": "application/json",
337+
},
338+
body: JSON.stringify({table: table, data: updateData})
339+
})
340+
.then((response) => {
341+
if (!response.ok) {
342+
return Promise.reject(response);
343+
}
344+
setRecords(rows);
345+
})
346+
.catch((error) => {
347+
// TODO Replace this by some prettier status message bar or so
348+
error.text().then((text) => {
349+
alert("Error updating rows. " + text);
350+
});
290351
});
291352
}
292353

@@ -307,25 +368,6 @@ export default function DatabaseView() {
307368
}
308369
}, []);
309370

310-
// Convert data to format required by grid view
311-
// TODO Just deliver the data in the right format to begin with
312-
let cols = [];
313-
columns.forEach(function(c) {
314-
cols.push({
315-
key: c,
316-
name: c
317-
});
318-
});
319-
320-
let rows = [];
321-
records.forEach(function(r) {
322-
let row = {};
323-
r.forEach(function(c) {
324-
row[c.Name] = c.Value;
325-
});
326-
rows.push(row);
327-
});
328-
329371
return (<>
330372
<DatabaseDescription oneLineDescription={meta.oneLineDescription} sourceUrl={meta.sourceUrl} />
331373
<DatabaseSubMenu />
@@ -337,10 +379,12 @@ export default function DatabaseView() {
337379
<DataGrid
338380
className="rdg-light"
339381
renderers={{noRowsFallback: <DataGridNoRowsRender />}}
340-
columns={cols}
341-
rows={rows}
382+
columns={columns}
383+
rows={records}
342384
sortColumns={sortColumns}
343385
onSortColumnsChange={(s) => changeView(table, offset, s.length ? s[0].columnKey : null, s.length ? s[0].direction : null)}
386+
rowKeyGetter={rowKeyGetter}
387+
onRowsChange={updateRowData}
344388
defaultColumnOptions={{
345389
sortable: true,
346390
resizable: true

0 commit comments

Comments
 (0)