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

ui: make details available for shared workflows #375

Merged
merged 4 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ The list of contributors in alphabetical order:

- [Alp Tuna](https://orcid.org/0009-0001-1915-3993)
- [Audrius Mecionis](https://orcid.org/0000-0002-3759-1663)
- [Daan Rosendal](https://orcid.org/0000-0002-3447-9000)
- [Diego Rodriguez](https://orcid.org/0000-0003-0649-2002)
- [Dinos Kousidis](https://orcid.org/0000-0002-4914-4289)
- [Domenic Gosein](https://orcid.org/0000-0002-1546-0435)
Expand Down
1 change: 1 addition & 0 deletions reana-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"react-redux": "^8.1.3",
"react-router-dom": "^6.17.0",
"react-scripts": "^5.0.0",
"react-semantic-ui-datepickers": "^2.17.2",
"redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-thunk": "^2.3.0",
Expand Down
106 changes: 99 additions & 7 deletions reana-ui/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,28 @@ import client, {
INTERACTIVE_SESSIONS_OPEN_URL,
USER_INFO_URL,
USER_SIGNOUT_URL,
USERS_SHARED_WITH_YOU_URL,
USERS_YOU_SHARED_WITH_URL,
WORKFLOW_FILES_URL,
WORKFLOW_LOGS_URL,
WORKFLOW_RETENTION_RULES_URL,
WORKFLOW_SET_STATUS_URL,
WORKFLOW_SHARE_STATUS_URL,
WORKFLOW_SPECIFICATION_URL,
} from "~/client";
import {
parseWorkflows,
parseWorkflowRetentionRules,
parseLogs,
parseFiles,
formatSearch,
} from "~/util";
import {
getConfig,
getWorkflow,
getWorkflowLogs,
getWorkflowSpecification,
} from "~/selectors";
import {
formatSearch,
parseFiles,
parseLogs,
parseWorkflowRetentionRules,
parseWorkflows,
} from "~/util";

export const ERROR = "Error";
export const NOTIFICATION = "Notification";
Expand Down Expand Up @@ -86,10 +89,21 @@ export const WORKFLOW_STOP_INIT = "Initialize workflow stopping";
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 OPEN_SHARE_WORKFLOW_MODAL = "Open share workflow modal";
export const CLOSE_SHARE_WORKFLOW_MODAL = "Close share 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 const WORKFLOW_SHARE_STATUS_FETCH = "Fetch workflow share status";
export const WORKFLOW_SHARE_STATUS_RECEIVED = "Workflow share status received";
export const WORKFLOW_SHARE_STATUS_FETCH_ERROR =
"Fetch workflow share status error";

export const USERS_SHARED_WITH_YOU_RECEIVED =
"Users who shared workflows with you received";
export const USERS_YOU_SHARED_WITH_RECEIVED =
"Users you shared workflows with received";

export function errorActionCreator(error, name) {
const { status, data } = error?.response;
Expand Down Expand Up @@ -262,21 +276,28 @@ export function fetchWorkflows({
pagination,
search,
status,
sharedBy,
sharedWith,
sort,
showLoader = true,
workflowIdOrName,
shared = false,
}) {
return async (dispatch) => {
if (showLoader) {
dispatch({ type: WORKFLOWS_FETCH });
}

return await client
.getWorkflows({
pagination,
search: formatSearch(search),
status,
sharedBy,
sharedWith,
sort,
workflowIdOrName,
shared,
})
.then((resp) =>
dispatch({
Expand Down Expand Up @@ -305,6 +326,7 @@ export function fetchWorkflow(id, { refetch = false, showLoader = true } = {}) {
fetchWorkflows({
workflowIdOrName: id,
showLoader,
shared: true,
}),
);
}
Expand Down Expand Up @@ -522,3 +544,73 @@ export function closeInteractiveSession(id) {
});
};
}

export function openShareWorkflowModal(workflow) {
return { type: OPEN_SHARE_WORKFLOW_MODAL, workflow };
}

export function closeShareWorkflowModal() {
return { type: CLOSE_SHARE_WORKFLOW_MODAL };
}

export function fetchUsersSharedWithYou() {
return async (dispatch) => {
return await client
.getUsersSharedWithYou()
.then((resp) => {
dispatch({
type: USERS_SHARED_WITH_YOU_RECEIVED,
usersSharedYouWith: resp.data.users_shared_with_you,
});
return resp;
})
.catch((err) => {
dispatch(errorActionCreator(err, USERS_SHARED_WITH_YOU_URL));
});
};
}

export function fetchUsersYouSharedWith() {
return async (dispatch) => {
return await client
.getUsersYouSharedWith()
.then((resp) => {
dispatch({
type: USERS_YOU_SHARED_WITH_RECEIVED,
usersYouSharedWith: resp.data.users_you_shared_with,
});
return resp;
})
.catch((err) => {
dispatch(errorActionCreator(err, USERS_YOU_SHARED_WITH_URL));
});
};
}

export function fetchWorkflowShareStatus(id) {
return async (dispatch) => {
dispatch({ type: WORKFLOW_SHARE_STATUS_FETCH });
return await client
.getWorkflowShareStatus(id)
.then((resp) => {
// convert to camel-case
const sharedWith = [];
for (const share of resp.data.shared_with) {
sharedWith.push({
userEmail: share.user_email,
validUntil: share.valid_until,
});
}
dispatch({
type: WORKFLOW_SHARE_STATUS_RECEIVED,
id,
sharedWith,
});
return resp;
})
.catch((err) => {
dispatch({ type: WORKFLOW_SHARE_STATUS_FETCH_ERROR });
dispatch(errorActionCreator(err, WORKFLOW_SHARE_STATUS_URL(id)));
});
};
}
50 changes: 49 additions & 1 deletion reana-ui/src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const USER_SIGNIN_URL = `${api}/api/login`;
export const USER_SIGNOUT_URL = `${api}/api/logout`;
export const USER_REQUEST_TOKEN_URL = `${api}/api/token`;
export const USER_CONFIRM_EMAIL_URL = `${api}/api/confirm-email`;
export const USERS_SHARED_WITH_YOU_URL = `${api}/api/users/shared-with-you`;
export const USERS_YOU_SHARED_WITH_URL = `${api}/api/users/you-shared-with`;
export const CLUSTER_STATUS_URL = `${api}/api/status`;
export const GITLAB_AUTH_URL = `${api}/api/gitlab/connect`;
export const GITLAB_PROJECTS_URL = (params) =>
Expand All @@ -43,6 +45,11 @@ export const WORKFLOW_FILE_URL = (id, filename, preview = true) =>
)}`;
export const WORKFLOW_SET_STATUS_URL = (id, status) =>
`${api}/api/workflows/${id}/status?${stringifyQueryParams(status)}`;
export const WORKFLOW_SHARE_STATUS_URL = (id) =>
`${api}/api/workflows/${id}/share-status`;
export const WORKFLOW_SHARE_URL = (id) => `${api}/api/workflows/${id}/share`;
export const WORKFLOW_UNSHARE_URL = (id) =>
`${api}/api/workflows/${id}/unshare`;
export const INTERACTIVE_SESSIONS_OPEN_URL = (id, type = "jupyter") =>
`${api}/api/workflows/${id}/open/${type}`;
export const INTERACTIVE_SESSIONS_CLOSE_URL = (id) =>
Expand Down Expand Up @@ -116,13 +123,25 @@ class Client {
});
}

getWorkflows({ pagination, search, status, sort, workflowIdOrName } = {}) {
getWorkflows({
pagination,
search,
status,
sharedBy,
sharedWith,
sort,
workflowIdOrName,
shared,
} = {}) {
return this._request(
WORKFLOWS_URL({
...(pagination ?? {}),
workflow_id_or_name: workflowIdOrName,
search,
status,
shared,
shared_by: sharedBy,
shared_with: sharedWith,
sort,
}),
);
Expand Down Expand Up @@ -198,6 +217,35 @@ class Client {
method: "post",
});
}

getUsersSharedWithYou() {
return this._request(USERS_SHARED_WITH_YOU_URL);
}

getUsersYouSharedWith() {
return this._request(USERS_YOU_SHARED_WITH_URL);
}

getWorkflowShareStatus(id) {
return this._request(WORKFLOW_SHARE_STATUS_URL(id));
}

shareWorkflow(id, { userEmailToShareWith, validUntil }) {
return this._request(WORKFLOW_SHARE_URL(id), {
data: {
user_email_to_share_with: userEmailToShareWith,
valid_until: validUntil,
},
method: "post",
});
}

unshareWorkflow(id, { userEmailToUnshareWith }) {
return this._request(WORKFLOW_UNSHARE_URL(id), {
data: { user_email_to_unshare_with: userEmailToUnshareWith },
method: "post",
});
}
}

const client = new Client();
Expand Down
25 changes: 19 additions & 6 deletions reana-ui/src/components/WorkflowActionsPopup.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@
under the terms of the MIT License; see LICENSE file for more details.
*/

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

import { workflowShape } from "~/props";
import {
deleteWorkflow,
closeInteractiveSession,
deleteWorkflow,
openDeleteWorkflowModal,
openStopWorkflowModal,
openInteractiveSessionModal,
openShareWorkflowModal,
openStopWorkflowModal,
} from "~/actions";
import { workflowShape } from "~/props";
import { getUserEmail } from "~/selectors";

import { JupyterNotebookIcon } from "~/components";

Expand All @@ -31,6 +33,7 @@ const JupyterIcon = <JupyterNotebookIcon className={styles["jupyter-icon"]} />;
export default function WorkflowActionsPopup({ workflow, className }) {
const dispatch = useDispatch();
const [open, setOpen] = useState(false);
const userEmail = useSelector(getUserEmail);
const { id, size, status, session_status: sessionStatus } = workflow;
const isDeleted = status === "deleted";
const isDeletedUsingWorkspace = isDeleted && size.raw > 0;
Expand All @@ -51,6 +54,16 @@ export default function WorkflowActionsPopup({ workflow, className }) {
});
}

menuItems.push({
key: "share",
content: "Share workflow",
icon: "share alternate",
onClick: (e) => {
dispatch(openShareWorkflowModal(workflow));
setOpen(false);
},
});

if (isSessionOpen) {
menuItems.push({
key: "closeNotebook",
Expand Down Expand Up @@ -101,7 +114,7 @@ export default function WorkflowActionsPopup({ workflow, className }) {

return (
<div className={className}>
{menuItems.length > 0 && (
{workflow.ownerEmail === userEmail && menuItems.length > 0 && (
<Popup
basic
trigger={
Expand Down
Loading
Loading