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

Feature/context based reducer #31

Merged
merged 35 commits into from
May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e210ce8
Refactor: simplify reducer action types
langdal Apr 28, 2021
0c43a94
Introduce experiment provider
langdal Apr 28, 2021
7d4ff02
Make action types present tense imperative form
langdal Apr 28, 2021
4946272
Collocate tests and code
langdal Apr 28, 2021
dd4d1f7
Fix action type names
langdal Apr 28, 2021
f010b89
Add local storage reducer
langdal Apr 28, 2021
a282011
Move page test to separate folder
langdal Apr 28, 2021
da83399
Update nextjs
langdal Apr 28, 2021
577838d
Add unique key to image list
langdal Apr 28, 2021
5362845
Don't send entire experiment to children
langdal Apr 28, 2021
0589a8c
Don't send entire experiment to children (cont.)
langdal Apr 28, 2021
d55a329
Store each experiment separetly in LocalStorage
langdal Apr 28, 2021
67c4b4f
Disable server side rendering
langdal Apr 29, 2021
7d1d12e
Simplify data points reducer
langdal Apr 29, 2021
2ffa84a
Remove selector example
langdal Apr 29, 2021
1ffbaa0
Fix useLocalStorageReducer
langdal Apr 29, 2021
3f0fa60
Intermediate step towards proper context
langdal Apr 29, 2021
d3e5642
Only show experiment if ID is present
langdal May 2, 2021
678ed66
Temporary show raw json
langdal May 2, 2021
a43bab8
Adjust data points test to new reducer
langdal May 2, 2021
43349ad
Adjust API to return 404 on missing experiment
langdal May 2, 2021
915f30f
Show loading indication
langdal May 2, 2021
d174238
Set id in initial state
langdal May 2, 2021
0472440
Return 404 status when data not found
langdal May 2, 2021
e4bafc5
Add debug view
langdal May 2, 2021
571e60d
Reformat provider
langdal May 2, 2021
92d32b6
Make data points parameters more precise
langdal May 2, 2021
8f324b6
Remove reference to empty type
langdal May 2, 2021
221c7d4
Add simple test for useExperiment
langdal May 2, 2021
e1e7e01
Add support for global context
langdal May 2, 2021
dfb7576
Add global context
langdal May 2, 2021
b33509e
Add switch to toggle local storage (sample)
langdal May 2, 2021
9c6de6d
Add debug properly to global state
langdal May 2, 2021
0e45384
Prevent test from failing due to shallow rendering
langdal May 3, 2021
c99be94
Fix copy-paste error
langdal May 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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