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
67 changes: 67 additions & 0 deletions src/components/DatasetDetailPage/FileTree/FileTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import FileTreeRow from "./FileTreeRow";
import type { TreeNode } from "./types";
import FolderIcon from "@mui/icons-material/Folder";
import { Box, Typography } from "@mui/material";
import React from "react";

type Props = {
title: string;
tree: TreeNode[];
filesCount: number;
totalBytes: number;
onPreview: (url: string, index: number) => void;
};

const formatSize = (n: number) => {
if (n < 1024) return `${n} B`;
if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`;
if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(2)} MB`;
if (n < 1024 ** 4) return `${(n / 1024 ** 3).toFixed(2)} GB`;
return `${(n / 1024 ** 4).toFixed(2)} TB`;
};

const FileTree: React.FC<Props> = ({
title,
tree,
filesCount,
totalBytes,
onPreview,
}) => (
<Box
sx={{
backgroundColor: "#fff",
borderRadius: 2,
border: "1px solid #e0e0e0",
height: "100%",
display: "flex",
flexDirection: "column",
minHeight: 0,
}}
>
<Box
sx={{
px: 2,
py: 1.5,
borderBottom: "1px solid #eee",
display: "flex",
alignItems: "center",
gap: 1,
flexShrink: 0,
}}
>
<FolderIcon />
<Typography sx={{ fontWeight: 700, flex: 1 }}>{title}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>
Files: {filesCount} &nbsp; Size: {formatSize(totalBytes)}
</Typography>
</Box>

<Box sx={{ flex: 1, minHeight: 0, overflowY: "auto", py: 0.5 }}>
{tree.map((n) => (
<FileTreeRow key={n.path} node={n} level={0} onPreview={onPreview} />
))}
</Box>
</Box>
);

export default FileTree;
130 changes: 130 additions & 0 deletions src/components/DatasetDetailPage/FileTree/FileTreeRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type { TreeNode } from "./types";
import { formatLeafValue, isPreviewable } from "./utils";
import DownloadIcon from "@mui/icons-material/Download";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import FolderIcon from "@mui/icons-material/Folder";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import VisibilityIcon from "@mui/icons-material/Visibility";
import { Box, Button, Collapse, Typography } from "@mui/material";
import React from "react";

type Props = {
node: TreeNode;
level: number;
onPreview: (url: string, index: number) => void;
};

const FileTreeRow: React.FC<Props> = ({ node, level, onPreview }) => {
const [open, setOpen] = React.useState(false);

if (node.kind === "folder") {
return (
<>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
py: 0.5,
px: 1,
cursor: "pointer",
"&:hover": { backgroundColor: "rgba(0,0,0,0.04)" },
}}
onClick={() => setOpen((o) => !o)}
>
<Box sx={{ pl: level * 1.25 }}>
<FolderIcon fontSize="small" />
</Box>
<Typography sx={{ fontWeight: 600, flex: 1 }}>{node.name}</Typography>
{open ? <ExpandLess /> : <ExpandMore />}
</Box>

<Collapse in={open} timeout="auto" unmountOnExit>
{node.children.map((child) => (
<FileTreeRow
key={child.path}
node={child}
level={level + 1}
onPreview={onPreview}
/>
))}
</Collapse>
</>
);
}

