diff --git a/docs/_static/workflow_batch_run/batch_button.png b/docs/_static/workflow_batch_run/batch_button.png new file mode 100644 index 0000000000..f4e5ad42d3 Binary files /dev/null and b/docs/_static/workflow_batch_run/batch_button.png differ diff --git a/docs/_static/workflow_batch_run/batch_record.png b/docs/_static/workflow_batch_run/batch_record.png new file mode 100644 index 0000000000..121123f0f3 Binary files /dev/null and b/docs/_static/workflow_batch_run/batch_record.png differ diff --git a/docs/_static/workflow_batch_run/batch_workflow_1.png b/docs/_static/workflow_batch_run/batch_workflow_1.png new file mode 100644 index 0000000000..77360dc04c Binary files /dev/null and b/docs/_static/workflow_batch_run/batch_workflow_1.png differ diff --git a/docs/_static/workflow_batch_run/batch_workflow_2.png b/docs/_static/workflow_batch_run/batch_workflow_2.png new file mode 100644 index 0000000000..ab0ca5ee19 Binary files /dev/null and b/docs/_static/workflow_batch_run/batch_workflow_2.png differ diff --git a/docs/_static/workflow_batch_run/batch_workflow_3.png b/docs/_static/workflow_batch_run/batch_workflow_3.png new file mode 100644 index 0000000000..4a1bd56f1c Binary files /dev/null and b/docs/_static/workflow_batch_run/batch_workflow_3.png differ diff --git a/docs/_static/workflow_batch_run/batch_workspaces.png b/docs/_static/workflow_batch_run/batch_workspaces.png new file mode 100644 index 0000000000..e8c31f944e Binary files /dev/null and b/docs/_static/workflow_batch_run/batch_workspaces.png differ diff --git a/docs/other/index.rst b/docs/other/index.rst index 58b87894ec..6f771287cb 100644 --- a/docs/other/index.rst +++ b/docs/other/index.rst @@ -8,4 +8,5 @@ Other host_for_multiuser/index cli_execution + workflow_batch_run debugging diff --git a/docs/other/workflow_batch_run.md b/docs/other/workflow_batch_run.md new file mode 100644 index 0000000000..da47aea0f9 --- /dev/null +++ b/docs/other/workflow_batch_run.md @@ -0,0 +1,124 @@ +(workflow-batch-run)= +Workflow Batch Run +================= + +By using the Workflow Batch Run function, you can batch-execute workflows set of input data. + +\*This feature is limited to ["Multi User Mode"](host_for_multiuser/index.rst). + +```{contents} +:depth: 3 +``` + +## Usage Procedure + +### Creating a Workspace (Batch Type) + +

+Workspaces +

+ +- Specify the type of workspace as Batch when creating a Workspace. + +### Batch Workspace specifics + +Batch workspaces have the following UI changes: + +

+Workflow +

+ +- (1) Display Type: Batch +- (2) From the Batch Data section in the left side menu, you can select each Batch Input Node. +- (3) The "Run" button will display as the "Batch Run" button. + +### Selecting Data + +

+Workflow +

+ +- (1) Batch Input Nodes + - Place a Batch Input Node from "Batch Data" in the left side menu. + - As an example, batch_image and batch_behavior are placed here. + - Each Batch Input Node corresponds to a standard Input Node. + - `batch_{image, csv, fluo, behavior, microscope, hdf5, matlab}` +- (2) File Select dialog + - Each Batch Input Node can select multiple files. + +```{eval-rst} +.. note:: + * **Data set handling** + * Data are processed in the order they are selected. + * All input data are matched based on their order, whereby image and behaviour in position 1 will be processed together, followed by all of the data in position 2, and so on. + * Data in each node can be easily reordered by dragging up or down in the data list of that node. + * **Validation rule** + * Verify that the number of data in each Batch Input Data Node matches. + * Only the number of data items is checked. File names and data contents are not checked. +``` + +### Setting dialogs + +

+Workflow +

+ +- (1) CSV Setting dialog + - For CSV-based Nodes (`batch_{cvs, fluo, behavior}`), various settings can be made in the CSV Setting dialog. + - In the CSV Setting dialog, **the contents of the first file** is previewed, and **same settings are applied to all files**. +- (2) Select Structure dialog + - For Structured data type Nodes (`batch_{hdf5, matlab}`), the path of Input Data can be selected in the Select Structure dialog. + - In the Select Structure dialog, **the contents of the first file (Structure)** is displayed, and **same settings are applied to all files**. +- (3) Node Param Setting + - The parameter settings for each node apply **the same settings to all workflows**. + +```{eval-rst} +.. note:: + * All workflows will complete with the **same parameter configuration**. +``` + +### Batch Run + +

+Batch Run Button +

+ +#### Batch Run Button execution behavior + +- Validation for Input Data + - Check the number of items for each Batch Input Data + - \***The only check condition is the number of items.** File names and data contents are not checked. +- Execute Batch Run + - No reservations, immediate execution only + - As soon as the process is successfully started, the Workflow will immediately have a completed status. + - The following records are generated + - Batch Workflow Template + - Generate one Template Workflow as the basis of Running Workflow . + - Batch Workflow + - Automatically create Workflows for the number of Data items based on Batch Workflow Template and start parallel execution. + +```{eval-rst} +.. note:: + * About workflow behavior after RUN BATCH + * When submitting a RUN BATCH command, "the template workflow" then submits a new workflow (job) for each set of data. + * The template workflow will be shown as successful if this job submission itself is successful. However, this does **not** mean any of the jobs themselves have been completed successfully. + * The progress of each job can be seen in the RECORDS tab. Note that individual workflows run independently and some may still succeed or fail depending on the data contents and parameters selected. +``` + +## Record Screen + +

+Record +

