Skip to content

Commit ae58f53

Browse files
authored
Merge pull request #79 from NeuroJSON/dev-fan
Feat: Show database logos, add AI summary section
2 parents 7fd733e + 7206110 commit ae58f53

File tree

9 files changed

+1912
-7
lines changed

9 files changed

+1912
-7
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import FileTreeRow from "./FileTreeRow";
2+
import type { TreeNode } from "./types";
3+
import FolderIcon from "@mui/icons-material/Folder";
4+
import { Box, Typography } from "@mui/material";
5+
import React from "react";
6+
7+
type Props = {
8+
title: string;
9+
tree: TreeNode[];
10+
filesCount: number;
11+
totalBytes: number;
12+
onPreview: (url: string, index: number) => void;
13+
};
14+
15+
const formatSize = (n: number) => {
16+
if (n < 1024) return `${n} B`;
17+
if (n < 1024 ** 2) return `${(n / 1024).toFixed(1)} KB`;
18+
if (n < 1024 ** 3) return `${(n / 1024 ** 2).toFixed(2)} MB`;
19+
if (n < 1024 ** 4) return `${(n / 1024 ** 3).toFixed(2)} GB`;
20+
return `${(n / 1024 ** 4).toFixed(2)} TB`;
21+
};
22+
23+
const FileTree: React.FC<Props> = ({
24+
title,
25+
tree,
26+
filesCount,
27+
totalBytes,
28+
onPreview,
29+
}) => (
30+
<Box
31+
sx={{
32+
backgroundColor: "#fff",
33+
borderRadius: 2,
34+
border: "1px solid #e0e0e0",
35+
height: "100%",
36+
display: "flex",
37+
flexDirection: "column",
38+
minHeight: 0,
39+
}}
40+
>
41+
<Box
42+
sx={{
43+
px: 2,
44+
py: 1.5,
45+
borderBottom: "1px solid #eee",
46+
display: "flex",
47+
alignItems: "center",
48+
gap: 1,
49+
flexShrink: 0,
50+
}}
51+
>
52+
<FolderIcon />
53+
<Typography sx={{ fontWeight: 700, flex: 1 }}>{title}</Typography>
54+
<Typography variant="body2" sx={{ color: "text.secondary" }}>
55+
Files: {filesCount} &nbsp; Size: {formatSize(totalBytes)}
56+
</Typography>
57+
</Box>
58+
59+
<Box sx={{ flex: 1, minHeight: 0, overflowY: "auto", py: 0.5 }}>
60+
{tree.map((n) => (
61+
<FileTreeRow key={n.path} node={n} level={0} onPreview={onPreview} />
62+
))}
63+
</Box>
64+
</Box>
65+
);
66+
67+
export default FileTree;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import type { TreeNode } from "./types";
2+
import { formatLeafValue, isPreviewable } from "./utils";
3+
import DownloadIcon from "@mui/icons-material/Download";
4+
import ExpandLess from "@mui/icons-material/ExpandLess";
5+
import ExpandMore from "@mui/icons-material/ExpandMore";
6+
import FolderIcon from "@mui/icons-material/Folder";
7+
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
8+
import VisibilityIcon from "@mui/icons-material/Visibility";
9+
import { Box, Button, Collapse, Typography } from "@mui/material";
10+
import React from "react";
11+
12+
type Props = {
13+
node: TreeNode;
14+
level: number;
15+
onPreview: (url: string, index: number) => void;
16+
};
17+
18+
const FileTreeRow: React.FC<Props> = ({ node, level, onPreview }) => {
19+
const [open, setOpen] = React.useState(false);
20+
21+
if (node.kind === "folder") {
22+
return (
23+
<>
24+
<Box
25+
sx={{
26+
display: "flex",
27+
alignItems: "center",
28+
gap: 1,
29+
py: 0.5,
30+
px: 1,
31+
cursor: "pointer",
32+
"&:hover": { backgroundColor: "rgba(0,0,0,0.04)" },
33+
}}
34+
onClick={() => setOpen((o) => !o)}
35+
>
36+
<Box sx={{ pl: level * 1.25 }}>
37+
<FolderIcon fontSize="small" />
38+
</Box>
39+
<Typography sx={{ fontWeight: 600, flex: 1 }}>{node.name}</Typography>
40+
{open ? <ExpandLess /> : <ExpandMore />}
41+
</Box>
42+
43+
<Collapse in={open} timeout="auto" unmountOnExit>
44+
{node.children.map((child) => (
45+
<FileTreeRow
46+
key={child.path}
47+
node={child}
48+
level={level + 1}
49+
onPreview={onPreview}
50+
/>
51+
))}
52+
</Collapse>
53+
</>
54+
);
55+
}
56+
57+
return (
58+
<Box
59+
sx={{ display: "flex", alignItems: "flex-start", gap: 1, py: 0.5, px: 1 }}
60+
>
61+
<Box sx={{ pl: level * 1.25, pt: "2px" }}>
62+
<InsertDriveFileIcon fontSize="small" />
63+
</Box>
64+
65+
<Box sx={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
66+
<Typography
67+
title={node.name}
68+
sx={{
69+
fontWeight: 500,
70+
whiteSpace: "nowrap",
71+
overflow: "hidden",
72+
textOverflow: "ellipsis",
73+
}}
74+
>
75+
{node.name}
76+
</Typography>
77+
78+
{!node.link && node.value !== undefined && (
79+
<Typography
80+
title={
81+
node.name === "_ArrayZipData_"
82+
? "[compressed data]"
83+
: typeof node.value === "string"
84+
? node.value
85+
: JSON.stringify(node.value)
86+
}
87+
sx={{
88+
fontFamily: "monospace",
89+
fontSize: "0.85rem",
90+
color: "text.secondary",
91+
whiteSpace: "nowrap",
92+
overflow: "hidden",
93+
textOverflow: "ellipsis",
94+
mt: 0.25,
95+
}}
96+
>
97+
{node.name === "_ArrayZipData_"
98+
? "[compressed data]"
99+
: formatLeafValue(node.value)}
100+
</Typography>
101+
)}
102+
</Box>
103+
104+
{node.link?.url && (
105+
<Box sx={{ display: "flex", gap: 1, flexShrink: 0 }}>
106+
<Button
107+
size="small"
108+
variant="text"
109+
onClick={() => window.open(node.link!.url, "_blank")}
110+
startIcon={<DownloadIcon fontSize="small" />}
111+
>
112+
Download
113+
</Button>
114+
{isPreviewable(node.link.url) && (
115+
<Button
116+
size="small"
117+
variant="text"
118+
startIcon={<VisibilityIcon fontSize="small" />}
119+
onClick={() => onPreview(node.link!.url, node.link!.index)}
120+
>
121+
Preview
122+
</Button>
123+
)}
124+
</Box>
125+
)}
126+
</Box>
127+
);
128+
};
129+
130+
export default FileTreeRow;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type LinkMeta = { url: string; index: number };
2+
3+
// this value can be one of these types
4+
export type TreeNode =
5+
| { kind: "folder"; name: string; path: string; children: TreeNode[] }
6+
| { kind: "file"; name: string; path: string; value?: any; link?: LinkMeta };
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { LinkMeta, TreeNode } from "./types";
2+
3+
export const isPreviewable = (url: string) =>
4+
/\.(nii(\.gz)?|bnii|jdt|jdb|jmsh|bmsh)$/i.test(
5+
(url.match(/file=([^&]+)/)?.[1] ?? url).toLowerCase()
6+
);
7+
8+
export const formatLeafValue = (v: any): string => {
9+
if (v === null) return "null";
10+
const t = typeof v;
11+
if (t === "number" || t === "boolean") return String(v);
12+
if (t === "string") return v.length > 120 ? v.slice(0, 120) + "…" : v;
13+
if (Array.isArray(v)) {
14+
const n = v.length;
15+
const head = v
16+
.slice(0, 5)
17+
.map((x) => (typeof x === "number" ? x : JSON.stringify(x)));
18+
return n <= 5
19+
? `[${head.join(", ")}]`
20+
: `[${head.join(", ")}, …] (${n} items)`;
21+
}
22+
return ""; // if it is object, return as a folder
23+
};
24+
25+
// ignore meta keys
26+
export const shouldSkipKey = (key: string) =>
27+
key === "_id" || key === "_rev" || key.startsWith(".");
28+
29+
// build path -> {url, index} lookup, built from extractDataLinks function
30+
// if external link objects have {path, url, index}, build a Map for the tree
31+
export const makeLinkMap = <
32+
T extends { path: string; url: string; index: number }
33+
>(
34+
links: T[]
35+
): Map<string, LinkMeta> => {
36+
const m = new Map<string, LinkMeta>();
37+
links.forEach((l) => m.set(l.path, { url: l.url, index: l.index }));
38+
return m;
39+
};
40+
41+
// Recursively convert the dataset JSON to a file-tree
42+
export const buildTreeFromDoc = (
43+
doc: any,
44+
linkMap: Map<string, LinkMeta>,
45+
curPath = ""
46+
): TreeNode[] => {
47+
if (!doc || typeof doc !== "object") return [];
48+
const out: TreeNode[] = [];
49+
50+
Object.keys(doc).forEach((key) => {
51+
if (shouldSkipKey(key)) return;
52+
53+
const val = doc[key];
54+
const path = `${curPath}/${key}`;
55+
const link = linkMap.get(path);
56+
57+
if (link) {
58+
out.push({ kind: "file", name: key, path, link });
59+
return;
60+
}
61+
62+
if (val && typeof val === "object" && !Array.isArray(val)) {
63+
out.push({
64+
kind: "folder",
65+
name: key,
66+
path,
67+
children: buildTreeFromDoc(val, linkMap, path),
68+
});
69+
return;
70+
}
71+
72+
out.push({ kind: "file", name: key, path, value: val });
73+
});
74+
75+
return out;
76+
};

src/components/Routes.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import DatasetDetailPage from "pages/DatasetDetailPage";
66
import DatasetPage from "pages/DatasetPage";
77
import Home from "pages/Home";
88
import SearchPage from "pages/SearchPage";
9+
import UpdatedDatasetDetailPage from "pages/UpdatedDatasetDetailPage";
910
import NewDatasetPage from "pages/UpdatedDatasetPage";
1011
import React from "react";
1112
import { Navigate, Route, Routes as RouterRoutes } from "react-router-dom";
@@ -33,6 +34,7 @@ const Routes = () => (
3334
<Route
3435
path={`${RoutesEnum.DATABASES}/:dbName/:docId`}
3536
element={<DatasetDetailPage />}
37+
// element={<UpdatedDatasetDetailPage />}
3638
/>
3739

3840
{/* Search Page */}

src/design/ReadMoreText.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Colors } from "./theme";
2+
import { Box, Typography, Button } from "@mui/material";
3+
import React, { useState } from "react";
4+
5+
const ReadMoreText: React.FC<{ text: string }> = ({ text }) => {
6+
const [expanded, setExpanded] = useState(false);
7+
8+
return (
9+
<Box sx={{ position: "relative" }}>
10+
<Typography
11+
variant="body1"
12+
sx={{
13+
display: "-webkit-box",
14+
WebkitLineClamp: expanded ? "unset" : 3, // show only 3 lines
15+
WebkitBoxOrient: "vertical",
16+
overflow: "hidden",
17+
textOverflow: "ellipsis",
18+
}}
19+
>
20+
{text}
21+
</Typography>
22+
23+
<Button
24+
size="small"
25+
sx={{
26+
mt: 1,
27+
fontWeight: 600,
28+
textTransform: "uppercase",
29+
fontSize: "0.8rem",
30+
color: Colors.purple,
31+
"&:hover": {
32+
color: Colors.secondaryPurple,
33+
transform: "scale(1.05)",
34+
},
35+
}}
36+
onClick={() => setExpanded(!expanded)}
37+
>
38+
{expanded ? "Read Less" : "Read More"}
39+
</Button>
40+
</Box>
41+
);
42+
};
43+
44+
export default ReadMoreText;

0 commit comments

Comments
 (0)