Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring/experiments module #180

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
4 changes: 1 addition & 3 deletions DashAI/front/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ module.exports = {
jsx: true,
},
},
plugins: ["react", "react-hooks"],
plugins: ["react"],
rules: {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error",
"linebreak-style": [
"error",
process.platform === "win32" ? "windows" : "unix",
Expand Down
4 changes: 2 additions & 2 deletions DashAI/front/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";

import "./App.css";
import DatasetsPage from "./pages/DatasetsPage";
import ExperimentsPage from "./pages/ExperimentPage";
import Experiments from "./pages/experiments/Experiments";
import RunResults from "./components/results/RunResults";
import ResultsPage from "./pages/ResultsPage";
import Home from "./pages/Home";
Expand All @@ -18,7 +18,7 @@ function App() {
<Route path="/" element={<Home />} />
<Route path="/app" element={<Home />} />
<Route path="/app/data/" element={<DatasetsPage />} />
<Route path="/app/experiments" element={<ExperimentsPage />} />
<Route path="/app/experiments" element={<Experiments />} />
<Route path="/app/results">
<Route index element={<ResultsPage />} />
<Route path="experiments/:id">
Expand Down
1 change: 1 addition & 0 deletions DashAI/front/src/components/experiments/RunnerDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function RunnerDialog({ experiment, expRunning, setExpRunning }) {
const allRunsFinished = runs
.filter((run) => rowSelectionModel.includes(run.id)) // get only the runs that have been selected to be sent to the runner
.every((run) => run.status === 3 || run.status === 4); // finished or error

if (allRunsFinished) {
setExpRunning({ ...expRunning, [experiment.id]: false });
// only shows snackbar one time
Expand Down
32 changes: 32 additions & 0 deletions DashAI/front/src/components/shared/ItemDescription.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Box, Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import React from "react";

function ItemDescription({ title, description, images }) {
return (
<Stack>
<Typography variant="h6" sx={{ mb: 4 }}>
{title}
</Typography>
{images &&
images.map((img, i) => (
<Box
component={"img"}
src={img}
alt={`Description image ${img}`}
key={img}
style={{ borderRadius: "10px", maxWidth: "400px" }}
/>
))}
<Typography>{description}</Typography>
</Stack>
);
}

ItemDescription.propTypes = {
title: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.string),
description: PropTypes.string,
};

export default ItemDescription;
56 changes: 56 additions & 0 deletions DashAI/front/src/components/shared/ItemSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { List, ListItem, ListItemButton, ListItemText } from "@mui/material";
import PropTypes from "prop-types";
import React from "react";
/**
*This component renders a list of items so that the user can select one.
* @param {object[]} itemsList The list of items to select from
* @param {object} selectedItem The item from the list that has been selected.
* @param {function} setSelectedItem function to change the value of the selected item
* @param {bool} disabled true to disable the item selection, false to enable it
*/
function ItemSelector({
itemsList,
selectedItemName,
setSelectedItem,
disabled,
}) {
return (
<List sx={{ width: "100%" }}>
{itemsList.map((item) => {
return (
<ListItem
key={`list-button-${item.name}`}
disablePadding
sx={{
pointerEvents: disabled ? "none" : "auto",
opacity: disabled ? 0.5 : 1,
overflow: "hidden",
}}
>
<ListItemButton
selected={selectedItemName === item.name}
onClick={() => setSelectedItem(item)}
>
<ListItemText primary={item.name} />
</ListItemButton>
</ListItem>
);
})}
</List>
);
}

ItemSelector.propTypes = {
itemsList: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string }))
.isRequired,
selectedItemName: PropTypes.string,
setSelectedItem: PropTypes.func,
disabled: PropTypes.bool,
};

ItemSelector.defaultProps = {
selectedItem: undefined,
disabled: false,
};

export default ItemSelector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Grid, Paper } from "@mui/material";
import PropTypes from "prop-types";
import React from "react";
import ItemDescription from "./ItemDescription";
import ItemSelectorWithSearch from "./ItemSelectorWithSearch";
function ItemSelectorWithDescriptionContainer({
itemsList,
setSelectedItem,
disabled,
selectedItemName,
...descriptionProps
}) {
return (
<Grid container columns={{ xs: 12, md: 15 }}>
{/* Item list */}
<Grid item xs={12} md={7}>
<Paper>
<ItemSelectorWithSearch
itemsList={itemsList}
disabled={disabled}
selectedItemName={selectedItemName}
setSelectedItem={setSelectedItem}
/>
</Paper>
</Grid>
<Grid item xs={12} md={1} />
<Grid item xs={12} md={7}>
<Paper sx={{ p: 2, height: "100%" }}>
<ItemDescription {...descriptionProps} />
</Paper>
</Grid>
</Grid>
);
}

ItemSelectorWithDescriptionContainer.propTypes = {
itemsList: PropTypes.arrayOf(PropTypes.shape({ class: PropTypes.string }))
.isRequired,
setSelectedItem: PropTypes.func.isRequired,
disabled: PropTypes.bool,
selectedItemName: PropTypes.string,
};

ItemSelectorWithDescriptionContainer.defaultProps = {
selectedItemKey: null,
disabled: false,
};