return (
<Box
sx={{ display: "flex", alignItems: "flex-start", gap: 1, py: 0.5, px: 1 }}
>
<Box sx={{ pl: level * 1.25, pt: "2px" }}>
<InsertDriveFileIcon fontSize="small" />
</Box>

<Box sx={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
<Typography
title={node.name}
sx={{
fontWeight: 500,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{node.name}
</Typography>

{!node.link && node.value !== undefined && (
<Typography
title={
node.name === "_ArrayZipData_"
? "[compressed data]"
: typeof node.value === "string"
? node.value
: JSON.stringify(node.value)
}
sx={{
fontFamily: "monospace",
fontSize: "0.85rem",
color: "text.secondary",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
mt: 0.25,
}}
>
{node.name === "_ArrayZipData_"
? "[compressed data]"
: formatLeafValue(node.value)}
</Typography>
)}
</Box>

{node.link?.url && (
<Box sx={{ display: "flex", gap: 1, flexShrink: 0 }}>
<Button
size="small"
variant="text"
onClick={() => window.open(node.link!.url, "_blank")}
startIcon={<DownloadIcon fontSize="small" />}
>
Download
</Button>
{isPreviewable(node.link.url) && (
<Button
size="small"
variant="text"
startIcon={<VisibilityIcon fontSize="small" />}
onClick={() => onPreview(node.link!.url, node.link!.index)}
>
Preview
</Button>
)}
</Box>
)}
</Box>
);
};

export default FileTreeRow;
6 changes: 6 additions & 0 deletions src/components/DatasetDetailPage/FileTree/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type LinkMeta = { url: string; index: number };

// this value can be one of these types
export type TreeNode =
| { kind: "folder"; name: string; path: string; children: TreeNode[] }
| { kind: "file"; name: string; path: string; value?: any; link?: LinkMeta };
76 changes: 76 additions & 0 deletions src/components/DatasetDetailPage/FileTree/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { LinkMeta, TreeNode } from "./types";

export const isPreviewable = (url: string) =>
/\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test(
(url.match(/file=([^&]+)/)?.[1] ?? url).toLowerCase()
);

export const formatLeafValue = (v: any): string => {
if (v === null) return "null";
const t = typeof v;
if (t === "number" || t === "boolean") return String(v);
if (t === "string") return v.length > 120 ? v.slice(0, 120) + "…" : v;
if (Array.isArray(v)) {
const n = v.length;
const head = v
.slice(0, 5)
.map((x) => (typeof x === "number" ? x : JSON.stringify(x)));
return n <= 5
? `[${head.join(", ")}]`
: `[${head.join(", ")}, …] (${n} items)`;
}
return ""; // if it is object, return as a folder
};

// ignore meta keys
export const shouldSkipKey = (key: string) =>
key === "_id" || key === "_rev" || key.startsWith(".");

// build path -> {url, index} lookup, built from extractDataLinks function
// if external link objects have {path, url, index}, build a Map for the tree
export const makeLinkMap = <
T extends { path: string; url: string; index: number }
>(
links: T[]
): Map<string, LinkMeta> => {
const m = new Map<string, LinkMeta>();
links.forEach((l) => m.set(l.path, { url: l.url, index: l.index }));
return m;
};

// Recursively convert the dataset JSON to a file-tree
export const buildTreeFromDoc = (
doc: any,
linkMap: Map<string, LinkMeta>,
curPath = ""
): TreeNode[] => {
if (!doc || typeof doc !== "object") return [];
const out: TreeNode[] = [];

Object.keys(doc).forEach((key) => {
if (shouldSkipKey(key)) return;

const val = doc[key];
const path = `${curPath}/${key}`;
const link = linkMap.get(path);

if (link) {
out.push({ kind: "file", name: key, path, link });
return;
}

if (val && typeof val === "object" && !Array.isArray(val)) {
out.push({
kind: "folder",
name: key,
path,
children: buildTreeFromDoc(val, linkMap, path),
});
return;
}

out.push({ kind: "file", name: key, path, value: val });
});

return out;
};
2 changes: 2 additions & 0 deletions src/components/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import DatasetDetailPage from "pages/DatasetDetailPage";
import DatasetPage from "pages/DatasetPage";
import Home from "pages/Home";
import SearchPage from "pages/SearchPage";
import UpdatedDatasetDetailPage from "pages/UpdatedDatasetDetailPage";
import NewDatasetPage from "pages/UpdatedDatasetPage";
import React from "react";
import { Navigate, Route, Routes as RouterRoutes } from "react-router-dom";
Expand Down Expand Up @@ -33,6 +34,7 @@ const Routes = () => (
<Route
path={`${RoutesEnum.DATABASES}/:dbName/:docId`}
element={<DatasetDetailPage />}
// element={<UpdatedDatasetDetailPage />}
/>

{/* Search Page */}
Expand Down
44 changes: 44 additions & 0 deletions src/design/ReadMoreText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Colors } from "./theme";
import { Box, Typography, Button } from "@mui/material";
import React, { useState } from "react";

const ReadMoreText: React.FC<{ text: string }> = ({ text }) => {
const [expanded, setExpanded] = useState(false);

return (
<Box sx={{ position: "relative" }}>
<Typography
variant="body1"
sx={{
display: "-webkit-box",
WebkitLineClamp: expanded ? "unset" : 3, // show only 3 lines
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{text}
</Typography>

<Button
size="small"
sx={{
mt: 1,
fontWeight: 600,
textTransform: "uppercase",
fontSize: "0.8rem",
color: Colors.purple,
"&:hover": {
color: Colors.secondaryPurple,
transform: "scale(1.05)",
},
}}
onClick={() => setExpanded(!expanded)}
>
{expanded ? "Read Less" : "Read More"}
</Button>
</Box>
);
};

export default ReadMoreText;
Loading