+ +- (1) Record of Batch Workflow Template + - Created when Batch Run Button is executed. + - This Workflow does not run snakemake directly. +- (2) Record of Batch Workflow + - Records of each workflow actually executed based on Batch Workflow Template + - Same content as regular Workflow Record + - "Name" is automatically generated based on the Template Name +- (3) Other menu of Batch Workflow Template + - Reproduce ... Batch Workflow Template can be reproduced + - Download workflow yaml ... You can also download and import Batch Workflow Template workflow.yaml + - Download snakemake yaml ... It is not possible to download snakemake.yaml of Batch Workflow Template (because it is not subject to snakemake execution) diff --git a/frontend/src/api/run/Run.ts b/frontend/src/api/run/Run.ts index 0217c3dd28..6c9920514f 100644 --- a/frontend/src/api/run/Run.ts +++ b/frontend/src/api/run/Run.ts @@ -81,6 +81,17 @@ export async function runByUidApi( return response.data } +export async function batchRunApi( + workspaceId: number, + data: RunPostData, +): Promise { + const response = await axios.post( + `${BASE_URL}/run/util/batch_run/${workspaceId}`, + data, + ) + return response.data +} + export type RunResultDTO = { [nodeId: string]: { status: string diff --git a/frontend/src/api/workspace/index.ts b/frontend/src/api/workspace/index.ts index 8cbfb1da36..d84f56a97d 100644 --- a/frontend/src/api/workspace/index.ts +++ b/frontend/src/api/workspace/index.ts @@ -1,5 +1,6 @@ import { stringify } from "qs" +import { WORKSPACE_TYPE } from "const/Workspace" import { ItemsWorkspace, WorkspaceDataDTO, @@ -7,7 +8,11 @@ import { } from "store/slice/Workspace/WorkspaceType" import axios from "utils/axios" -export type WorkspacePostDataDTO = { name: string; id?: number } +export type WorkspacePostDataDTO = { + id?: number + name: string + type: WORKSPACE_TYPE +} export const getWorkspaceApi = async (id: number): Promise => { const response = await axios.get(`/workspace/${id}`) diff --git a/frontend/src/components/Workspace/FlowChart/Buttons/CreateWorkflow.tsx b/frontend/src/components/Workspace/FlowChart/Buttons/CreateWorkflow.tsx index ad6397ac2d..948a0c5193 100644 --- a/frontend/src/components/Workspace/FlowChart/Buttons/CreateWorkflow.tsx +++ b/frontend/src/components/Workspace/FlowChart/Buttons/CreateWorkflow.tsx @@ -5,15 +5,17 @@ import { AddToPhotos } from "@mui/icons-material" import { IconButton, Tooltip } from "@mui/material" import { ConfirmDialog } from "components/common/ConfirmDialog" -import { WORKSPACE_TYPE } from "const/Workspace" import { clearFlowElements } from "store/slice/FlowElement/FlowElementSlice" import { selectPipelineIsStartedSuccess } from "store/slice/Pipeline/PipelineSelectors" +import { RootState } from "store/store" export const CreateWorkflowButton = memo(function CreateWorkflowButton() { const [open, setOpen] = useState(false) const dispatch = useDispatch() const isPending = useSelector(selectPipelineIsStartedSuccess) - const workspaceType = WORKSPACE_TYPE.DEFAULT // Currently a fixed value + const workspaceType = useSelector( + (state: RootState) => state.workspace.currentWorkspace.type, + ) const openDialog = () => { setOpen(true) diff --git a/frontend/src/components/Workspace/FlowChart/Buttons/RunButtons.tsx b/frontend/src/components/Workspace/FlowChart/Buttons/RunButtons.tsx index 9340f8990e..9b3cb77105 100644 --- a/frontend/src/components/Workspace/FlowChart/Buttons/RunButtons.tsx +++ b/frontend/src/components/Workspace/FlowChart/Buttons/RunButtons.tsx @@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux" import { useSnackbar } from "notistack" -import { PlayArrow } from "@mui/icons-material" +import { FastForward, PlayArrow } from "@mui/icons-material" import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown" import BlockIcon from "@mui/icons-material/Block" import ReplayIcon from "@mui/icons-material/Replay" @@ -22,6 +22,10 @@ import Paper from "@mui/material/Paper" import Popper from "@mui/material/Popper" import TextField from "@mui/material/TextField" +import { WORKSPACE_TYPE } from "const/Workspace" +import { selectFlowNodes } from "store/slice/FlowElement/FlowElementSelectors" +import { selectInputNode } from "store/slice/InputNode/InputNodeSelectors" +import { isBatchAnyInputNode } from "store/slice/InputNode/InputNodeUtils" import { UseRunPipelineReturnType } from "store/slice/Pipeline/PipelineHook" import { selectPipelineIsStartedSuccess, @@ -33,6 +37,7 @@ import { RUN_BTN_OPTIONS, RUN_BTN_TYPE, } from "store/slice/Pipeline/PipelineType" +import { selectCurrentWorkspaceType } from "store/slice/Workspace/WorkspaceSelector" export const RunButtons = memo(function RunButtons( props: UseRunPipelineReturnType, @@ -44,6 +49,7 @@ export const RunButtons = memo(function RunButtons( algorithmNodeNotExist, handleCancelPipeline, handleRunPipeline, + handleBatchRunPipeline, handleRunPipelineByUid, } = props @@ -51,10 +57,14 @@ export const RunButtons = memo(function RunButtons( const runBtnOption = useSelector(selectPipelineRunBtn) const isStartedSuccess = useSelector(selectPipelineIsStartedSuccess) + const workspaceType = useSelector(selectCurrentWorkspaceType) + const flowNodes = useSelector(selectFlowNodes) + const inputNodes = useSelector(selectInputNode) const sendingRunRequest = useRef(false) const [dialogOpen, setDialogOpen] = useState(false) + const [batchDialogOpen, setBatchDialogOpen] = useState(false) const { enqueueSnackbar } = useSnackbar() const handleClick = () => { let errorMessage: string | null = null @@ -93,6 +103,77 @@ export const RunButtons = memo(function RunButtons( const onClickCancel = () => { handleCancelPipeline() } + /** + * Validates batch input nodes in the flowchart + * @returns Error message if validation fails, null otherwise + */ + const validateBatchInputNodes = (): string | null => { + // Find all batch input nodes in the flowchart + const batchInputNodes = flowNodes + .filter((node) => { + const inputNode = inputNodes[node.id] + return inputNode && isBatchAnyInputNode(inputNode) + }) + .map((node) => ({ + nodeId: node.id, + inputNode: inputNodes[node.id], + })) + + // Check if there are any batch nodes + if (batchInputNodes.length === 0) { + return "There are no batch input nodes." + } + + // Check file count consistency across all batch nodes + const fileCounts = batchInputNodes.map((node) => { + const filePath = node.inputNode.selectedFilePath + if (Array.isArray(filePath)) { + return filePath.length + } + return filePath ? 1 : 0 + }) + + const minCount = Math.min(...fileCounts) + const maxCount = Math.max(...fileCounts) + + if (minCount !== maxCount) { + return `Number of batch input files does not match. [${minCount} - ${maxCount}]` + } + + return null + } + + const onClickBatchRun = () => { + let errorMessage: string | null = null + if (algorithmNodeNotExist) { + errorMessage = "please add some algorithm nodes to the flowchart" + } + if (filePathIsUndefined) { + errorMessage = "please select input file" + } + + // Validate batch nodes + if (errorMessage == null) { + errorMessage = validateBatchInputNodes() + } + + if (errorMessage != null) { + enqueueSnackbar(errorMessage, { + variant: "error", + }) + } else { + setBatchDialogOpen(true) + } + } + const onClickDialogBatchRun = (name: string) => { + if (sendingRunRequest.current) return + sendingRunRequest.current = true + handleBatchRunPipeline(name) + setTimeout(() => { + sendingRunRequest.current = false + }, 3000) + setBatchDialogOpen(false) + } const [menuOpen, setMenuOpen] = useState(false) const anchorRef = useRef(null) @@ -116,68 +197,79 @@ export const RunButtons = memo(function RunButtons( setMenuOpen(false) } const uidExists = uid != null + const isBatchWorkspace = workspaceType === WORKSPACE_TYPE.BATCH + return ( <> - - - - - - {({ TransitionProps, placement }) => ( - + + + + + - - - - {Object.values(RUN_BTN_OPTIONS).map((option) => ( - handleMenuItemClick(event, option)} - > - {RUN_BTN_LABELS[option]} - - ))} - - - - - )} - + {({ TransitionProps, placement }) => ( + + + + + {Object.values(RUN_BTN_OPTIONS).map((option) => ( + + handleMenuItemClick(event, option) + } + > + {RUN_BTN_LABELS[option]} + + ))} + + + + + )} + + + )} + + {/* Show Cancel button for all workspace types when workflow is running */} {isStartedSuccess && ( @@ -185,11 +277,29 @@ export const RunButtons = memo(function RunButtons( )} + + {/* Show Batch Run button only for batch workspaces */} + {isBatchWorkspace && ( + + )} setDialogOpen(false)} /> + setBatchDialogOpen(false)} + /> ) }) @@ -198,14 +308,18 @@ interface RunDialogProps { open: boolean handleRun: (name: string) => void handleClose: () => void + title?: string + defaultName?: string } const RunDialog = memo(function RunDialog({ open, handleClose, handleRun, + title = "Name and run workflow", + defaultName = "New flow", }: RunDialogProps) { - const [name, setName] = useState("New flow") + const [name, setName] = useState(defaultName) const [error, setError] = useState(null) const onClickRun = () => { if (name !== "") { @@ -222,7 +336,7 @@ const RunDialog = memo(function RunDialog({ } return ( - Name and run workflow + {title} ) }) + +interface BatchRunDialogProps { + open: boolean + handleRun: (name: string) => void + handleClose: () => void +} + +const BatchRunDialog = memo(function BatchRunDialog({ + open, + handleClose, + handleRun, +}: BatchRunDialogProps) { + return ( + + ) +}) diff --git a/frontend/src/components/Workspace/FlowChart/Dialog/CsvParamSettingDialog.tsx b/frontend/src/components/Workspace/FlowChart/Dialog/CsvParamSettingDialog.tsx index a089cefa09..da7e2e078b 100644 --- a/frontend/src/components/Workspace/FlowChart/Dialog/CsvParamSettingDialog.tsx +++ b/frontend/src/components/Workspace/FlowChart/Dialog/CsvParamSettingDialog.tsx @@ -15,6 +15,7 @@ import { LinearProgress, Typography, IconButton, + Chip, } from "@mui/material" import { PresentationalCsvPlot } from "components/Workspace/Visualize/Plot/CsvPlot" @@ -82,7 +83,15 @@ export const CsvParamSettingDialog = memo(function CsvParamSettingDialog({ - Csv Setting + + Csv Setting + + void config: FileNodeConfig + filePath?: string } & NodeIdProps export const StructureItemSelectDialog = memo( @@ -37,6 +45,7 @@ export const StructureItemSelectDialog = memo( open, setOpen, config, + filePath, }: StructureItemSelectProps) { const dispatch = useDispatch() const [fileSelect, setFileSelect] = useState("") @@ -61,7 +70,17 @@ export const StructureItemSelectDialog = memo( : "No structure is selected."} setOpen(false)} fullWidth> - Select Structure + + Select Structure + {filePath && ( + + )} + )} (state: RootState) => string | string[] | undefined + selectStructurePath: ( + nodeId: string, + ) => (state: RootState) => string | undefined + setStructurePath: (params: { + nodeId: string + path: string + }) => Action + getTree: (params: { + path: string + workspaceId: number + }) => ThunkAction> + selectTree: () => (state: RootState) => TreeNodeType[] | undefined + selectIsLoading: () => (state: RootState) => boolean +} + +function createBatchConfigAdapter( + config: BatchFileNodeConfig, + _filePath: string[] | undefined, +): FileNodeConfig { + return { + ...config, + selectFilePath: (nodeId: string) => (state: RootState) => { + const paths = config.selectFilePath(nodeId)(state) + if (Array.isArray(paths) && paths.length > 0) { + return paths[0] + } + return paths + }, + } +} + +export function createBatchStructuredFileNode(config: BatchFileNodeConfig) { + const BatchFileNode = memo(function BatchFileNode(element: NodeProps) { + const defined = useSelector(selectInputNodeDefined(element.id)) + if (defined) { + return + } else { + return null + } + }) + BatchFileNode.displayName = `Batch${config.fileType}FileNode` + return BatchFileNode +} + +const BatchFileNodeImple = memo(function BatchFileNodeImple({ + id: nodeId, + selected, + config, +}: NodeProps & { config: BatchFileNodeConfig }) { + const dispatch = useDispatch() + const filePath = useSelector(config.selectFilePath(nodeId), (a, b) => + a != null && b != null && Array.isArray(a) && Array.isArray(b) + ? arrayEqualityFn(a, b) + : a === b, + ) + + const [open, setOpen] = useState(false) + const onChangeFilePath = (path: string[]) => { + dispatch(setInputNodeFilePath({ nodeId, filePath: path })) + } + + const onClickDeleteIcon = () => { + dispatch(deleteFlowNodeById(nodeId)) + } + + return ( + + + { + if (Array.isArray(path)) { + onChangeFilePath(path) + } + }} + setOpen={setOpen} + fileType={config.fileType} + filePath={ + Array.isArray(filePath) ? filePath : filePath ? [filePath] : [] + } + /> + {filePath !== undefined && + Array.isArray(filePath) && + filePath.length > 0 && ( + + )} + + + ) +}) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchBehaviorFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchBehaviorFileNode.tsx new file mode 100644 index 0000000000..324abf6fd4 --- /dev/null +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchBehaviorFileNode.tsx @@ -0,0 +1,86 @@ +import { memo } from "react" +import { useDispatch, useSelector } from "react-redux" +import { Handle, Position, NodeProps } from "reactflow" + +import { FileSelect } from "components/Workspace/FlowChart/FlowChartNode/FileSelect" +import { toHandleId } from "components/Workspace/FlowChart/FlowChartNode/FlowChartUtils" +import { useHandleColor } from "components/Workspace/FlowChart/FlowChartNode/HandleColorHook" +import { NodeContainer } from "components/Workspace/FlowChart/FlowChartNode/NodeContainer" +import { HANDLE_STYLE } from "const/flowchart" +import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" +import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" +import { + selectInputNodeDefined, + selectBehaviorLikeInputNodeSelectedFilePath, +} from "store/slice/InputNode/InputNodeSelectors" +import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" +import { arrayEqualityFn } from "utils/EqualityUtils" + +export const BatchBehaviorFileNode = memo(function BatchBehaviorFileNode( + element: NodeProps, +) { + const defined = useSelector(selectInputNodeDefined(element.id)) + if (defined) { + return + } else { + return null + } +}) + +const BatchBehaviorFileNodeImple = memo(function BatchBehaviorFileNodeImple({ + id: nodeId, + selected: elementSelected, +}: NodeProps) { + const dispatch = useDispatch() + const filePath = useSelector( + selectBehaviorLikeInputNodeSelectedFilePath(nodeId), + (a, b) => + a != null && b != null && Array.isArray(a) && Array.isArray(b) + ? arrayEqualityFn(a, b) + : a === b, + ) + const onChangeFilePath = (path: string[]) => { + dispatch(setInputNodeFilePath({ nodeId, filePath: path })) + } + + const returnType = "BehaviorData" + const behaviorColor = useHandleColor(returnType) + + const onClickDeleteIcon = () => { + dispatch(deleteFlowNodeById(nodeId)) + } + + return ( + + + { + if (Array.isArray(path)) { + onChangeFilePath(path) + } + }} + fileType={FILE_TYPE_SET.BATCH_BEHAVIOR} + filePath={ + Array.isArray(filePath) ? filePath : filePath ? [filePath] : [] + } + /> + + + ) +}) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchCsvFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchCsvFileNode.tsx new file mode 100644 index 0000000000..c399d88155 --- /dev/null +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchCsvFileNode.tsx @@ -0,0 +1,86 @@ +import { memo } from "react" +import { useDispatch, useSelector } from "react-redux" +import { Handle, Position, NodeProps } from "reactflow" + +import { FileSelect } from "components/Workspace/FlowChart/FlowChartNode/FileSelect" +import { toHandleId } from "components/Workspace/FlowChart/FlowChartNode/FlowChartUtils" +import { useHandleColor } from "components/Workspace/FlowChart/FlowChartNode/HandleColorHook" +import { NodeContainer } from "components/Workspace/FlowChart/FlowChartNode/NodeContainer" +import { HANDLE_STYLE } from "const/flowchart" +import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" +import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" +import { + selectInputNodeDefined, + selectCsvLikeInputNodeSelectedFilePath, +} from "store/slice/InputNode/InputNodeSelectors" +import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" +import { arrayEqualityFn } from "utils/EqualityUtils" + +export const BatchCsvFileNode = memo(function BatchCsvFileNode( + element: NodeProps, +) { + const defined = useSelector(selectInputNodeDefined(element.id)) + if (defined) { + return + } else { + return null + } +}) + +const BatchCsvFileNodeImple = memo(function BatchCsvFileNodeImple({ + id: nodeId, + selected: elementSelected, +}: NodeProps) { + const dispatch = useDispatch() + const filePath = useSelector( + selectCsvLikeInputNodeSelectedFilePath(nodeId), + (a, b) => { + if (Array.isArray(a) && Array.isArray(b)) { + return arrayEqualityFn(a, b) + } + return a === b + }, + ) + const onChangeFilePath = (path: string[]) => { + dispatch(setInputNodeFilePath({ nodeId, filePath: path })) + } + + const returnType = "CsvData" + const csvColor = useHandleColor(returnType) + + const onClickDeleteIcon = () => { + dispatch(deleteFlowNodeById(nodeId)) + } + + return ( + + + { + if (Array.isArray(path)) { + onChangeFilePath(path) + } + }} + fileType={FILE_TYPE_SET.BATCH_CSV} + filePath={typeof filePath === "string" ? [filePath] : filePath || []} + /> + + + ) +}) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchFluoFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchFluoFileNode.tsx new file mode 100644 index 0000000000..1d4dc05a52 --- /dev/null +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchFluoFileNode.tsx @@ -0,0 +1,86 @@ +import { memo } from "react" +import { useDispatch, useSelector } from "react-redux" +import { Handle, Position, NodeProps } from "reactflow" + +import { FileSelect } from "components/Workspace/FlowChart/FlowChartNode/FileSelect" +import { toHandleId } from "components/Workspace/FlowChart/FlowChartNode/FlowChartUtils" +import { useHandleColor } from "components/Workspace/FlowChart/FlowChartNode/HandleColorHook" +import { NodeContainer } from "components/Workspace/FlowChart/FlowChartNode/NodeContainer" +import { HANDLE_STYLE } from "const/flowchart" +import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" +import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" +import { + selectInputNodeDefined, + selectFluoLikeInputNodeSelectedFilePath, +} from "store/slice/InputNode/InputNodeSelectors" +import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" +import { arrayEqualityFn } from "utils/EqualityUtils" + +export const BatchFluoFileNode = memo(function BatchFluoFileNode( + element: NodeProps, +) { + const defined = useSelector(selectInputNodeDefined(element.id)) + if (defined) { + return + } else { + return null + } +}) + +const BatchFluoFileNodeImple = memo(function BatchFluoFileNodeImple({ + id: nodeId, + selected: elementSelected, +}: NodeProps) { + const dispatch = useDispatch() + const filePath = useSelector( + selectFluoLikeInputNodeSelectedFilePath(nodeId), + (a, b) => + a != null && b != null && Array.isArray(a) && Array.isArray(b) + ? arrayEqualityFn(a, b) + : a === b, + ) + const onChangeFilePath = (path: string[]) => { + dispatch(setInputNodeFilePath({ nodeId, filePath: path })) + } + + const returnType = "FluoData" + const fluoColor = useHandleColor(returnType) + + const onClickDeleteIcon = () => { + dispatch(deleteFlowNodeById(nodeId)) + } + + return ( + + + { + if (Array.isArray(path)) { + onChangeFilePath(path) + } + }} + fileType={FILE_TYPE_SET.BATCH_FLUO} + filePath={ + Array.isArray(filePath) ? filePath : filePath ? [filePath] : [] + } + /> + + + ) +}) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchHDF5FileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchHDF5FileNode.tsx new file mode 100644 index 0000000000..dad1c7fbe4 --- /dev/null +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchHDF5FileNode.tsx @@ -0,0 +1,30 @@ +import { + createBatchStructuredFileNode, + BatchFileNodeConfig, +} from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchBaseStructuredFileNode" +import { FILE_TYPE_SET } from "config/fileTypes.config" +import { getHDF5Tree } from "store/slice/HDF5/HDF5Action" +import { + selectHDF5IsLoading, + selectHDF5Nodes, +} from "store/slice/HDF5/HDF5Selectors" +import { + selectHdf5LikeInputNodeSelectedFilePath, + selectInputNodeHDF5Path, +} from "store/slice/InputNode/InputNodeSelectors" +import { setInputNodeHDF5Path } from "store/slice/InputNode/InputNodeSlice" + +const batchHdf5Config: BatchFileNodeConfig = { + fileType: FILE_TYPE_SET.BATCH_HDF5, + handleId: "hdf5", + handleType: "HDF5Data", + treeKeyPrefix: "hdf5tree", + selectFilePath: selectHdf5LikeInputNodeSelectedFilePath, + selectStructurePath: selectInputNodeHDF5Path, + setStructurePath: setInputNodeHDF5Path, + getTree: getHDF5Tree, + selectTree: selectHDF5Nodes, + selectIsLoading: selectHDF5IsLoading, +} + +export const BatchHDF5FileNode = createBatchStructuredFileNode(batchHdf5Config) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchImageFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchImageFileNode.tsx new file mode 100644 index 0000000000..e89389a0c3 --- /dev/null +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchImageFileNode.tsx @@ -0,0 +1,88 @@ +import { memo } from "react" +import { useSelector, useDispatch } from "react-redux" +import { Handle, Position, NodeProps } from "reactflow" + +import { FileSelect } from "components/Workspace/FlowChart/FlowChartNode/FileSelect" +import { + toHandleId, + isValidConnection, +} from "components/Workspace/FlowChart/FlowChartNode/FlowChartUtils" +import { useHandleColor } from "components/Workspace/FlowChart/FlowChartNode/HandleColorHook" +import { NodeContainer } from "components/Workspace/FlowChart/FlowChartNode/NodeContainer" +import { HANDLE_STYLE } from "const/flowchart" +import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" +import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" +import { + selectInputNodeDefined, + selectImageLikeInputNodeSelectedFilePath, +} from "store/slice/InputNode/InputNodeSelectors" +import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" +import { arrayEqualityFn } from "utils/EqualityUtils" + +export const BatchImageFileNode = memo(function BatchImageFileNode( + element: NodeProps, +) { + const defined = useSelector(selectInputNodeDefined(element.id)) + if (defined) { + return + } else { + return null + } +}) + +const BatchImageFileNodeImple = memo(function BatchImageFileNodeImple({ + id: nodeId, + selected: elementSelected, +}: NodeProps) { + const dispatch = useDispatch() + const filePath = useSelector( + selectImageLikeInputNodeSelectedFilePath(nodeId), + (a, b) => + a != null && b != null && Array.isArray(a) && Array.isArray(b) + ? arrayEqualityFn(a, b) + : a === b, + ) + const onChangeFilePath = (path: string[]) => { + dispatch(setInputNodeFilePath({ nodeId, filePath: path })) + } + + const returnType = "ImageData" + const imageColor = useHandleColor(returnType) + + const onClickDeleteIcon = () => { + dispatch(deleteFlowNodeById(nodeId)) + } + + return ( + + + { + if (Array.isArray(path)) { + onChangeFilePath(path) + } + }} + fileType={FILE_TYPE_SET.BATCH_IMAGE} + filePath={typeof filePath === "string" ? [filePath] : filePath || []} + /> + + + ) +}) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMatlabFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMatlabFileNode.tsx new file mode 100644 index 0000000000..ed412aa746 --- /dev/null +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMatlabFileNode.tsx @@ -0,0 +1,31 @@ +import { + createBatchStructuredFileNode, + BatchFileNodeConfig, +} from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchBaseStructuredFileNode" +import { FILE_TYPE_SET } from "config/fileTypes.config" +import { + selectInputNodeMatlabPath, + selectMatlabLikeInputNodeSelectedFilePath, +} from "store/slice/InputNode/InputNodeSelectors" +import { setInputNodeMatlabPath } from "store/slice/InputNode/InputNodeSlice" +import { getMatlabTree } from "store/slice/Matlab/MatlabAction" +import { + selectMatlabIsLoading, + selectMatlabNodes, +} from "store/slice/Matlab/MatlabSelectors" + +const batchMatlabConfig: BatchFileNodeConfig = { + fileType: FILE_TYPE_SET.BATCH_MATLAB, + handleId: "matlab", + handleType: "MatlabData", + treeKeyPrefix: "matlabtree", + selectFilePath: selectMatlabLikeInputNodeSelectedFilePath, + selectStructurePath: selectInputNodeMatlabPath, + setStructurePath: setInputNodeMatlabPath, + getTree: getMatlabTree, + selectTree: selectMatlabNodes, + selectIsLoading: selectMatlabIsLoading, +} + +export const BatchMatlabFileNode = + createBatchStructuredFileNode(batchMatlabConfig) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMicroscopeFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMicroscopeFileNode.tsx new file mode 100644 index 0000000000..ee0c8367b8 --- /dev/null +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMicroscopeFileNode.tsx @@ -0,0 +1,88 @@ +import { memo } from "react" +import { useSelector, useDispatch } from "react-redux" +import { Handle, Position, NodeProps } from "reactflow" + +import { FileSelect } from "components/Workspace/FlowChart/FlowChartNode/FileSelect" +import { toHandleId } from "components/Workspace/FlowChart/FlowChartNode/FlowChartUtils" +import { useHandleColor } from "components/Workspace/FlowChart/FlowChartNode/HandleColorHook" +import { NodeContainer } from "components/Workspace/FlowChart/FlowChartNode/NodeContainer" +import { HANDLE_STYLE } from "const/flowchart" +import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" +import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" +import { + selectInputNodeDefined, + selectMicroscopeLikeInputNodeSelectedFilePath, +} from "store/slice/InputNode/InputNodeSelectors" +import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" +import { arrayEqualityFn } from "utils/EqualityUtils" + +export const BatchMicroscopeFileNode = memo(function BatchMicroscopeFileNode( + element: NodeProps, +) { + const defined = useSelector(selectInputNodeDefined(element.id)) + if (defined) { + return + } else { + return null + } +}) + +const BatchMicroscopeFileNodeImple = memo( + function BatchMicroscopeFileNodeImple({ + id: nodeId, + selected: elementSelected, + }: NodeProps) { + const dispatch = useDispatch() + const filePath = useSelector( + selectMicroscopeLikeInputNodeSelectedFilePath(nodeId), + (a, b) => + a != null && b != null && Array.isArray(a) && Array.isArray(b) + ? arrayEqualityFn(a, b) + : a === b, + ) + const onChangeFilePath = (path: string[]) => { + dispatch(setInputNodeFilePath({ nodeId, filePath: path })) + } + + const returnType = "MicroscopeData" + const microscopeColor = useHandleColor(returnType) + + const onClickDeleteIcon = () => { + dispatch(deleteFlowNodeById(nodeId)) + } + + return ( + + + { + if (Array.isArray(path)) { + onChangeFilePath(path) + } + }} + fileType={FILE_TYPE_SET.BATCH_MICROSCOPE} + filePath={ + Array.isArray(filePath) ? filePath : filePath ? [filePath] : [] + } + /> + + + ) + }, +) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BehaviorFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BehaviorFileNode.tsx index a6e691385b..73c5f6fc4b 100644 --- a/frontend/src/components/Workspace/FlowChart/FlowChartNode/BehaviorFileNode.tsx +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/BehaviorFileNode.tsx @@ -10,7 +10,7 @@ import { HANDLE_STYLE } from "const/flowchart" import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" import { - selectCsvInputNodeSelectedFilePath, + selectBehaviorLikeInputNodeSelectedFilePath, selectInputNodeDefined, } from "store/slice/InputNode/InputNodeSelectors" import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" @@ -31,7 +31,9 @@ const BehaviorFileNodeImple = memo(function BehaviorFileNodeImple({ selected, }: NodeProps) { const dispatch = useDispatch() - const filePath = useSelector(selectCsvInputNodeSelectedFilePath(nodeId)) + const filePath = useSelector( + selectBehaviorLikeInputNodeSelectedFilePath(nodeId), + ) const onChangeFilePath = (path: string) => { dispatch(setInputNodeFilePath({ nodeId, filePath: path })) } @@ -60,7 +62,7 @@ const BehaviorFileNodeImple = memo(function BehaviorFileNodeImple({ }} nameNode={FILE_TYPE_SET.BEHAVIOR} fileType={FILE_TYPE_SET.CSV} - filePath={filePath ?? ""} + filePath={typeof filePath === "string" ? filePath : ""} /> () - const filePath = useSelector(selectCsvInputNodeSelectedFilePath(nodeId)) + const filePath = useSelector(selectCsvLikeInputNodeSelectedFilePath(nodeId)) const onChangeFilePath = (path: string) => { dispatch(setInputNodeFilePath({ nodeId, filePath: path })) } diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/FileSelect.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/FileSelect.tsx index 93f32063c5..bd202ec25d 100644 --- a/frontend/src/components/Workspace/FlowChart/FlowChartNode/FileSelect.tsx +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/FileSelect.tsx @@ -12,7 +12,7 @@ import AccountTreeIcon from "@mui/icons-material/AccountTree" import AddLinkIcon from "@mui/icons-material/AddLink" import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate" import ChecklistRtlIcon from "@mui/icons-material/ChecklistRtl" -import { IconButton, Tooltip, Typography } from "@mui/material" +import { IconButton, Tooltip, Typography, Chip, Box } from "@mui/material" import ButtonGroup from "@mui/material/ButtonGroup" import { FILE_TREE_TYPE, FILE_TREE_TYPE_SET } from "api/files/Files" @@ -191,6 +191,7 @@ export const FileSelectImple = memo(function FileSelectImple({ const accept = getFileInputAccept(fileTreeType) const fileName = getLabelByPath(filePath) + const fileCount = Array.isArray(filePath) ? filePath.length : 1 return (
@@ -250,21 +251,25 @@ export const FileSelectImple = memo(function FileSelectImple({ ) : null} - {fileTreeType === FILE_TREE_TYPE_SET.CSV && !!filePath && !!nodeId && ( - - - - - - )} + {fileTreeType === FILE_TREE_TYPE_SET.CSV && + !!filePath && + (Array.isArray(filePath) ? filePath.length > 0 : true) && + !!nodeId && ( + + + + + + )} {( [FILE_TREE_TYPE_SET.HDF5, FILE_TREE_TYPE_SET.MATLAB] as string[] ).includes(fileTreeType as string) && !!filePath && + (Array.isArray(filePath) ? filePath.length > 0 : true) && !!nodeId && ( @@ -289,12 +294,24 @@ export const FileSelectImple = memo(function FileSelectImple({ visibility: "hidden", width: 0, height: 0, + position: "absolute", }} /> - - {fileName ? fileName : "No file is selected."} - + + {fileCount > 1 && ( + + )} + + {fileName ? fileName : "No file is selected."} + +
diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/FluoFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/FluoFileNode.tsx index 16204184f1..9de7dce285 100644 --- a/frontend/src/components/Workspace/FlowChart/FlowChartNode/FluoFileNode.tsx +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/FluoFileNode.tsx @@ -10,7 +10,7 @@ import { HANDLE_STYLE } from "const/flowchart" import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" import { - selectCsvInputNodeSelectedFilePath, + selectFluoLikeInputNodeSelectedFilePath, selectInputNodeDefined, } from "store/slice/InputNode/InputNodeSelectors" import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" @@ -29,7 +29,7 @@ const FluoFileNodeImple = memo(function FluoFileNodeImple({ selected, }: NodeProps) { const dispatch = useDispatch() - const filePath = useSelector(selectCsvInputNodeSelectedFilePath(nodeId)) + const filePath = useSelector(selectFluoLikeInputNodeSelectedFilePath(nodeId)) const onChangeFilePath = (path: string) => { dispatch(setInputNodeFilePath({ nodeId, filePath: path })) } @@ -58,7 +58,7 @@ const FluoFileNodeImple = memo(function FluoFileNodeImple({ }} nameNode={FILE_TYPE_SET.FLUO} fileType={FILE_TYPE_SET.CSV} - filePath={filePath ?? ""} + filePath={typeof filePath === "string" ? filePath : ""} /> - a != null && b != null - ? arrayEqualityFn(a as string[], b as string[]) + a != null && b != null && Array.isArray(a) && Array.isArray(b) + ? arrayEqualityFn(a, b) : a === b, ) const onChangeFilePath = (path: string[]) => { diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/MatlabFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/MatlabFileNode.tsx index 2cb198e8ac..d8b1f5461f 100644 --- a/frontend/src/components/Workspace/FlowChart/FlowChartNode/MatlabFileNode.tsx +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/MatlabFileNode.tsx @@ -5,7 +5,7 @@ import { import { FILE_TYPE_SET } from "config/fileTypes.config" import { selectInputNodeMatlabPath, - selectMatlabInputNodeSelectedFilePath, + selectMatlabLikeInputNodeSelectedFilePath, } from "store/slice/InputNode/InputNodeSelectors" import { setInputNodeMatlabPath } from "store/slice/InputNode/InputNodeSlice" import { getMatlabTree } from "store/slice/Matlab/MatlabAction" @@ -19,7 +19,7 @@ const matlabConfig: FileNodeConfig = { handleId: "matlab", handleType: "MatlabData", treeKeyPrefix: "matlabtree", - selectFilePath: selectMatlabInputNodeSelectedFilePath, + selectFilePath: selectMatlabLikeInputNodeSelectedFilePath, selectStructurePath: selectInputNodeMatlabPath, setStructurePath: setInputNodeMatlabPath, getTree: getMatlabTree, diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/MicroscopeFileNode.tsx b/frontend/src/components/Workspace/FlowChart/FlowChartNode/MicroscopeFileNode.tsx index 0b7ddcdf19..7a79a67f28 100644 --- a/frontend/src/components/Workspace/FlowChart/FlowChartNode/MicroscopeFileNode.tsx +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/MicroscopeFileNode.tsx @@ -13,7 +13,7 @@ import { HANDLE_STYLE } from "const/flowchart" import { deleteFlowNodeById } from "store/slice/FlowElement/FlowElementSlice" import { setInputNodeFilePath } from "store/slice/InputNode/InputNodeActions" import { - selectMicroscopeInputNodeSelectedFilePath, + selectMicroscopeLikeInputNodeSelectedFilePath, selectInputNodeDefined, } from "store/slice/InputNode/InputNodeSelectors" import { FILE_TYPE_SET } from "store/slice/InputNode/InputNodeType" @@ -35,7 +35,7 @@ const MicroscopeFileNodeImple = memo(function MicroscopeFileNodeImple({ }: NodeProps) { const dispatch = useDispatch() const filePath = useSelector( - selectMicroscopeInputNodeSelectedFilePath(nodeId), + selectMicroscopeLikeInputNodeSelectedFilePath(nodeId), ) const onChangeFilePath = (path: string) => { dispatch(setInputNodeFilePath({ nodeId, filePath: path })) diff --git a/frontend/src/components/Workspace/FlowChart/FlowChartNode/NodeComponentRegistry.ts b/frontend/src/components/Workspace/FlowChart/FlowChartNode/NodeComponentRegistry.ts index dece33f2c8..d387532e9d 100644 --- a/frontend/src/components/Workspace/FlowChart/FlowChartNode/NodeComponentRegistry.ts +++ b/frontend/src/components/Workspace/FlowChart/FlowChartNode/NodeComponentRegistry.ts @@ -3,6 +3,13 @@ import { NodeProps } from "reactflow" // Import all node components import { AlgorithmNode } from "components/Workspace/FlowChart/FlowChartNode/AlgorithmNode" +import { BatchBehaviorFileNode } from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchBehaviorFileNode" +import { BatchCsvFileNode } from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchCsvFileNode" +import { BatchFluoFileNode } from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchFluoFileNode" +import { BatchHDF5FileNode } from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchHDF5FileNode" +import { BatchImageFileNode } from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchImageFileNode" +import { BatchMatlabFileNode } from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMatlabFileNode" +import { BatchMicroscopeFileNode } from "components/Workspace/FlowChart/FlowChartNode/BatchInputNode/BatchMicroscopeFileNode" import { BehaviorFileNode } from "components/Workspace/FlowChart/FlowChartNode/BehaviorFileNode" import { CsvFileNode } from "components/Workspace/FlowChart/FlowChartNode/CsvFileNode" import { FluoFileNode } from "components/Workspace/FlowChart/FlowChartNode/FluoFileNode" @@ -24,6 +31,13 @@ export const nodeComponentRegistry: Record = { FluoFileNode, BehaviorFileNode, MicroscopeFileNode, + BatchImageFileNode, + BatchCsvFileNode, + BatchFluoFileNode, + BatchBehaviorFileNode, + BatchMicroscopeFileNode, + BatchHDF5FileNode, + BatchMatlabFileNode, } // Get component by node type name diff --git a/frontend/src/components/Workspace/FlowChart/TreeView.tsx b/frontend/src/components/Workspace/FlowChart/TreeView.tsx index 2f3472b00a..9eb32ee3a8 100644 --- a/frontend/src/components/Workspace/FlowChart/TreeView.tsx +++ b/frontend/src/components/Workspace/FlowChart/TreeView.tsx @@ -23,7 +23,10 @@ import { import { getFileTypeConfigsByHierarchy, REACT_FLOW_NODE_TYPE_KEY, + WORKSPACE_TYPE_HIERARCHY_MAPPING, + TreeHierarchyType, } from "config/fileTypes.config" +import { WORKSPACE_TYPE } from "const/Workspace" import { FileNodeFactory } from "factories/FileNodeFactory" import { getAlgoList } from "store/slice/AlgorithmList/AlgorithmListActions" import { @@ -42,6 +45,7 @@ import { import { NODE_TYPE_SET } from "store/slice/FlowElement/FlowElementType" import { FILE_TYPE } from "store/slice/InputNode/InputNodeType" import { selectPipelineLatestUid } from "store/slice/Pipeline/PipelineSelectors" +import { selectCurrentWorkspaceType } from "store/slice/Workspace/WorkspaceSelector" import { AppDispatch } from "store/store" import { getNanoId } from "utils/nanoid/NanoIdUtils" @@ -50,6 +54,7 @@ export const AlgorithmTreeView = memo(function AlgorithmTreeView() { const algoList = useSelector(selectAlgorithmListTree) const isLatest = useSelector(selectAlgorithmListIsLatest) const workflowId = useSelector(selectPipelineLatestUid) + const workspaceType = useSelector(selectCurrentWorkspaceType) const runAlready = typeof workflowId !== "undefined" useEffect(() => { @@ -86,6 +91,19 @@ export const AlgorithmTreeView = memo(function AlgorithmTreeView() { // Get file type configs grouped by hierarchy const fileTypesByHierarchy = getFileTypeConfigsByHierarchy() + // Get allowed hierarchies for the current workspace type + const allowedHierarchies = + WORKSPACE_TYPE_HIERARCHY_MAPPING[ + (workspaceType ?? WORKSPACE_TYPE.DEFAULT) as WORKSPACE_TYPE + ] + + // Filter hierarchies based on workspace type + const filteredHierarchies = Object.entries(fileTypesByHierarchy).filter( + ([hierarchyName]) => { + return allowedHierarchies.includes(hierarchyName as TreeHierarchyType) + }, + ) + return ( } > {/* Dynamically generate hierarchy nodes for file types */} - {Object.entries(fileTypesByHierarchy).map(([hierarchyName, configs]) => ( + {filteredHierarchies.map(([hierarchyName, configs]) => ( { algorithmNodeNotExist: false, handleCancelPipeline: jest.fn(), handleRunPipeline: jest.fn(), + handleBatchRunPipeline: jest.fn(), handleRunPipelineByUid: jest.fn(), } @@ -37,6 +38,19 @@ describe("RunButtons component", () => { uid: "test-uid", }, runBtn: RUN_BTN_OPTIONS.RUN_NEW, + workspace: { + currentWorkspace: { + type: 1, // WORKSPACE_TYPE.NORMAL + }, + }, + flowElement: { + flowNodes: [], + flowEdges: [], + flowPosition: { x: 0, y: 0, zoom: 1 }, + elementCoord: {}, + loading: false, + }, + inputNode: {}, }) }) diff --git a/frontend/src/components/common/CurrentPipelineInfo.tsx b/frontend/src/components/common/CurrentPipelineInfo.tsx index 6cb9e220a5..0ad626aff4 100644 --- a/frontend/src/components/common/CurrentPipelineInfo.tsx +++ b/frontend/src/components/common/CurrentPipelineInfo.tsx @@ -4,6 +4,7 @@ import { useSelector } from "react-redux" import { Typography, Grid, Popover } from "@mui/material" import { ParamSection } from "components/common/ParamSection" +import { WORKSPACE_TYPE, WORKSPACE_TYPE_LABEL } from "const/Workspace" import { selectCurrentPipelineName, selectPipelineLatestUid, @@ -12,11 +13,13 @@ import { selectModeStandalone } from "store/slice/Standalone/StandaloneSeclector import { selectCurrentWorkspaceId, selectCurrentWorkspaceName, + selectCurrentWorkspaceType, } from "store/slice/Workspace/WorkspaceSelector" export const CurrentPipelineInfo: FC = () => { const workspaceId = useSelector(selectCurrentWorkspaceId) const workspaceName = useSelector(selectCurrentWorkspaceName) + const workspaceType = useSelector(selectCurrentWorkspaceType) const workflowId = useSelector(selectPipelineLatestUid) const workflowName = useSelector(selectCurrentPipelineName) const isStandalone = useSelector(selectModeStandalone) @@ -40,6 +43,16 @@ export const CurrentPipelineInfo: FC = () => { section="workspace" /> )} + {workspaceType !== undefined && ( + + )} )} diff --git a/frontend/src/config/fileTypes.config.ts b/frontend/src/config/fileTypes.config.ts index 87307da8d5..32dd041ea5 100644 --- a/frontend/src/config/fileTypes.config.ts +++ b/frontend/src/config/fileTypes.config.ts @@ -1,11 +1,24 @@ +import { WORKSPACE_TYPE } from "const/Workspace" + // Define tree hierarchy constants for better maintainability export const TREE_HIERARCHY = { DATA: "Data", + BATCH_DATA: "Batch Data", } as const export type TreeHierarchyType = (typeof TREE_HIERARCHY)[keyof typeof TREE_HIERARCHY] +// Define mapping between workspace types and allowed tree hierarchies +export const WORKSPACE_TYPE_HIERARCHY_MAPPING: Record< + WORKSPACE_TYPE, + TreeHierarchyType[] +> = { + [WORKSPACE_TYPE.DEFAULT]: [TREE_HIERARCHY.DATA], + [WORKSPACE_TYPE.NORMAL]: [TREE_HIERARCHY.DATA], + [WORKSPACE_TYPE.BATCH]: [TREE_HIERARCHY.DATA, TREE_HIERARCHY.BATCH_DATA], +} + export interface FileTypeConfig { key: string displayName?: string // Optional: If not provided, key will be used for display @@ -65,6 +78,13 @@ export const REACT_FLOW_NODE_TYPE_KEY = { BehaviorFileNode: "BehaviorFileNode", MatlabFileNode: "MatlabFileNode", MicroscopeFileNode: "MicroscopeFileNode", + BatchImageFileNode: "BatchImageFileNode", + BatchCsvFileNode: "BatchCsvFileNode", + BatchFluoFileNode: "BatchFluoFileNode", + BatchBehaviorFileNode: "BatchBehaviorFileNode", + BatchMicroscopeFileNode: "BatchMicroscopeFileNode", + BatchHDF5FileNode: "BatchHDF5FileNode", + BatchMatlabFileNode: "BatchMatlabFileNode", } as const // Streamlined config - nodeType references REACT_FLOW_NODE_TYPE_KEY @@ -141,6 +161,91 @@ export const FILE_TYPE_CONFIGS: Record = { dataType: "matlab", // Special: uses matlab data type nodeType: REACT_FLOW_NODE_TYPE_KEY.MicroscopeFileNode, }, + BATCH_IMAGE: { + key: "batch_image", + hasFilePath: true, + filePathType: "array", + defaultParam: {}, + treeType: "image", + nodeType: REACT_FLOW_NODE_TYPE_KEY.BatchImageFileNode, + treeHierarchy: TREE_HIERARCHY.BATCH_DATA, + }, + BATCH_CSV: { + key: "batch_csv", + hasFilePath: true, + filePathType: "array", + defaultParam: { + setHeader: null, + setIndex: false, + transpose: false, + }, + treeType: "csv", + nodeType: REACT_FLOW_NODE_TYPE_KEY.BatchCsvFileNode, + treeHierarchy: TREE_HIERARCHY.BATCH_DATA, + }, + BATCH_HDF5: { + key: "batch_hdf5", + hasFilePath: true, + filePathType: "array", + hasSpecialPath: { + name: "hdf5Path", + type: "hdf5Path", + }, + defaultParam: {}, + treeType: "hdf5", + nodeType: REACT_FLOW_NODE_TYPE_KEY.BatchHDF5FileNode, + treeHierarchy: TREE_HIERARCHY.BATCH_DATA, + }, + BATCH_FLUO: { + key: "batch_fluo", + hasFilePath: true, + filePathType: "array", + defaultParam: { + setHeader: null, + setIndex: false, + transpose: false, + }, + stateFileType: "batch_csv", // Special: stored as batch_csv in state + treeType: "csv", + nodeType: REACT_FLOW_NODE_TYPE_KEY.BatchFluoFileNode, + treeHierarchy: TREE_HIERARCHY.BATCH_DATA, + }, + BATCH_BEHAVIOR: { + key: "batch_behavior", + hasFilePath: true, + filePathType: "array", + defaultParam: { + setHeader: null, + setIndex: false, + transpose: false, + }, + stateFileType: "batch_csv", // Special: stored as batch_csv in state + treeType: "csv", + nodeType: REACT_FLOW_NODE_TYPE_KEY.BatchBehaviorFileNode, + treeHierarchy: TREE_HIERARCHY.BATCH_DATA, + }, + BATCH_MATLAB: { + key: "batch_matlab", + hasFilePath: true, + filePathType: "array", + hasSpecialPath: { + name: "matPath", + type: "matPath", + }, + defaultParam: {}, + treeType: "matlab", + nodeType: REACT_FLOW_NODE_TYPE_KEY.BatchMatlabFileNode, + treeHierarchy: TREE_HIERARCHY.BATCH_DATA, + }, + BATCH_MICROSCOPE: { + key: "batch_microscope", + hasFilePath: true, + filePathType: "array", + defaultParam: {}, + treeType: "microscope", + nodeType: REACT_FLOW_NODE_TYPE_KEY.BatchMicroscopeFileNode, + treeHierarchy: TREE_HIERARCHY.BATCH_DATA, + }, } as const // Enhanced configs with computed properties diff --git a/frontend/src/const/Workspace.ts b/frontend/src/const/Workspace.ts index 71e211f39d..187a41090f 100644 --- a/frontend/src/const/Workspace.ts +++ b/frontend/src/const/Workspace.ts @@ -1,9 +1,22 @@ export enum WORKSPACE_TYPE { DEFAULT = 0, NORMAL = 1, + BATCH = 2, } export const WORKSPACE_TYPE_LABEL: Record = { [WORKSPACE_TYPE.DEFAULT]: "Normal", // Same as NORMAL [WORKSPACE_TYPE.NORMAL]: "Normal", + [WORKSPACE_TYPE.BATCH]: "Batch", } + +export const WORKSPACE_TYPE_OPTIONS = [ + { + value: WORKSPACE_TYPE.NORMAL, + label: WORKSPACE_TYPE_LABEL[WORKSPACE_TYPE.NORMAL], + }, + { + value: WORKSPACE_TYPE.BATCH, + label: WORKSPACE_TYPE_LABEL[WORKSPACE_TYPE.BATCH], + }, +] diff --git a/frontend/src/factories/FileNodeFactory.ts b/frontend/src/factories/FileNodeFactory.ts index afc2e7a57a..6669a3a7d5 100644 --- a/frontend/src/factories/FileNodeFactory.ts +++ b/frontend/src/factories/FileNodeFactory.ts @@ -31,15 +31,24 @@ export class FileNodeFactory { throw new Error(`Unsupported file type: ${fileType}`) } - // Map special cases + // Map special cases using config's stateFileType let actualFileType: FILE_TYPE - switch (fileType) { - case FILE_TYPE_SET.FLUO: - case FILE_TYPE_SET.BEHAVIOR: - actualFileType = FILE_TYPE_SET.CSV - break - default: - actualFileType = fileType + if (config.stateFileType) { + actualFileType = config.stateFileType as FILE_TYPE + } else { + // Fallback to legacy switch statement + switch (fileType) { + case FILE_TYPE_SET.FLUO: + case FILE_TYPE_SET.BEHAVIOR: + actualFileType = FILE_TYPE_SET.CSV + break + case FILE_TYPE_SET.BATCH_FLUO: + case FILE_TYPE_SET.BATCH_BEHAVIOR: + actualFileType = FILE_TYPE_SET.BATCH_CSV + break + default: + actualFileType = fileType + } } // Return proper typed InputNode based on fileType @@ -138,10 +147,12 @@ export class FileNodeFactory { const config = getFileTypeConfig(fileType) if (!config) return `selectGeneric${suffix}` - // Capitalize first letter for function name - const capitalizedName = - config.key.charAt(0).toUpperCase() + config.key.slice(1) - return `select${capitalizedName}InputNode${suffix}` + // Convert snake_case to PascalCase (e.g., batch_csv -> BatchCsv) + const pascalCaseName = config.key + .split("_") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join("") + return `select${pascalCaseName}InputNode${suffix}` } /** diff --git a/frontend/src/pages/Workspace/index.tsx b/frontend/src/pages/Workspace/index.tsx index 673f85e3f4..39df0e93bc 100644 --- a/frontend/src/pages/Workspace/index.tsx +++ b/frontend/src/pages/Workspace/index.tsx @@ -16,9 +16,14 @@ import { DialogTitle, DialogContent, DialogActions, - Input, + TextField, Tooltip, IconButton, + FormControl, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, } from "@mui/material" import { GridEventListener, @@ -36,6 +41,11 @@ import DeleteConfirmModal from "components/common/DeleteConfirmModal" import Loading from "components/common/Loading" import PaginationCustom from "components/common/PaginationCustom" import PopupShare from "components/Workspace/PopupShare" +import { + WORKSPACE_TYPE, + WORKSPACE_TYPE_LABEL, + WORKSPACE_TYPE_OPTIONS, +} from "const/Workspace" import { selectCurrentUser } from "store/slice/User/UserSelector" import { resetVisualizeLayout } from "store/slice/VisualizeItem/VisualizeItemSlice" import { @@ -59,8 +69,8 @@ type PopupType = { open: boolean handleClose: () => void handleOkDel?: () => void - setNewWorkSpace?: (name: string) => void - value?: string + setNewWorkSpace?: (workspace: { name: string; type: WORKSPACE_TYPE }) => void + value?: { name: string; type: WORKSPACE_TYPE } handleOkNew?: () => void handleOkSave?: () => void error?: string @@ -147,6 +157,20 @@ const columns = (
), }, + { + field: "type", + headerName: "Type", + filterable: false, + sortable: false, + flex: 1, + minWidth: 80, + renderCell: (params: GridRenderCellParams) => ( + + {WORKSPACE_TYPE_LABEL[params.value as WORKSPACE_TYPE] || + WORKSPACE_TYPE_LABEL[WORKSPACE_TYPE.DEFAULT]} + + ), + }, { field: "created_at", headerName: "Created", @@ -276,7 +300,18 @@ const PopupNew = ({ }: PopupType) => { if (!setNewWorkSpace) return null const handleName = (event: ChangeEvent) => { - setNewWorkSpace(event.target.value) + setNewWorkSpace({ + ...value, + name: event.target.value, + type: value?.type || WORKSPACE_TYPE.NORMAL, + }) + } + + const handleType = (event: ChangeEvent) => { + setNewWorkSpace({ + name: value?.name || "", + type: Number(event.target.value) as WORKSPACE_TYPE, + }) } return ( @@ -284,14 +319,33 @@ const PopupNew = ({ New Workspace - -
- {error ? {error} : null} + + Type + + {WORKSPACE_TYPE_OPTIONS.map((option) => ( + } + label={option.label} + /> + ))} + +