Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/img/search_page/search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
290 changes: 290 additions & 0 deletions src/components/DatasetDetailPage/MetaDataPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
import ArrowCircleRightIcon from "@mui/icons-material/ArrowCircleRight";
import {
Box,
Typography,
FormControl,
InputLabel,
Select,
MenuItem,
Chip,
IconButton,
Tooltip,
} from "@mui/material";
import { Colors } from "design/theme";
import React, { useMemo, useState } from "react";

type Props = {
dbViewInfo: any;
datasetDocument: any;
dbName: string | undefined;
docId: string | undefined;
// NEW:
currentRev?: string; // from URL (?rev=...)
onChangeRev?: (rev?: string | null) => void; // to update URL
revsList?: { rev: string }[];
};

type RevInfo = { rev: string };

const MetaDataPanel: React.FC<Props> = ({
dbViewInfo,
datasetDocument,
dbName,
docId,
currentRev,
onChangeRev,
revsList = [], // default empty
}) => {
// const revs: RevInfo[] = useMemo(
// () =>
// Array.isArray(datasetDocument?.["_revs_info"])
// ? (datasetDocument!["_revs_info"] as RevInfo[])
// : [],
// [datasetDocument]
// );
const revs = revsList;

// derive index from currentRev; fallback to 0 (latest)
const deriveIdx = React.useCallback((revList: RevInfo[], cur?: string) => {
if (!revList.length) return 0;
if (!cur) return 0;
const idx = revList.findIndex((r) => r.rev === cur);
return idx >= 0 ? idx : 0;
}, []);

const [revIdx, setRevIdx] = useState<number>(deriveIdx(revs, currentRev));

// keep local idx synced when URL rev or list changes
React.useEffect(() => {
setRevIdx(deriveIdx(revs, currentRev));
}, [revs, currentRev, deriveIdx]);

const selected = revs[revIdx];
// const [revIdx, setRevIdx] = useState(0);
// const selected = revs[revIdx];

return (
<Box
sx={{
backgroundColor: "#fff",
borderRadius: "8px",
display: "flex",
flexDirection: "column",
overflow: "hidden",
height: "100%",
minHeight: 0,
}}
>
<Box
sx={{
flex: 1,
minHeight: 0, // <-- for scroller
overflowY: "auto", // <-- keep the scroller here
p: 2,
display: "flex",
flexDirection: "column",
gap: 1,
}}
>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
Modalities
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{dbViewInfo?.rows?.[0]?.value?.modality?.join(", ") ?? "N/A"}
</Typography>
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
DOI
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{(() => {
const doi =
datasetDocument?.["dataset_description.json"]?.DatasetDOI ||
datasetDocument?.["dataset_description.json"]?.ReferenceDOI;

if (!doi) return "N/A";

// Normalize into a clickable URL
let url = doi;
if (/^10\./.test(doi)) {
url = `https://doi.org/${doi}`;
} else if (/^doi:/.test(doi)) {
url = `https://doi.org/${doi.replace(/^doi:/, "")}`;
}

return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
style={{
color: "inherit",
textDecoration: "underline",
}}
>
{url}
</a>
);
})()}
</Typography>
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
Subjects
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{dbViewInfo?.rows?.[0]?.value?.subj?.length ?? "N/A"}
</Typography>
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
License
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{datasetDocument?.["dataset_description.json"]?.License ?? "N/A"}
</Typography>
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
BIDS Version
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{datasetDocument?.["dataset_description.json"]?.BIDSVersion ??
"N/A"}
</Typography>
</Box>
<Box>
<Typography sx={{ color: Colors.darkPurple, fontWeight: "600" }}>
References and Links
</Typography>
<Typography sx={{ color: "text.secondary" }}>
{Array.isArray(
datasetDocument?.["dataset_description.json"]?.ReferencesAndLinks
)
? datasetDocument["dataset_description.json"].ReferencesAndLinks
.length > 0
? datasetDocument[
"dataset_description.json"
].ReferencesAndLinks.join(", ")
: "N/A"
: datasetDocument?.["dataset_description.json"]
?.ReferencesAndLinks ?? "N/A"}
</Typography>
</Box>

{revs.length > 0 && (
<Box
sx={{
mt: 2,
p: 2,
border: `1px solid ${Colors.lightGray}`,
borderRadius: 1,
}}
>
<Typography
// variant="subtitle1"
sx={{ mb: 1, fontWeight: 600, color: Colors.darkPurple }}
>
Revisions
</Typography>

<FormControl
fullWidth
size="small"
sx={{
mb: 1,
"& .MuiOutlinedInput-root": {
"& fieldset": {
borderColor: Colors.green,
},
"&:hover fieldset": {
borderColor: Colors.green,
},
"&.Mui-focused fieldset": {
borderColor: Colors.green,
},
},
"& .MuiInputLabel-root.Mui-focused": {
color: Colors.green,
},
}}
>
<InputLabel id="rev-select-label">Select revision</InputLabel>
<Select
labelId="rev-select-label"
label="Select revision"
value={revIdx}
onChange={(e) => {
const idx = Number(e.target.value);
setRevIdx(idx);
const chosen = revs[idx]?.rev;
// update URL -> parent will refetch with ?rev=chosen
onChangeRev?.(chosen || null);
}}
// onChange={(e) => setRevIdx(Number(e.target.value))}
>
{revs.map((r, idx) => {
const [verNum, hash] = r.rev.split("-", 2);
return (
<MenuItem key={r.rev} value={idx}>
<Typography component="span">
Revision {verNum} ({r.rev.slice(0, 8)}…{r.rev.slice(-4)}
)
</Typography>
</MenuItem>
);
})}
</Select>
</FormControl>

{selected && (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 1,
}}
>
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" sx={{ color: "text.secondary" }}>
Selected rev:
</Typography>
<Typography
variant="body2"
sx={{ fontFamily: "monospace", wordBreak: "break-all" }}
title={selected.rev}
>
{selected.rev}
</Typography>
</Box>
<Tooltip title="Open this revision in NeuroJSON.io">
<IconButton
size="small"
onClick={() =>
window.open(
`https://neurojson.io:7777/${dbName}/${docId}?rev=${selected.rev}`,
"_blank"
)
}
>
<ArrowCircleRightIcon
fontSize="small"
sx={{
color: Colors.green,
}}
/>
</IconButton>
</Tooltip>
</Box>
)}
</Box>
)}
</Box>
</Box>
);
};

