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

Suggested edits for PR #40 #77

Merged
merged 14 commits into from
Jun 25, 2024
Merged
3 changes: 2 additions & 1 deletion gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@
"typescript": "^5.0.2",
"vite": "^5.2.12",
"vitest": "^1.6.0"
}
},
"packageManager": "[email protected]+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}
58 changes: 0 additions & 58 deletions gui/src/app/ApplicationBar.tsx

This file was deleted.

8 changes: 2 additions & 6 deletions gui/src/app/MainWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useWindowDimensions } from "@fi-sci/misc";
import { FunctionComponent } from "react";
import ApplicationBar, { applicationBarHeight } from "./ApplicationBar";
import StatusBar, { statusBarHeight } from "./StatusBar";
import HomePage from "./pages/HomePage/HomePage";
import useRoute from "./useRoute";
Expand All @@ -12,13 +11,10 @@ type Props = {
const MainWindow: FunctionComponent<Props> = () => {
const {route} = useRoute()
const {width, height} = useWindowDimensions()
const H = height - applicationBarHeight - statusBarHeight
const H = height - statusBarHeight
return (
<div className="MainWindow" style={{position: 'absolute', width, height, overflow: 'hidden'}}>
<div className="MainWindowApplicationBar" style={{position: 'absolute', width, height: applicationBarHeight, overflow: 'hidden'}}>
<ApplicationBar />
</div>
<div className="MainWindowContent" style={{position: 'absolute', top: applicationBarHeight, width, height: H, overflow: 'hidden'}}>
<div className="MainWindowContent" style={{position: 'absolute', top: 0, width, height: H, overflow: 'hidden'}}>
{
route.page === 'home' ? (
<HomePage width={width} height={H} />
Expand Down
57 changes: 57 additions & 0 deletions gui/src/app/SPAnalysis/SPAnalysisContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { createContext, FunctionComponent, PropsWithChildren, useEffect, useReducer } from "react"
import { deserializeAnalysis, initialDataModel, serializeAnalysis, SPAnalysisDataModel } from "./SPAnalysisDataModel"
import { SPAnalysisReducer, SPAnalysisReducerAction, SPAnalysisReducerType } from "./SPAnalysisReducer"

type SPAnalysisContextType = {
data: SPAnalysisDataModel
update: React.Dispatch<SPAnalysisReducerAction>
}

type SPAnalysisContextProviderProps = {
// may be used in the future when we allow parameters to be passed through the string
sourceDataUri: string
}

export const SPAnalysisContext = createContext<SPAnalysisContextType>({
data: initialDataModel,
update: () => {}
})

const SPAnalysisContextProvider: FunctionComponent<PropsWithChildren<SPAnalysisContextProviderProps>> = ({children}) => {
const [data, update] = useReducer<SPAnalysisReducerType>(SPAnalysisReducer, initialDataModel)

////////////////////////////////////////////////////////////////////////////////////////
// For convenience, we save the state to local storage so it is available on
// reload of the page But this will be revised in the future to use a more
// sophisticated storage mechanism.
useEffect(() => {
// as user reloads the page or closes the tab, save state to local storage
const handleBeforeUnload = () => {
const state = serializeAnalysis(data)
localStorage.setItem('stan-playground-saved-state', state)
};
window.addEventListener('beforeunload', handleBeforeUnload);

return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [data])

useEffect(() => {
// load the saved state on first load
const savedState = localStorage.getItem('stan-playground-saved-state')
if (!savedState) return
const parsedData = deserializeAnalysis(savedState)
update({ type: 'loadLocalStorage', state: parsedData })
}, [])
////////////////////////////////////////////////////////////////////////////////////////

return (
<SPAnalysisContext.Provider value={{data, update}}>
{children}
</SPAnalysisContext.Provider>
)
}

export default SPAnalysisContextProvider

58 changes: 58 additions & 0 deletions gui/src/app/SPAnalysis/SPAnalysisDataModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { SamplingOpts, defaultSamplingOpts } from "../StanSampler/StanSampler"

export enum SPAnalysisKnownFiles {
STANFILE = 'stanFileContent',
DATAFILE = 'dataFileContent',
}

type SPAnalysisFiles = {
[filetype in SPAnalysisKnownFiles]: string
}

type SPAnalysisBase = SPAnalysisFiles &
{
samplingOpts: SamplingOpts
}

type SPAnalysisMetadata = {
title: string
}

type SPAnalysisEphemeralData = SPAnalysisFiles & {
// possible future things to track include the compilation status
// of the current stan src file(s)
// not implemented in this PR, but we need some content for the type
server?: string
}

export type SPAnalysisDataModel = SPAnalysisBase &
{
meta: SPAnalysisMetadata,
ephemera: SPAnalysisEphemeralData
}

export const initialDataModel: SPAnalysisDataModel = {
meta: { title: "Undefined" },
ephemera: {
stanFileContent: "",
dataFileContent: "",
},
stanFileContent: "",
dataFileContent: "",
samplingOpts: defaultSamplingOpts
}

export const serializeAnalysis = (data: SPAnalysisDataModel): string => {
const intermediary = {
...data, ephemera: undefined }
return JSON.stringify(intermediary)
}

export const deserializeAnalysis = (serialized: string): SPAnalysisDataModel => {
const intermediary = JSON.parse(serialized)
// Not sure if this is strictly necessary
intermediary.ephemera = {}
const stringFileKeys = Object.values(SPAnalysisKnownFiles).filter((v) => isNaN(Number(v)));
stringFileKeys.forEach((k) => intermediary.ephemera[k] = intermediary[k]);
return intermediary as SPAnalysisDataModel
}
75 changes: 75 additions & 0 deletions gui/src/app/SPAnalysis/SPAnalysisReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Reducer } from "react"
import { Stanie } from "../exampleStanies/exampleStanies"
import { defaultSamplingOpts, SamplingOpts } from '../StanSampler/StanSampler'
import { initialDataModel, SPAnalysisDataModel, SPAnalysisKnownFiles } from "./SPAnalysisDataModel"


export type SPAnalysisReducerType = Reducer<SPAnalysisDataModel, SPAnalysisReducerAction>

export type SPAnalysisReducerAction = {
type: 'loadStanie',
stanie: Stanie
} | {
type: 'retitle',
title: string
} | {
type: 'editFile',
content: string,
filename: SPAnalysisKnownFiles
} | {
type: 'commitFile',
filename: SPAnalysisKnownFiles
} | {
type: 'setSamplingOpts',
opts: Partial<SamplingOpts>
} | {
type: 'loadLocalStorage',
state: SPAnalysisDataModel
} | {
type: 'clear'
}

export const SPAnalysisReducer: SPAnalysisReducerType = (s: SPAnalysisDataModel, a: SPAnalysisReducerAction) => {
switch (a.type) {
case "loadStanie": {
return {
...s,
stanFileContent: a.stanie.stan,
dataFileContent: JSON.stringify(a.stanie.data),
samplingOpts: defaultSamplingOpts,
meta: { ...s.meta, title: a.stanie.meta.title ?? 'Untitled' },
ephemera: {
...s.ephemera,
stanFileContent: a.stanie.stan,
dataFileContent: JSON.stringify(a.stanie.data)
}
}
}
case "retitle": {
return {
...s,
meta: { ...s.meta, title: a.title }
}
}
case "editFile": {
const newEphemera = { ...s.ephemera }
newEphemera[a.filename] = a.content
return { ...s, ephemera: newEphemera }
}
case "commitFile": {
const newState = { ...s }
newState[a.filename] = s.ephemera[a.filename]
return newState
}
case "setSamplingOpts": {
return { ...s, samplingOpts: { ...s.samplingOpts, ...a.opts }}
}
case "loadLocalStorage": {
return a.state;
}
case "clear": {
return initialDataModel
}
}
}

Loading