Skip to content

Commit

Permalink
Merge pull request #15 from MetaCell/feature/CELE-52
Browse files Browse the repository at this point in the history
Feature/cele 52
  • Loading branch information
aranega authored Aug 13, 2024
2 parents afb7467 + e3db63c commit f8f32db
Show file tree
Hide file tree
Showing 35 changed files with 1,714 additions and 637 deletions.
2 changes: 1 addition & 1 deletion applications/visualizer/frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
overflow: auto;
}
43 changes: 32 additions & 11 deletions applications/visualizer/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,54 @@ import { ThemeProvider } from "@mui/material/styles";
import { Provider } from "react-redux";
import theme from "./theme/index.tsx";
import "./App.css";
import React from "react";
import AppLauncher from "./components/AppLauncher.tsx";
import Layout from "./components/ViewerContainer/Layout.tsx";
import WorkspaceComponent from "./components/WorkspaceComponent.tsx";
import CompareWrapper from "./components/wrappers/Compare.tsx";
import DefaultWrapper from "./components/wrappers/Default.tsx";
import { useGlobalContext } from "./contexts/GlobalContext.tsx";
import { ViewMode } from "./models";

function App() {
const { workspaces, currentWorkspaceId, viewMode, selectedWorkspacesIds } = useGlobalContext();
const [sidebarOpen, setSidebarOpen] = React.useState(true);

const hasLaunched = currentWorkspaceId !== undefined;

const renderCompareMode = (workspaceIds: string[]) => (
<CompareWrapper sidebarOpen={sidebarOpen}>
{workspaceIds.map((id) => (
<Provider key={id} store={workspaces[id].store}>
<WorkspaceComponent sidebarOpen={sidebarOpen} />
</Provider>
))}
</CompareWrapper>
);

const renderDefaultMode = (currentWorkspaceId: string) => (
<Provider store={workspaces[currentWorkspaceId].store}>
<DefaultWrapper>
<WorkspaceComponent sidebarOpen={sidebarOpen} />
</DefaultWrapper>
</Provider>
);

const renderWorkspaces = () => {
if (viewMode === ViewMode.Compare) {
return renderCompareMode(Array.from(selectedWorkspacesIds));
}
return renderDefaultMode(currentWorkspaceId as string);
};

return (
<>
<ThemeProvider theme={theme}>
<CssBaseline />
{hasLaunched ? (
<Box className={"layout-manager-container"}>
{viewMode === ViewMode.Compare ? (
Array.from(selectedWorkspacesIds).map((id) => (
<Provider key={id} store={workspaces[id].store}>
<WorkspaceComponent />
</Provider>
))
) : (
<Provider store={workspaces[currentWorkspaceId].store}>
<WorkspaceComponent />
</Provider>
)}
<Layout sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
{renderWorkspaces()}
</Box>
) : (
<AppLauncher />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@ import footerImage from "../assets/summary-neurons.png";
import { useGlobalContext } from "../contexts/GlobalContext.tsx";
import { parseURLParams } from "../helpers/parseURLHelper.ts";
import { TEMPLATE_ACTIVE_DATASETS, TEMPLATE_ACTIVE_NEURONS } from "../settings/templateWorkspaceSettings.ts";

function AppLauncher() {
const { workspaces, createWorkspace, setCurrentWorkspace } = useGlobalContext();
const { workspaces, createWorkspace, setCurrentWorkspace, setSelectedWorkspacesIds } = useGlobalContext();

const handleTemplateClick = async () => {
const workspaceId = `workspace-${Date.now()}`;
const workspaceName = `Template Workspace ${Object.keys(workspaces).length + 1}`;

createWorkspace(workspaceId, workspaceName, new Set(TEMPLATE_ACTIVE_DATASETS), new Set(TEMPLATE_ACTIVE_NEURONS));
setCurrentWorkspace(workspaceId);
setSelectedWorkspacesIds(new Set<string>([workspaceId]));
};

const handleBlankClick = () => {
const workspaceId = `workspace-${Date.now()}`;
const workspaceName = `Workspace ${Object.keys(workspaces).length + 1}`;

createWorkspace(workspaceId, workspaceName);
createWorkspace(workspaceId, workspaceName, new Set(TEMPLATE_ACTIVE_DATASETS));
setCurrentWorkspace(workspaceId);
setSelectedWorkspacesIds(new Set<string>([workspaceId]));
};

const handlePasteUrlClick = () => {
Expand All @@ -29,7 +29,6 @@ function AppLauncher() {
const parsedParams = parseURLParams(exampleURL);
console.log(parsedParams);
};

return (
<>
<Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { Box, Button, FormLabel, IconButton, TextField, Typography } from "@mui/material";
import { debounce } from "lodash";
import { useCallback, useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { useGlobalContext } from "../contexts/GlobalContext.tsx";
import { CaretIcon, CheckIcon, CloseIcon } from "../icons";
import { type Dataset, type Neuron, NeuronsService } from "../rest";
import { vars as colors } from "../theme/variables.ts";
import CustomAutocomplete from "./CustomAutocomplete.tsx";
import CustomDialog from "./CustomDialog.tsx";

const CreateNewWorkspaceDialog = ({ onCloseCreateWorkspace, showCreateWorkspaceDialog, isCompareMode, title, subTitle, submitButtonText }) => {
const [neurons, setNeurons] = useState<Neuron[]>([]);
const { workspaces, datasets, createWorkspace, setSelectedWorkspacesIds } = useGlobalContext();
const [searchedNeuron, setSearchedNeuron] = useState("");
const [formValues, setFormValues] = useState<{
workspaceName: string;
selectedDatasets: Dataset[];
selectedNeurons: Neuron[];
}>({
workspaceName: "",
selectedDatasets: [],
selectedNeurons: [],
});

const [errorMessage, setErrorMessage] = useState<string>("");

const workspaceFieldName = "workspaceName";
const fetchNeurons = async (name, datasetsIds) => {
try {
const Ids = datasetsIds.map((dataset) => dataset.id);
const response = await NeuronsService.searchCells({
name: name,
datasetIds: Ids,
});
setNeurons(response);
} catch (error) {
console.error("Failed to fetch datasets", error);
}
};

const debouncedFetchNeurons = useCallback(debounce(fetchNeurons, 300), []);
const onSearchNeurons = (value) => {
setSearchedNeuron(value);
debouncedFetchNeurons(value, formValues.selectedDatasets);
};
const handleInputChange = (event) => {
const { name, value } = event.target;
setFormValues({ ...formValues, [name]: value });
if (name === workspaceFieldName && errorMessage) {
setErrorMessage("");
}
};

const handleDatasetChange = (value) => {
setFormValues({ ...formValues, selectedDatasets: value });
debouncedFetchNeurons(searchedNeuron, value);
};

const handleNeuronChange = (value) => {
setFormValues({ ...formValues, selectedNeurons: value });
};

const isWorkspaceNameDuplicate = (name) => {
return Object.values(workspaces).some((workspace) => workspace.name === name);
};

const handleSubmit = () => {
if (!formValues.workspaceName.trim()) {
setErrorMessage("Workspace name is required!");
return;
}

if (isWorkspaceNameDuplicate(formValues.workspaceName.trim())) {
setErrorMessage("Workspace name already exists!");
return;
}

const randomNumber = uuidv4().replace(/\D/g, "").substring(0, 13);
const newWorkspaceId = `workspace-${randomNumber}`;
const activeNeurons = new Set(formValues.selectedNeurons.map((neuron) => neuron.name));
const activeDatasets = new Set(formValues.selectedDatasets.map((dataset) => dataset.id));
createWorkspace(newWorkspaceId, formValues.workspaceName, activeDatasets, activeNeurons);

if (isCompareMode) {
const updatedWorkspaces = new Set([...Object.keys(workspaces), newWorkspaceId]);
setSelectedWorkspacesIds(updatedWorkspaces);
}
onCloseCreateWorkspace();
};
const datasetsArray = Object.values(datasets);

return (
<CustomDialog onClose={onCloseCreateWorkspace} showModal={showCreateWorkspaceDialog} title={title}>
<Box px="1rem" py="1.5rem" gap={2.5} display="flex" flexDirection="column">
{subTitle && <Typography>{subTitle}</Typography>}
<Box>
<FormLabel>
Workspace name <Typography variant="caption">(REQUIRED)</Typography>
</FormLabel>
<TextField
fullWidth
variant="outlined"
placeholder="Start typing workspace name"
name={workspaceFieldName}
value={formValues.workspaceName}
onChange={handleInputChange}
error={!!errorMessage}
helperText={errorMessage}
/>
</Box>

<Box>
<FormLabel>Datasets</FormLabel>
<CustomAutocomplete
options={datasetsArray}
getOptionLabel={(option) => option.name}
renderOption={(props, option) => (
<li {...props}>
<CheckIcon />
<Typography>{option.name}</Typography>
</li>
)}
placeholder="Start typing to search"
id="grouped-demo"
popupIcon={<CaretIcon />}
ChipProps={{
deleteIcon: (
<IconButton sx={{ p: "0 !important", margin: "0 !important" }}>
<CloseIcon />
</IconButton>
),
}}
value={formValues.selectedDatasets}
onChange={handleDatasetChange}
/>
</Box>
<Box>
<FormLabel>Neurons</FormLabel>
<CustomAutocomplete
options={neurons}
getOptionLabel={(option) => option.name}
renderOption={(props, option) => (
<li {...props}>
<CheckIcon />
<Typography>{option.name}</Typography>
</li>
)}
onInputChange={onSearchNeurons}
placeholder="Start typing to search"
className="secondary"
id="tags-standard"
popupIcon={<CaretIcon />}
disabled={formValues.selectedDatasets.length === 0}
ChipProps={{
deleteIcon: (
<IconButton sx={{ p: "0 !important", margin: "0 !important" }}>
<CloseIcon />
</IconButton>
),
}}
clearIcon={false}
value={formValues.selectedNeurons}
onChange={handleNeuronChange}
/>
</Box>
</Box>
<Box borderTop={`0.0625rem solid ${colors.gray100}`} px="1rem" py="0.75rem" gap={0.5} display="flex" justifyContent="flex-end">
<Button variant="contained" color="info" onClick={handleSubmit} disabled={!formValues.workspaceName}>
{submitButtonText}
</Button>
</Box>
</CustomDialog>
);
};

export default CreateNewWorkspaceDialog;
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ interface CustomAutocompleteProps<T> {
id: string;
multiple?: boolean;
popupIcon: React.ReactNode;
clearIcon?: React.ReactNode; // Change to React.ReactNode to be consistent with popupIcon
clearIcon?: React.ReactNode;
ChipProps?: ChipProps;
sx?: SxProps;
componentsProps?: AutocompleteProps<T, boolean, boolean, boolean>["componentsProps"];
value?: any;
onChange: (v) => void;
disabled?: boolean;
onInputChange?: (v) => void;
}

const CommonAutocomplete = <T,>({
Expand All @@ -35,13 +39,24 @@ const CommonAutocomplete = <T,>({
clearIcon,
ChipProps,
sx = {},
value,
componentsProps = {},
onChange,
disabled = false,
onInputChange,
}: CustomAutocompleteProps<T>) => {
// @ts-ignore
return (
<Autocomplete
value={value}
multiple={multiple}
className={className}
id={id}
disabled={disabled}
onChange={(event: React.SyntheticEvent, value) => {
event.preventDefault();
onChange(value);
}}
clearIcon={clearIcon}
options={options}
popupIcon={popupIcon}
Expand All @@ -50,7 +65,7 @@ const CommonAutocomplete = <T,>({
groupBy={groupBy}
renderGroup={renderGroup}
renderOption={renderOption}
renderInput={(params) => <TextField {...params} placeholder={placeholder} />}
renderInput={(params) => <TextField {...params} placeholder={placeholder} onChange={(e) => onInputChange(e.target.value)} />}
sx={sx}
componentsProps={componentsProps}
/>
Expand Down
38 changes: 38 additions & 0 deletions applications/visualizer/frontend/src/components/CustomDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Box, Dialog, IconButton, Typography } from "@mui/material";
import type React from "react";
import { CloseIcon } from "../icons";
import { vars } from "../theme/variables.ts";

const { gray100 } = vars;
interface CustomDialogProps {
onClose: () => void;
showModal: boolean;
title: string;
children: React.ReactNode;
}

const CustomDialog = ({ onClose, showModal, title, children }: CustomDialogProps) => {
return (
<Dialog
onClose={onClose}
open={showModal}
sx={{
"& .MuiBackdrop-root": {
background: "rgba(0,0,0,0.25)",
},
}}
fullWidth
maxWidth="lg"
>
<Box borderBottom={`0.0625rem solid ${gray100}`} px="1rem" py="0.5rem" display="flex" alignItems="center" justifyContent="space-between">
<Typography component="h3">{title}</Typography>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</Box>
{children}
</Dialog>
);
};

export default CustomDialog;
Loading

0 comments on commit f8f32db

Please sign in to comment.