Skip to content

Commit

Permalink
Merge pull request #236 from flatironinstitute/indicate-data-origin
Browse files Browse the repository at this point in the history
Indicate data origin
  • Loading branch information
WardBrian authored Nov 4, 2024
2 parents c612989 + fa1c0e9 commit 5c88cc6
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 28 deletions.
15 changes: 14 additions & 1 deletion gui/src/app/Project/ProjectDataModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,26 @@ const isProjectBase = (x: any): x is ProjectBase => {
return true;
};

export enum DataSource {
GENERATED_BY_R = "generated_by_r",
GENERATED_BY_STALE_R = "generated_by_stale_r",
GENERATED_BY_PYTHON = "generated_by_python",
GENERATED_BY_STALE_PYTHON = "generated_by_stale_python",
}

type ProjectMetadata = {
title: string;
dataSource?: DataSource;
};

export const isProjectMetaData = (x: any): x is ProjectMetadata => {
if (!baseObjectCheck(x)) return false;
if (typeof x.title !== "string") return false;
if (
x.dataSource !== undefined && // undefined = manually edited or unknown provenance
!Object.values(DataSource).includes(x.dataSource)
)
return false;
return true;
};

Expand Down Expand Up @@ -128,7 +141,7 @@ export const isProjectDataModel = (x: any): x is ProjectDataModel => {
export type ProjectPersistentDataModel = Omit<ProjectDataModel, "ephemera">;

export const initialDataModel: ProjectDataModel = {
meta: { title: "Untitled" },
meta: { title: "Untitled", dataSource: undefined },
ephemera: {
stanFileContent: "",
dataFileContent: "",
Expand Down
41 changes: 41 additions & 0 deletions gui/src/app/Project/ProjectReducer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FieldsContentsMap } from "@SpCore/FileMapping";
import {
DataSource,
initialDataModel,
ProjectDataModel,
ProjectKnownFiles,
Expand All @@ -24,6 +25,7 @@ export type ProjectReducerAction =
type: "retitle";
title: string;
}
| { type: "generateData"; content: string; dataSource: DataSource }
| {
type: "editFile";
content: string;
Expand Down Expand Up @@ -69,9 +71,27 @@ const ProjectReducer = (s: ProjectDataModel, a: ProjectReducerAction) => {
}
case "commitFile": {
const newState = { ...s };
const newDataSource = confirmDataSourceForCommit(
s.meta.dataSource,
a.filename,
);
if (newDataSource !== s.meta.dataSource) {
newState.meta = { ...s.meta, dataSource: newDataSource };
}
newState[a.filename] = s.ephemera[a.filename];
return newState;
}
case "generateData": {
return {
...s,
[ProjectKnownFiles.DATAFILE]: a.content,
ephemera: {
...s.ephemera,
[ProjectKnownFiles.DATAFILE]: a.content,
},
meta: { ...s.meta, dataSource: a.dataSource },
};
}
case "setSamplingOpts": {
return { ...s, samplingOpts: { ...s.samplingOpts, ...a.opts } };
}
Expand All @@ -86,4 +106,25 @@ const ProjectReducer = (s: ProjectDataModel, a: ProjectReducerAction) => {
}
};

const confirmDataSourceForCommit = (
currentSource: DataSource | undefined,
editedFile: ProjectKnownFiles,
): DataSource | undefined => {
if (editedFile === ProjectKnownFiles.DATAFILE) return undefined;
if (
editedFile === ProjectKnownFiles.DATAPYFILE &&
currentSource === DataSource.GENERATED_BY_PYTHON
) {
return DataSource.GENERATED_BY_STALE_PYTHON;
}
if (
editedFile === ProjectKnownFiles.DATARFILE &&
currentSource === DataSource.GENERATED_BY_R
) {
return DataSource.GENERATED_BY_STALE_R;
}

return currentSource;
};

export default ProjectReducer;
2 changes: 1 addition & 1 deletion gui/src/app/Scripting/DataGeneration/DataPyWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const handleHelp = () =>
);

const DataPyWindow: FunctionComponent<Props> = () => {
const { consoleRef, status, onStatus, onData } = useDataGenState();
const { consoleRef, status, onStatus, onData } = useDataGenState("python");

const callbacks = useMemo(
() => ({
Expand Down
2 changes: 1 addition & 1 deletion gui/src/app/Scripting/DataGeneration/DataRWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const handleHelp = () =>
);

const DataRWindow: FunctionComponent<Props> = () => {
const { consoleRef, status, onStatus, onData } = useDataGenState();
const { consoleRef, status, onStatus, onData } = useDataGenState("r");

const { run } = useWebR({ consoleRef, onStatus, onData });
const handleRun = useCallback(
Expand Down
12 changes: 7 additions & 5 deletions gui/src/app/Scripting/DataGeneration/useDataGenState.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useCallback, useContext, useRef, useState } from "react";
import { ProjectKnownFiles } from "@SpCore/ProjectDataModel";
import { DataSource } from "@SpCore/ProjectDataModel";
import { writeConsoleOutToDiv } from "@SpScripting/OutputDivUtils";
import { InterpreterStatus } from "@SpScripting/InterpreterTypes";
import { ProjectContext } from "@SpCore/ProjectContextProvider";

// A custom hook to share logic between the Python and R data generation windows
// This contains the output div ref, the interpreter state, and the callback to update the data.
const useDataGenState = () => {
const useDataGenState = (source: "python" | "r") => {
const [status, setStatus] = useState<InterpreterStatus>("idle");
const consoleRef = useRef<HTMLDivElement>(null);

Expand All @@ -21,11 +21,13 @@ const useDataGenState = () => {
if (dataJson !== lastData.current) {
lastData.current = dataJson;
update({
type: "editFile",
type: "generateData",
content: dataJson,
filename: ProjectKnownFiles.DATAFILE,
dataSource:
source === "python"
? DataSource.GENERATED_BY_PYTHON
: DataSource.GENERATED_BY_R,
});
update({ type: "commitFile", filename: ProjectKnownFiles.DATAFILE });
// Use "stan-playground" prefix to distinguish from console output of the running code
writeConsoleOutToDiv(
consoleRef,
Expand Down
105 changes: 85 additions & 20 deletions gui/src/app/pages/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ import useMediaQuery from "@mui/material/useMediaQuery";
import StanFileEditor from "@SpComponents/StanFileEditor";
import TabWidget from "@SpComponents/TabWidget";
import TextEditor from "@SpComponents/TextEditor";
import { ColorOptions, ToolbarItem } from "@SpComponents/ToolBar";
import { FileNames } from "@SpCore/FileMapping";
import { ProjectContext } from "@SpCore/ProjectContextProvider";
import {
DataSource,
modelHasUnsavedChanges,
ProjectKnownFiles,
} from "@SpCore/ProjectDataModel";
import Sidebar, { drawerWidth } from "@SpPages/Sidebar";
import TopBar from "@SpPages/TopBar";
import DataPyWindow from "@SpScripting/DataGeneration/DataPyWindow";
import DataRWindow from "@SpScripting/DataGeneration/DataRWindow";
import { unreachable } from "@SpUtil/unreachable";
import {
FunctionComponent,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
Expand Down Expand Up @@ -92,6 +97,7 @@ type LeftViewProps = {

const LeftView: FunctionComponent<LeftViewProps> = () => {
const { data, update } = useContext(ProjectContext);

return (
<Split horizontal>
<StanFileEditor
Expand All @@ -114,28 +120,87 @@ const LeftView: FunctionComponent<LeftViewProps> = () => {
}
readOnly={false}
/>
<TextEditor
language="json"
label={FileNames.DATAFILE}
text={data.dataFileContent}
onSaveText={() =>
update({
type: "commitFile",
filename: ProjectKnownFiles.DATAFILE,
})
<DataEditor />
</Split>
);
};

const DataEditor: FunctionComponent<unknown> = () => {
const { data, update } = useContext(ProjectContext);

const dataIsEdited = useMemo(() => {
return data.dataFileContent !== data.ephemera.dataFileContent;
}, [data.dataFileContent, data.ephemera.dataFileContent]);

const dataSourceDesc: undefined | { msg: string; color: ColorOptions } =
useMemo(() => {
if (dataIsEdited) return undefined;

switch (data.meta.dataSource) {
case undefined: {
return undefined;
}
editedText={data.ephemera.dataFileContent}
onSetEditedText={(content: string) =>
update({
type: "editFile",
content,
filename: ProjectKnownFiles.DATAFILE,
})
case DataSource.GENERATED_BY_PYTHON: {
return { msg: "data.py", color: "info.main" };
}
readOnly={false}
contentOnEmpty={"Enter JSON data or use the data generation tab"}
/>
</Split>
case DataSource.GENERATED_BY_R: {
return { msg: "data.R", color: "info.main" };
}
case DataSource.GENERATED_BY_STALE_PYTHON: {
return { msg: "a prior version of data.py.", color: "warning.main" };
}
case DataSource.GENERATED_BY_STALE_R: {
return { msg: "a prior version of data.R.", color: "warning.main" };
}
default:
return unreachable(data.meta.dataSource);
}
}, [dataIsEdited, data.meta.dataSource]);

const dataMessage: ToolbarItem[] = useMemo(() => {
if (dataSourceDesc === undefined) {
return [];
} else {
return [
{
type: "text",
label: `Data generated by ${dataSourceDesc.msg}`,
color: dataSourceDesc.color,
},
];
}
}, [dataSourceDesc]);

const onSetEditedText = useCallback(
(content: string) => {
update({
type: "editFile",
content,
filename: ProjectKnownFiles.DATAFILE,
});
},
[update],
);

const onSaveText = useCallback(() => {
update({
type: "commitFile",
filename: ProjectKnownFiles.DATAFILE,
});
}, [update]);

return (
<TextEditor
language="json"
label={FileNames.DATAFILE}
text={data.dataFileContent}
onSaveText={onSaveText}
editedText={data.ephemera.dataFileContent}
onSetEditedText={onSetEditedText}
readOnly={false}
toolbarItems={dataMessage}
contentOnEmpty={"Enter JSON data or use the data generation tab"}
/>
);
};

Expand Down
18 changes: 18 additions & 0 deletions gui/test/app/Project/ProjectDataModel.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DataSource,
defaultSamplingOpts,
exportedForTesting,
getStringKnownFileKeys,
Expand Down Expand Up @@ -248,6 +249,23 @@ describe("Project data model type guards", () => {
expect(isProjectMetaData({ title: 6 })).toBe(false);
expect(isProjectMetaData({ no_title: "title" })).toBe(false);
});
test("Returns true for valid data source", () => {
expect(
isProjectMetaData({
title: "title",
dataSource: DataSource.GENERATED_BY_PYTHON,
}),
).toBe(true);
expect(
isProjectMetaData({
title: "title",
dataSource: DataSource.GENERATED_BY_R,
}),
).toBe(true);
});
test("Returns false on bad data source", () => {
expect(isProjectMetaData({ title: "title", dataSource: 1 })).toBe(false);
});
});
describe("Project ephemeral-data typeguard", () => {
test("Returns true for valid project files object", () => {
Expand Down
Loading

0 comments on commit 5c88cc6

Please sign in to comment.