Skip to content

Commit 7282894

Browse files
authored
Merge pull request #94 from NeuroJSON/staging
Dataset page: revision selector + accurate metadata size; Search page: add suggested DB card and UI improvements
2 parents a7c61fd + 29fef36 commit 7282894

File tree

11 files changed

+1075
-327
lines changed

11 files changed

+1075
-327
lines changed

public/img/search_page/search.png

37.4 KB
Loading
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
2+
import {
3+
Box,
4+
Typography,
5+
FormControl,
6+
InputLabel,
7+
Select,
8+
MenuItem,
9+
Chip,
10+
IconButton,
11+
Tooltip,
12+
} from "@mui/material";
13+
import { Colors } from "design/theme";
14+
import React, { useMemo, useState } from "react";
15+
16+
type Props = {
17+
dbViewInfo: any;
18+
datasetDocument: any;
19+
dbName: string | undefined;
20+
docId: string | undefined;
21+
// NEW:
22+
currentRev?: string; // from URL (?rev=...)
23+
onChangeRev?: (rev?: string | null) => void; // to update URL
24+
revsList?: { rev: string }[];
25+
};
26+
27+
type RevInfo = { rev: string };
28+
29+
const MetaDataPanel: React.FC<Props> = ({
30+
dbViewInfo,
31+
datasetDocument,
32+
dbName,
33+
docId,
34+
currentRev,
35+
onChangeRev,
36+
revsList = [], // default empty
37+
}) => {
38+
// const revs: RevInfo[] = useMemo(
39+
// () =>
40+
// Array.isArray(datasetDocument?.["_revs_info"])
41+
// ? (datasetDocument!["_revs_info"] as RevInfo[])
42+
// : [],
43+
// [datasetDocument]
44+
// );
45+
const revs = revsList;
46+
47+
// derive index from currentRev; fallback to 0 (latest)
48+
const deriveIdx = React.useCallback((revList: RevInfo[], cur?: string) => {
49+
if (!revList.length) return 0;
50+
if (!cur) return 0;
51+
const idx = revList.findIndex((r) => r.rev === cur);
52+
return idx >= 0 ? idx : 0;
53+
}, []);
54+
55+
const [revIdx, setRevIdx] = useState<number>(deriveIdx(revs, currentRev));
56+
57+
// keep local idx synced when URL rev or list changes
58+
React.useEffect(() => {
59+
setRevIdx(deriveIdx(revs, currentRev));
60+
}, [revs, currentRev, deriveIdx]);
61+
62+
const selected = revs[revIdx];
63+
// const [revIdx, setRevIdx] = useState(0);
64+
// const selected = revs[revIdx];
65+
66+
return (
67+
<Box
68+
sx={{
69+
backgroundColor: "#fff",
70+
borderRadius: "8px",
71+
display: "flex",
72+
flexDirection: "column",
73+
overflow: "hidden",
74+
height: "100%",
75+
minHeight: 0,
76+
}}
77+
>
78+
<Box
79+
sx={{
80+
flex: 1,
81+
minHeight: 0, // <-- for scroller
82+
overflowY: "auto", // <-- keep the scroller here
83+
p: 2,
84+
display: "flex",
85+
flexDirection: "column",
86+
gap: 1,
87+
}}
88+
>
89+
<Box>
90+
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
91+
Modalities
92+
</Typography>
93+
<Typography sx={{ color: "text.secondary" }}>
94+
{dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "N/A"}
95+
</Typography>
96+
</Box>
97+
<Box>
98+
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
99+
DOI
100+
</Typography>
101+
<Typography sx={{ color: "text.secondary" }}>
102+
{(() => {
103+
const doi =
104+
datasetDocument?.["dataset_description.json"]?.DatasetDOI ||
105+
datasetDocument?.["dataset_description.json"]?.ReferenceDOI;
106+
107+
if (!doi) return "N/A";
108+
109+
// Normalize into a clickable URL
110+
let url = doi;
111+
if (/^10\./.test(doi)) {
112+
url = `https://doi.org/${doi}`;
113+
} else if (/^doi:/.test(doi)) {
114+
url = `https://doi.org/${doi.replace(/^doi:/, "")}`;
115+
}
116+
117+
return (
118+
<a
119+
href={url}
120+
target="_blank"
121+
rel="noopener noreferrer"
122+
style={{
123+
color: "inherit",
124+
textDecoration: "underline",
125+
}}
126+
>
127+
{url}
128+
</a>
129+
);
130+
})()}
131+
</Typography>
132+
</Box>
133+
<Box>
134+
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
135+
Subjects
136+
</Typography>
137+
<Typography sx={{ color: "text.secondary" }}>
138+
{dbViewInfo?.rows?.[0]?.value?.subj?.length ?? "N/A"}
139+
</Typography>
140+
</Box>
141+
<Box>
142+
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
143+
License
144+
</Typography>
145+
<Typography sx={{ color: "text.secondary" }}>
146+
{datasetDocument?.["dataset_description.json"]?.License ?? "N/A"}
147+
</Typography>
148+
</Box>
149+
<Box>
150+
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
151+
BIDS Version
152+
</Typography>
153+
<Typography sx={{ color: "text.secondary" }}>
154+
{datasetDocument?.["dataset_description.json"]?.BIDSVersion ??
155+
"N/A"}
156+
</Typography>
157+
</Box>
158+
<Box>
159+
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
160+
References and Links
161+
</Typography>
162+
<Typography sx={{ color: "text.secondary" }}>
163+
{Array.isArray(
164+
datasetDocument?.["dataset_description.json"]?.ReferencesAndLinks
165+
)
166+
? datasetDocument["dataset_description.json"].ReferencesAndLinks
167+
.length > 0
168+
? datasetDocument[
169+
"dataset_description.json"
170+
].ReferencesAndLinks.join(", ")
171+
: "N/A"
172+
: datasetDocument?.["dataset_description.json"]
173+
?.ReferencesAndLinks ?? "N/A"}
174+
</Typography>
175+
</Box>
176+
177+
{revs.length > 0 && (
178+
<Box
179+
sx={{
180+
mt: 2,
181+
p: 2,
182+
border: `1px solid ${Colors.lightGray}`,
183+
borderRadius: 1,
184+
}}
185+
>
186+
<Typography
187+
// variant="subtitle1"
188+
sx={{ mb: 1, fontWeight: 600, color: Colors.darkPurple }}
189+
>
190+
Revisions
191+
</Typography>
192+
193+
<FormControl
194+
fullWidth
195+
size="small"
196+
sx={{
197+
mb: 1,
198+
"& .MuiOutlinedInput-root": {
199+
"& fieldset": {
200+
borderColor: Colors.green,
201+
},
202+
"&:hover fieldset": {
203+
borderColor: Colors.green,
204+
},
205+
"&.Mui-focused fieldset": {
206+
borderColor: Colors.green,
207+
},
208+
},
209+
"& .MuiInputLabel-root.Mui-focused": {
210+
color: Colors.green,
211+
},
212+
}}
213+
>
214+
<InputLabel id="rev-select-label">Select revision</InputLabel>
215+
<Select
216+
labelId="rev-select-label"
217+
label="Select revision"
218+
value={revIdx}
219+
onChange={(e) => {
220+
const idx = Number(e.target.value);
221+
setRevIdx(idx);
222+
const chosen = revs[idx]?.rev;
223+
// update URL -> parent will refetch with ?rev=chosen
224+
onChangeRev?.(chosen || null);
225+
}}
226+
// onChange={(e) => setRevIdx(Number(e.target.value))}
227+
>
228+
{revs.map((r, idx) => {
229+
const [verNum, hash] = r.rev.split("-", 2);
230+
return (
231+
<MenuItem key={r.rev} value={idx}>
232+
<Typography component="span">
233+
Revision {verNum} ({r.rev.slice(0, 8)}{r.rev.slice(-4)}
234+
)
235+
</Typography>
236+
</MenuItem>
237+
);
238+
})}
239+
</Select>
240+
</FormControl>
241+
242+
{selected && (
243+
<Box
244+
sx={{
245+
display: "flex",
246+
alignItems: "center",
247+
justifyContent: "space-between",
248+
gap: 1,
249+
}}
250+
>
251+
<Box sx={{ minWidth: 0 }}>
252+
<Typography variant="body2" sx={{ color: "text.secondary" }}>
253+
Selected rev:
254+
</Typography>
255+
<Typography
256+
variant="body2"
257+
sx={{ fontFamily: "monospace", wordBreak: "break-all" }}
258+
title={selected.rev}
259+
>
260+
{selected.rev}
261+
</Typography>
262+
</Box>
263+
<Tooltip title="Open this revision in NeuroJSON.io">
264+
<IconButton
265+
size="small"
266+
onClick={() =>
267+
window.open(
268+
`https://neurojson.io:7777/${dbName}/${docId}?rev=${selected.rev}`,
269+
"_blank"
270+
)
271+
}
272+
>
273+
<ArrowCircleRightIcon
274+
fontSize="small"
275+
sx={{
276+
color: Colors.green,
277+
}}
278+
/>
279+
</IconButton>
280+
</Tooltip>
281+
</Box>
282+
)}
283+
</Box>
284+
)}
285+
</Box>
286+
</Box>
287+
);
288+
};
289+
290+
export default MetaDataPanel;

