Skip to content

Commit f81ef2f

Browse files
authored
Merge pull request #23 from broadinstitute/data-preview-26q1
Datafile preview 26q1
2 parents afb4840 + bf83df2 commit f81ef2f

16 files changed

Lines changed: 801 additions & 3 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ docker stop ministack # stop
123123
docker rm ministack # remove entirely
124124
```
125125

126+
### File uploads and the S3 copy workaround
127+
128+
> **Warning:** File uploads may fail with MiniStack because MiniStack omits the `ETag` header that boto3's high-level `Bucket.copy()` uses for validation. When `S3_ENDPOINT_URL` is set in `settings.cfg`, `aws.copy_object()` (in `taiga2/third_party_clients/aws.py`) automatically uses the low-level client API which does not require ETags. When `S3_ENDPOINT_URL` is empty or unset, the standard resource-level `Bucket.copy()` is used.
129+
>
130+
> If you see upload failures locally (errors mentioning ETag or copy validation), make sure `S3_ENDPOINT_URL` is set to your MiniStack endpoint (`http://localhost:4566`).
131+
>
132+
> **Note on tests:** The test suite does not set `S3_ENDPOINT_URL`, so tests always exercise the `Bucket.copy()` path. If you add `S3_ENDPOINT_URL` to the test config, `imp_conv_test.py` will fail because `MockS3Client` does not implement `copy_object`. This is intentional — tests validate the production (real AWS) code path.
133+
126134
### Switching back to no-S3 mode
127135
128136
Set `S3_ENDPOINT_URL = ''` and clear the AWS keys in `settings.cfg`. The app runs fine without S3 — you just can't upload files.

autoapp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from werkzeug.middleware.dispatcher import DispatcherMiddleware
1111

12-
settings_file = os.getenv("TAIGASETTINGSFILE", "settings.cfg")
12+
settings_file = os.getenv("TAIGA_SETTINGS_FILE", "settings.cfg")
1313

1414
print("Using settings from: {}".format(settings_file))
1515

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""add datafile_previews table
2+
3+
Revision ID: 495216f833f4
4+
Revises: c1be0bfabe2e
5+
Create Date: 2026-04-27 12:44:54.336754
6+
7+
"""
8+
9+
from alembic import op
10+
import sqlalchemy as sa
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "495216f833f4"
14+
down_revision = "c1be0bfabe2e"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
op.create_table(
21+
"datafile_previews",
22+
sa.Column("datafile_id", sa.String(80), sa.ForeignKey("datafiles.id"), primary_key=True),
23+
sa.Column("preview_data", sa.JSON(), nullable=True),
24+
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
25+
)
26+
27+
28+
def downgrade():
29+
op.drop_table("datafile_previews")

react_frontend/src/components/DatasetView.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import update from "immutability-helper";
77

88
import { LeftNav } from "./LeftNav";
99
import { EntryUsersPermissions } from "./modals/EntryUsersPermissions";
10+
import { PreviewModal } from "./modals/PreviewModal";
1011

1112
import * as Models from "../models/models";
1213
import { TaigaApi } from "../models/api";
@@ -94,6 +95,9 @@ export interface DatasetViewState {
9495
readableTaigaId?: string;
9596
}
9697
>;
98+
99+
previewFileId?: string;
100+
previewFileName?: string;
97101
}
98102

99103
const buttonUploadNewVersionStyle = {
@@ -1071,6 +1075,20 @@ export class DatasetView extends React.Component<
10711075
{this.state.figshareLinkedFiles && <td>{figshareLinked}</td>}
10721076
<td>{conversionTypesOutput}</td>
10731077
<td>{copy_button}</td>
1078+
<td>
1079+
<button
1080+
className="btn btn-default btn-xs"
1081+
title="Preview data"
1082+
onClick={() =>
1083+
this.setState({
1084+
previewFileId: df.id,
1085+
previewFileName: df.name,
1086+
})
1087+
}
1088+
>
1089+
<span className="glyphicon glyphicon-eye-open" />
1090+
</button>
1091+
</td>
10741092
</tr>
10751093
);
10761094
});
@@ -1363,6 +1381,7 @@ export class DatasetView extends React.Component<
13631381
)}
13641382
<th>Download</th>
13651383
<th>Datafile Id</th>
1384+
<th>Preview</th>
13661385
</tr>
13671386
</thead>
13681387
<tbody>{entries}</tbody>
@@ -1423,6 +1442,16 @@ export class DatasetView extends React.Component<
14231442
cancel={() => this.cancelDeprecation()}
14241443
save={(reason) => this.deprecateDatasetVersion(reason)}
14251444
/>
1445+
1446+
<PreviewModal
1447+
isOpen={!!this.state.previewFileId}
1448+
datafileId={this.state.previewFileId || null}
1449+
datafileName={this.state.previewFileName || null}
1450+
tapi={this.getTapi()}
1451+
onClose={() =>
1452+
this.setState({ previewFileId: undefined, previewFileName: undefined })
1453+
}
1454+
/>
14261455
</div>
14271456
);
14281457
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
.Modal {
2+
position: absolute;
3+
margin: auto;
4+
top: 30px;
5+
left: 5vw;
6+
right: 5vw;
7+
bottom: 30px;
8+
border-radius: 4px;
9+
outline: none;
10+
border: none;
11+
padding: 0;
12+
overflow: visible;
13+
max-width: 1800px;
14+
}
15+
16+
.Overlay {
17+
position: fixed;
18+
top: 0;
19+
left: 0;
20+
right: 0;
21+
bottom: 0;
22+
background-color: rgba(0, 0, 0, 0.5);
23+
z-index: 1050;
24+
}
25+
26+
.modalBody {
27+
overflow: auto;
28+
max-height: calc(100vh - 180px);
29+
padding: 15px 20px;
30+
}
31+
32+
.tableWrapper {
33+
overflow: auto;
34+
max-height: calc(100vh - 260px);
35+
border: 1px solid #ddd;
36+
}
37+
38+
.previewTable {
39+
margin-bottom: 0;
40+
font-size: 13px;
41+
white-space: nowrap;
42+
43+
th, td {
44+
padding: 4px 8px !important;
45+
border: 1px solid #ddd !important;
46+
vertical-align: middle !important;
47+
}
48+
49+
thead th {
50+
position: sticky;
51+
top: 0;
52+
background-color: #f5f5f5;
53+
z-index: 2;
54+
font-weight: 600;
55+
}
56+
}
57+
58+
.rowNameCell {
59+
position: sticky;
60+
left: 0;
61+
background-color: #f9f9f9;
62+
font-weight: 600;
63+
z-index: 1;
64+
}
65+
66+
.cornerCell {
67+
position: sticky;
68+
top: 0;
69+
left: 0;
70+
z-index: 3;
71+
background-color: #f5f5f5;
72+
}
73+
74+
.dimensionsBadge {
75+
color: #777;
76+
font-size: 13px;
77+
font-weight: normal;
78+
margin-left: 10px;
79+
}
80+
81+
.footerNote {
82+
color: #777;
83+
font-size: 12px;
84+
margin-top: 8px;
85+
font-style: italic;
86+
}
87+
88+
.loadingContainer {
89+
display: flex;
90+
justify-content: center;
91+
align-items: center;
92+
min-height: 200px;
93+
color: #777;
94+
font-size: 16px;
95+
}
96+
97+
.emptyState {
98+
display: flex;
99+
justify-content: center;
100+
align-items: center;
101+
min-height: 200px;
102+
color: #999;
103+
font-size: 15px;
104+
}

0 commit comments

Comments
 (0)