export default MetaDataPanel;
28 changes: 26 additions & 2 deletions src/components/DatasetPageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ import { useNavigate } from "react-router-dom";
import { Row } from "redux/neurojson/types/neurojson.interface";
import RoutesEnum from "types/routes.enum";

const formatSize = (sizeInBytes: number): string => {
if (sizeInBytes < 1024) {
return `${sizeInBytes} Bytes`;
} else if (sizeInBytes < 1024 * 1024) {
return `${(sizeInBytes / 1024).toFixed(1)} KB`;
} else if (sizeInBytes < 1024 * 1024 * 1024) {
return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} MB`;
} else if (sizeInBytes < 1024 * 1024 * 1024 * 1024) {
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
} else {
return `${(sizeInBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
}
};

// for showing the size
const jsonBytes = (obj: unknown) =>
obj ? new TextEncoder().encode(JSON.stringify(obj)).length : 0;

interface DatasetPageCardProps {
doc: Row;
index: number;
Expand All @@ -32,6 +50,11 @@ const DatasetPageCard: React.FC<DatasetPageCardProps> = ({
}) => {
const navigate = useNavigate();
const datasetIndex = (page - 1) * pageSize + index + 1;
const sizeInBytes = React.useMemo(() => {
const len = (doc as any)?.value?.length; // bytes from length key
if (typeof len === "number" && Number.isFinite(len)) return len;
return jsonBytes(doc.value); // fallback: summary object size
}, [doc.value]);
return (
<Grid item xs={12} sm={6} key={doc.id}>
<Card
Expand Down Expand Up @@ -133,9 +156,10 @@ const DatasetPageCard: React.FC<DatasetPageCardProps> = ({
<Stack direction="row" spacing={2} alignItems="center">
<Typography variant="body2" color={Colors.textPrimary}>
<strong>Size:</strong>{" "}
{doc.value.length
{/* {doc.value.length
? `${(doc.value.length / 1024 / 1024).toFixed(2)} MB`
: "Unknown"}
: "Unknown"} */}
{formatSize(sizeInBytes)}
</Typography>

{doc.value.info?.DatasetDOI && (
Expand Down
40 changes: 40 additions & 0 deletions src/components/SearchPage/ClickTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Box, Tooltip, ClickAwayListener, TooltipProps } from "@mui/material";
import { useState, PropsWithChildren } from "react";

type ClickTooltipProps = PropsWithChildren<{
title: TooltipProps["title"];
placement?: TooltipProps["placement"];
componentsProps?: TooltipProps["componentsProps"];
}>;

export default function ClickTooltip({
title,
placement = "right",
componentsProps,
children,
}: ClickTooltipProps) {
const [open, setOpen] = useState(false);
const toggle = () => setOpen((o) => !o);
const close = () => setOpen(false);

return (
<ClickAwayListener onClickAway={close}>
<Box sx={{ display: "inline-flex" }}>
<Tooltip
open={open}
onClose={close}
disableFocusListener
disableHoverListener
disableTouchListener
placement={placement}
componentsProps={componentsProps}
title={title}
arrow
>
{/* span to ensure Tooltip always has a single DOM child */}
<span onClick={toggle}>{children}</span>
</Tooltip>
</Box>
</ClickAwayListener>
);
}
Loading