Skip to content

Commit e526764

Browse files
authored
Merge pull request #92 from NeuroJSON/dev-fan
Search: show DatabaseCard on keyword or DB selection; dataset page polish (metadata size fix, revision dropdown) — closes #81
2 parents 6b211d8 + 9967d51 commit e526764

File tree

8 files changed

+835
-208
lines changed

8 files changed

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

0 commit comments

Comments
 (0)