Skip to content

Commit

Permalink
ui: add way to open, list and close interactive sessions
Browse files Browse the repository at this point in the history
closes #77
  • Loading branch information
audrium committed Oct 15, 2020
1 parent ab36fd4 commit 47be429
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 88 deletions.
55 changes: 54 additions & 1 deletion reana-ui/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export const WORKFLOW_SPECIFICATION_RECEIVED =
"Workflow specification received";
export const WORKFLOW_DELETE_INIT = "Initialize workflow deletion";
export const WORKFLOW_DELETED = "Workflow deleted";
export const OPEN_DELETE_WORKFLOW_MODAL = "Open delete workflow modal";
export const CLOSE_DELETE_WORKFLOW_MODAL = "Close delete workflow modal";
export const OPEN_INTERACTIVE_SESSION_INIT =
"Initialize opening new interactive session";
export const OPEN_INTERACTIVE_SESSION_FULLFILED =
"Succeeded to open new interactive session";

const CONFIG_URL = `${api}/api/config`;
const USER_INFO_URL = `${api}/api/you`;
Expand All @@ -70,7 +76,9 @@ const USER_SIGNIN_URL = `${api}/api/login`;
const USER_SIGNOUT_URL = `${api}/api/logout`;
const USER_REQUEST_TOKEN_URL = `${api}/api/token`;
const WORKFLOWS_URL = (params) =>
`${api}/api/workflows?verbose=true&${stringifyQueryParams(params)}`;
`${api}/api/workflows?verbose=true&type=interactive&${stringifyQueryParams(
params
)}`;
const WORKFLOW_LOGS_URL = (id) => `${api}/api/workflows/${id}/logs`;
const WORKFLOW_SPECIFICATION_URL = (id) =>
`${api}/api/workflows/${id}/specification`;
Expand All @@ -80,6 +88,12 @@ const WORKFLOW_FILES_URL = (id, pagination) =>
const WORKFLOW_SET_STATUS_URL = (id, status) =>
`${api}/api/workflows/${id}/status?${stringifyQueryParams(status)}`;

const INTERACTIVE_SESSIONS_OPEN_URL = (id, type = "jupyter") =>
`${api}/api/workflows/${id}/open/${type}`;

const INTERACTIVE_SESSIONS_CLOSE_URL = (id) =>
`${api}/api/workflows/${id}/close/`;

function errorActionCreator(error, name) {
const { status, data } = error?.response;
const { message } = data;
Expand Down Expand Up @@ -346,3 +360,42 @@ export function deleteWorkflow(id, workspace = false) {
});
};
}

export function openDeleteWorkflowModal(workflow) {
return { type: OPEN_DELETE_WORKFLOW_MODAL, workflow };
}

export function closeDeleteWorkflowModal() {
return { type: CLOSE_DELETE_WORKFLOW_MODAL };
}

export function openInteractiveSession(id) {
return async (dispatch) => {
return await axios
.post(INTERACTIVE_SESSIONS_OPEN_URL(id), null, { withCredentials: true })
.then((resp) => {
dispatch(
triggerNotification(
"Success!",
"The interactive session has been created. However, it could take several minutes to start the Jupyter Notebook."
)
);
})
.catch((err) => {
dispatch(errorActionCreator(err, INTERACTIVE_SESSIONS_OPEN_URL(id)));
});
};
}

export function closeInteractiveSession(id) {
return async (dispatch) => {
return await axios
.post(INTERACTIVE_SESSIONS_CLOSE_URL(id), null, { withCredentials: true })
.then((resp) => {
dispatch(triggerNotification("Success!", resp.data.message));
})
.catch((err) => {
dispatch(errorActionCreator(err, INTERACTIVE_SESSIONS_CLOSE_URL(id)));
});
};
}
58 changes: 58 additions & 0 deletions reana-ui/src/components/JupyterNotebookIcon.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions reana-ui/src/components/JupyterNotebookIcon.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
-*- coding: utf-8 -*-
This file is part of REANA.
Copyright (C) 2020 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 "@palette";

.icon {
display: inline-block;
vertical-align: middle;
}
1 change: 1 addition & 0 deletions reana-ui/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export { default as Pagination } from "./Pagination";
export { default as Title } from "./Title";
export { default as TopHeader } from "./TopHeader";
export { default as TooltipIfTruncated } from "./TooltipIfTruncated";
export { default as JupyterNotebookIcon } from "./JupyterNotebookIcon";
2 changes: 2 additions & 0 deletions reana-ui/src/pages/workflowDetails/WorkflowDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
WorkflowFiles,
WorkflowSpecification,
} from "./components";
import { WorkflowDeleteModal } from "../workflowList/components";

export default function WorkflowDetailsPage() {
return (
Expand Down Expand Up @@ -91,6 +92,7 @@ function WorkflowDetails() {
<Container>
<WorkflowInfo workflow={workflow} />
<Tab menu={{ secondary: true, pointing: true }} panes={panes} />
<WorkflowDeleteModal />
</Container>
);
}
36 changes: 20 additions & 16 deletions reana-ui/src/pages/workflowDetails/components/WorkflowInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Icon, Popup } from "semantic-ui-react";

