Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
padding: 0 40px;
}

.load-button {
margin-right: 20px;
}

.footer {
display: flex;
flex-direction: row;
Expand Down
39 changes: 29 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { v4 as uuidv4 } from "uuid";
import { Layout, Typography } from "antd";
import { Button, Layout, Typography } from "antd";
import { getJobStatus, addRecipe } from "./utils/firebase";
import { getFirebaseRecipe, jsonToString } from "./utils/recipeLoader";
import { getSubmitPackingUrl, JOB_STATUS } from "./constants/aws";
Expand All @@ -15,6 +15,7 @@ import {
useSetPackingResults,
} from "./state/store";
import PackingInput from "./components/PackingInput";
import UploadModal from "./components/UploadModal";
import Viewer from "./components/Viewer";
import StatusBar from "./components/StatusBar";

Expand All @@ -25,6 +26,7 @@ const { Link } = Typography;

function App() {
const [jobStatus, setJobStatus] = useState<string>("");
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

const setJobLogs = useSetJobLogs();
const jobLogs = useJobLogs();
Expand All @@ -40,10 +42,13 @@ function App() {
return new Promise((resolve) => setTimeout(resolve, ms));
}

const recipeHasChanged = async (
const isUploadNeeded = async (
recipeId: string,
recipeString: string
): Promise<boolean> => {
if (!recipeId) {
return true;
}
const originalRecipe = await getFirebaseRecipe(recipeId);
return !(jsonToString(originalRecipe) == recipeString);
};
Expand Down Expand Up @@ -73,11 +78,11 @@ function App() {
const firebaseConfig = configId
? "firebase:configs/" + configId
: undefined;
const recipeChanged: boolean = await recipeHasChanged(
const uploadNeeded: boolean = await isUploadNeeded(
recipeId,
recipeString
);
if (recipeChanged) {
if (uploadNeeded) {
const recipeId = uuidv4();
firebaseRecipe = "firebase:recipes_edited/" + recipeId;
const recipeJson = recipeToFirebase(
Expand Down Expand Up @@ -156,20 +161,34 @@ function App() {
}
};

const showUploadDialog = () => {
setIsModalOpen(true);
};

const handleModalClose = () => {
setIsModalOpen(false);
};

return (
<Layout className="app-container">
<Header
className="header"
style={{ justifyContent: "space-between" }}
>
<h2 className="header-title">cellPACK Studio</h2>
<Link
href="https://github.com/mesoscope/cellpack"
className="header-link"
>
GitHub
</Link>
<div className="header-buttons">
<Button className="load-button" color="primary" variant="filled" onClick={showUploadDialog}>
Load My Own Recipe
</Button>
<Link
href="https://github.com/mesoscope/cellpack"
className="header-link"
>
GitHub
</Link>
</div>
</Header>
<UploadModal isOpen={isModalOpen} onClose={handleModalClose} />
<Layout>
<Sider width="35%" theme="light" className="sider">
<PackingInput startPacking={startPacking} />
Expand Down
45 changes: 45 additions & 0 deletions src/components/LocalRecipe/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Button, Input } from 'antd';
import { CloseOutlined } from '@ant-design/icons'
import { useLocalRecipeString, useSetLocalRecipe } from '../../state/store';

import "./style.css";

const { TextArea } = Input;

interface LocalRecipeProps {
startPacking: (recipeId: string, configId: string, recipeString: string) => Promise<void>;
maxHeight?: number;
}

const LocalRecipe: React.FC<LocalRecipeProps> = ({ startPacking, maxHeight }) => {
const localRecipeString = useLocalRecipeString();
const setLocalRecipe = useSetLocalRecipe();

const closeLocalRecipe = () => {
setLocalRecipe(undefined);
};

if (!localRecipeString) {
return <div>No local recipe loaded.</div>;
}
return (
<div className="local-recipe">
<div className="upload-recipe-header">
<h3>Uploaded Recipe</h3>
<Button icon={<CloseOutlined />} onClick={closeLocalRecipe} color="default" variant="text"/>
</div>
<TextArea value={localRecipeString} disabled style={{ height: maxHeight }} />
<Button
className="packing-button"
color="primary"
variant="filled"
style={{ width: "100%", minHeight: 38, marginTop: 6 }}
onClick={() => startPacking("", "", localRecipeString)}
>
<strong>Run Packing</strong>
</Button>
</div>
);
};

export default LocalRecipe;
6 changes: 6 additions & 0 deletions src/components/LocalRecipe/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.upload-recipe-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
13 changes: 13 additions & 0 deletions src/components/PackingInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
useCurrentRecipeObject,
useInputOptions,
useLoadInputOptions,
useLocalRecipeString,
} from "../../state/store";
import Dropdown from "../Dropdown";
import JSONViewer from "../JSONViewer";
import RecipeForm from "../RecipeForm";
import ExpandableText from "../ExpandableText";
import LocalRecipe from "../LocalRecipe";
import "./style.css";
import { useSiderHeight } from "../../hooks/useSiderHeight";
import {
Expand All @@ -36,6 +38,7 @@ const PackingInput = (props: PackingInputProps): JSX.Element => {
const selectedRecipeId = useSelectedRecipeId();
const recipeObj = useCurrentRecipeObject();
const inputOptions = useInputOptions();
const localRecipeString = useLocalRecipeString();

const loadInputOptions = useLoadInputOptions();
const loadAllRecipes = useLoadAllRecipes();
Expand Down Expand Up @@ -70,6 +73,16 @@ const PackingInput = (props: PackingInputProps): JSX.Element => {
await storeStartPacking(startPacking);
};

// Local recipe loaded
if (localRecipeString) {
return (
<LocalRecipe
startPacking={startPacking}
maxHeight={availableRecipeHeight}
/>
)
}

const loadingText = <div className="recipe-select">Loading...</div>;

// No recipe or dropdown options to load
Expand Down
75 changes: 75 additions & 0 deletions src/components/UploadModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Button, Divider, message, Modal, Upload } from "antd";
import type { UploadProps } from 'antd';
import { useState } from "react";
import { useSetLocalRecipe } from "../../state/store";


interface UploadModalProps {
isOpen: boolean;
onClose: () => void;
}

const UploadModal = (props: UploadModalProps): JSX.Element => {
const { isOpen, onClose } = props;
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const setLocalRecipe = useSetLocalRecipe();

const uploadProps: UploadProps = {
name: 'file',
multiple: false,
accept: ".json",
fileList: [],
beforeUpload: (file) => handleUpload(file),
};

const handleUpload = (file: File) => {
message.success(`${file.name} file uploaded successfully`);
setSelectedFile(file);
return false; // Prevent automatic upload
};

const handleSubmit = () => {
if (selectedFile) {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result;
setLocalRecipe(content as string);
};
reader.readAsText(selectedFile);
}
handleClose();
};

const handleClose = () => {
setSelectedFile(null);
onClose();
}

return (
<Modal
title="Choose a Recipe JSON File to Load"
open={isOpen}
onCancel={onClose}
footer={[
<Button onClick={handleClose}>
Cancel
</Button>,
<Button onClick={handleSubmit} color="primary" variant="filled" disabled={!selectedFile}>
Load
</Button>
]}
>
<Divider size="small" />
<Upload {...uploadProps}>
<Button color="primary" variant="filled">Select a File</Button>
</Upload>
{selectedFile && (
<div>
<p>{selectedFile.name}</p>
</div>
)}
</Modal>
);
};

export default UploadModal;
26 changes: 26 additions & 0 deletions src/state/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface RecipeState {

export interface UIState {
isPacking: boolean;
localRecipeString?: string;
}

type Actions = {
Expand All @@ -44,17 +45,20 @@ type Actions = {
setPackingResults: (results: PackingResult) => void;
setJobLogs: (logs: string) => void;
setJobId: (jobId: string) => void;
setLocalRecipe: (recipeString?: string) => void;
};

export type RecipeStore = RecipeState & UIState & Actions;

export const INITIAL_RECIPE_ID = "peroxisome_v_gradient_packing";
export const LOCAL_RECIPE_ID = "LOCAL_RECIPE";

const initialState: RecipeState & UIState = {
selectedRecipeId: INITIAL_RECIPE_ID,
inputOptions: {},
recipes: {},
isPacking: false,
localRecipeString: undefined,
packingResults: { [INITIAL_RECIPE_ID]: EMPTY_PACKING_RESULT },
};

Expand Down Expand Up @@ -127,6 +131,26 @@ export const useRecipeStore = create<RecipeStore>()(
}
},

setLocalRecipe: (recipeString?: string) => {
if (recipeString) {
set({
localRecipeString: recipeString,
selectedRecipeId: LOCAL_RECIPE_ID
});
} else {
// Clear local recipe
set({
localRecipeString: undefined,
selectedRecipeId: INITIAL_RECIPE_ID,
packingResults: {
...get().packingResults,
[LOCAL_RECIPE_ID]: EMPTY_PACKING_RESULT,
},
});
}

},

setPackingResults: (results: PackingResult) => {
const currentRecipeId = get().selectedRecipeId;
set({
Expand Down Expand Up @@ -272,6 +296,7 @@ export const useFieldsToDisplay = () =>
useRecipeStore((s) => s.recipes[s.selectedRecipeId]?.editableFields);
export const useRecipes = () => useRecipeStore((s) => s.recipes);
export const usePackingResults = () => useRecipeStore((s) => s.packingResults);
export const useLocalRecipeString = () => useRecipeStore((s) => s.localRecipeString);

export const useCurrentRecipeObject = () => {
const recipe = useCurrentRecipeData();
Expand Down Expand Up @@ -364,3 +389,4 @@ export const useSetPackingResults = () =>
useRecipeStore((s) => s.setPackingResults);
export const useSetJobLogs = () => useRecipeStore((s) => s.setJobLogs);
export const useSetJobId = () => useRecipeStore((s) => s.setJobId);
export const useSetLocalRecipe = () => useRecipeStore((s) => s.setLocalRecipe);