Skip to content

Commit

Permalink
webui: Migrate profile page to React
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
MKleusberg committed Apr 26, 2023
1 parent 938c738 commit 5c78e52
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 476 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ webui/js/discussion-comments.js
webui/js/discussion-create-mr.js
webui/js/discussion-list.js
webui/js/markdown-editor.js
webui/js/profile-page.js
webui/js/user-page.js

# Local secrets
Expand Down
2 changes: 1 addition & 1 deletion api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func databasesHandler(w http.ResponseWriter, r *http.Request) {
}
} else {
// Get the list of live databases
databases, err = com.LiveUserDBs(loggedInUser)
databases, err = com.LiveUserDBs(loggedInUser, com.DB_BOTH)
if err != nil {
jsonErr(w, err.Error(), http.StatusInternalServerError)
return
Expand Down
9 changes: 0 additions & 9 deletions common/live_types.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package common

import (
"time"

sqlite "github.com/gwenn/gosqlite"
)

Expand Down Expand Up @@ -79,13 +77,6 @@ type LiveDBRowsResponse struct {
Error string `json:"error"`
}

// LiveDBs is used for general purpose holding of details about live databases
type LiveDBs struct {
DBOwner string `json:"owner_name"`
DBName string `json:"database_name"`
DateCreated time.Time `json:"date_created"`
}

// LiveDBSizeResponse holds the fields used for receiving database size responses from our AMQP backend
type LiveDBSizeResponse struct {
Node string `json:"node"`
Expand Down
40 changes: 35 additions & 5 deletions common/postgresql.go
Original file line number Diff line number Diff line change
Expand Up @@ -2741,15 +2741,34 @@ func LiveGetMinioNames(loggedInUser, dbOwner, dbName string) (bucketName, object
}

// LiveUserDBs returns the list of live databases owned by the user
func LiveUserDBs(dbOwner string) (list []DBInfo, err error) {
func LiveUserDBs(dbOwner string, public AccessType) (list []DBInfo, err error) {
dbQuery := `
SELECT db_name, date_created, public
SELECT db_name, date_created, last_modified, public, live_db, live_node,
db.watchers, db.stars, discussions, contributors,
coalesce(one_line_description, ''), coalesce(source_url, ''),
download_count, page_views
FROM sqlite_databases AS db, users
WHERE users.user_id = db.user_id
AND lower(users.user_name) = lower($1)
AND is_deleted = false
AND live_db = true
ORDER BY date_created DESC`
AND live_db = true`

switch public {
case DB_PUBLIC:
// Only public databases
dbQuery += ` AND public = true`
case DB_PRIVATE:
// Only private databases
dbQuery += ` AND public = false`
case DB_BOTH:
// Both public and private, so no need to add a query clause
default:
// This clause shouldn't ever be reached
return nil, fmt.Errorf("Incorrect 'public' value '%v' passed to LiveUserDBs() function.", public)
}
dbQuery += " ORDER BY date_created DESC"


rows, err := pdb.Query(dbQuery, dbOwner)
if err != nil {
log.Printf("Database query failed: %v", err)
Expand All @@ -2758,11 +2777,22 @@ func LiveUserDBs(dbOwner string) (list []DBInfo, err error) {
defer rows.Close()
for rows.Next() {
var oneRow DBInfo
err = rows.Scan(&oneRow.Database, &oneRow.DateCreated, &oneRow.Public)
var liveNode string
err = rows.Scan(&oneRow.Database, &oneRow.DateCreated, &oneRow.RepoModified, &oneRow.Public, &oneRow.IsLive, &liveNode,
&oneRow.Watchers, &oneRow.Stars, &oneRow.Discussions, &oneRow.Contributors,
&oneRow.OneLineDesc, &oneRow.SourceURL, &oneRow.Downloads, &oneRow.Views)
if err != nil {
log.Printf("Error when retrieving list of live databases for user '%s': %v", dbOwner, err)
return nil, err
}

// Ask the AMQP backend for the database file size
oneRow.Size, err = LiveSize(liveNode, dbOwner, dbOwner, oneRow.Database)
if err != nil {
log.Printf("Error when retrieving size of live databases for user '%s': %v", dbOwner, err)
return nil, err
}

list = append(list, oneRow)
}
return
Expand Down
28 changes: 13 additions & 15 deletions cypress/e2e/1-webui/database_sharing.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,16 @@ describe('database sharing', () => {
cy.visit("default")

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

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

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

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

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

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

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

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

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

// Ensure trying to load the other test databases only works where appropriate
cy.visit('first/Assembly%20Election%202017.sqlite')
Expand Down
6 changes: 3 additions & 3 deletions cypress/e2e/1-webui/logged-in-user-profile-page.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('logged-in user profile page', () => {

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

// Confirm the details for the test database are now showing up correctly in the private list
it('private database has expected details', () => {
cy.get('[data-cy="privexpand"]').click()
cy.get('[data-cy="privdbs"]').find(".fa-plus").first().click()
cy.get('[data-cy="privdbs"]').contains('Source:').next().should('contain', 'http://data.nicva.org/dataset/assembly-election-2017')
cy.get('[data-cy="privdbs"]').contains('Size:').next().should('contain', '72 KB')
cy.get('[data-cy="privdbs"]').contains('Contributors:').next().should('contain', '1')
Expand Down Expand Up @@ -146,4 +146,4 @@ describe('logged-in user profile page', () => {
cy.readFile(cert, { timeout: 10000 }).should('have.length.gt', 512)
cy.task('rmFile', { path: cert })
})
})
})
9 changes: 9 additions & 0 deletions webui/jsx/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import DiscussionComments from "./discussion-comments";
import DiscussionCreateMr from "./discussion-create-mr";
import DiscussionList from "./discussion-list";
import MarkdownEditor from "./markdown-editor";
import ProfilePage from "./profile-page";
import UserPage from "./user-page";

{
Expand Down Expand Up @@ -157,3 +158,11 @@ import UserPage from "./user-page";
root.render(<UserPage />);
}
}

{
const rootNode = document.getElementById("profile-page");
if (rootNode) {
const root = ReactDOM.createRoot(rootNode);
root.render(<ProfilePage />);
}
}
146 changes: 146 additions & 0 deletions webui/jsx/profile-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
const React = require("react");
const ReactDOM = require("react-dom");

import {DatabasePanelGroup} from "./user-page";

function WatchPanel({data, dateText}) {
const [isExpanded, setExpanded] = React.useState(false);

return (
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">
<a className="blackLink" href={"/" + data.Owner}>{data.Owner}</a>&nbsp;/&nbsp;<a className="blackLink" href={"/" + data.Owner + "/" + data.DBName}>{data.DBName}</a>
<span className="pull-right">
<a href="#/" className="blackLink" onClick={() => setExpanded(!isExpanded)}><i className={isExpanded ? "fa fa-minus" : "fa fa-plus"}></i></a>
</span>
</h3>
</div>
{isExpanded ? (<>
<div className="panel-body">
<p>
<strong>{dateText}: </strong><span className="text-info" title={new Date(data.DateEntry).toLocaleString()}>{getTimePeriod(data.DateEntry, false)}</span>
</p>
</div>
</>) : null}
</div>
);
}

function WatchPanelGroup({title, noDatabasesMessage, databases, dateText}) {
const databaseRows = databases === null ? null : databases.map(d => WatchPanel({data: d, dateText: dateText}));

return (<>
<h3>{title}</h3>
{databaseRows ? databaseRows : (<h4><em>{noDatabasesMessage}</em></h4>)}
</>);
}

function SharedWithYouPanel({data}) {
return (
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">
<a className="blackLink" href={"/" + data.owner_name + "/" + data.database_name}>{data.owner_name} / {data.database_name}</a>: {data.permission === "rw" ? "Read Write" : "Read Only"}
</h3>
</div>
</div>
);
}

function SharedWithYouPanelGroup({databases}) {
const databaseRows = databases === null ? null : databases.map(d => SharedWithYouPanel({data: d}));

return (<>
<h3>Databases shared with you</h3>
{databaseRows ? databaseRows : (<h4><em>No databases shared with you yet</em></h4>)}
</>);
}

function SharedWithOthersPanel({data}) {
const [isExpanded, setExpanded] = React.useState(false);

let permissionRows = [];
for (const [user, perm] of Object.entries(data.user_permissions)) {
permissionRows.push(<tr><td><a href={"/" + user} className="blackLink">{user}</a></td><td>{perm === "rw" ? "Read Write" : "Read Only"}</td></tr>);
}

return (
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">
<a className="blackLink" href={"/settings/" + authInfo.loggedInUser + "/" + data.database_name}><i className="fa fa-cog"></i></a>&nbsp;
<a className="blackLink" href={"/" + authInfo.loggedInUser + "/" + data.database_name}>{data.database_name}</a>
<span className="pull-right">
<a href="#/" className="blackLink" onClick={() => setExpanded(!isExpanded)}><i className={isExpanded ? "fa fa-minus" : "fa fa-plus"}></i></a>
</span>
</h3>
</div>
{isExpanded ? (<>
<table className="table">
<thead>
<tr><th>User</th><th>Permission</th></tr>
</thead>
<tbody>
{permissionRows}
</tbody>
</table>
</>) : null}
</div>
);
}

function SharedWithOthersPanelGroup({databases}) {
const databaseRows = databases === null ? null : databases.map(d => SharedWithOthersPanel({data: d}));

return (<>
<h3>Databases shared with others</h3>
{databaseRows ? databaseRows : (<h4><em>No databases shared with others yet</em></h4>)}
</>);
}

export default function ProfilePage() {
return (<>
<h2>
{authInfo.avatarUrl ? <img src={authInfo.avatarUrl} height="48" width="48" style={{border: "1px solid #8c8c8c"}} /> : null}&nbsp;Your page
</h2>
<div className="row">
<div className="col-md-12">
<a className="btn btn-success" href="/upload/" data-cy="uploadbtn">Upload database</a>&nbsp;
<a className="btn btn-primary" href="/x/gencert" role="button" data-cy="gencertbtn">Generate client certificate</a>
</div>
</div>
<div className="row">
<div className="col-md-6" data-cy="pubdbs">
<DatabasePanelGroup title="Public standard databases" noDatabasesMessage="No public standard databases yet" databases={userData.publicDbs} username={authInfo.loggedInUser} />
</div>
<div className="col-md-6" data-cy="privdbs">
<DatabasePanelGroup title="Private standard databases" noDatabasesMessage="No private standard databases yet" databases={userData.privateDbs} username={authInfo.loggedInUser} />
</div>
</div>
<div className="row">
<div className="col-md-6">
<DatabasePanelGroup title="Public live databases" noDatabasesMessage="No public live databases yet" databases={userData.publicLiveDbs} username={authInfo.loggedInUser} />
</div>
<div className="col-md-6">
<DatabasePanelGroup title="Private live databases" noDatabasesMessage="No private live databases yet" databases={userData.privateLiveDbs} username={authInfo.loggedInUser} />
</div>
</div>
<div className="row">
<div className="col-md-6" data-cy="stars">
<WatchPanelGroup title="Databases you've starred" noDatabasesMessage="No starred databases yet" databases={userData.starredDbs} dateText="Starred" />
</div>
<div className="col-md-6" data-cy="watches">
<WatchPanelGroup title="Datebases you're watching" noDatabasesMessage="Not watching any databases yet" databases={userData.watchedDbs} dateText="Started watching" />
</div>
</div>
<div className="row">
<div className="col-md-6" data-cy="sharedwithyou">
<SharedWithYouPanelGroup databases={userData.sharedWithYouDbs} />
</div>
<div className="col-md-6" data-cy="sharedwithothers">
<SharedWithOthersPanelGroup databases={userData.sharedWithOthersDbs} />
</div>
</div>
</>);
}
Loading

3 comments on commit 5c78e52

@justinclift
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now live in production. So @chrisjlocke can do screenshots / video / etc of this stuff now. 😄

@justinclift
Copy link
Member

@justinclift justinclift commented on 5c78e52 Apr 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, just noticed what looks like a bug. Not sure if it's been hanging around for a while, or if it's a new thing though.

Working behaviour -> On this live visualisation page, when logged in the visualisation names in the drop down are correct. eg ascending order, and descending order

Broken behaviour -> Viewing that same page when logged out however, and the drop down visualisation list shows default as selected (which doesn't exist), and won't allow choosing any of the saved visualisations.

I'll try looking into it in a bit.

@justinclift
Copy link
Member

@justinclift justinclift commented on 5c78e52 Apr 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, found the problem. Was introduced in my commit the other day that hides the save/delete buttons for logged out users. 🤦

Now fixed, and added a Cypress test for it. 😉

Please sign in to comment.