Skip to content

Commit 1ce5acc

Browse files
committed
feat: add revision select dropdown menu with link in dataset detail page
1 parent 4e77612 commit 1ce5acc

File tree

4 files changed

+153
-78
lines changed

4 files changed

+153
-78
lines changed

src/components/DatasetDetailPage/MetaDataPanel.tsx

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
1-
import { Box, Typography } from "@mui/material";
1+
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
2+
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
3+
import {
4+
Box,
5+
Typography,
6+
FormControl,
7+
InputLabel,
8+
Select,
9+
MenuItem,
10+
Chip,
11+
IconButton,
12+
Tooltip,
13+
} from "@mui/material";
214
import { Colors } from "design/theme";
3-
import React from "react";
15+
import React, { useMemo, useState } from "react";
416

517
type Props = {
618
dbViewInfo: any;
719
datasetDocument: any;
20+
dbName: string | undefined;
21+
docId: string | undefined;
822
};
923

10-
const MetaDataPanel: React.FC<Props> = ({ dbViewInfo, datasetDocument }) => {
24+
type RevInfo = { rev: string };
25+
26+
const MetaDataPanel: React.FC<Props> = ({
27+
dbViewInfo,
28+
datasetDocument,
29+
dbName,
30+
docId,
31+
}) => {
32+
const revs: RevInfo[] = useMemo(
33+
() =>
34+
Array.isArray(datasetDocument?.["_revs_info"])
35+
? (datasetDocument!["_revs_info"] as RevInfo[])
36+
: [],
37+
[datasetDocument]
38+
);
39+
const [revIdx, setRevIdx] = useState(0);
40+
const selected = revs[revIdx];
41+
1142
return (
1243
<Box
1344
sx={{
@@ -39,7 +70,6 @@ const MetaDataPanel: React.FC<Props> = ({ dbViewInfo, datasetDocument }) => {
3970
{dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "N/A"}
4071
</Typography>
4172
</Box>
42-
4373
<Box>
4474
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
4575
DOI
@@ -76,7 +106,6 @@ const MetaDataPanel: React.FC<Props> = ({ dbViewInfo, datasetDocument }) => {
76106
})()}
77107
</Typography>
78108
</Box>
79-
80109
<Box>
81110
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
82111
Subjects
@@ -120,6 +149,87 @@ const MetaDataPanel: React.FC<Props> = ({ dbViewInfo, datasetDocument }) => {
120149
?.ReferencesAndLinks ?? "N/A"}
121150
</Typography>
122151
</Box>
152+
153+
{revs.length > 0 && (
154+
<Box
155+
sx={{
156+
mt: 2,
157+
p: 2,
158+
border: `1px solid ${Colors.lightGray}`,
159+
borderRadius: 1,
160+
}}
161+
>
162+
<Typography
163+
// variant="subtitle1"
164+
sx={{ mb: 1, fontWeight: 600, color: Colors.darkPurple }}
165+
>
166+
Revisions
167+
</Typography>
168+
169+
<FormControl fullWidth size="small" sx={{ mb: 1 }}>
170+
<InputLabel id="rev-select-label">Select revision</InputLabel>
171+
<Select
172+
labelId="rev-select-label"
173+
label="Select revision"
174+
value={revIdx}
175+
onChange={(e) => setRevIdx(Number(e.target.value))}
176+
>
177+
{revs.map((r, idx) => (
178+
<MenuItem key={r.rev} value={idx}>
179+
<Typography
180+
component="span"
181+
sx={{ fontFamily: "monospace" }}
182+
>
183+
{r.rev.slice(0, 8)}{r.rev.slice(-4)}
184+
</Typography>
185+
</MenuItem>
186+
))}
187+
</Select>
188+
</FormControl>
189+
190+
{selected && (
191+
<Box
192+
sx={{
193+
display: "flex",
194+
alignItems: "center",
195+
justifyContent: "space-between",
196+
gap: 1,
197+
}}
198+
>
199+
<Box sx={{ minWidth: 0 }}>
200+
<Typography variant="body2" sx={{ color: "text.secondary" }}>
201+
Selected rev:
202+
</Typography>
203+
<Typography
204+
variant="body2"
205+
sx={{ fontFamily: "monospace", wordBreak: "break-all" }}
206+
title={selected.rev}
207+
>
208+
{selected.rev}
209+
</Typography>
210+
</Box>
211+
<Tooltip title="Open this revision in NeuroJSON.io">
212+
<IconButton
213+
size="small"
214+
onClick={() =>
215+
window.open(
216+
`https://neurojson.io:7777/${dbName}/${docId}?rev=${selected.rev}`,
217+
"_blank"
218+
)
219+
}
220+
>
221+
<ArrowCircleRightIcon
222+
fontSize="small"
223+
sx={{
224+
color: Colors.purple,
225+
}}
226+
/>
227+
</IconButton>
228+
</Tooltip>
229+
</Box>
230+
)}
231+
</Box>
232+
)}
123233
</Box>
124234
</Box>
125235
);

src/components/DatasetPageCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const DatasetPageCard: React.FC<DatasetPageCardProps> = ({
5151
const navigate = useNavigate();
5252
const datasetIndex = (page - 1) * pageSize + index + 1;
5353
const sizeInBytes = React.useMemo(() => {
54-
const len = (doc as any)?.value?.length; // bytes from backend (full doc)
54+
const len = (doc as any)?.value?.length; // bytes from length key
5555
if (typeof len === "number" && Number.isFinite(len)) return len;
5656
return jsonBytes(doc.value); // fallback: summary object size
5757
}, [doc.value]);

src/pages/UpdatedDatasetDetailPage.tsx

Lines changed: 34 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { Colors } from "design/theme";
2626
import { useAppDispatch } from "hooks/useAppDispatch";
2727
import { useAppSelector } from "hooks/useAppSelector";
2828
import React, { useEffect, useMemo, useState } from "react";
29-
import ReactJson from "react-json-view";
29+
// import ReactJson from "react-json-view";
3030
import { useParams, useNavigate } from "react-router-dom";
3131
import {
3232
fetchDocumentDetails,
@@ -74,22 +74,16 @@ const UpdatedDatasetDetailPage: React.FC = () => {
7474
const [previewIndex, setPreviewIndex] = useState<number>(0);
7575
const aiSummary = datasetDocument?.[".datainfo"]?.AISummary ?? "";
7676

77-
useEffect(() => {
78-
if (!datasetDocument) {
79-
setJsonSize(0);
80-
return;
81-
}
82-
const bytes = new TextEncoder().encode(
83-
JSON.stringify(datasetDocument)
84-
).length;
85-
setJsonSize(bytes);
86-
}, [datasetDocument]);
87-
88-
// 1) detect subjects at the top level, return true or false
89-
const hasTopLevelSubjects = useMemo(
90-
() => Object.keys(datasetDocument || {}).some((k) => /^sub-/i.test(k)),
91-
[datasetDocument]
92-
);
77+
// useEffect(() => {
78+
// if (!datasetDocument) {
79+
// setJsonSize(0);
80+
// return;
81+
// }
82+
// const bytes = new TextEncoder().encode(
83+
// JSON.stringify(datasetDocument)
84+
// ).length;
85+
// setJsonSize(bytes);
86+
// }, [datasetDocument]);
9387

9488
const linkMap = useMemo(() => makeLinkMap(externalLinks), [externalLinks]);
9589

@@ -98,26 +92,6 @@ const UpdatedDatasetDetailPage: React.FC = () => {
9892
[datasetDocument, linkMap]
9993
);
10094

101-
// “rest” JSON only when we actually have subjects
102-
// const rest = useMemo(() => {
103-
// if (!datasetDocument || !hasTopLevelSubjects) return {};
104-
// const r: any = {};
105-
// Object.keys(datasetDocument).forEach((k) => {
106-
// if (!/^sub-/i.test(k)) r[k] = (datasetDocument as any)[k];
107-
// });
108-
// return r;
109-
// }, [datasetDocument, hasTopLevelSubjects]);
110-
111-
// JSON panel should always render:
112-
// - if we have subjects -> JSON show "rest" (everything except sub-*)
113-
// - if we don't have subjects -> JSON show the whole document
114-
// const jsonPanelData = useMemo(
115-
// () => (hasTopLevelSubjects ? rest : datasetDocument || {}),
116-
// [hasTopLevelSubjects, rest, datasetDocument]
117-
// );
118-
119-
// 5) header title + counts also fall back
120-
// const treeTitle = hasTopLevelSubjects ? "Subjects" : "Files";
12195
const treeTitle = "Files";
12296
const filesCount = externalLinks.length;
12397
const totalBytes = useMemo(() => {
@@ -268,15 +242,24 @@ const UpdatedDatasetDetailPage: React.FC = () => {
268242
return internalLinks;
269243
};
270244

245+
// useEffect(() => {
246+
// const fetchData = async () => {
247+
// if (dbName && docId) {
248+
// await dispatch(fetchDocumentDetails({ dbName, docId }));
249+
// await dispatch(fetchDbInfoByDatasetId({ dbName, docId }));
250+
// }
251+
// };
252+
253+
// fetchData();
254+
// }, [dbName, docId, dispatch]);
255+
271256
useEffect(() => {
272-
const fetchData = async () => {
273-
if (dbName && docId) {
274-
await dispatch(fetchDocumentDetails({ dbName, docId }));
275-
await dispatch(fetchDbInfoByDatasetId({ dbName, docId }));
276-
}
277-
};
257+
if (!dbName || !docId) return;
278258

279-
fetchData();
259+
(async () => {
260+
await dispatch(fetchDocumentDetails({ dbName, docId })); // render tree ASAP
261+
dispatch(fetchDbInfoByDatasetId({ dbName, docId })); // don't await
262+
})();
280263
}, [dbName, docId, dispatch]);
281264

282265
useEffect(() => {
@@ -289,7 +272,10 @@ const UpdatedDatasetDetailPage: React.FC = () => {
289272
index, // Assign index correctly
290273
})
291274
);
292-
275+
const bytes = new TextEncoder().encode(
276+
JSON.stringify(datasetDocument)
277+
).length;
278+
setJsonSize(bytes);
293279
// Extract Internal Data & Assign `index`
294280
const internalData = extractInternalData(datasetDocument).map(
295281
(data, index) => ({
@@ -414,18 +400,7 @@ const UpdatedDatasetDetailPage: React.FC = () => {
414400
if (typeof window !== "undefined" && (window as any).__previewType) {
415401
return (window as any).__previewType === "2d";
416402
}
417-
// if (window.__previewType) {
418-
// console.log("work~~~~~~~");
419-
// return window.__previewType === "2d";
420-
// }
421-
// console.log("is 2d preview candidate !== 2d");
422-
// console.log("obj", obj);
423-
// if (typeof obj === "string" && obj.includes("db=optics-at-martinos")) {
424-
// return false;
425-
// }
426-
// if (typeof obj === "string" && obj.endsWith(".jdb")) {
427-
// return true;
428-
// }
403+
429404
if (!obj || typeof obj !== "object") {
430405
return false;
431406
}
@@ -434,8 +409,6 @@ const UpdatedDatasetDetailPage: React.FC = () => {
434409
return false;
435410
}
436411
const dim = obj._ArraySize_;
437-
// console.log("array.isarray(dim)", Array.isArray(dim));
438-
// console.log("dim.length", dim.length === 1 || dim.length === 2);
439412

440413
return (
441414
Array.isArray(dim) &&
@@ -512,18 +485,6 @@ const UpdatedDatasetDetailPage: React.FC = () => {
512485
typeof dataOrUrl === "string" ? extractFileName(dataOrUrl) : "";
513486
if (isPreviewableFile(fileName)) {
514487
(window as any).previewdataurl(dataOrUrl, idx);
515-
// const is2D = is2DPreviewCandidate(dataOrUrl);
516-
// const panel = document.getElementById("chartpanel");
517-
// console.log("is2D", is2D);
518-
// console.log("panel", panel);
519-
520-
// if (is2D) {
521-
// // console.log("📊 2D data → rendering inline with dopreview()");
522-
// if (panel) panel.style.display = "block"; // Show it!
523-
// setPreviewOpen(false); // Don't open modal
524-
// } else {
525-
// if (panel) panel.style.display = "none"; // Hide chart panel on 3D external
526-
// }
527488
} else {
528489
console.warn("⚠️ Unsupported file format for preview:", dataOrUrl);
529490
}
@@ -871,6 +832,8 @@ const UpdatedDatasetDetailPage: React.FC = () => {
871832
<MetaDataPanel
872833
datasetDocument={datasetDocument}
873834
dbViewInfo={dbViewInfo}
835+
dbName={dbName}
836+
docId={docId}
874837
/>
875838
</Box>
876839
</Box>

src/services/neurojson.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ export const NeurojsonService = {
3030
},
3131
getDocumentById: async (dbName: string, documentId: string): Promise<any> => {
3232
try {
33-
const response = await api.get(`${baseURL}/${dbName}/${documentId}`);
33+
const response = await api.get(
34+
`${baseURL}/${dbName}/${documentId}?revs_info=true`
35+
);
3436
return response.data;
3537
} catch (error) {
3638
console.error(

0 commit comments

Comments
 (0)