Skip to content

Commit

Permalink
feat(ui-debug): add Folder component
Browse files Browse the repository at this point in the history
  • Loading branch information
skamril committed Sep 6, 2024
1 parent 541a8a7 commit 3ea0fae
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 36 deletions.
1 change: 1 addition & 0 deletions webapp/public/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
"study.bindingconstraints": "Binding Constraints",
"study.debug": "Debug",
"study.debug.file.image": "Image file",
"study.debug.folder.empty": "Folder is empty",
"study.failtofetchlogs": "Failed to fetch logs",
"study.failtokilltask": "Failed to kill task",
"study.publicMode": "Public mode",
Expand Down
1 change: 1 addition & 0 deletions webapp/public/locales/fr/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
"study.bindingconstraints": "Contraintes Couplantes",
"study.debug": "Debug",
"study.debug.file.image": "Fichier image",
"study.debug.folder.empty": "Le dossier est vide",
"study.failtofetchlogs": "Échec du chargement des logs",
"study.failtokilltask": "Échec de l'annulation de l'étude",
"study.publicMode": "Mode public",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
Divider,
List,
ListItemButton,
ListItemIcon,
ListItemText,
ListSubheader,
} from "@mui/material";
import FolderIcon from "@mui/icons-material/Folder";
import {
getFileIcon,
getFileType,
type TreeFolder,
type DataCompProps,
} from "../utils";
import ViewWrapper from "../../../../../common/page/ViewWrapper";
import { Fragment } from "react";
import EmptyView from "../../../../../common/page/SimpleContent";
import { useTranslation } from "react-i18next";

function Folder({
filename,
filePath,
treeData,
setSelectedFile,
}: DataCompProps) {
const { t } = useTranslation();
const list = Object.entries(treeData as TreeFolder);

return (
<ViewWrapper>
<List subheader={<ListSubheader>{filename}</ListSubheader>} dense>
{list.length > 0 ? (
list.map(([filename, data], index, arr) => {
const fileType = getFileType(data);
const Icon = getFileIcon(fileType);
const isLast = index === arr.length - 1;

return (
<Fragment key={filename}>
<ListItemButton
onClick={() =>
setSelectedFile({
fileType,
filename,
filePath: `${filePath}/${filename}`,
treeData: data,
})
}
>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={filename} />
</ListItemButton>
{!isLast && <Divider variant="fullWidth" />}
</Fragment>
);
})
) : (
<EmptyView title={t("study.debug.folder.empty")} icon={FolderIcon} />
)}
</List>
</ViewWrapper>
);
}

export default Folder;
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import MatrixInput from "../../../../../common/MatrixInput";
import ViewWrapper from "../../../../../common/page/ViewWrapper";
import type { DataCompProps } from "../utils";

