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

Feature/cloud/record screen #377

Merged
merged 21 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
11 changes: 11 additions & 0 deletions frontend/src/api/experiments/Experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ExperimentDTO = {
workspace_id: number
unique_id: string
hasNWB: boolean
is_remote_synced?: boolean
nwb: NWBType
}

Expand Down Expand Up @@ -109,3 +110,13 @@ export async function renameExperiment(
)
return response.data
}

export async function syncRemoteExperimentApi(
workspaceId: number,
uid: string,
) {
const response = await axios.get(
`${BASE_URL}/experiments/sync_remote/${workspaceId}/${uid}`,
)
return response.data
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { useSelector } from "react-redux"

import { useSnackbar } from "notistack"

import CloudQueueIcon from "@mui/icons-material/CloudQueue"
import SimCardDownloadOutlinedIcon from "@mui/icons-material/SimCardDownloadOutlined"
import { Tooltip } from "@mui/material"
import IconButton from "@mui/material/IconButton"

import {
Expand All @@ -12,6 +14,7 @@ import {
} from "api/experiments/Experiments"
import { downloadWorkflowConfigApi } from "api/workflow/Workflow"
import { ExperimentUidContext } from "components/Workspace/Experiment/ExperimentTable"
import { selectExperimentIsRemoteSynced } from "store/slice/Experiments/ExperimentsSelectors"
import { selectCurrentWorkspaceId } from "store/slice/Workspace/WorkspaceSelector"

interface NWBDownloadButtonProps {
Expand All @@ -29,6 +32,7 @@ export const NWBDownloadButton = memo(function NWBDownloadButton({
const uid = useContext(ExperimentUidContext)
const ref = useRef<HTMLAnchorElement | null>(null)
const [url, setFileUrl] = useState<string>()
const isRemoteSynced = useSelector(selectExperimentIsRemoteSynced(uid))
const { enqueueSnackbar } = useSnackbar()

const onClick = async () => {
Expand All @@ -51,12 +55,25 @@ export const NWBDownloadButton = memo(function NWBDownloadButton({

return (
<>
<IconButton onClick={onClick} color="primary" disabled={!hasNWB}>
<SimCardDownloadOutlinedIcon />
</IconButton>
<a href={url} download={`nwb_${name}.nwb`} className="hidden" ref={ref}>
{/* 警告が出るので空文字を入れておく */}{" "}
</a>
{isRemoteSynced ? (
<>
<IconButton onClick={onClick} color="primary" disabled={!hasNWB}>
<SimCardDownloadOutlinedIcon />
</IconButton>
<a
href={url}
download={`nwb_${name}.nwb`}
className="hidden"
ref={ref}
>
{/* 警告が出るので空文字を入れておく */}{" "}
</a>
</>
) : (
<Tooltip title="Data is unsynchronized">
<CloudQueueIcon color="disabled" style={{ padding: "8px" }} />
</Tooltip>
)}
</>
)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { memo, useContext, useState } from "react"
import { useSelector, useDispatch } from "react-redux"

import { useSnackbar } from "notistack"

import CloudDownloadIcon from "@mui/icons-material/CloudDownloadOutlined"
import DoneIcon from "@mui/icons-material/Done"
import IconButton from "@mui/material/IconButton"

import { ConfirmDialog } from "components/common/ConfirmDialog"
import { ExperimentUidContext } from "components/Workspace/Experiment/ExperimentTable"
import { syncRemoteExperiment } from "store/slice/Experiments/ExperimentsActions"
import {
selectExperimentName,
selectExperimentIsRemoteSynced,
} from "store/slice/Experiments/ExperimentsSelectors"
import {
selectPipelineLatestUid,
selectPipelineIsStartedSuccess,
} from "store/slice/Pipeline/PipelineSelectors"
import { AppDispatch, RootState } from "store/store"

export const RemoteSyncButton = memo(function CloudSyncButton() {
const dispatch = useDispatch<AppDispatch>()
const uid = useContext(ExperimentUidContext)
const isRunning = useSelector((state: RootState) => {
const currentUid = selectPipelineLatestUid(state)
const isPending = selectPipelineIsStartedSuccess(state)
return uid === currentUid && isPending
})
const name = useSelector(selectExperimentName(uid))
const isRemoteSynced = useSelector(selectExperimentIsRemoteSynced(uid))
const [open, setOpen] = useState(false)
const { enqueueSnackbar } = useSnackbar()

const openDialog = () => {
setOpen(true)
}
const handleSyncRemote = () => {
dispatch(syncRemoteExperiment(uid))
.unwrap()
.then(() => {
enqueueSnackbar("Successfully synchronize", { variant: "success" })
})
.catch(() => {
enqueueSnackbar("Failed to synchronize", { variant: "error" })
})
}

return (
<>
{isRemoteSynced ? (
<DoneIcon color="success" />
) : (
<>
<IconButton
onClick={openDialog}
disabled={isRunning}
color="primary"
style={{ padding: 0 }}
>
<CloudDownloadIcon />
</IconButton>
<ConfirmDialog
open={open}
setOpen={setOpen}
onConfirm={handleSyncRemote}
title="Sync remote storage record?"
content={`${name} (${uid})`}
confirmLabel="OK"
iconType="info"
/>
</>
)}
</>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const CollapsibleTable = memo(function CollapsibleTable({
}: CollapsibleTableProps) {
return (
<TableRow>
<TableCell sx={{ paddingBottom: 0, paddingTop: 0 }} colSpan={11}>
<TableCell sx={{ paddingBottom: 0, paddingTop: 0 }} colSpan={12}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box margin={1}>
<Typography variant="h6" gutterBottom component="div">
Expand Down
21 changes: 19 additions & 2 deletions frontend/src/components/Workspace/Experiment/ExperimentTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import DeleteIcon from "@mui/icons-material/Delete"
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"
import ReplayIcon from "@mui/icons-material/Replay"
import { Tooltip } from "@mui/material"
import Alert from "@mui/material/Alert"
import AlertTitle from "@mui/material/AlertTitle"
import Box from "@mui/material/Box"
Expand Down Expand Up @@ -43,6 +44,7 @@ import {
SnakemakeDownloadButton,
WorkflowDownloadButton,
} from "components/Workspace/Experiment/Button/DownloadButton"
import { RemoteSyncButton } from "components/Workspace/Experiment/Button/RemoteSyncButton"
import { ReproduceButton } from "components/Workspace/Experiment/Button/ReproduceButton"
import { CollapsibleTable } from "components/Workspace/Experiment/CollapsibleTable"
import { ExperimentStatusIcon } from "components/Workspace/Experiment/ExperimentStatusIcon"
Expand All @@ -61,6 +63,7 @@ import {
selectExperimentsErrorMessage,
selectExperimentList,
selectExperimentHasNWB,
selectExperimentIsRemoteSynced,
} from "store/slice/Experiments/ExperimentsSelectors"
import { ExperimentSortKeys } from "store/slice/Experiments/ExperimentsType"
import {
Expand Down Expand Up @@ -395,6 +398,7 @@ const HeadItem = memo(function HeadItem({
<TableCell>Workflow</TableCell>
<TableCell>Snakemake</TableCell>
<TableCell>NWB</TableCell>
<TableCell>Sync</TableCell>
{isOwner && <TableCell>Delete</TableCell>}
</TableRow>
</TableHead>
Expand Down Expand Up @@ -424,6 +428,7 @@ const RowItem = memo(function RowItem({
const [errorEdit, setErrorEdit] = useState("")
const [valueEdit, setValueEdit] = useState(name)
const dispatch = useDispatch<AppDispatch>()
const isRemoteSynced = useSelector(selectExperimentIsRemoteSynced(uid))

const onBlurEdit = (event: FocusEvent) => {
event.preventDefault()
Expand Down Expand Up @@ -503,9 +508,18 @@ const RowItem = memo(function RowItem({
)}
</TableCell>
<TableCell>{uid}</TableCell>
<TableCell sx={{ width: 160, position: "relative" }} onClick={onEdit}>
<TableCell
sx={{ width: 160, position: "relative" }}
onClick={isRemoteSynced ? onEdit : undefined}
>
{!isEdit ? (
valueEdit
isRemoteSynced ? (
valueEdit
) : (
<Tooltip title="Data is unsynchronized">
<Typography sx={{ color: "gray" }}>{valueEdit}</Typography>
</Tooltip>
)
) : (
<>
<Input
Expand Down Expand Up @@ -535,6 +549,9 @@ const RowItem = memo(function RowItem({
<TableCell>
<NWBDownloadButton name={uid} hasNWB={hasNWB} />
</TableCell>
<TableCell>
<RemoteSyncButton />
</TableCell>
{isOwner && (
<TableCell>
{" "}
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/store/slice/Experiments/ExperimentsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getExperimentsApi,
deleteExperimentByUidApi,
deleteExperimentByListApi,
syncRemoteExperimentApi,
} from "api/experiments/Experiments"
import { EXPERIMENTS_SLICE_NAME } from "store/slice/Experiments/ExperimentsType"
import { selectCurrentWorkspaceId } from "store/slice/Workspace/WorkspaceSelector"
Expand Down Expand Up @@ -63,3 +64,21 @@ export const deleteExperimentByList = createAsyncThunk<
return thunkAPI.rejectWithValue("workspace id does not exist.")
}
})

export const syncRemoteExperiment = createAsyncThunk<
boolean,
string,
ThunkApiConfig
>(`${EXPERIMENTS_SLICE_NAME}/syncRemoteExperiment`, async (uid, thunkAPI) => {
const workspaceId = selectCurrentWorkspaceId(thunkAPI.getState())
if (workspaceId) {
try {
const response = await syncRemoteExperimentApi(workspaceId, uid)
return response
} catch (e) {
return thunkAPI.rejectWithValue(e)
}
} else {
return thunkAPI.rejectWithValue("sync remote storage experiment failed.")
}
})
4 changes: 4 additions & 0 deletions frontend/src/store/slice/Experiments/ExperimentsSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export const selectExperimentName = (uid: string) => (state: RootState) =>
export const selectExperimentHasNWB = (uid: string) => (state: RootState) =>
selectExperiment(uid)(state).hasNWB

export const selectExperimentIsRemoteSynced =
(uid: string) => (state: RootState) =>
selectExperiment(uid)(state).isRemoteSynced

export const selectExperimentStatus =
(uid: string) =>
(state: RootState): EXPERIMENTS_STATUS => {
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/store/slice/Experiments/ExperimentsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getExperiments,
deleteExperimentByUid,
deleteExperimentByList,
syncRemoteExperiment,
} from "store/slice/Experiments/ExperimentsActions"
import {
EXPERIMENTS_SLICE_NAME,
Expand Down Expand Up @@ -69,6 +70,12 @@ export const experimentsSlice = createSlice({
action.meta.arg.map((v) => delete state.experimentList[v])
}
})
.addCase(syncRemoteExperiment.fulfilled, (state, action) => {
state.loading = false
if (action.payload && state.status === "fulfilled") {
state.experimentList[action.meta.arg].isRemoteSynced = true
}
})
.addCase(pollRunResult.fulfilled, (state, action) => {
if (state.status === "fulfilled") {
const uid = action.meta.arg.uid
Expand All @@ -83,7 +90,11 @@ export const experimentsSlice = createSlice({
}
})
.addMatcher(
isAnyOf(deleteExperimentByUid.pending, deleteExperimentByList.pending),
isAnyOf(
deleteExperimentByUid.pending,
deleteExperimentByList.pending,
syncRemoteExperiment.pending,
),
(state) => {
state.loading = true
},
Expand All @@ -92,6 +103,7 @@ export const experimentsSlice = createSlice({
isAnyOf(
deleteExperimentByUid.rejected,
deleteExperimentByList.rejected,
syncRemoteExperiment.rejected,
),
(state) => {
state.loading = false
Expand Down
1 change: 1 addition & 0 deletions frontend/src/store/slice/Experiments/ExperimentsType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type ExperimentType = {
finishedAt?: string
hasNWB: boolean
frameRate?: number
isRemoteSynced?: boolean
}

export type ExperimentFunction = {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/store/slice/Experiments/ExperimentsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function convertToExperimentType(dto: ExperimentDTO): ExperimentType {
status: dto.success,
name: dto.name,
hasNWB: dto.hasNWB,
isRemoteSynced: dto.is_remote_synced,
functions,
frameRate: dto.nwb?.imaging_plane.imaging_rate,
}
Expand Down
5 changes: 5 additions & 0 deletions studio/app/common/core/experiment/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ class ExptConfig:
snakemake: SmkParam


@dataclass
class ExptExtConfig(ExptConfig):
is_remote_synced: Optional[bool] = None


@dataclass
class ExptOutputPathIds:
output_dir: Optional[str] = None
Expand Down
21 changes: 20 additions & 1 deletion studio/app/common/core/experiment/experiment_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from studio.app.common.core.experiment.experiment import ExptConfig, ExptFunction
from studio.app.common.core.experiment.experiment_builder import ExptConfigBuilder
from studio.app.common.core.experiment.experiment_reader import ExptConfigReader
from studio.app.common.core.storage.remote_storage_controller import (
RemoteStorageController,
)
from studio.app.common.core.utils.config_handler import ConfigWriter
from studio.app.common.core.utils.filepath_creater import join_filepath
from studio.app.common.core.workflow.workflow import ProcessType
Expand Down Expand Up @@ -129,7 +132,16 @@ def delete_data(self) -> bool:
shutil.rmtree(
join_filepath([DIRPATH.OUTPUT_DIR, self.workspace_id, self.unique_id])
)
return True

result = True

# Operate remote storage data.
if RemoteStorageController.use_remote_storage():
result = RemoteStorageController().delete_experiment(
self.workspace_id, self.unique_id
)

return result

def rename(self, new_name: str) -> ExptConfig:
filepath = join_filepath(
Expand All @@ -147,6 +159,13 @@ def rename(self, new_name: str) -> ExptConfig:
f.seek(0) # requires seek(0) before write.
yaml.dump(config, f, sort_keys=False)

# Operate remote storage data.
if RemoteStorageController.use_remote_storage():
# upload latest EXPERIMENT_YML
RemoteStorageController().upload_experiment(
self.workspace_id, self.unique_id, [DIRPATH.EXPERIMENT_YML]
)

return ExptConfig(
workspace_id=config["workspace_id"],
unique_id=config["unique_id"],
Expand Down
Loading
Loading