Skip to content

Commit

Permalink
feat(sessions): add modal for choosing session environment (reanahub#405
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonadoni committed Apr 16, 2024
1 parent 43ced0c commit 79366da
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 36 deletions.
45 changes: 35 additions & 10 deletions reana-ui/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2020, 2021, 2022, 2023 CERN.
Copyright (C) 2020, 2021, 2022, 2023, 2024 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -12,15 +12,15 @@ import isEmpty from "lodash/isEmpty";

import client, {
CONFIG_URL,
INTERACTIVE_SESSIONS_CLOSE_URL,
INTERACTIVE_SESSIONS_OPEN_URL,
USER_INFO_URL,
USER_SIGNOUT_URL,
WORKFLOW_FILES_URL,
WORKFLOW_LOGS_URL,
WORKFLOW_SPECIFICATION_URL,
WORKFLOW_RETENTION_RULES_URL,
WORKFLOW_FILES_URL,
WORKFLOW_SET_STATUS_URL,
INTERACTIVE_SESSIONS_OPEN_URL,
INTERACTIVE_SESSIONS_CLOSE_URL,
WORKFLOW_SPECIFICATION_URL,
} from "~/client";
import {
parseWorkflows,
Expand Down Expand Up @@ -87,6 +87,9 @@ export const WORKFLOW_STOPPED = "Workflow stopped";
export const OPEN_STOP_WORKFLOW_MODAL = "Open stop workflow modal";
export const CLOSE_STOP_WORKFLOW_MODAL = "Close stop workflow modal";
export const WORKFLOW_LIST_REFRESH = "Refresh workflow list";
export const OPEN_INTERACTIVE_SESSION_MODAL = "Open interactive session modal";
export const CLOSE_INTERACTIVE_SESSION_MODAL =
"Close interactive session modal";

export function errorActionCreator(error, name) {
const { status, data } = error?.response;
Expand Down Expand Up @@ -469,16 +472,39 @@ export function closeStopWorkflowModal() {
return { type: CLOSE_STOP_WORKFLOW_MODAL };
}

export function openInteractiveSession(id) {
return async (dispatch) => {
export function openInteractiveSessionModal(workflow) {
return { type: OPEN_INTERACTIVE_SESSION_MODAL, workflow };
}

export function closeInteractiveSessionModal() {
return { type: CLOSE_INTERACTIVE_SESSION_MODAL };
}

export function openInteractiveSession(id, options = {}) {
return async (dispatch, getStore) => {
const state = getStore();
const config = getConfig(state);
return await client
.openInteractiveSession(id)
.openInteractiveSession(id, options)
.then((resp) => {
dispatch({ type: WORKFLOW_LIST_REFRESH });

const interactiveSessionInactivityWarning =
config.maxInteractiveSessionInactivityPeriod
? `Please note that it will be automatically closed after ${config.maxInteractiveSessionInactivityPeriod} days of inactivity.`
: "";
dispatch(
triggerNotification(
"Success!",
"The interactive session has been created. " +
"However, it could take several minutes to start the Jupyter Notebook. " +
"Click on the Jupyter logo to access it. " +
`${interactiveSessionInactivityWarning}`,
),
);
})
.catch((err) => {
dispatch(errorActionCreator(err, INTERACTIVE_SESSIONS_OPEN_URL(id)));
throw err;
});
};
}
Expand All @@ -493,7 +519,6 @@ export function closeInteractiveSession(id) {
})
.catch((err) => {
dispatch(errorActionCreator(err, INTERACTIVE_SESSIONS_CLOSE_URL(id)));
throw err;
});
};
}
9 changes: 6 additions & 3 deletions reana-ui/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2021, 2022, 2023 CERN.
Copyright (C) 2021, 2022, 2023, 2024 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
Expand Down Expand Up @@ -164,8 +164,11 @@ class Client {
});
}