function Matrix({ studyId, filePath, enableImport }: DataCompProps) {
const filename = filePath.split("/").pop();

function Matrix({ studyId, filename, filePath, enableImport }: DataCompProps) {
return (
<ViewWrapper>
<MatrixInput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import Text from "./Text";
import Image from "./Image";
import Json from "./Json";
import Matrix from "./Matrix";
import Folder from "./Folder";
import type { FileInfo, FileType } from "../utils";
import type { DataCompProps } from "../utils";

interface Props extends FileInfo {
studyId: string;
setSelectedFile: (file: FileInfo) => void;
}

type DataComponent = React.ComponentType<DataCompProps>;
Expand All @@ -16,18 +18,20 @@ const componentByFileType: Record<FileType, DataComponent> = {
json: Json,
text: Text,
image: Image,
folder: ({ filePath }) => filePath,
folder: Folder,
} as const;

function Data({ studyId, fileType, filePath }: Props) {
function Data({ studyId, setSelectedFile, ...fileInfo }: Props) {
const { fileType, filePath } = fileInfo;
const isUserFolder = filePath.startsWith("/user/");
const DataViewer = componentByFileType[fileType];

return (
<DataViewer
{...fileInfo}
studyId={studyId}
filePath={filePath}
enableImport={isUserFolder}
setSelectedFile={setSelectedFile}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { FileInfo } from "./utils";
import { voidFn } from "../../../../../utils/fnUtils";

const initialDebugContextValue = {
onFileSelect: voidFn<[FileInfo]>,
setSelectedFile: voidFn<[FileInfo]>,
reloadTreeData: voidFn,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,22 @@ import { useContext } from "react";

interface Props {
name: string;
content: TreeData;
path: string;
treeData: TreeData;
}

function FileTreeItem({ name, content, path }: Props) {
const { onFileSelect } = useContext(DebugContext);
const filePath = `${path}/${name}`;
const fileType = getFileType(content);
function FileTreeItem({ name, treeData, path }: Props) {
const { setSelectedFile } = useContext(DebugContext);
const filePath = path ? `${path}/${name}` : name;
const fileType = getFileType(treeData);
const FileIcon = getFileIcon(fileType);

////////////////////////////////////////////////////////////////
// Event handlers
////////////////////////////////////////////////////////////////

const handleClick = () => {
if (fileType !== "folder") {
onFileSelect({ fileType, filePath });
}
setSelectedFile({ fileType, filename: name, filePath, treeData });
};

////////////////////////////////////////////////////////////////
Expand All @@ -41,13 +39,13 @@ function FileTreeItem({ name, content, path }: Props) {
}
onClick={handleClick}
>
{isFolder(content) &&
Object.keys(content).map((childName) => (
{isFolder(treeData) &&
Object.keys(treeData).map((childName) => (
<FileTreeItem
key={childName}
name={childName}
content={content[childName]}
path={filePath}
treeData={treeData[childName]}
/>
))}
</TreeItem>
Expand Down
52 changes: 48 additions & 4 deletions webapp/src/components/App/Singlestudy/explore/Debug/Tree/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,60 @@
import { SimpleTreeView } from "@mui/x-tree-view/SimpleTreeView";
import FileTreeItem from "./FileTreeItem";
import type { TreeFolder } from "../utils";
import { useState } from "react";

interface Props {
data: TreeFolder;
// `selectedItems` must not be undefined to make `SimpleTreeView` controlled
selectedItemId: string | null;
}

function Tree({ data }: Props) {
function getParentItemIds(itemId: string) {
// "a/b/c/d" -> ["a", "a/b", "a/b/c"]
return itemId
.split("/")
.slice(0, -1) // Remove the last item
.map((_, index, arr) => arr.slice(0, index + 1).join("/"));
}

function Tree({ data, selectedItemId }: Props) {
const [expandedItems, setExpandedItems] = useState<string[]>([]);

////////////////////////////////////////////////////////////////
// Event Handlers
////////////////////////////////////////////////////////////////

const handleExpandedItemsChange = (
event: React.SyntheticEvent,
itemIds: string[],
) => {
setExpandedItems(itemIds);
};

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

// `SimpleTreeView` must be controlled because selected item can be changed manually
// by `Folder` component

return (
<SimpleTreeView>
{Object.keys(data).map((key) => (
<FileTreeItem key={key} name={key} content={data[key]} path="" />
<SimpleTreeView
selectedItems={selectedItemId}
expandedItems={
selectedItemId
? [...expandedItems, ...getParentItemIds(selectedItemId)]
: expandedItems
}
onExpandedItemsChange={handleExpandedItemsChange}
>
{Object.keys(data).map((filename) => (
<FileTreeItem
key={filename}
name={filename}
treeData={data[filename]}
path=""
/>
))}
</SimpleTreeView>
);
Expand Down
45 changes: 40 additions & 5 deletions webapp/src/components/App/Singlestudy/explore/Debug/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ import UsePromiseCond from "../../../../common/utils/UsePromiseCond";
import usePromiseWithSnackbarError from "../../../../../hooks/usePromiseWithSnackbarError";
import { getStudyData } from "../../../../../services/api/study";
import DebugContext from "./DebugContext";
import type { FileInfo, TreeFolder } from "./utils";
import {
getFileType,
type TreeData,
type FileInfo,
type TreeFolder,
} from "./utils";
import * as R from "ramda";
import SplitView from "../../../../common/SplitView";
import { useUpdateEffect } from "react-use";

function Debug() {
const [t] = useTranslation();
const { study } = useOutletContext<{ study: StudyMetadata }>();
const [selectedFile, setSelectedFile] = useState<FileInfo>();
const [selectedFile, setSelectedFile] = useState<FileInfo | null>(null);

const res = usePromiseWithSnackbarError(
async () => {
Expand All @@ -31,12 +37,30 @@ function Debug() {

const contextValue = useMemo(
() => ({
onFileSelect: setSelectedFile,
setSelectedFile,
reloadTreeData: res.reload,
}),
[res.reload],
);

useUpdateEffect(() => {
const firstChildName = Object.keys(res.data ?? {})[0];
const treeData = R.path<TreeData>([firstChildName], res.data);

if (treeData) {
const fileInfo = {
fileType: getFileType(treeData),
filename: firstChildName,
filePath: firstChildName,
treeData,
};

setSelectedFile(fileInfo);
} else {
setSelectedFile(null);
}
}, [res?.data]);

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////
Expand All @@ -48,12 +72,23 @@ function Debug() {
response={res}
ifResolved={(data) => (
<DebugContext.Provider value={contextValue}>
<Tree data={data} />
<Tree
data={data}
selectedItemId={selectedFile?.filePath || null}
/>
</DebugContext.Provider>
)}
/>
</Box>
<Box>{selectedFile && <Data {...selectedFile} studyId={study.id} />}</Box>
<Box>
{selectedFile && (
<Data
{...selectedFile}
setSelectedFile={setSelectedFile}
studyId={study.id}
/>
)}
</Box>
</SplitView>
);
}
Expand Down
20 changes: 11 additions & 9 deletions webapp/src/components/App/Singlestudy/explore/Debug/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,6 @@ import * as RA from "ramda-adjunct";
// Types
////////////////////////////////////////////////////////////////

export type FileType = "json" | "matrix" | "text" | "image" | "folder";

export interface FileInfo {
fileType: FileType;
filePath: string;
}

export type TreeFile = string | string[];

export interface TreeFolder {
Expand All @@ -25,10 +18,19 @@ export interface TreeFolder {

export type TreeData = TreeFolder | TreeFile;

export interface DataCompProps {
studyId: string;
export type FileType = "json" | "matrix" | "text" | "image" | "folder";

export interface FileInfo {
fileType: FileType;
filename: string;
filePath: string;
treeData: TreeData;
}

export interface DataCompProps extends FileInfo {
studyId: string;
enableImport: boolean;
setSelectedFile: (file: FileInfo) => void;
}

////////////////////////////////////////////////////////////////
Expand Down
7 changes: 7 additions & 0 deletions webapp/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ const theme = createTheme({
},
},
},
MuiListSubheader: {
styleOverrides: {
root: {
backgroundColor: "#222333",
},
},
},
MuiAlert: {
styleOverrides: {
root: {
Expand Down

0 comments on commit 3ea0fae

Please sign in to comment.