Skip to content

Commit 070bc74

Browse files
committed
Implement output file preview
1 parent 796b08d commit 070bc74

File tree

3 files changed

+196
-25
lines changed

3 files changed

+196
-25
lines changed

src/components/FilesPanel.jsx

Lines changed: 61 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@ import {
77
Card,
88
CardContent,
99
CircularProgress,
10-
Link,
10+
IconButton,
1111
Stack,
1212
Tooltip,
1313
Typography,
1414
} from "@mui/material";
1515

1616
import {
17+
Download as DownloadIcon,
1718
FolderOpen as FolderOpenIcon,
1819
InsertDriveFile as FileIcon,
19-
Info as InfoIcon,
20+
Visibility as VisibilityIcon,
2021
} from "@mui/icons-material";
2122
import Panel from "./Panel";
23+
import Preview from "./Preview";
24+
import { guessContentType } from "../utils";
2225

2326
function removePrefixFromPath(prefix, path) {
2427
return path.slice(prefix.length);
@@ -27,7 +30,7 @@ function removePrefixFromPath(prefix, path) {
2730
function formatContentType(contentType) {
2831
if (contentType) {
2932
return (
30-
<Typography variant="body2" color="gray" sx={{ lineHeight: "24px" }}>
33+
<Typography variant="body2" color="gray" sx={{ lineHeight: "24px" }} noWrap>
3134
{contentType}
3235
</Typography>
3336
);
@@ -38,24 +41,28 @@ function formatContentType(contentType) {
3841

3942
function formatFileSize(size) {
4043
if (size) {
41-
if (size < 1024) {
42-
return (
43-
<Typography variant="body2" color="gray" sx={{ lineHeight: "24px" }}>
44-
{size} bytes
45-
</Typography>
46-
);
47-
} else {
48-
return (
49-
<Typography variant="body2" color="gray" sx={{ lineHeight: "24px" }}>
50-
{(size / 1024).toFixed(1)} KiB
51-
</Typography>
52-
);
44+
let text = `${size} bytes`;
45+
if (size >= 1024) {
46+
text = `${(size / 1024).toFixed(1)} KiB`;
5347
}
48+
return (
49+
<Typography variant="body2" color="gray" sx={{ lineHeight: "24px" }} noWrap>
50+
{text}
51+
</Typography>
52+
);
5453
} else {
5554
return "";
5655
}
5756
}
5857

58+
function getContentType(contentType, url) {
59+
if (contentType) {
60+
return contentType;
61+
} else {
62+
return guessContentType(url);
63+
}
64+
}
65+
5966
function CopyButtons(props) {
6067
const submit = useSubmit();
6168
const [copying, setCopying] = useState(false);
@@ -108,6 +115,14 @@ function CopyButtons(props) {
108115
}
109116

110117
function FilesPanel(props) {
118+
const [previewTarget, setPreviewTarget] = useState("");
119+
const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
120+
121+
const handleOpenPreview = (target) => {
122+
setPreviewTarget(target);
123+
setPreviewDialogOpen(true);
124+
};
125+
111126
if (props.dataset) {
112127
return (
113128
<Panel
@@ -118,20 +133,35 @@ function FilesPanel(props) {
118133
<Typography variant="body2" sx={{ paddingTop: 2, paddingBottom: 1 }}>
119134
{props.dataset.repository}
120135
</Typography>
121-
<Stack direction="row" spacing={1}>
136+
<Stack direction="row" spacing={1} sx={{ flexWrap: "wrap", gap: 1 }}>
122137
{props.dataset.files.map((file) => (
123138
<Card key={file.path} sx={{ paddingTop: 1, backgroundColor: "#f8f8f8" }}>
124-
<CardContent sx={{ minHeight: "40px" }}>
125-
<Stack direction="row" spacing={1}>
139+
<CardContent>
140+
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
126141
<FileIcon color="disabled" />
127-
<Link href={file.url} target="_blank">
128-
{removePrefixFromPath(`/${props.collab}`, file.path)}
129-
</Link>
130-
{formatContentType(file.content_type)}
131-
{formatFileSize(file.size)}
142+
132143
<Tooltip title={`digest: ${file.hash}`}>
133-
<InfoIcon color="info" />
144+
<Typography variant="body2" color="primary" noWrap>
145+
{removePrefixFromPath(`/${props.collab}`, file.path)}
146+
</Typography>
134147
</Tooltip>
148+
149+
{formatFileSize(file.size)}
150+
151+
<Tooltip title="Download">
152+
<IconButton href={file.url} target="_blank">
153+
<DownloadIcon />
154+
</IconButton>
155+
</Tooltip>
156+
{getContentType(file.content_type, file.url) ? (
157+
<Tooltip title="Preview">
158+
<IconButton onClick={() => handleOpenPreview(file)}>
159+
<VisibilityIcon />
160+
</IconButton>
161+
</Tooltip>
162+
) : (
163+
""
164+
)}
135165
</Stack>
136166
</CardContent>
137167
</Card>
@@ -142,6 +172,13 @@ function FilesPanel(props) {
142172
collab={props.collab}
143173
jobId={props.jobId}
144174
/>
175+
<Preview
176+
url={previewTarget.url}
177+
size={previewTarget.size}
178+
contentType={previewTarget.content_type}
179+
open={previewDialogOpen}
180+
onClose={() => setPreviewDialogOpen(false)}
181+
/>
145182
</Panel>
146183
);
147184
} else {

src/components/Preview.jsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useEffect, useState } from "react";
2+
import { Box, Dialog, DialogContent, IconButton } from "@mui/material";
3+
import { Close as CloseIcon } from "@mui/icons-material";
4+
5+
import { guessContentType } from "../utils";
6+
7+
const MAX_TEXT_SIZE_FOR_DISPLAY = 10000;
8+
9+
async function getContent(url) {
10+
let response = null;
11+
try {
12+
response = await fetch(url);
13+
} catch {
14+
return "Unable to retrieve file contents";
15+
}
16+
if (response.ok) {
17+
const rawContent = await response.blob();
18+
if (rawContent) {
19+
return rawContent.text();
20+
} else {
21+
return "Unable to display file contents";
22+
}
23+
} else {
24+
console.log("Couldn't get file content");
25+
return "Unable to retrieve file contents";
26+
}
27+
}
28+
29+
async function getSize(url) {
30+
const response = await fetch(url, { method: "HEAD" });
31+
if (response.ok) {
32+
console.log(response.headers);
33+
return response.headers.get("content-length");
34+
} else {
35+
return null;
36+
}
37+
}
38+
39+
function Preview(props) {
40+
const [content, setContent] = useState("");
41+
42+
useEffect(() => {
43+
async function fetchData(url, contentType, size) {
44+
if (!contentType) {
45+
contentType = guessContentType(url);
46+
}
47+
if (contentType.startsWith("image")) {
48+
setContent(<img src={url} />);
49+
} else if (contentType === "text/plain") {
50+
if (size === null || size === undefined) {
51+
size = await getSize(url);
52+
}
53+
if (size !== null && size <= MAX_TEXT_SIZE_FOR_DISPLAY) {
54+
const content2 = await getContent(url);
55+
setContent(
56+
<Box paddingTop={2}>
57+
<pre
58+
style={{
59+
fontSize: "12px",
60+
overflow: "auto",
61+
paddingBottom: "12px",
62+
}}
63+
>
64+
{content2}
65+
</pre>
66+
</Box>
67+
);
68+
} else {
69+
setContent(
70+
<Box padding={3}>
71+
File is too large for preview, or else file size cannot be determined
72+
</Box>
73+
);
74+
}
75+
}
76+
}
77+
if (props.url && props.open) {
78+
fetchData(props.url, props.content_type, props.size);
79+
}
80+
}, [props.url]);
81+
82+
const handleClose = () => {
83+
props.onClose();
84+
setContent("");
85+
};
86+
87+
return (
88+
<Dialog open={props.open} onClose={handleClose} maxWidth="xl" fullWidth={false}>
89+
<IconButton
90+
onClick={handleClose}
91+
sx={{
92+
position: "absolute",
93+
right: 8,
94+
top: 8,
95+
color: "gray",
96+
}}
97+
>
98+
<CloseIcon />
99+
</IconButton>
100+
<DialogContent>{content}</DialogContent>
101+
</Dialog>
102+
);
103+
}
104+
105+
export default Preview;

src/utils.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,33 @@ function jobIsIncomplete(job) {
3838
return ["submitted", "running", "validated"].includes(job.status);
3939
}
4040

41-
export { timeFormat, parseArray, formatArray, isEmpty, isAlmostEmpty, jobIsIncomplete };
41+
const EXTENSION_CONTENT_TYPE_MAP = {
42+
png: "image/png",
43+
jpg: "image/jpeg",
44+
jpeg: "image/jpeg",
45+
txt: "text/plain",
46+
out: "text/plain",
47+
err: "text/plain",
48+
zip: "application/zip",
49+
pdf: "application/pdf",
50+
};
51+
52+
function guessContentType(url) {
53+
const parts = url.split(".");
54+
const extension = parts[parts.length - 1];
55+
if (extension) {
56+
return EXTENSION_CONTENT_TYPE_MAP[extension.toLowerCase()];
57+
} else {
58+
return null;
59+
}
60+
}
61+
62+
export {
63+
timeFormat,
64+
parseArray,
65+
formatArray,
66+
isEmpty,
67+
isAlmostEmpty,
68+
jobIsIncomplete,
69+
guessContentType,
70+
};

0 commit comments

Comments
 (0)