Skip to content

Commit

Permalink
Merge pull request #31 from BoostV/feature/context_based_reducer
Browse files Browse the repository at this point in the history
Feature/context based reducer
  • Loading branch information
langdal authored May 3, 2021
2 parents 58cdc53 + c99be94 commit d6cbe4f
Show file tree
Hide file tree
Showing 27 changed files with 2,019 additions and 2,265 deletions.
25 changes: 15 additions & 10 deletions components/data-points.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Card, CardContent, Typography } from "@material-ui/core";
import { useEffect, useReducer } from "react";
import { dataPointsReducer, DataPointsState, DATA_POINTS_TABLE_EDITED, DATA_POINTS_TABLE_EDIT_CANCELLED, DATA_POINTS_TABLE_EDIT_TOGGLED, DATA_POINTS_TABLE_ROW_ADDED, DATA_POINTS_TABLE_ROW_DELETED } from "../reducers/data-points-reducer";
import { ExperimentType, DataPointType, TableDataPoint, TableDataRow, CombinedVariableType } from "../types/common";
import { dataPointsReducer, DataPointsState } from "../reducers/data-points-reducer";
import { DataPointType, TableDataPoint, TableDataRow, CombinedVariableType, ValueVariableType, CategoricalVariableType } from "../types/common";
import { EditableTable } from "./editable-table";

type DataPointProps = {
experiment: ExperimentType
valueVariables: ValueVariableType[]
categoricalVariables: CategoricalVariableType[]
dataPoints: DataPointType[][]
onUpdateDataPoints: (dataPoints: DataPointType[][]) => void
}

const SCORE = "score"

export default function DataPoints(props: DataPointProps) {
const { experiment: { valueVariables, categoricalVariables, dataPoints }, onUpdateDataPoints } = props
const { valueVariables, categoricalVariables, dataPoints , onUpdateDataPoints } = props
const combinedVariables: CombinedVariableType[] = (valueVariables as CombinedVariableType[]).concat(categoricalVariables as CombinedVariableType[])

const emptyRow: TableDataRow = {
Expand Down Expand Up @@ -52,20 +54,24 @@ export default function DataPoints(props: DataPointProps) {

const [state, dispatch] = useReducer(dataPointsReducer, initialState)

// useEffect(() => {
// dispatch({ type: 'setInitialState', payload: {rows: dataPointRows, prevRows: dataPointRows}})
// }, [dataPointRows])

useEffect(() => {
updateDataPoints(state.rows.filter(item => !item.isNew) as TableDataRow[])
}, [state.rows])

function toggleEditMode(rowIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_EDIT_TOGGLED, payload: rowIndex })
dispatch({ type: 'DATA_POINTS_TABLE_EDIT_TOGGLED', payload: rowIndex })
}

function cancelEdit(rowIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_EDIT_CANCELLED, payload: rowIndex })
dispatch({ type: 'DATA_POINTS_TABLE_EDIT_CANCELLED', payload: rowIndex })
}