src/components/DatasetPageCard.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ import { useNavigate } from "react-router-dom";
1515
import { Row } from "redux/neurojson/types/neurojson.interface";
1616
import RoutesEnum from "types/routes.enum";
1717

18+
const formatSize = (sizeInBytes: number): string => {
19+
if (sizeInBytes < 1024) {
20+
return `${sizeInBytes} Bytes`;
21+
} else if (sizeInBytes < 1024 * 1024) {
22+
return `${(sizeInBytes / 1024).toFixed(1)} KB`;
23+
} else if (sizeInBytes < 1024 * 1024 * 1024) {
24+
return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`;
25+
} else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) {
26+
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
27+
} else {
28+
return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
29+
}
30+
};
31+
32+
// for showing the size
33+
const jsonBytes = (obj: unknown) =>
34+
obj ? new TextEncoder().encode(JSON.stringify(obj)).length : 0;
35+
1836
interface DatasetPageCardProps {
1937
doc: Row;
2038
index: number;
@@ -32,6 +50,11 @@ const DatasetPageCard: React.FC<DatasetPageCardProps> = ({
3250
}) => {
3351
const navigate = useNavigate();
3452
const datasetIndex = (page - 1) * pageSize + index + 1;
53+
const sizeInBytes = React.useMemo(() => {
54+
const len = (doc as any)?.value?.length; // bytes from length key
55+
if (typeof len === "number" && Number.isFinite(len)) return len;
56+
return jsonBytes(doc.value); // fallback: summary object size
57+
}, [doc.value]);
3558
return (
3659
<Grid item xs={12} sm={6} key={doc.id}>
3760
<Card
@@ -133,9 +156,10 @@ const DatasetPageCard: React.FC<DatasetPageCardProps> = ({
133156
<Stack direction="row" spacing={2} alignItems="center">
134157
<Typography variant="body2" color={Colors.textPrimary}>
135158
<strong>Size:</strong>{" "}
136-
{doc.value.length
159+
{/* {doc.value.length
137160
? `${(doc.value.length / 1024 / 1024).toFixed(2)} MB`
138-
: "Unknown"}
161+
: "Unknown"} */}
162+
{formatSize(sizeInBytes)}
139163
</Typography>
140164

141165
{doc.value.info?.DatasetDOI && (
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Box, Tooltip, ClickAwayListener, TooltipProps } from "@mui/material";
2+
import { useState, PropsWithChildren } from "react";
3+
4+
type ClickTooltipProps = PropsWithChildren<{
5+
title: TooltipProps["title"];
6+
placement?: TooltipProps["placement"];
7+
componentsProps?: TooltipProps["componentsProps"];
8+
}>;
9+
10+
export default function ClickTooltip({
11+
title,
12+
placement = "right",
13+
componentsProps,
14+
children,
15+
}: ClickTooltipProps) {
16+
const [open, setOpen] = useState(false);
17+
const toggle = () => setOpen((o) => !o);
18+
const close = () => setOpen(false);
19+
20+
return (
21+
<ClickAwayListener onClickAway={close}>
22+
<Box sx={{ display: "inline-flex" }}>
23+
<Tooltip
24+
open={open}
25+
onClose={close}
26+
disableFocusListener
27+
disableHoverListener
28+
disableTouchListener
29+
placement={placement}
30+
componentsProps={componentsProps}
31+
title={title}
32+
arrow
33+
>
34+
{/* span to ensure Tooltip always has a single DOM child */}
35+
<span onClick={toggle}>{children}</span>
36+
</Tooltip>
37+
</Box>
38+
</ClickAwayListener>
39+
);
40+
}

0 commit comments

Comments
 (0)