openInteractiveSession(id) {
return this._request(INTERACTIVE_SESSIONS_OPEN_URL(id), { method: "post" });
openInteractiveSession(id, { type = "jupyter", image } = {}) {
return this._request(INTERACTIVE_SESSIONS_OPEN_URL(id, type), {
data: { image },
method: "post",
});
}

closeInteractiveSession(id) {
Expand Down
194 changes: 194 additions & 0 deletions reana-ui/src/components/InteractiveSessionModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2024 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
*/

import { useCallback, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
Button,
Form,
FormDropdown,
FormGroup,
FormInput,
FormRadio,
Modal,
} from "semantic-ui-react";

import {
closeInteractiveSessionModal,
openInteractiveSession,
} from "~/actions";
import {
getConfig,
getInteractiveSessionModalItem,
getInteractiveSessionModalOpen,
} from "~/selectors";

const EnvironmentType = {
Recommended: "recommended",
Custom: "custom",
};

export default function InteractiveSessionModal() {
const dispatch = useDispatch();
const config = useSelector(getConfig);
const open = useSelector(getInteractiveSessionModalOpen);
const workflow = useSelector(getInteractiveSessionModalItem);
const environments = config.interactiveSessions.environments;
const [sessionType, setSessionType] = useState(null);
const [environmentType, setEnvironmentType] = useState(
EnvironmentType.Recommended,
);
const [recommendedImage, setRecommendedImage] = useState(null);
const [customImage, setCustomImage] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({});

const resetForm = useCallback(
(newSessionType = null) => {
if (!newSessionType) {
// get default session type
newSessionType = Object.keys(environments).sort()[0];
}
setSessionType(newSessionType);
setEnvironmentType(EnvironmentType.Recommended);
// default image is first of recommended
setRecommendedImage(environments[newSessionType].recommended[0].image);
setCustomImage("");
setIsLoading(false);
setErrors({});
},
[environments],
);

useEffect(() => {
// reset local state on workflow change
resetForm();
}, [workflow, resetForm]);

// no workflow passed, nothing to show
if (!workflow) return null;

if (!sessionType || !recommendedImage) {
// initialize state
resetForm();
return null;
}

const allSessionTypes = Object.keys(environments).sort();
const sessionTypeOptions = allSessionTypes.map((type) => ({
key: type,
text: type,
value: type,
}));

const recommendedImageOptions = environments[sessionType].recommended.map(
(recommended) => ({
key: recommended.image,
text: recommended.name ?? recommended.image,
value: recommended.image,
}),
);

const isCustomAllowed = environments[sessionType].allow_custom;

const checkFields = () => {
const errors = {};
if (environmentType === EnvironmentType.Custom && !customImage) {
errors.customImage = "Please provide a custom image";
}
setErrors(errors);
return Object.keys(errors).length === 0;
};

const onCloseModal = () => {
resetForm();
dispatch(closeInteractiveSessionModal());
};

const onSubmit = () => {
if (!checkFields()) return;

setIsLoading(true);

const image =
environmentType === EnvironmentType.Recommended
? recommendedImage
: customImage;
dispatch(
openInteractiveSession(workflow.id, { type: sessionType, image }),
).finally(onCloseModal);
};

return (
<Modal open={open} onClose={onCloseModal} closeIcon size="small">
<Modal.Header>Open interactive session</Modal.Header>
<Modal.Content>
<Form id="formOpenSession" onSubmit={onSubmit} loading={isLoading}>
{/* only show dropdown if there are at least two session types to choose from */}
{allSessionTypes.length > 1 && (
<FormDropdown
selection
label="Session type"
options={sessionTypeOptions}
value={sessionType}
onChange={(_, { value }) => resetForm(value)}
disabled={sessionTypeOptions.length === 1}
/>
)}
{/* show selector only if custom images are allowed */}
{isCustomAllowed && (
<FormGroup inline>
<label>Environment</label>
<FormRadio
name="environmentType"
label="Recommended environments"
value="recommended"
checked={environmentType === EnvironmentType.Recommended}
onChange={(_, { value }) => setEnvironmentType(value)}
/>
<FormRadio
name="environmentType"
label="Custom environment"
value="custom"
checked={environmentType === EnvironmentType.Custom}
onChange={(_, { value }) => setEnvironmentType(value)}
/>
</FormGroup>
)}
{environmentType === EnvironmentType.Recommended && (
<FormDropdown
selection
label="Recommended environments"
options={recommendedImageOptions}
value={recommendedImage}
onChange={(_, { value }) => setRecommendedImage(value)}
search={true}
/>
)}
{environmentType === EnvironmentType.Custom && (
<FormInput
label="Custom environment"
value={customImage}
onChange={(_, { value }) => setCustomImage(value)}
placeholder={"Custom container image"}
error={errors.customImage}
/>
)}
</Form>
</Modal.Content>
<Modal.Actions>
<Button primary type="submit" form="formOpenSession">
Open
</Button>
<Button onClick={onCloseModal}>Cancel</Button>
</Modal.Actions>
</Modal>
);
}
21 changes: 3 additions & 18 deletions reana-ui/src/components/WorkflowActionsPopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2020, 2021, 2022, 2023 CERN.
Copyright (C) 2020, 2021, 2022, 2023, 2024 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -17,11 +17,10 @@ import { workflowShape } from "~/props";
import { getConfig } from "~/selectors";
import {
deleteWorkflow,
openInteractiveSession,
closeInteractiveSession,
openDeleteWorkflowModal,
openStopWorkflowModal,
triggerNotification,
openInteractiveSessionModal,
} from "~/actions";

import { JupyterNotebookIcon } from "~/components";
Expand All @@ -48,21 +47,7 @@ export default function WorkflowActionsPopup({ workflow, className }) {
content: "Open Jupyter Notebook",
icon: JupyterIcon,
onClick: (e) => {
dispatch(openInteractiveSession(id)).then(() => {
const interactiveSessionInactivityWarning =
config.maxInteractiveSessionInactivityPeriod
? `Please note that it will be automatically closed after ${config.maxInteractiveSessionInactivityPeriod} days of inactivity.`
: "";
dispatch(
triggerNotification(
"Success!",
"The interactive session has been created. " +
"However, it could take several minutes to start the Jupyter Notebook. " +
"Click on the Jupyter logo to access it. " +
`${interactiveSessionInactivityWarning}`,
),
);
});
dispatch(openInteractiveSessionModal(workflow));
setOpen(false);
e.stopPropagation();
},
Expand Down
3 changes: 2 additions & 1 deletion reana-ui/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2020, 2021, 2022, 2023 CERN.
Copyright (C) 2020, 2021, 2022, 2023, 2024 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -23,3 +23,4 @@ export { default as WorkflowActionsPopup } from "./WorkflowActionsPopup";
export { default as PieChart } from "./PieChart";
export { default as Search } from "./Search";
export { default as Box } from "./Box";
export { default as InteractiveSessionModal } from "./InteractiveSessionModal";
4 changes: 3 additions & 1 deletion reana-ui/src/pages/workflowDetails/WorkflowDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2020, 2022, 2023 CERN.
Copyright (C) 2020, 2022, 2023, 2024 CERN.
REANA is free software; you can redistribute it and/or modify it
under the terms of the MIT License; see LICENSE file for more details.
Expand All @@ -23,6 +23,7 @@ import {
} from "~/selectors";
import BasePage from "../BasePage";
import {
InteractiveSessionModal,
Notification,
WorkflowDeleteModal,
WorkflowStopModal,
Expand Down Expand Up @@ -146,6 +147,7 @@ export default function WorkflowDetails() {
panes={panes}
defaultActiveIndex={defaultActiveIndex}
/>
<InteractiveSessionModal />
<WorkflowDeleteModal />
<WorkflowStopModal />
</Container>
Expand Down
Loading

0 comments on commit 79366da

Please sign in to comment.