function edit(editValue: string, rowIndex: number, itemIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_EDITED, payload: {
dispatch({ type: 'DATA_POINTS_TABLE_EDITED', payload: {
itemIndex,
rowIndex,
useArrayForValue: SCORE,
Expand All @@ -74,11 +80,11 @@ export default function DataPoints(props: DataPointProps) {
}

function deleteRow(rowIndex: number) {
dispatch({ type: DATA_POINTS_TABLE_ROW_DELETED, payload: rowIndex })
dispatch({ type: 'DATA_POINTS_TABLE_ROW_DELETED', payload: rowIndex })
}

function addRow(emptyRow: TableDataRow) {
dispatch({ type: DATA_POINTS_TABLE_ROW_ADDED, payload: emptyRow })
dispatch({ type: 'DATA_POINTS_TABLE_ROW_ADDED', payload: emptyRow })
}

function updateDataPoints(dataRows: TableDataRow[]) {
Expand All @@ -105,7 +111,6 @@ export default function DataPoints(props: DataPointProps) {
return (
<Card>
<CardContent>

<Typography variant="h6" gutterBottom>
Data points
</Typography>
Expand Down
17 changes: 17 additions & 0 deletions components/debugexperiment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Button, Card, CardContent, Grid, Snackbar, Typography } from '@material-ui/core'
import { useExperiment } from "../context/experiment-context"

export default function DebugExperiment() {
const { state } = useExperiment()
return (
<>
<Card>
<CardContent>
<Typography variant="body2" component="p">
<pre>{JSON.stringify(state.experiment, null, 2)}</pre>
</Typography>
</CardContent>
</Card>
</>
)
}
160 changes: 160 additions & 0 deletions components/experiment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Button, Card, CardContent, Grid, Snackbar, Typography } from '@material-ui/core'
import Layout from './layout'
import OptimizerModel from './optimizer-model';
import OptimizerConfigurator from './optimizer-configurator';
import { Alert } from '@material-ui/lab';
import ModelEditor from './model-editor';
import DataPoints from './data-points';
import { useStyles } from '../styles/experiment.style';
import { useExperiment, saveExperiment, runExperiment } from '../context/experiment-context';
import React, { useState, useEffect } from 'react';
import { ValueVariableType, CategoricalVariableType, OptimizerConfig, DataPointType } from '../types/common';

export default function Experiment() {
const classes = useStyles();
const { state: {
experiment
}, dispatch, loading } = useExperiment()

const [lastSavedExperiment, setLastSavedExperiment] = useState(experiment)
const [isDirty, setDirty] = useState(false)
const [isSnackbarOpen, setSnackbarOpen] = useState(false)

useEffect(() => {
if (lastSavedExperiment && JSON.stringify(lastSavedExperiment) !== JSON.stringify(experiment)) {
setDirty(true)
}
}, [experiment])

const onSave = async () => {
try {
await saveExperiment(experiment)
setLastSavedExperiment(experiment)
setDirty(false)
setSnackbarOpen(true)
} catch (error) {
console.error('fetch error', error)
}
}

const onRun = async () => {
try {
await runExperiment(dispatch, experiment)
setDirty(true)
//setSnackbarOpen(true)
} catch (error) {
console.error('fetch error', error)
}

}

function handleCloseSnackbar() {
setSnackbarOpen(false)
}

const valueVariables = experiment.valueVariables
const categoricalVariables = experiment.categoricalVariables
if (loading) {
return <>Loading experiment...</>
}
return (
<Layout>
<Card className={[classes.experimentContainer, isDirty ? classes.experimentContainerDirty : ''].join(' ')}>
<CardContent>
<Grid container spacing={2}>
<Grid item xs={10}>
<Typography variant="body2">
{experiment.id}
</Typography>
<Typography variant="h4" gutterBottom>
{/* Experiment {experiment.id} {isDirty && '(unsaved)'} [{experiment.results.rawResult || 'No results'}] */}
{experiment.info.name} {isDirty && '(unsaved)'}
</Typography>
</Grid>

<Grid item xs={2} className={classes.actionContainer}>
<Button variant="contained" className={isDirty ? classes.saveButtonDirty : ''} onClick={onSave} color="primary">Save</Button>
<Button variant="contained" className={classes.runButton} color="primary" onClick={onRun}>Run</Button>
</Grid>
<Grid item xs={3}>
<Grid container spacing={2}>
<Grid item xs={12}>
<ModelEditor
info={experiment.info}
updateName={(name: string) => dispatch({ type: 'updateExperimentName', payload: name })}
updateDescription={(description: string) => dispatch({ type: 'updateExperimentDescription', payload: description })} />
</Grid>

<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6">
Next experiment
</Typography>
<Typography variant="body2">
{experiment.results.next && experiment.results.next.join(',')}
</Typography>
</CardContent>
</Card>
</Grid>

<Grid item xs={12}>
<OptimizerConfigurator
config={experiment.optimizerConfig}
onConfigUpdated={(config: OptimizerConfig) => dispatch({ type: 'updateConfiguration', payload: config })} />
</Grid>
</Grid>
</Grid>

<Grid item xs={8} lg={7}>

<Grid container spacing={2}>
<Grid item xs={12}>
<OptimizerModel
valueVariables={valueVariables}
categoricalVariables={categoricalVariables}
disabled={experiment.dataPoints.length > 0}
onDeleteValueVariable={(valueVariable: ValueVariableType) => { dispatch({ type: 'deleteValueVariable', payload: valueVariable }) }}
onDeleteCategoricalVariable={(categoricalVariable: CategoricalVariableType) => { dispatch({ type: 'deleteCategorialVariable', payload: categoricalVariable }) }}
addValueVariable={(valueVariable: ValueVariableType) => dispatch({ type: 'addValueVariable', payload: valueVariable })}
addCategoricalVariable={(categoricalVariable: CategoricalVariableType) => dispatch({ type: 'addCategorialVariable', payload: categoricalVariable })} />
</Grid>
<Grid item xs={12}>
<DataPoints
valueVariables={experiment.valueVariables}
categoricalVariables={experiment.categoricalVariables}
dataPoints={experiment.dataPoints}
onUpdateDataPoints={(dataPoints: DataPointType[][]) => dispatch({ type: 'updateDataPoints', payload: dataPoints })} />
</Grid>
<Grid item xs={12}>
{experiment.results.plots.length > 0 &&
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Plots
</Typography>
<ul>
{experiment.results.plots && experiment.results.plots.map(plot => <li key={plot.id}><img src={`data:image/png;base64, ${plot.plot}`} alt={plot.id}></img></li>)}
</ul>
</CardContent>
</Card>
}
</Grid>
</Grid>
</Grid>
<Grid item xs={2}></Grid>
</Grid>
</CardContent>
</Card>

<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
open={isSnackbarOpen}
autoHideDuration={3000}
onClose={handleCloseSnackbar}>
<Alert onClose={handleCloseSnackbar} severity="success">Experiment saved</Alert>
</Snackbar>

</Layout>
)
}
11 changes: 10 additions & 1 deletion components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { AppBar, Button, Toolbar, Typography } from '@material-ui/core'
import { AppBar, Button, Switch, Toolbar, Typography } from '@material-ui/core'
import Link from 'next/link'
import useStyles from '../styles/layout.style'
import Image from 'next/image'
import { useGlobal } from '../context/global-context'

export default function Layout ( {children} ) {
const classes = useStyles()
const { state, dispatch } = useGlobal()

const handleSwitch = (flagName) => (event: React.ChangeEvent<HTMLInputElement>) => {
dispatch({type: flagName, payload: event.target.checked})
}

return (
<>
<AppBar>
<Toolbar variant="dense">
<Switch checked={state.debug} onChange={handleSwitch('debug')} name="debug" inputProps={{ 'aria-label': 'secondary checkbox' }}/>
<Switch checked={state.useLocalStorage} onChange={handleSwitch('useLocalStorage')} name="useLocalStorage" inputProps={{ 'aria-label': 'secondary checkbox' }}/>
<div className={classes.logo}>
<Image src="/logo.png" alt="logo" width="32" height="32" />
</div>
Expand All @@ -26,6 +34,7 @@ export default function Layout ( {children} ) {
</Toolbar>
</AppBar>
<div className={classes.mainContent}>
{state.debug && <pre>{JSON.stringify(state, null, 2)}</pre>}
{children}
</div>
</>
Expand Down
22 changes: 12 additions & 10 deletions components/optimizer-model.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { Box, Button, Card, CardContent, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Typography } from '@material-ui/core'
import { CategoricalVariableType, ExperimentType, ValueVariableType } from '../types/common'
import DeleteIcon from '@material-ui/icons/Delete'
import { ReactNode, useState } from 'react'
import { FC, useState } from 'react'
import VariableEditor from './variable-editor'

type OptimizerModelProps = {
experiment: ExperimentType
valueVariables: ValueVariableType[]
categoricalVariables: CategoricalVariableType[]
disabled:boolean
onDeleteValueVariable: (valueVariable: ValueVariableType) => void
onDeleteCategoricalVariable: (categoricalVariable: CategoricalVariableType) => void
addValueVariable: (valueVariable: ValueVariableType) => void
addCategoricalVariable: (categoricalVariable: CategoricalVariableType) => void
}

export default function OptimizerModel(props: OptimizerModelProps) {
const { experiment: { valueVariables, categoricalVariables, dataPoints } } = props
const { valueVariables, categoricalVariables, disabled, onDeleteValueVariable, onDeleteCategoricalVariable, addValueVariable, addCategoricalVariable } = props
const [isAddOpen, setAddOpen] = useState(false)

return (
Expand Down Expand Up @@ -43,10 +45,10 @@ export default function OptimizerModel(props: OptimizerModelProps) {
<TableCell align="right">{valueVar.maxVal}</TableCell>
<TableCell align="right">
<IconButton
disabled={dataPoints.length > 0}
disabled={disabled}
size="small"
onClick={() => {props.onDeleteValueVariable(valueVar)}}>
<DeleteIcon color={dataPoints.length > 0 ? "inherit" : "primary"} fontSize="small"/>
<DeleteIcon color={disabled ? "inherit" : "primary"} fontSize="small"/>
</IconButton>
</TableCell>
</TableRow>
Expand Down Expand Up @@ -80,10 +82,10 @@ export default function OptimizerModel(props: OptimizerModelProps) {
</TableCell>
<TableCell align="right">
<IconButton
disabled={dataPoints.length > 0}
disabled={disabled}
size="small"
onClick={() => {props.onDeleteCategoricalVariable(catVar)}}>
<DeleteIcon color={dataPoints.length > 0 ? "inherit" : "primary"} fontSize="small"/>
<DeleteIcon color={disabled ? "inherit" : "primary"} fontSize="small"/>
</IconButton>
</TableCell>
</TableRow>
Expand All @@ -94,20 +96,20 @@ export default function OptimizerModel(props: OptimizerModelProps) {
}
{!isAddOpen &&
<Button
disabled={dataPoints.length > 0}
disabled={disabled}
variant="outlined"
size="small"
onClick={() => setAddOpen(true)}>Add variable</Button>
}
{isAddOpen &&
<VariableEditor
isAddVariableDisabled={dataPoints.length > 0}
isAddVariableDisabled={disabled}
addCategoricalVariable={(categoricalVariable: CategoricalVariableType) => props.addCategoricalVariable(categoricalVariable)}
addValueVariable={(valueVariable: ValueVariableType) => props.addValueVariable(valueVariable)}
close={() => setAddOpen(false)} />
}

{dataPoints.length > 0 &&
{disabled &&
<Box mt={2}>
<Typography variant="body2" color="textSecondary">
Note: Model cannot be updated while there are data points
Expand Down
22 changes: 22 additions & 0 deletions context/experiment-context.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { getByTestId, render, screen, waitFor } from "@testing-library/react";
import { useExperiment, TestExperimentProvider } from "./experiment-context";

describe("useExperiment", () => {
it("fails if called outside provider", async () => {
function ExperimentTester() {
const context = useExperiment()
return <>{JSON.stringify(context)}</>
}
expect(() => render(<ExperimentTester />)).toThrow("useExperiment must be used within an ExperimentProvider")
})

it("provides context when called inside provider", async () => {
function ExperimentTester() {
const context = useExperiment()
return <div data-testid="json">{JSON.stringify(context)}</div>
}
render(<TestExperimentProvider value={{ name: 'test' }}><ExperimentTester /></TestExperimentProvider>)
const rawJson = screen.getByTestId("json")
expect(rawJson.innerHTML).toEqual(JSON.stringify({ name: "test" }))
})
})
Loading

0 comments on commit d6cbe4f

Please sign in to comment.