Skip to content

Commit 2c2d76f

Browse files
authored
1061 filesystem UI component (#1064)
* Createda testing space and initial setup, need to figure fetch * Using a new filestructure system * Fetching folers from files also * File selector MVP complete * Cleaned out the UI and code * Extractor now has capability to use Fileselector widget * Extractor now has capability to use Fileselector widget * Cleanup * Quick fix * linting change * Dataset Icon and limit increase
1 parent ead8d85 commit 2c2d76f

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed

frontend/src/components/listeners/SubmitExtraction.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { RootState } from "../../types/data";
2525
import { EventListenerOut as Extractor } from "../../openapi/v2";
2626
import { ClowderRjsfSelectWidget } from "../styledComponents/ClowderRjsfSelectWidget";
2727
import { ClowderRjsfTextWidget } from "../styledComponents/ClowderRjsfTextWidget";
28+
import { ClowderFileSelector } from "../styledComponents/ClowderFileSelector";
2829
import ExtractorStatus from "./ExtractorStatus";
2930
import CloseIcon from "@mui/icons-material/Close";
3031

@@ -40,6 +41,11 @@ type SubmitExtractionProps = {
4041
const widgets = {
4142
TextWidget: ClowderRjsfTextWidget,
4243
SelectWidget: ClowderRjsfSelectWidget,
44+
clowderFile: ClowderFileSelector,
45+
};
46+
47+
const uiSchema = {
48+
clowderFile: { "ui:widget": "FileSelector" },
4349
};
4450

4551
export default function SubmitExtraction(props: SubmitExtractionProps) {
@@ -156,6 +162,7 @@ export default function SubmitExtraction(props: SubmitExtractionProps) {
156162
<Container>
157163
<Form
158164
schema={{ properties: {} }}
165+
uiSchema={uiSchema}
159166
onSubmit={({ formData }) => {
160167
onSubmit(formData);
161168
}}
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
import React, { useState, useEffect } from "react";
2+
import { useDispatch, useSelector } from "react-redux";
3+
import {
4+
Button,
5+
Box,
6+
Modal,
7+
List,
8+
ListItem,
9+
ListItemIcon,
10+
ListItemText,
11+
Typography,
12+
IconButton,
13+
Collapse,
14+
} from "@mui/material";
15+
import FolderIcon from "@mui/icons-material/Folder";
16+
import DatasetIcon from "@mui/icons-material/Dataset";
17+
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
18+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
19+
import ImageIcon from "@mui/icons-material/Image";
20+
import AudioIcon from "@mui/icons-material/Audiotrack";
21+
import VideoIcon from "@mui/icons-material/VideoFile";
22+
import TextIcon from "@mui/icons-material/Description";
23+
24+
import { fetchDatasets } from "../../actions/dataset";
25+
import { V2 } from "../../openapi";
26+
27+
// Define the file details
28+
interface FileDetails {
29+
fileId: string;
30+
fileName: string;
31+
}
32+
33+
// Define the RecursiveComponent component with props type
34+
interface RecursiveComponentProps {
35+
item: FSItem;
36+
depth?: number;
37+
onSelectFile: (fileId: string, fileName: string) => void;
38+
}
39+
40+
// Define a type for items in the directory structure
41+
interface FSItem {
42+
datasetId: string;
43+
id?: string;
44+
label: string;
45+
children?: FSItem[] | undefined;
46+
type: string; // A FSItem can be a folder or a file,
47+
content_type?: string;
48+
}
49+
50+
const RecursiveComponent: React.FC<RecursiveComponentProps> = ({
51+
item,
52+
depth = 0,
53+
onSelectFile,
54+
}) => {
55+
const [expanded, setExpanded] = useState(false);
56+
const [children, setChildren] = useState<FSItem[] | undefined>(item.children);
57+
const isFolderOrDataset = item.type === "folder" || item.type === "dataset";
58+
59+
// Function to generate Icon based on item type
60+
const getIcon = () => {
61+
if (item.type === "folder") {
62+
return <FolderIcon />;
63+
} else if (item.type === "dataset") {
64+
return <DatasetIcon />;
65+
}
66+
switch (item.content_type) {
67+
case "image":
68+
return <ImageIcon />;
69+
case "audio":
70+
return <AudioIcon />;
71+
case "video":
72+
return <VideoIcon />;
73+
case "text":
74+
return <TextIcon />;
75+
default:
76+
return <InsertDriveFileIcon />;
77+
}
78+
};
79+
80+
// Function to fetch children of a folder
81+
async function fetchFolderFiles(
82+
datasetid: string,
83+
folderId: string | undefined
84+
) {
85+
try {
86+
const response =
87+
await V2.DatasetsService.getDatasetFoldersAndFilesApiV2DatasetsDatasetIdFoldersAndFilesGet(
88+
datasetid,
89+
folderId,
90+
// TODO: Remove hardcoded values
91+
0,
92+
3000
93+
);
94+
const data = response.data;
95+
const FSItems: FSItem[] =
96+
data !== undefined
97+
? data.map((FSItem: any) => ({
98+
datasetId: datasetid,
99+
id: FSItem.id,
100+
label: FSItem.name,
101+
children: FSItem.object_type === "folder" ? [] : undefined,
102+
type: FSItem.object_type,
103+
content_type:
104+
FSItem.object_type !== "folder"
105+
? FSItem.content_type.main_type
106+
: undefined,
107+
}))
108+
: [];
109+
return FSItems;
110+
} catch (error) {
111+
console.error("Error fetching folders and files", error);
112+
}
113+
}
114+
115+
// Function to handle selection of folder or file
116+
const onSelect = () => {
117+
if (isFolderOrDataset) {
118+
if (!expanded) {
119+
fetchFolderFiles(item.datasetId, item.id).then((data) => {
120+
setChildren(data);
121+
});
122+
}
123+
setExpanded(!expanded);
124+
} else {
125+
if (item.id !== undefined) {
126+
onSelectFile(item.id, item.label);
127+
}
128+
}
129+
};
130+
131+
return (
132+
<List disablePadding>
133+
{/* Indentation of item proportional to depth */}
134+
<ListItem
135+
sx={{
136+
pl: depth * 2,
137+
borderBottom: "none",
138+
py: 0.5,
139+
"&:hover": {
140+
backgroundColor: "rgba(0, 0, 0, 0.1)", // or any other color
141+
cursor: "pointer",
142+
},
143+
}}
144+
onClick={onSelect}
145+
>
146+
<ListItemIcon sx={{ minWidth: "auto", mr: 1 }}>
147+
<IconButton
148+
size="small"
149+
sx={{ visibility: isFolderOrDataset ? "visible" : "hidden" }}
150+
>
151+
<ExpandMoreIcon
152+
style={{
153+
transform: expanded ? "rotate(0deg)" : "rotate(-90deg)",
154+
}}
155+
/>
156+
</IconButton>
157+
</ListItemIcon>
158+
<ListItemIcon sx={{ minWidth: "auto" }}>{getIcon()}</ListItemIcon>
159+
<ListItemText primary={item.label} />
160+
</ListItem>
161+
<Collapse in={expanded} timeout="auto" unmountOnExit>
162+
{isFolderOrDataset && (
163+
<Box sx={{ ml: 2 }}>
164+
{children?.map((child) => (
165+
<RecursiveComponent
166+
key={child.id}
167+
item={child}
168+
depth={depth + 1}
169+
onSelectFile={onSelectFile}
170+
/>
171+
))}
172+
</Box>
173+
)}
174+
</Collapse>
175+
</List>
176+
);
177+
};
178+
179+
const FileSystemViewer: React.FC<{
180+
onSelectFile: (fileId: string, fileName: string) => void;
181+
}> = ({ onSelectFile }) => {
182+
const dispatch = useDispatch();
183+
const datasets = useSelector((state: any) => state.dataset.datasets);
184+
const [FSItems, setFSItems] = useState<FSItem[]>([]);
185+
186+
// API function call to get Datasets
187+
const listDatasets = (skip?: number, limit?: number, mine?: boolean) => {
188+
dispatch(fetchDatasets(skip, limit, mine));
189+
};
190+
191+
// Fetch datasets on component mount
192+
useEffect(() => {
193+
// TODO: Remove hardcoded values for skip and limit
194+
listDatasets(0, 3000, true);
195+
}, []); //
196+
197+
useEffect(() => {
198+
if (datasets.data) {
199+
setFSItems(
200+
datasets.data.map((dataset: any) => ({
201+
datasetId: dataset.id,
202+
label: dataset.name,
203+
children: [],
204+
type: "dataset",
205+
}))
206+
);
207+
}
208+
}, [datasets]);
209+
210+
return FSItems.length > 0 ? (
211+
<Box
212+
sx={{
213+
width: "100%",
214+
height: 360,
215+
maxWidth: 360,
216+
overflowY: "auto",
217+
overflowX: "auto",
218+
bgcolor: "background.paper",
219+
}}
220+
>
221+
<Typography variant="h6" sx={{ ml: 2, my: 2 }}>
222+
File Selector
223+
</Typography>
224+
{FSItems.map((FSItem) => (
225+
<RecursiveComponent
226+
key={FSItem.id}
227+
item={FSItem}
228+
onSelectFile={onSelectFile}
229+
/>
230+
))}
231+
</Box>
232+
) : null;
233+
};
234+
235+
const FileSelector: React.FC<{ onChange: (fileId: string) => void }> = ({
236+
onChange,
237+
}) => {
238+
const [open, setOpen] = useState(false);
239+
const [selectedFile, setSelectedFile] = useState<FileDetails>({
240+
fileId: "",
241+
fileName: "",
242+
});
243+
244+
const handleOpen = () => setOpen(true);
245+
const handleClose = () => setOpen(false);
246+
247+
const handleFileSelect = (fileId: string, fileName: string) => {
248+
setSelectedFile({ fileId: fileId, fileName: fileName });
249+
onChange(fileId);
250+
handleClose();
251+
};
252+
253+
return (
254+
<Box sx={{ display: "flex", alignItems: "center", gap: 2, p: 2 }}>
255+
{selectedFile.fileName && (
256+
<Typography variant="subtitle1" sx={{ ml: 2 }}>
257+
{selectedFile.fileName}
258+
</Typography>
259+
)}
260+
<Button
261+
variant="outlined"
262+
onClick={handleOpen}
263+
startIcon={<InsertDriveFileIcon />}
264+
>
265+
Choose File
266+
</Button>
267+
<Modal
268+
open={open}
269+
onClose={handleClose}
270+
aria-labelledby="file-selection-modal"
271+
aria-describedby="modal-modal-description"
272+
>
273+
<Box
274+
sx={{
275+
position: "absolute",
276+
top: "50%",
277+
left: "50%",
278+
transform: "translate(-50%, -50%)",
279+
width: 400,
280+
bgcolor: "background.paper",
281+
boxShadow: 24,
282+
p: 4,
283+
}}
284+
>
285+
<FileSystemViewer onSelectFile={handleFileSelect} />
286+
</Box>
287+
</Modal>
288+
</Box>
289+
);
290+
};
291+
292+
export default FileSelector;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react";
2+
import FileSelectorButton from "../navigation/FileSelector";
3+
import { ClowderInputLabel } from "./ClowderInputLabel";
4+
5+
export const ClowderFileSelector = (item) => {
6+
const handleChange = (value) => {
7+
item.onChange(value);
8+
};
9+
return (
10+
<>
11+
<ClowderInputLabel>{item.schema.title}</ClowderInputLabel>
12+
<FileSelectorButton onChange={handleChange} />
13+
</>
14+
);
15+
};

0 commit comments

Comments
 (0)