Skip to content

Commit 6ce7de6

Browse files
authored
Taiga: Too easy to clobber contents of a dataset with old version (#15)
This adds warnings in all of the following scenarios: - Creating a new version based on a version older than latest - Creating a new version based on a deprecated or deleted version - Attempting to upload a version that is no longer the latest - While the first two are checked as soon as you click "create new version," this one is checked at upload time. It warns only if a new version has been created at some point after the page was loaded.
2 parents 199d579 + 66efdb1 commit 6ce7de6

File tree

11 files changed

+473
-71
lines changed

11 files changed

+473
-71
lines changed

react_frontend/src/components/DatasetView.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import * as Models from "../models/models";
1212
import { TaigaApi } from "../models/api";
1313

1414
import * as Dialogs from "./Dialogs";
15-
import { CreateVersionDialog } from "./modals/UploadDialogs";
15+
import { CreateVersionDialog } from "./modals/UploadDialogs";
1616

1717
import { toLocalDateString } from "../utilities/formats";
1818
import { LoadingOverlay } from "../utilities/loading";
@@ -39,6 +39,8 @@ import {
3939
import ClipboardButton from "../utilities/r-clipboard";
4040
import { UploadTracker } from "./UploadTracker";
4141
import FigshareSection from "./dataset_view/FigshareSection";
42+
import confirmUserWantsToBranchVersion from "./dataset_view/confirmUserWantsToBranchVersion";
43+
import confirmNewVersionFromDeprecated from "./dataset_view/confirmNewVersionFromDeprecated";
4244

4345
interface DatasetViewMatchParams {
4446
datasetId: string;
@@ -388,10 +390,30 @@ export class DatasetView extends React.Component<
388390
}
389391

390392
// Upload
391-
showUploadNewVersion() {
392-
this.setState({
393-
showUploadDataset: true,
394-
});
393+
async showUploadNewVersion() {
394+
const { dataset, datasetVersion } = this.state;
395+
const latestVersion = dataset.versions[dataset.versions.length - 1];
396+
let confirmed = true;
397+
398+
if (datasetVersion.id !== latestVersion.id) {
399+
confirmed = await confirmUserWantsToBranchVersion(
400+
dataset,
401+
datasetVersion,
402+
);
403+
} else if (["deprecated", "deleted"].includes(datasetVersion.state)) {
404+
confirmed = await confirmNewVersionFromDeprecated(datasetVersion);
405+
}
406+
407+
if (confirmed) {
408+
this.setState({ showUploadDataset: true });
409+
}
410+
}
411+
412+
async checkConcurrentEdit() {
413+
const datasetThen = this.state.dataset;
414+
const datasetNow = await this.getTapi().get_dataset(datasetThen.id);
415+
416+
return datasetThen.versions.length < datasetNow.versions.length;
395417
}
396418

397419
// filesUploadedAndConverted(sid: string, name: string, description: string, previousDatafileIds: Array<string>) {
@@ -1194,6 +1216,7 @@ export class DatasetView extends React.Component<
11941216
previousVersionNumber={this.state.datasetVersion.name}
11951217
previousVersionFiles={this.state.datasetVersion.datafiles}
11961218
datasetPermaname={this.state.dataset.permanames[0]}
1219+
checkConcurrentEdit={this.checkConcurrentEdit.bind(this)}
11971220
/>
11981221
)}
11991222

react_frontend/src/components/Dialogs.tsx

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ export class EditName extends React.Component<EditStringProps, any> {
9191
onRequestClose={this.props.cancel}
9292
contentLabel="EditName"
9393
>
94-
<form>
94+
<form
95+
onSubmit={e => {
96+
formSubmitSave(this, e, this.textInput.value, null);
97+
}}
98+
>
9599
<div className="modal-content">
96100
<div className="modal-body">
97101
<div className="form-group">
@@ -115,13 +119,7 @@ export class EditName extends React.Component<EditStringProps, any> {
115119
>
116120
Close
117121
</button>
118-
<button
119-
type="submit"
120-
className="btn btn-primary"
121-
onClick={(e) => {
122-
formSubmitSave(this, e, this.textInput.value, null);
123-
}}
124-
>
122+
<button type="submit" className="btn btn-primary">
125123
Save changes
126124
</button>
127125
</div>
@@ -145,7 +143,11 @@ export class EditDescription extends React.Component<EditStringProps, any> {
145143
onRequestClose={this.props.cancel}
146144
contentLabel="EditDescription"
147145
>
148-
<form>
146+
<form
147+
onSubmit={e => {
148+
formSubmitSave(this, e, this.textArea.value, null);
149+
}}
150+
>
149151
<div className="modal-content">
150152
<div className="modal-body">
151153
<div className="form-group">
@@ -181,13 +183,7 @@ export class EditDescription extends React.Component<EditStringProps, any> {
181183
>
182184
Close
183185
</button>
184-
<button
185-
type="submit"
186-
className="btn btn-primary"
187-
onClick={(e) => {
188-
formSubmitSave(this, e, this.textArea.value, null);
189-
}}
190-
>
186+
<button type="submit" className="btn btn-primary">
191187
Save changes
192188
</button>
193189
</div>
@@ -211,7 +207,11 @@ export class CreateFolder extends React.Component<CreateFolderProps, any> {
211207
onRequestClose={this.props.cancel}
212208
contentLabel="CreateFolder"
213209
>
214-
<form>
210+
<form
211+
onSubmit={e => {
212+
formSubmitSave(this, e, this.textInput.value, this.textArea.value);
213+
}}
214+
>
215215
<div className="modal-content">
216216
<div className="modal-body">
217217
<div className="form-group">
@@ -246,18 +246,7 @@ export class CreateFolder extends React.Component<CreateFolderProps, any> {
246246
>
247247
Close
248248
</button>
249-
<button
250-
type="submit"
251-
className="btn btn-primary"
252-
onClick={(e) => {
253-
formSubmitSave(
254-
this,
255-
e,
256-
this.textInput.value,
257-
this.textArea.value
258-
);
259-
}}
260-
>
249+
<button type="submit" className="btn btn-primary">
261250
Save changes
262251
</button>
263252
</div>
@@ -498,7 +487,12 @@ export class DeprecationReason extends React.Component<
498487
<div className="modal-header">
499488
<h2 ref="subtitle">Reason to deprecate this dataset version</h2>
500489
</div>
501-
<form>
490+
<form
491+
onSubmit={e => {
492+
e.preventDefault();
493+
this.save();
494+
}}
495+
>
502496
<div className="modal-body">
503497
<input
504498
type="text"
@@ -517,9 +511,9 @@ export class DeprecationReason extends React.Component<
517511
Close
518512
</button>
519513
<button
520-
type="button"
514+
type="submit"
521515
className="btn btn-primary"
522-
onClick={(e) => this.save()}
516+
disabled={!this.state.reason}
523517
>
524518
Save changes
525519
</button>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@use "../../styles/mixins";
2+
3+
.tableContainer {
4+
min-height: 150px;
5+
max-height: calc(100vh - 318px);
6+
overflow: auto;
7+
font-size: 16px;
8+
@include mixins.scroll-shadow;
9+
10+
table {
11+
width: 100%;
12+
13+
thead {
14+
position: sticky;
15+
top: 0;
16+
background-color: #fff;
17+
outline: 2px solid #ccc;
18+
z-index: 1;
19+
}
20+
21+
tr td {
22+
padding: 5px;
23+
border-bottom: 2px solid #eee;
24+
}
25+
}
26+
}
27+
28+
.deprecated {
29+
color: orange;
30+
}
31+
32+
.deleted {
33+
color: red;
34+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from "react";
2+
import { DatasetVersion } from "../../models/models";
3+
import getConfirmation from "../../utilities/getConfirmation";
4+
5+
function confirmNewVersionFromDeprecated(datasetVersion: DatasetVersion) {
6+
const { state } = datasetVersion;
7+
8+
return getConfirmation({
9+
title: "Are you sure?",
10+
message: (
11+
<div>
12+
<p>
13+
This version is {state === "deprecated" ? "deprecated" : "deleted"}.
14+
Are you sure you want to create a new version based on it?
15+
</p>
16+
</div>
17+
),
18+
noText: "Cancel",
19+
yesText: "Create new version",
20+
yesButtonBsStyle: "warning",
21+
});
22+
}
23+
24+
export default confirmNewVersionFromDeprecated;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as React from "react";
2+
import { Dataset, DatasetVersion } from "../../models/models";
3+
import { relativePath } from "../../utilities/route";
4+
import getConfirmation from "../../utilities/getConfirmation";
5+
import styles from "./confirm.scss";
6+
7+
function confirmUserWantsToBranchVersion(
8+
dataset: Dataset,
9+
datasetVersion: DatasetVersion
10+
) {
11+
const thisVersion = Number(datasetVersion.version);
12+
const newerVersions = dataset.versions.slice(thisVersion);
13+
const permaname = dataset.permanames.slice(-1)[0];
14+
15+
return getConfirmation({
16+
title: "Are you sure?",
17+
message: (
18+
<div>
19+
<p>
20+
This is not the latest version. If you create a new version based on
21+
this one,
22+
<br />
23+
it could be missing important files that were added later.
24+
</p>
25+
<p>
26+
{newerVersions.length === 1
27+
? "This newer verision already exists:"
28+
: "These newer versions already exist:"}
29+
</p>
30+
<VersionList permaname={permaname} newerVersions={newerVersions} />
31+
</div>
32+
),
33+
noText: "Cancel",
34+
yesText: "Use this version anyway",
35+
yesButtonBsStyle: "warning",
36+
});
37+
}
38+
39+
const Icon = ({ className }: { className: string }) => (
40+
<>
41+
<i className={`glyphicon ${className}`} />{" "}
42+
</>
43+
);
44+
45+
function VersionList({
46+
newerVersions,
47+
permaname,
48+
}: {
49+
newerVersions: Dataset["versions"];
50+
permaname: string;
51+
}) {
52+
return (
53+
<div className={styles.tableContainer}>
54+
<table>
55+
<thead>
56+
<tr>
57+
<th>Version</th>
58+
<th />
59+
<th>Link</th>
60+
</tr>
61+
</thead>
62+
<tbody>
63+
{newerVersions.map(version => (
64+
<tr key={version.id}>
65+
<td>{version.name}</td>
66+
<td>
67+
{version.state === "deprecated" && (
68+
<span className={styles.deprecated}>
69+
<Icon className="glyphicon-warning-sign" />
70+
Deprecated
71+
</span>
72+
)}
73+
{version.state === "deleted" && (
74+
<span className={styles.deleted}>
75+
<Icon className="glyphicon-exclamation-sign" />
76+
Deleted
77+
</span>
78+
)}
79+
</td>
80+
<td>
81+
<a
82+
target="_blank"
83+
href={relativePath(`/dataset/${permaname}/${version.name}`)}
84+
>
85+
View <Icon className="glyphicon-new-window" />
86+
</a>
87+
</td>
88+
</tr>
89+
))}
90+
</tbody>
91+
</table>
92+
</div>
93+
);
94+
}
95+
96+
export default confirmUserWantsToBranchVersion;

0 commit comments

Comments
 (0)