Skip to content

Commit

Permalink
Merge pull request #77 from flatironinstitute/general-data-model-js
Browse files Browse the repository at this point in the history
  • Loading branch information
jsoules authored Jun 25, 2024
2 parents 5836532 + 6d4ff35 commit fe7d9c5
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 260 deletions.
3 changes: 2 additions & 1 deletion gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,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

0 comments on commit fe7d9c5

Please sign in to comment.