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

Commit 5c78e52

Browse files
committed
webui: Migrate profile page to React
Besides migrating this page to React this also updates the common code for getting a list of the live databases of a user easier to use. Also the information shown in the profile and in the user pages is more similar. The same is true for the information on standard and on live databases. This also means that download and view count for public databases are now also visible to other users. Please note though that this is *not* a change in the permissions as this information has been sent to the client before too - it just was not shown in the frontend. Additionally this removes the "live" label from shared live databases and removed the "beta testing" label from them as well. The idea here is to a) not put too much effort into the profile page because it should probably be redesigned anyway and b) start working towards making live and standard databases more similar. Finally this commit removes the "Expand/Collapse" buttons and replaces them by individual toggle buttons for each database.
1 parent 938c738 commit 5c78e52

File tree

11 files changed

+243
-476
lines changed

11 files changed

+243
-476
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ webui/js/discussion-comments.js
6464
webui/js/discussion-create-mr.js
6565
webui/js/discussion-list.js
6666
webui/js/markdown-editor.js
67+
webui/js/profile-page.js
6768
webui/js/user-page.js
6869

6970
# Local secrets

api/handlers.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ func databasesHandler(w http.ResponseWriter, r *http.Request) {
290290
}
291291
} else {
292292
// Get the list of live databases
293-
databases, err = com.LiveUserDBs(loggedInUser)
293+
databases, err = com.LiveUserDBs(loggedInUser, com.DB_BOTH)
294294
if err != nil {
295295
jsonErr(w, err.Error(), http.StatusInternalServerError)
296296
return

common/live_types.go

-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package common
22

33
import (
4-
"time"
5-
64
sqlite "github.com/gwenn/gosqlite"
75
)
86

@@ -79,13 +77,6 @@ type LiveDBRowsResponse struct {
7977
Error string `json:"error"`
8078
}
8179

82-
// LiveDBs is used for general purpose holding of details about live databases
83-
type LiveDBs struct {
84-
DBOwner string `json:"owner_name"`
85-
DBName string `json:"database_name"`
86-
DateCreated time.Time `json:"date_created"`
87-
}
88-
8980
// LiveDBSizeResponse holds the fields used for receiving database size responses from our AMQP backend
9081
type LiveDBSizeResponse struct {
9182
Node string `json:"node"`

common/postgresql.go

+35-5
Original file line numberDiff line numberDiff line change
@@ -2741,15 +2741,34 @@ func LiveGetMinioNames(loggedInUser, dbOwner, dbName string) (bucketName, object
27412741
}
27422742

27432743
// LiveUserDBs returns the list of live databases owned by the user
2744-
func LiveUserDBs(dbOwner string) (list []DBInfo, err error) {
2744+
func LiveUserDBs(dbOwner string, public AccessType) (list []DBInfo, err error) {
27452745
dbQuery := `
2746-
SELECT db_name, date_created, public
2746+
SELECT db_name, date_created, last_modified, public, live_db, live_node,
2747+
db.watchers, db.stars, discussions, contributors,
2748+
coalesce(one_line_description, ''), coalesce(source_url, ''),
2749+
download_count, page_views
27472750
FROM sqlite_databases AS db, users
27482751
WHERE users.user_id = db.user_id
27492752
AND lower(users.user_name) = lower($1)
27502753
AND is_deleted = false
2751-
AND live_db = true
2752-
ORDER BY date_created DESC`
2754+
AND live_db = true`
2755+
2756+
switch public {
2757+
case DB_PUBLIC:
2758+
// Only public databases
2759+
dbQuery += ` AND public = true`
2760+
case DB_PRIVATE:
2761+
// Only private databases
2762+
dbQuery += ` AND public = false`
2763+
case DB_BOTH:
2764+
// Both public and private, so no need to add a query clause
2765+
default:
2766+
// This clause shouldn't ever be reached
2767+
return nil, fmt.Errorf("Incorrect 'public' value '%v' passed to LiveUserDBs() function.", public)
2768+
}
2769+
dbQuery += " ORDER BY date_created DESC"
2770+
2771+
27532772
rows, err := pdb.Query(dbQuery, dbOwner)
27542773
if err != nil {
27552774
log.Printf("Database query failed: %v", err)
@@ -2758,11 +2777,22 @@ func LiveUserDBs(dbOwner string) (list []DBInfo, err error) {
27582777
defer rows.Close()
27592778
for rows.Next() {
27602779
var oneRow DBInfo
2761-
err = rows.Scan(&oneRow.Database, &oneRow.DateCreated, &oneRow.Public)
2780+
var liveNode string
2781+
err = rows.Scan(&oneRow.Database, &oneRow.DateCreated, &oneRow.RepoModified, &oneRow.Public, &oneRow.IsLive, &liveNode,
2782+
&oneRow.Watchers, &oneRow.Stars, &oneRow.Discussions, &oneRow.Contributors,
2783+
&oneRow.OneLineDesc, &oneRow.SourceURL, &oneRow.Downloads, &oneRow.Views)
27622784
if err != nil {
27632785
log.Printf("Error when retrieving list of live databases for user '%s': %v", dbOwner, err)
27642786
return nil, err
27652787
}
2788+
2789+
// Ask the AMQP backend for the database file size
2790+
oneRow.Size, err = LiveSize(liveNode, dbOwner, dbOwner, oneRow.Database)
2791+
if err != nil {
2792+
log.Printf("Error when retrieving size of live databases for user '%s': %v", dbOwner, err)
2793+
return nil, err
2794+
}
2795+
27662796
list = append(list, oneRow)
27672797
}
27682798
return

cypress/e2e/1-webui/database_sharing.cy.js

+13-15
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,16 @@ describe('database sharing', () => {
150150
cy.visit("default")
151151

152152
// Ensure the standard test databases are listed on the profile page where appropriate
153-
cy.get('[data-cy="sharedwithyoutbl"').should('not.contain', 'first/Assembly Election 2017.sqlite')
154-
cy.get('[data-cy="sharedwithyoutbl"').should('contain', 'second/Assembly Election 2017.sqlite')
155-
cy.get('[data-cy="sharedwithyoutbl"').should('not.contain', 'third/Assembly Election 2017.sqlite')
153+
cy.get('[data-cy="sharedwithyou"]').should('not.contain', 'first / Assembly Election 2017.sqlite')
154+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'second / Assembly Election 2017.sqlite')
155+
cy.get('[data-cy="sharedwithyou"]').should('not.contain', 'third / Assembly Election 2017.sqlite')
156156

157157
// Ensure the live test database is listed correctly in the "Databases shared with you" section
158-
cy.get('[data-cy="sharedwithyoutbl"').should('contain', 'first/Join Testing with index.sqlite')
159-
cy.get('[data-cy="swuperm-row0"').should('contain', 'Read Write')
160-
cy.get('[data-cy="swulive-row0"').should('contain', 'live database')
158+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'first / Join Testing with index.sqlite')
159+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'Read Write')
161160

162161
// Ensure the live test database is listed correctly in the "Databases shared with others" section
163-
cy.get('[data-cy="sharedwithotherstbl"').should('contain', 'Join Testing with index.sqlite')
164-
cy.get('[data-cy="swolive-Join Testing with index.sqlite-row1"').should('contain', 'live database')
162+
cy.get('[data-cy="sharedwithothers"]').should('contain', 'Join Testing with index.sqlite')
165163

166164
// Ensure trying to load the test databases only works where appropriate
167165
cy.visit('default/Assembly%20Election%202017.sqlite')
@@ -180,9 +178,9 @@ describe('database sharing', () => {
180178
cy.visit("first")
181179

182180
// Ensure the other test databases are only listed on the profile page where appropriate
183-
cy.get('[data-cy="sharedwithyoutbl"').should('not.contain', 'default/Assembly Election 2017.sqlite')
184-
cy.get('[data-cy="sharedwithyoutbl"').should('contain', 'second/Assembly Election 2017.sqlite')
185-
cy.get('[data-cy="sharedwithyoutbl"').should('contain', 'third/Assembly Election 2017.sqlite')
181+
cy.get('[data-cy="sharedwithyou"]').should('not.contain', 'default / Assembly Election 2017.sqlite')
182+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'second / Assembly Election 2017.sqlite')
183+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'third / Assembly Election 2017.sqlite')
186184

187185
// Ensure trying to load the other test databases only works where appropriate
188186
cy.visit('second/Assembly%20Election%202017.sqlite')
@@ -198,8 +196,8 @@ describe('database sharing', () => {
198196
cy.visit("second")
199197

200198
// Ensure the other test databases are only listed on the profile page where appropriate
201-
cy.get('[data-cy="sharedwithyoutbl"').should('contain', 'first/Assembly Election 2017.sqlite')
202-
cy.get('[data-cy="sharedwithyoutbl"').should('contain', 'third/Assembly Election 2017.sqlite')
199+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'first / Assembly Election 2017.sqlite')
200+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'third / Assembly Election 2017.sqlite')
203201

204202
// Ensure trying to load the other test databases only works where appropriate
205203
cy.visit('first/Assembly%20Election%202017.sqlite')
@@ -215,8 +213,8 @@ describe('database sharing', () => {
215213
cy.visit("third")
216214

217215
// Ensure the other test databases are only listed on the profile page where appropriate
218-
cy.get('[data-cy="sharedwithyoutbl"').should('contain', 'first/Assembly Election 2017.sqlite')
219-
cy.get('[data-cy="sharedwithyoutbl"').should('not.contain', 'second/Assembly Election 2017.sqlite')
216+
cy.get('[data-cy="sharedwithyou"]').should('contain', 'first / Assembly Election 2017.sqlite')
217+
cy.get('[data-cy="sharedwithyou"]').should('not.contain', 'second / Assembly Election 2017.sqlite')
220218

221219
// Ensure trying to load the other test databases only works where appropriate
222220
cy.visit('first/Assembly%20Election%202017.sqlite')

cypress/e2e/1-webui/logged-in-user-profile-page.cy.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('logged-in user profile page', () => {
1818

1919
// Confirm test database has the expected details
2020
it('assembly election 2017 database has expected details', () => {
21-
cy.get('[data-cy="pubexpand"]').click()
21+
cy.get('[data-cy="pubdbs"]').find(".fa-plus").click()
2222
cy.get('[data-cy="pubdbs"]').contains('Source:').next().should('contain', 'http://data.nicva.org/dataset/assembly-election-2017')
2323
cy.get('[data-cy="pubdbs"]').contains('Size:').next().should('contain', '72 KB')
2424
cy.get('[data-cy="pubdbs"]').contains('Contributors:').next().should('contain', '1')
@@ -54,7 +54,7 @@ describe('logged-in user profile page', () => {
5454

5555
// Confirm the details for the test database are now showing up correctly in the private list
5656
it('private database has expected details', () => {
57-
cy.get('[data-cy="privexpand"]').click()
57+
cy.get('[data-cy="privdbs"]').find(".fa-plus").first().click()
5858
cy.get('[data-cy="privdbs"]').contains('Source:').next().should('contain', 'http://data.nicva.org/dataset/assembly-election-2017')
5959
cy.get('[data-cy="privdbs"]').contains('Size:').next().should('contain', '72 KB')
6060
cy.get('[data-cy="privdbs"]').contains('Contributors:').next().should('contain', '1')
@@ -146,4 +146,4 @@ describe('logged-in user profile page', () => {
146146
cy.readFile(cert, { timeout: 10000 }).should('have.length.gt', 512)
147147
cy.task('rmFile', { path: cert })
148148
})
149-
})
149+
})

webui/jsx/app.js

+9
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import DiscussionComments from "./discussion-comments";
1515
import DiscussionCreateMr from "./discussion-create-mr";
1616
import DiscussionList from "./discussion-list";
1717
import MarkdownEditor from "./markdown-editor";
18+
import ProfilePage from "./profile-page";
1819
import UserPage from "./user-page";
1920

2021
{
@@ -157,3 +158,11 @@ import UserPage from "./user-page";
157158
root.render(<UserPage />);
158159
}
159160
}
161+
162+
{
163+
const rootNode = document.getElementById("profile-page");
164+
if (rootNode) {
165+
const root = ReactDOM.createRoot(rootNode);
166+
root.render(<ProfilePage />);
167+
}
168+
}

webui/jsx/profile-page.js

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
const React = require("react");
2+
const ReactDOM = require("react-dom");
3+
4+
import {DatabasePanelGroup} from "./user-page";
5+
6+
function WatchPanel({data, dateText}) {
7+
const [isExpanded, setExpanded] = React.useState(false);
8+
9+
return (
10+
<div className="panel panel-default">
11+
<div className="panel-heading">
12+
<h3 className="panel-title">
13+
<a className="blackLink" href={"/" + data.Owner}>{data.Owner}</a>&nbsp;/&nbsp;<a className="blackLink" href={"/" + data.Owner + "/" + data.DBName}>{data.DBName}</a>
14+
<span className="pull-right">
15+
<a href="#/" className="blackLink" onClick={() => setExpanded(!isExpanded)}><i className={isExpanded ? "fa fa-minus" : "fa fa-plus"}></i></a>
16+
</span>
17+
</h3>
18+
</div>
19+
{isExpanded ? (<>
20+
<div className="panel-body">
21+
<p>
22+
<strong>{dateText}: </strong><span className="text-info" title={new Date(data.DateEntry).toLocaleString()}>{getTimePeriod(data.DateEntry, false)}</span>
23+
</p>
24+
</div>
25+
</>) : null}
26+
</div>
27+
);
28+
}
29+
30+
function WatchPanelGroup({title, noDatabasesMessage, databases, dateText}) {
31+
const databaseRows = databases === null ? null : databases.map(d => WatchPanel({data: d, dateText: dateText}));
32+
33+
return (<>
34+
<h3>{title}</h3>
35+
{databaseRows ? databaseRows : (<h4><em>{noDatabasesMessage}</em></h4>)}
36+
</>);
37+
}
38+
39+
function SharedWithYouPanel({data}) {
40+
return (
41+
<div className="panel panel-default">
42+
<div className="panel-heading">
43+
<h3 className="panel-title">
44+
<a className="blackLink" href={"/" + data.owner_name + "/" + data.database_name}>{data.owner_name} / {data.database_name}</a>: {data.permission === "rw" ? "Read Write" : "Read Only"}
45+
</h3>
46+
</div>
47+
</div>
48+
);
49+
}
50+
51+
function SharedWithYouPanelGroup({databases}) {
52+
const databaseRows = databases === null ? null : databases.map(d => SharedWithYouPanel({data: d}));
53+
54+
return (<>
55+
<h3>Databases shared with you</h3>
56+
{databaseRows ? databaseRows : (<h4><em>No databases shared with you yet</em></h4>)}
57+
</>);
58+
}
59+
60+
function SharedWithOthersPanel({data}) {
61+
const [isExpanded, setExpanded] = React.useState(false);
62+
63+
let permissionRows = [];
64+
for (const [user, perm] of Object.entries(data.user_permissions)) {
65+
permissionRows.push(<tr><td><a href={"/" + user} className="blackLink">{user}</a></td><td>{perm === "rw" ? "Read Write" : "Read Only"}</td></tr>);
66+
}
67+
68+
return (
69+
<div className="panel panel-default">
70+
<div className="panel-heading">
71+
<h3 className="panel-title">
72+
<a className="blackLink" href={"/settings/" + authInfo.loggedInUser + "/" + data.database_name}><i className="fa fa-cog"></i></a>&nbsp;
73+
<a className="blackLink" href={"/" + authInfo.loggedInUser + "/" + data.database_name}>{data.database_name}</a>
74+
<span className="pull-right">
75+
<a href="#/" className="blackLink" onClick={() => setExpanded(!isExpanded)}><i className={isExpanded ? "fa fa-minus" : "fa fa-plus"}></i></a>
76+
</span>
77+
</h3>
78+
</div>
79+
{isExpanded ? (<>
80+
<table className="table">
81+
<thead>
82+
<tr><th>User</th><th>Permission</th></tr>
83+
</thead>
84+
<tbody>
85+
{permissionRows}
86+
</tbody>
87+
</table>
88+
</>) : null}
89+
</div>
90+
);
91+
}
92+
93+
function SharedWithOthersPanelGroup({databases}) {
94+
const databaseRows = databases === null ? null : databases.map(d => SharedWithOthersPanel({data: d}));
95+
96+
return (<>
97+
<h3>Databases shared with others</h3>
98+
{databaseRows ? databaseRows : (<h4><em>No databases shared with others yet</em></h4>)}
99+
</>);
100+
}
101+
102+
export default function ProfilePage() {
103+
return (<>
104+
<h2>
105+
{authInfo.avatarUrl ? <img src={authInfo.avatarUrl} height="48" width="48" style={{border: "1px solid #8c8c8c"}} /> : null}&nbsp;Your page
106+
</h2>
107+
<div className="row">
108+
<div className="col-md-12">
109+
<a className="btn btn-success" href="/upload/" data-cy="uploadbtn">Upload database</a>&nbsp;
110+
<a className="btn btn-primary" href="/x/gencert" role="button" data-cy="gencertbtn">Generate client certificate</a>
111+
</div>
112+
</div>
113+
<div className="row">
114+
<div className="col-md-6" data-cy="pubdbs">
115+
<DatabasePanelGroup title="Public standard databases" noDatabasesMessage="No public standard databases yet" databases={userData.publicDbs} username={authInfo.loggedInUser} />
116+
</div>
117+
<div className="col-md-6" data-cy="privdbs">
118+
<DatabasePanelGroup title="Private standard databases" noDatabasesMessage="No private standard databases yet" databases={userData.privateDbs} username={authInfo.loggedInUser} />
119+
</div>
120+
</div>
121+
<div className="row">
122+
<div className="col-md-6">
123+
<DatabasePanelGroup title="Public live databases" noDatabasesMessage="No public live databases yet" databases={userData.publicLiveDbs} username={authInfo.loggedInUser} />
124+
</div>
125+
<div className="col-md-6">
126+
<DatabasePanelGroup title="Private live databases" noDatabasesMessage="No private live databases yet" databases={userData.privateLiveDbs} username={authInfo.loggedInUser} />
127+
</div>
128+
</div>
129+
<div className="row">
130+
<div className="col-md-6" data-cy="stars">
131+
<WatchPanelGroup title="Databases you've starred" noDatabasesMessage="No starred databases yet" databases={userData.starredDbs} dateText="Starred" />
132+
</div>
133+
<div className="col-md-6" data-cy="watches">
134+
<WatchPanelGroup title="Datebases you're watching" noDatabasesMessage="Not watching any databases yet" databases={userData.watchedDbs} dateText="Started watching" />
135+
</div>
136+
</div>
137+
<div className="row">
138+
<div className="col-md-6" data-cy="sharedwithyou">
139+
<SharedWithYouPanelGroup databases={userData.sharedWithYouDbs} />
140+
</div>
141+
<div className="col-md-6" data-cy="sharedwithothers">
142+
<SharedWithOthersPanelGroup databases={userData.sharedWithOthersDbs} />
143+
</div>
144+
</div>
145+
</>);
146+
}

0 commit comments

Comments
 (0)