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

Commit fe86f4d

Browse files
committed
webui: Migrate releases and tags pages to React
1 parent e074979 commit fe86f4d

File tree

9 files changed

+208
-410
lines changed

9 files changed

+208
-410
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ webui/js/app.js
5454
webui/js/auth.js
5555
webui/js/branches.js
5656
webui/js/database-settings.js
57+
webui/js/database-tags.js
5758
webui/js/database-view.js
5859
webui/js/database-watchers.js
5960
webui/js/db-header.js

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ describe('releases', () => {
2828
cy.get('[data-cy="nameinput"]').should('have.value', 'Some release name')
2929

3030
// Description
31-
cy.get('[data-cy="rendereddiv"]').should('contain', 'Some release description')
31+
cy.get('[data-cy="Some release name_desc-preview"]').should('contain', 'Some release description')
3232

3333
// Edit description field
34-
cy.get('[data-cy="desctext"]').should('have.value', 'Some release description')
34+
cy.get('[data-cy="Some release name_desc"]').should('have.value', 'Some release description')
3535

3636
// URL for tag creator
37-
cy.get('[data-cy="releaserlnk"]').click()
37+
cy.get('[data-cy="taggerlnk"]').click()
3838
cy.location('pathname').should('equal', '/default')
3939

4040
// URL for commit id
@@ -59,18 +59,18 @@ describe('releases', () => {
5959
// Change description text
6060
it('change release description', () => {
6161
cy.visit('releases/default/Assembly%20Election%202017.sqlite')
62-
cy.get('[data-cy="rendereddiv"]').should('contain', 'Some release description')
63-
cy.get('[data-cy="edittab"]').click()
64-
cy.get('[data-cy="desctext"]').type('{selectall}{backspace}').type('A new description').should('have.value', 'A new description')
62+
cy.get('[data-cy="Some other name_desc-preview"]').should('contain', 'Some release description')
63+
cy.get('[data-cy="Some other name_desc-edit-tab"]').click()
64+
cy.get('[data-cy="Some other name_desc"]').type('{selectall}{backspace}').type('A new description').should('have.value', 'A new description')
6565
cy.get('[data-cy="updatebtn"]').click()
6666
cy.reload()
67-
cy.get('[data-cy="rendereddiv"]').should('contain', 'A new description')
67+
cy.get('[data-cy="Some other name_desc-preview"]').should('contain', 'A new description')
6868
})
6969

7070
// Delete release
7171
it('delete release', () => {
7272
cy.visit('releases/default/Assembly%20Election%202017.sqlite')
7373
cy.get('[data-cy="delbtn"]').click()
74-
cy.get('[data-cy="norelstxt"]').should('not.have.attr', 'hidden')
74+
cy.get('[data-cy="notagstxt"]').should('not.have.attr', 'hidden')
7575
})
7676
})

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ describe('tags', () => {
2727
cy.get('[data-cy="nameinput"]').should('have.value', 'Some tag name')
2828

2929
// Description
30-
cy.get('[data-cy="rendereddiv"]').should('contain', 'Some tag description')
30+
cy.get('[data-cy="Some tag name_desc-preview"]').should('contain', 'Some tag description')
3131

32-
// Edit description tag
33-
cy.get('[data-cy="desctext"]').should('have.value', 'Some tag description')
32+
// Edit description field
33+
cy.get('[data-cy="Some tag name_desc"]').should('have.value', 'Some tag description')
3434

3535
// URL for tag creator
3636
cy.get('[data-cy="taggerlnk"]').click()
@@ -58,12 +58,12 @@ describe('tags', () => {
5858
// Change description text
5959
it('change tag description', () => {
6060
cy.visit('tags/default/Assembly%20Election%202017.sqlite')
61-
cy.get('[data-cy="rendereddiv"]').should('contain', 'Some tag description')
62-
cy.get('[data-cy="edittab"]').click()
63-
cy.get('[data-cy="desctext"]').type('{selectall}{backspace}').type('A new description').should('have.value', 'A new description')
61+
cy.get('[data-cy="Some other name_desc-preview"]').should('contain', 'Some tag description')
62+
cy.get('[data-cy="Some other name_desc-edit-tab"]').click()
63+
cy.get('[data-cy="Some other name_desc"]').type('{selectall}{backspace}').type('A new description').should('have.value', 'A new description')
6464
cy.get('[data-cy="updatebtn"]').click()
6565
cy.reload()
66-
cy.get('[data-cy="rendereddiv"]').should('contain', 'A new description')
66+
cy.get('[data-cy="Some other name_desc-preview"]').should('contain', 'A new description')
6767
})
6868

6969
// Delete tag

webui/jsx/app.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ModalImage from "react-modal-image";
66
import Auth from "./auth";
77
import BranchesTable from "./branches";
88
import DatabaseSettings from "./database-settings";
9+
import DatabaseTags from "./database-tags";
910
import DatabaseView from "./database-view";
1011
import DatabaseWatchers from "./database-watchers";
1112
import DbHeader from "./db-header";
@@ -43,6 +44,16 @@ import MarkdownEditor from "./markdown-editor";
4344
}
4445
}
4546

47+
{
48+
const rootNode = document.getElementById("database-tags");
49+
if (rootNode) {
50+
const releases = rootNode.dataset.releases;
51+
52+
const root = ReactDOM.createRoot(rootNode);
53+
root.render(<DatabaseTags releases={releases} />);
54+
}
55+
}
56+
4657
{
4758
const rootNode = document.getElementById("database-view");
4859
if (rootNode) {

webui/jsx/database-tags.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
const React = require("react");
2+
const ReactDOM = require("react-dom");
3+
4+
import MarkdownEditor from "./markdown-editor";
5+
6+
function DatabaseTagRow({name, data, releases, setStatusMessage, setStatusMessageColour}) {
7+
// This is the tag name currently shown in the front end
8+
const [tagName, setTagName] = React.useState(name);
9+
10+
// This is the tag name currently saved in the database on the server
11+
const [savedTagName, setSavedTagName] = React.useState(name);
12+
13+
// Delete the tag
14+
function deleteTag() {
15+
fetch(releases ? "/x/deleterelease/" : "/x/deletetag/" , {
16+
method: "post",
17+
headers: {
18+
"Content-Type": "application/x-www-form-urlencoded"
19+
},
20+
body: new URLSearchParams({
21+
"tag": savedTagName,
22+
"dbname": meta.database,
23+
"username": meta.owner
24+
}),
25+
}).then((response) => {
26+
if (!response.ok) {
27+
return Promise.reject(response);
28+
}
29+
30+
window.location = "/" + (releases ? "releases" : "tags") + "/" + meta.owner + "/" + meta.database;
31+
})
32+
.catch((error) => {
33+
// The delete failed, so display an error message
34+
setStatusMessageColour("red");
35+
setStatusMessage("Error: Something went wrong when trying to delete.");
36+
});
37+
}
38+
39+
// Send the update details to the server
40+
function updateTag() {
41+
let newDesc = document.getElementById(savedTagName + "_desc").value;
42+
43+
fetch(releases ? "/x/updaterelease/" : "/x/updatetag/" , {
44+
method: "post",
45+
headers: {
46+
"Content-Type": "application/x-www-form-urlencoded"
47+
},
48+
body: new URLSearchParams({
49+
"tag": savedTagName,
50+
"dbname": meta.database,
51+
"username": meta.owner,
52+
"newmsg": newDesc,
53+
"newtag": tagName,
54+
}),
55+
}).then((response) => {
56+
if (!response.ok) {
57+
return Promise.reject(response);
58+
}
59+
60+
setSavedTagName(tagName);
61+
setStatusMessageColour("green");
62+
setStatusMessage(releases ? "Release updated" : "Tag updated");
63+
})
64+
.catch((error) => {
65+
// The delete failed, so display an error message
66+
setStatusMessageColour("red");
67+
setStatusMessage(releases ? "Release update failed" : "Tag update failed");
68+
});
69+
}
70+
71+
let actionsCol = null;
72+
if (meta.owner === authInfo.loggedInUser) {
73+
actionsCol = (
74+
<td>
75+
<p><button className="btn btn-primary" onClick={() => updateTag()} data-cy="updatebtn">Update</button></p>
76+
<p><button className="btn btn-danger" onClick={() => deleteTag()} data-cy="delbtn">Delete</button></p>
77+
</td>
78+
);
79+
}
80+
81+
let nameCol = null;
82+
if (meta.owner === authInfo.loggedInUser) {
83+
nameCol = (
84+
<td>
85+
<input name={savedTagName + "_name"} id={savedTagName + "_name"} size="20" maxlength="20" value={tagName} onChange={(e) => setTagName(e.target.value)} data-cy="nameinput" />
86+
</td>
87+
);
88+
} else {
89+
nameCol = (
90+
<td>
91+
<a className="blackLink" href={"/" + meta.owner + "/" + meta.database + (releases ? "?release=" : "?tag=") + savedTagName}>{tagName}</a>
92+
</td>
93+
);
94+
}
95+
96+
return (
97+
<tr>
98+
<td>
99+
{releases ? <>
100+
<a href={"/x/download/" + meta.owner + "/" + meta.database + "?commit=" + data.commit} className="btn btn-success">Download</a>
101+
<p>{Math.round(data.size / 1024).toLocaleString()} KB</p>
102+
</> : null}
103+
</td>
104+
{actionsCol}
105+
{nameCol}
106+
<td>
107+
<MarkdownEditor editorId={savedTagName + "_desc"} rows={10} placeholder={"A description for this " + (releases ? "release" : "tag")} defaultIndex={1} initialValue={data.description} viewOnly={meta.owner !== authInfo.loggedInUser} />
108+
</td>
109+
<td>
110+
{data.avatar_url !== "" ? <img src={data.avatar_url} height="28" width="28" style={{border: "1px solid #8c8c8c"}} /> : null}&nbsp;
111+
<a className="blackLink" href={"/" + data.tagger_user_name} data-cy="taggerlnk">{data.tagger_display_name}</a>
112+
</td>
113+
<td>
114+
<span title={new Date(data.date).toLocaleString()}>{getTimePeriod(data.date, false)}</span>
115+
</td>
116+
<td>
117+
<a className="blackLink" href={"/" + meta.owner + "/" + meta.database + "?commit=" + data.commit} data-cy="commitlnk">{data.commit.substring(0, 8)}</a>
118+
</td>
119+
</tr>
120+
);
121+
}
122+
123+
export default function DatabaseTags({releases}) {
124+
const [statusMessage, setStatusMessage] = React.useState("");
125+
const [statusMessageColour, setStatusMessageColour] = React.useState("");
126+
127+
let rows = [];
128+
for (const [name, data] of Object.entries(tagsData)) {
129+
rows.push(<DatabaseTagRow name={name} data={data} releases={releases} setStatusMessage={setStatusMessage} setStatusMessageColour={setStatusMessageColour} />);
130+
}
131+
132+
if (rows.length === 0) {
133+
return <h3 data-cy="notagstxt" style={{textAlign: "center"}}>This database does not have any {releases ? "releases" : "tags"} yet</h3>;
134+
}
135+
136+
return (<>
137+
{statusMessage !== "" ? (
138+
<div className="row">
139+
<div className="col-md-12">
140+
<div style={{textAlign: "center", paddingBottom: "8px"}}>
141+
<h4 style={{color: statusMessageColour}}>{statusMessage}</h4>
142+
</div>
143+
</div>
144+
</div>
145+
) : null}
146+
<table id="contents" className="table table-striped table-responsive">
147+
<thead>
148+
<tr>
149+
<th></th>
150+
{meta.owner === authInfo.loggedInUser ? <th>Actions</th> : null}
151+
<th>Name</th><th>Description</th><th>Creator</th><th>Creation date</th><th>Commit ID</th>
152+
</tr>
153+
</thead>
154+
<tbody>
155+
{rows}
156+
</tbody>
157+
</table>
158+
</>);
159+
}

webui/main.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2341,8 +2341,8 @@ func deleteReleaseHandler(w http.ResponseWriter, r *http.Request) {
23412341
}
23422342
dbOwner := strings.ToLower(usr)
23432343

2344-
// Ensure a release name was supplied
2345-
relName, err := com.GetFormRelease(r)
2344+
// Ensure a release name was supplied in the tag parameter
2345+
relName, err := com.GetFormTag(r)
23462346
if err != nil {
23472347
w.WriteHeader(http.StatusBadRequest)
23482348
return
@@ -5238,7 +5238,7 @@ func updateReleaseHandler(w http.ResponseWriter, r *http.Request) {
52385238
dbOwner := strings.ToLower(usr)
52395239

52405240
// Validate new release name
5241-
a := r.PostFormValue("newrel")
5241+
a := r.PostFormValue("newtag")
52425242
nr, err := url.QueryUnescape(a)
52435243
if err != nil {
52445244
w.WriteHeader(http.StatusBadRequest)
@@ -5268,8 +5268,8 @@ func updateReleaseHandler(w http.ResponseWriter, r *http.Request) {
52685268
newDesc = nd
52695269
}
52705270

5271-
// Ensure a release name was supplied
5272-
relName, err := com.GetFormRelease(r)
5271+
// Ensure a release name was supplied in the tag parameter
5272+
relName, err := com.GetFormTag(r)
52735273
if err != nil {
52745274
w.WriteHeader(http.StatusBadRequest)
52755275
return

webui/pages.go

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,20 +1701,19 @@ func profilePage(w http.ResponseWriter, r *http.Request, userName string) {
17011701
// Render the releases page, which displays the releases for a database.
17021702
func releasesPage(w http.ResponseWriter, r *http.Request) {
17031703
// Structure to hold page data
1704-
type relEntry struct {
1704+
type tgEntry struct {
17051705
AvatarURL string `json:"avatar_url"`
17061706
Commit string `json:"commit"`
17071707
Date time.Time `json:"date"`
17081708
Description string `json:"description"`
1709-
DescriptionMarkdown string `json:"description_markdown"`
1710-
ReleaserUserName string `json:"releaser_user_name"`
1711-
ReleaserDisplayName string `json:"releaser_display_name"`
17121709
Size int64 `json:"size"`
1710+
TaggerUserName string `json:"tagger_user_name"`
1711+
TaggerDisplayName string `json:"tagger_display_name"`
17131712
}
17141713
var pageData struct {
17151714
DB com.SQLiteDBinfo
17161715
PageMeta PageMetaInfo
1717-
ReleaseList map[string]relEntry
1716+
TagList map[string]tgEntry
17181717
}
17191718
pageData.PageMeta.Title = "Release list"
17201719
pageData.PageMeta.PageSection = "db_data"
@@ -1754,7 +1753,7 @@ func releasesPage(w http.ResponseWriter, r *http.Request) {
17541753
userNameCache := make(map[string]userCacheEntry)
17551754

17561755
// Fill out the metadata
1757-
pageData.ReleaseList = make(map[string]relEntry)
1756+
pageData.TagList = make(map[string]tgEntry)
17581757
if len(releases) > 0 {
17591758
for i, j := range releases {
17601759
// If the username/email address entry is already in the username cache then use it, else grab it from the
@@ -1772,21 +1771,14 @@ func releasesPage(w http.ResponseWriter, r *http.Request) {
17721771
}
17731772

17741773
// Create the tag info we pass to the tag list rendering page
1775-
var r string
1776-
if j.Description == "" {
1777-
r = "No description"
1778-
} else {
1779-
r = string(gfm.Markdown([]byte(j.Description)))
1780-
}
1781-
pageData.ReleaseList[i] = relEntry{
1774+
pageData.TagList[i] = tgEntry{
17821775
AvatarURL: userNameCache[j.ReleaserEmail].AvatarURL,
17831776
Commit: j.Commit,
17841777
Date: j.Date,
17851778
Description: j.Description,
1786-
DescriptionMarkdown: r,
1787-
ReleaserUserName: userNameCache[j.ReleaserEmail].Email,
1788-
ReleaserDisplayName: j.ReleaserName,
17891779
Size: j.Size,
1780+
TaggerUserName: userNameCache[j.ReleaserEmail].Email,
1781+
TaggerDisplayName: j.ReleaserName,
17901782
}
17911783
}
17921784
}
@@ -2057,7 +2049,7 @@ func tagsPage(w http.ResponseWriter, r *http.Request) {
20572049
Commit string `json:"commit"`
20582050
Date time.Time `json:"date"`
20592051
Description string `json:"description"`
2060-
DescriptionMarkdown string `json:"description_markdown"`
2052+
Size int `json:"size"`
20612053
TaggerUserName string `json:"tagger_user_name"`
20622054
TaggerDisplayName string `json:"tagger_display_name"`
20632055
}
@@ -2122,18 +2114,11 @@ func tagsPage(w http.ResponseWriter, r *http.Request) {
21222114
}
21232115

21242116
// Create the tag info we pass to the tag list rendering page
2125-
var r string
2126-
if j.Description == "" {
2127-
r = "No description"
2128-
} else {
2129-
r = string(gfm.Markdown([]byte(j.Description)))
2130-
}
21312117
pageData.TagList[i] = tgEntry{
21322118
AvatarURL: userNameCache[j.TaggerEmail].AvatarURL,
21332119
Commit: j.Commit,
21342120
Date: j.Date,
21352121
Description: j.Description,
2136-
DescriptionMarkdown: r,
21372122
TaggerUserName: userNameCache[j.TaggerEmail].Email,
21382123
TaggerDisplayName: j.TaggerName,
21392124
}

0 commit comments

Comments
 (0)