export default ItemSelectorWithDescriptionContainer;
58 changes: 58 additions & 0 deletions DashAI/front/src/components/shared/ItemSelectorWithSearch.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import ClearIcon from "@mui/icons-material/Clear";
import {
Box,
IconButton,
InputAdornment,
Stack,
TextField,
} from "@mui/material";
import PropTypes from "prop-types";
import React, { useState } from "react";
import ItemSelector from "./ItemSelector";

function ItemSelectorWithSearch({ itemsList, ...props }) {
const [searchField, setSearchField] = useState("");

const handleSearchFieldChange = (event) => {
setSearchField(event.target.value.toLowerCase());
};

const filteredItems = itemsList.filter((val) =>
val.name.toLowerCase().includes(searchField),
);

return (
<Box sx={{ p: 2 }}>
<Stack>
<TextField
id="item-search-input"
fullWidth
label="Search..."
type="search"
variant="standard"
value={searchField}
onChange={handleSearchFieldChange}
size="small"
sx={{ mb: 2 }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={() => setSearchField("")}>
<ClearIcon />
</IconButton>
</InputAdornment>
),
}}
/>
<ItemSelector itemsList={filteredItems} {...props} />
</Stack>
</Box>
);
}

ItemSelectorWithSearch.propTypes = {
itemsList: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string }))
.isRequired,
};

export default ItemSelectorWithSearch;
50 changes: 50 additions & 0 deletions DashAI/front/src/components/shared/StepperActions.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Button, ButtonGroup, DialogActions } from "@mui/material";
import PropTypes from "prop-types";
import React from "react";

function StepperActions({
onStartEdge,
onEndEdge,
activeStep,
setActiveStep,
nextEnabled,
}) {
const isStartStep = activeStep === 0;
const isEndStep = activeStep === 2;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be a parameter?


const handleBackButton = () => {
isStartStep ? onStartEdge() : setActiveStep(activeStep - 1);
};

const handleNextButton = () => {
isEndStep ? onEndEdge() : setActiveStep(activeStep + 1);
};
return (
<DialogActions>
<ButtonGroup size="large">
<Button onClick={handleBackButton}>
{isStartStep ? "Close" : "Back"}
</Button>
<Button
onClick={handleNextButton}
autoFocus
variant="contained"
color="primary"
disabled={!nextEnabled}
>
{isEndStep ? "Save" : "Next"}
</Button>
Comment on lines +28 to +36
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be too complicated to add an upload button at the end of the process (to indicate that the experiment and runs are being uploaded)?

https://mui.com/material-ui/react-button/#loading-button

It's a euphemism, I know it's a lot of code haha.
This idea has been on my mind for a long time since there is no way for the user to know if it is uploading or not.

As I understand it, I would have to add a loading to the hook of the request to create experiments and then coordinate it between all the components?

Let me know if you think it would be better to leave it for another time or if you can integrate it right away.

</ButtonGroup>
</DialogActions>
);
}

StepperActions.propTypes = {
onStartEdge: PropTypes.func.isRequired,
onEndEdge: PropTypes.func.isRequired,
activeStep: PropTypes.number.isRequired,
setActiveStep: PropTypes.func.isRequired,
nextEnabled: PropTypes.bool.isRequired,
};

export default StepperActions;
92 changes: 92 additions & 0 deletions DashAI/front/src/components/shared/StepperContainer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { DialogContent } from "@mui/material";
import PropTypes from "prop-types";
import React, { createContext, useContext, useState } from "react";
import StepperActions from "./StepperActions";
import StepperTitle from "./StepperTitle";

/*
* This component renders a stepper dialog.
* It contains the stepper title, body and actions.
* It also contains a context to share the state between the components.
* This pattern is called compound components, you can read more about it here:
* https://kentcdodds.com/blog/compound-components-with-react-hooks
Comment on lines +11 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

*/

const StepperContext = createContext();

const StepperProvider = ({ children }) => {
const [activeStep, setActiveStep] = useState(0);
const [nextEnabled, setNextEnabled] = useState(false);

const sharedData = {
activeStep,
setActiveStep,
nextEnabled,
setNextEnabled,
};

return (
<StepperContext.Provider value={sharedData}>
{children}
</StepperContext.Provider>
);
};

function StepperContainer({ children }) {
return <StepperProvider>{children}</StepperProvider>;
}

function Title(props) {
const { activeStep, setActiveStep } = useContext(StepperContext);
return (
<StepperTitle
{...props}
activeStep={activeStep}
setActiveStep={setActiveStep}
/>
);
}

function Body({ stepsComponents, stepsComponentsProps }) {
const { activeStep, setNextEnabled } = useContext(StepperContext);

const StepContent = stepsComponents[activeStep];

return (
<DialogContent dividers>
<StepContent {...stepsComponentsProps} setNextEnabled={setNextEnabled} />
</DialogContent>
);
}

function Actions(props) {
const { activeStep, setActiveStep, nextEnabled } = useContext(StepperContext);

return (
<StepperActions
{...props}
activeStep={activeStep}
setActiveStep={setActiveStep}
nextEnabled={nextEnabled}
/>
);
}

StepperProvider.propTypes = {
children: PropTypes.node.isRequired,
};

StepperContainer.propTypes = {
children: PropTypes.node.isRequired,
};

Body.propTypes = {
stepsComponents: PropTypes.arrayOf(PropTypes.func.isRequired).isRequired,
stepsComponentsProps: PropTypes.object.isRequired,
};

StepperContainer.Title = Title;
StepperContainer.Body = Body;
StepperContainer.Actions = Actions;

export default StepperContainer;
Loading