import { statusMapping } from "~/util";
import { WorkflowProgress } from "../components";

import { WorkflowActionsPopup } from "../../workflowList/components";
import styles from "./WorkflowInfo.module.scss";

const NON_FINISHED_STATUSES = ["created", "queued", "running"];
Expand Down Expand Up @@ -63,23 +63,27 @@ export default function WorkflowInfo({ workflow }) {
}
/>
</div>
<div>
<span
className={`${styles["status"]} sui-${statusMapping[status].color}`}
>
{status}
</span>{" "}
{statusMapping[status].preposition} {status !== "deleted" && duration}
{NON_FINISHED_STATUSES.includes(status) && (
<Icon
name="refresh"
className={styles.refresh}
onClick={() => window.location.reload()}
/>
)}
<div className={styles.info}>
<div>
step {completed}/{total}
<span
className={`${styles["status"]} sui-${statusMapping[status].color}`}
>
{status}
</span>{" "}
{statusMapping[status].preposition}{" "}
{status !== "deleted" && duration}
{NON_FINISHED_STATUSES.includes(status) && (
<Icon
name="refresh"
className={styles.refresh}
onClick={() => window.location.reload()}
/>
)}
<div>
step {completed}/{total}
</div>
</div>
<WorkflowActionsPopup workflow={workflow} />
</div>
</section>
<WorkflowProgress workflow={workflow} />
Expand Down
107 changes: 74 additions & 33 deletions reana-ui/src/pages/workflowList/components/WorkflowActionsPopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,61 @@
under the terms of the MIT License; see LICENSE file for more details.
*/

import PropTypes from "prop-types";
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Icon, Menu, Popup } from "semantic-ui-react";
import { useDispatch } from "react-redux";

import { workflowShape } from "~/props";
import { deleteWorkflow } from "~/actions";
import {
deleteWorkflow,
openInteractiveSession,
closeInteractiveSession,
openDeleteWorkflowModal,
} from "~/actions";

import { JupyterNotebookIcon } from "~/components";

import styles from "./WorkflowActionsPopup.module.scss";

export default function WorkflowActionsPopup({
workflow,
setOpenDeleteModal,
setSelectedWorkflow,
}) {
const JupyterIcon = <JupyterNotebookIcon className={styles["jupyter-icon"]} />;

export default function WorkflowActionsPopup({ workflow, className }) {
const dispatch = useDispatch();
const [open, setOpen] = useState(false);
const { id, name, run, size, status } = workflow;
const { id, size, status, session_uri } = workflow;
const isDeleted = status === "deleted";
const isDeletedUsingWorkspace = isDeleted && size !== "0K";

// TODO: Specify notebook name by type
let menuItems = [];
if (status === "deleted") {
if (size !== "0K") {

if (session_uri) {
menuItems.push({
key: "closeNotebook",
content: "Close Jupyter Notebook",
icon: JupyterIcon,
onClick: (e) => {
dispatch(closeInteractiveSession(id));
setOpen(false);
e.stopPropagation();
},
});
} else {
menuItems.push({
key: "openNotebook",
content: "Open Jupyter Notebook",
icon: JupyterIcon,
onClick: (e) => {
dispatch(openInteractiveSession(id));
setOpen(false);
e.stopPropagation();
},
});
}

if (isDeleted) {
if (isDeletedUsingWorkspace) {
menuItems.push({
key: "freeup",
content: "Free up disk",
Expand All @@ -46,38 +80,45 @@ export default function WorkflowActionsPopup({
content: "Delete workflow",
icon: "trash",
onClick: (e) => {
setOpenDeleteModal(true);
setSelectedWorkflow({ id, name, run, size });
dispatch(openDeleteWorkflowModal(workflow));
setOpen(false);
e.stopPropagation();
},
});
}

return (
<Popup
basic
trigger={
<Icon
name="ellipsis vertical"
className={styles.icon}
onClick={(e) => {
setOpen(true);
e.stopPropagation();
}}
/>
}
position="bottom left"
on="click"
open={open}
onClose={() => setOpen(false)}
>
<Menu items={menuItems} secondary vertical />
</Popup>
<div className={className || styles.container}>
{(!isDeleted || isDeletedUsingWorkspace) && (
<Popup
basic
trigger={
<Icon
name="ellipsis vertical"
className={styles.icon}
onClick={(e) => {
setOpen(true);
e.stopPropagation();
}}
/>
}
position="bottom left"
on="click"
open={open}
onClose={() => setOpen(false)}
>
<Menu items={menuItems} secondary vertical />
</Popup>
)}
</div>
);
}

WorkflowActionsPopup.defaultProps = {
className: "",
};

WorkflowActionsPopup.propTypes = {
workflow: workflowShape.isRequired,
setOpenDeleteModal: PropTypes.func.isRequired,
setSelectedWorkflow: PropTypes.func.isRequired,
className: PropTypes.string,
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,18 @@
justify-content: center;
height: 100%;
}

.container {
min-width: 22px;
margin-left: 2em;

&:hover {
color: darken($sepia, 30%);
}
}

.jupyter-icon {
width: 1.18em;
float: right;
margin: -0.2em 0em 0em 0.5em;
}
Loading

0 comments on commit 47be429

Please sign in to comment.